Skip to content

feat: add DAG-level aqua tools - #2150

Merged
yohamta0 merged 15 commits into
mainfrom
feature/aqua-dag-tools
May 15, 2026
Merged

yohamta0 merged 15 commits into
mainfrom
feature/aqua-dag-tools

Conversation

@yohamta0

@yohamta0 yohamta0 commented May 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Add DAG-level tools support backed by embedded aqua APIs so workflows can declare reproducible CLI dependencies without requiring a separate aqua binary.

Changes

  • Add tools parsing, validation, and JSON schema support for aqua packages.
  • Support shorthand package declarations such as tools: ["jqlang/jq@jq-1.7.1"] while keeping the full object syntax for overrides.
  • Install tools into worker-local cache directories with checksum enforcement, install locking, and generated manifests.
  • Expose declared commands to local command steps through resolved shims and runtime manifest lookup.
  • Make package commands optional by inferring executable names from aqua registry metadata, while keeping explicit commands as an override.
  • Default provider and registry, pin the default aqua registry to an immutable commit SHA, and accept package commit refs where aqua supports them.
  • Update Go to 1.26.3 to resolve stdlib vulnerabilities reported by govulncheck.
  • Address CodeRabbit feedback around command validation, manifest writes, platform path sanitization, duplicate command defense, and manifest path consistency.

Related Issues

N/A

Validation

  • go test -count=1 ./internal/core/spec ./internal/tools/aqua ./internal/cmn/schema ./internal/tools ./internal/runtime/builtin/command ./internal/cmd
  • GOOS=windows GOARCH=amd64 go test -exec=true ./internal/core/spec ./internal/tools/aqua ./internal/tools ./internal/runtime/builtin/command ./internal/cmd
  • DAGU_AQUA_INTEGRATION=1 go test ./internal/tools/aqua -run TestInstallerInstallIntegration -count=1
  • Manual sample DAG installed and ran jqlang/jq@jq-1.7.1 with omitted provider, registry, and commands.
  • Manual concurrent run check verified two DAG runs sharing one data dir reused one toolset manifest.

Checklist

  • Scope is limited to DAG-level aqua tools support and required dependency/toolchain updates.
  • Local focused tests pass.
  • Windows compile path is covered.
  • Aqua integration install path is covered.

@coderabbitai

coderabbitai Bot commented May 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 730a4475-3b4c-4743-8373-c790d45b3b33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements DAG-level tool installation via aqua, allowing workflows to declare and install external CLI dependencies before execution. It introduces tool configuration schemas, spec loading with validation, aqua-backed installer with file locking, environment variable generation, and runtime command resolution to expose installed tools to step processes.

Changes

Tool Installation Feature

Layer / File(s) Summary
Tool configuration data types
internal/core/tools.go, internal/core/dag.go
Defines ToolConfig, ToolRegistry, and ToolPackage structs with JSON serialization; adds Tools field to DAG struct and introduces DefaultAquaStandardRegistryRef constant for aqua registry defaults.
Schema validation for tools
internal/cmn/schema/dag.schema.json
Adds JSON schema definitions for toolConfig, toolRegistry, and toolPackage with validation for provider, registry type/ref, pinned versions (rejecting latest), and command names; adds top-level tools property to DAG schema.
DAG spec tools loading and validation
internal/core/spec/dag.go, internal/core/spec/dag_test.go
Implements spec types and build logic to parse YAML tools configuration, validate packages/registries/commands, enforce pinned versions, deduplicate commands, and reject invalid configurations; includes six test cases covering successful loads and rejection scenarios.
Tool cache and manifest utilities
internal/tools/tools.go, internal/tools/tools_test.go
Defines cache path layout, manifest structures, toolset hash computation via SHA-256, environment variable generation, and manifest reading; provides CachePaths, ToolsetHash, EnvVars, and ReadManifest helpers tested for deterministic paths and platform-specific behavior.
Aqua configuration rendering and installer
internal/tools/aqua/config.go, internal/tools/aqua/config_test.go, internal/tools/aqua/installer.go, internal/tools/aqua/installer_integration_test.go, internal/tools/aqua/shim.go, internal/tools/aqua/shim_test.go, internal/tools/aqua/manifest.go
Renders tool configs to aqua.yaml format with checksum enforcement and platform-specific supported envs; implements aqua-backed Installer with file locking via dirlock, directory setup, config rendering, aqua controller execution, command resolution with "which", shim creation (hard link → symlink → copy), and manifest writing; tested for config rendering correctness, registry defaults, commit-SHA version preservation, and full integration install flow.
CLI entry point integration
internal/cmd/start_tools.go, internal/cmd/start_tools_test.go, internal/cmd/start.go, internal/cmd/restart.go, internal/cmd/retry.go
Adds prepareDAGTools to validate tool support (rejecting DAGs with containers or unsupported executors) and run installer; derives base PATH from config core environment; wires tool preparation into executeDAGRun, executeDAGWithRunID, and executeRetry with error handling and environment passing via agent.Options.ExtraEnvs; includes unit tests for validation logic and base path derivation.
Runtime tool command resolution
internal/runtime/builtin/command/tools.go, internal/runtime/builtin/command/tools_test.go, internal/runtime/builtin/command/command.go
Implements resolveRuntimeToolCommand to map command names to installed tool paths via manifest lookup; falls back to original command on missing manifest or resolution failure; integrates into commandConfig.newCmd default path; tested for manifest-based command resolution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add DAG-level aqua tools' clearly and concisely summarizes the main feature addition: DAG-level support for aqua tools.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description follows the template structure with all required sections (Summary, Changes, Related Issues, Checklist) completed, though Validation details are provided separately.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/aqua-dag-tools

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
internal/tools/tools.go (1)

137-137: ⚡ Quick win

Hardcoded manifest filename creates inconsistency risk.

Line 137 constructs the manifest path by joining manifest.EnvDir with the hardcoded constant manifestFileName, but the Manifest struct doesn't validate that its fields are consistent with this convention. If the manifest was constructed with a different path structure, this could produce an incorrect path.

♻️ Proposed fix to use the manifest's existing path

Since CacheLayout already includes ManifestFile, and the manifest is written to that path by the installer, consider storing the manifest file path directly in the Manifest struct or using it from there:

 	return []string{
 		"AQUA_ROOT_DIR=" + manifest.RootDir,
 		"AQUA_CONFIG=" + manifest.Config,
 		"AQUA_DISABLE_LAZY_INSTALL=true",
 		"AQUA_CHECKSUM=true",
 		"AQUA_REQUIRE_CHECKSUM=true",
 		"AQUA_ENFORCE_CHECKSUM=true",
 		"AQUA_ENFORCE_REQUIRE_CHECKSUM=true",
-		EnvManifest + "=" + filepath.Join(manifest.EnvDir, manifestFileName),
+		EnvManifest + "=" + filepath.Join(manifest.EnvDir, "manifest.json"),
 		"PATH=" + pathValue,
 	}

Or better, add a ManifestFile field to the Manifest struct to track the actual path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tools/tools.go` at line 137, The code builds the env manifest path
using a hardcoded manifestFileName joined to manifest.EnvDir (EnvManifest + "="
+ filepath.Join(manifest.EnvDir, manifestFileName)), which can mismatch how the
manifest was originally written; update the code to derive the file path from
the manifest's recorded path instead of hardcoding—either add a ManifestFile
field to the Manifest struct or use the existing CacheLayout.ManifestFile (or
other canonical ManifestFile property) when available, and replace uses of
filepath.Join(manifest.EnvDir, manifestFileName) with that manifest.ManifestFile
(or cacheLayout.ManifestFile); ensure Manifest creation/installer sets and
validates this field so the constructed EnvManifest is always consistent with
where the manifest was actually written.
internal/tools/aqua/config.go (1)

23-23: 💤 Low value

Shallow copy of ToolPackage structs may cause mutation bugs.

Line 23 creates a new slice and copies ToolPackage structs by value, which is correct for avoiding slice aliasing. However, if any future fields in ToolPackage are pointers or slices (e.g., Commands []string), mutations to the effective config could affect the original. Currently Commands is a slice, so modifications to effective.Packages[i].Commands would affect the original.

♻️ Defensive deep copy for future safety
 	effective := *cfg
-	effective.Packages = append([]core.ToolPackage{}, cfg.Packages...)
+	effective.Packages = make([]core.ToolPackage, len(cfg.Packages))
+	for i, pkg := range cfg.Packages {
+		pkgCopy := pkg
+		pkgCopy.Commands = append([]string{}, pkg.Commands...)
+		effective.Packages[i] = pkgCopy
+	}

Note: This is defensive coding for future changes; the current code doesn't mutate these fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tools/aqua/config.go` at line 23, The current append into
effective.Packages copies ToolPackage structs shallowly which leaves nested
slices (e.g., ToolPackage.Commands) shared and susceptible to mutation; update
the code that sets effective.Packages (replace the append of cfg.Packages) to
allocate a new slice and deep-copy each ToolPackage: for each pkg in
cfg.Packages create a new ToolPackage value, copy primitive fields, and clone
any slice or pointer fields (at minimum duplicate Commands with a new []string
and copy its elements) before appending to effective.Packages so mutations to
effective do not affect cfg.Packages.
internal/core/spec/dag_test.go (1)

173-192: ⚡ Quick win

Add a regression test for shell-metacharacter command names.

Current new tests reject path-like commands, but they still miss shell-fragment tokens (e.g. jq;echo). Adding one negative case here will lock in the intended contract.

Suggested test addition
+func TestLoadDAGToolsRejectsShellFragmentCommand(t *testing.T) {
+	t.Parallel()
+
+	_, err := LoadYAML(context.Background(), []byte(`
+tools:
+  provider: aqua
+  packages:
+    - package: jqlang/jq
+      version: jq-1.7.1
+      commands: ["jq;echo"]
+steps:
+  - name: check
+    command: jq
+`))
+
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), `must be an executable name`)
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/spec/dag_test.go` around lines 173 - 192, Extend
TestLoadDAGToolsRejectsCommandPath to also assert that shell-metacharacter
fragments are rejected: call LoadYAML with a tools/packages entry where commands
contains a token like "jq;echo" (or "jq && echo") and require an error, then
assert the error message contains the same validation text (e.g. `must be an
executable name` or mention of invalid characters). Keep the test structure
(t.Parallel(), use LoadYAML, require.Error and assert.Contains) and place the
new negative case alongside the existing path-like command assertion in
TestLoadDAGToolsRejectsCommandPath so both rejects are covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/core/spec/dag.go`:
- Around line 2445-2447: The validator isToolCommandName currently only blocks
path separators and whitespace but still accepts shell fragments like "jq;rm" or
"jq|cat"; update isToolCommandName to enforce a strict allowed-character set
(e.g. letters, digits and a small safe subset of punctuation such as . _ - +)
using a full-match check (anchor the pattern) or by iterating rune-by-rune and
rejecting any shell/operator characters such as ; | & $ ` > < ( ) ' " * ? [ ] {
} ~ # and control characters; modify the isToolCommandName function to perform
this full-match validation and return false on any disallowed rune so command
names cannot contain shell metacharacters or operators.

In `@internal/tools/aqua/installer.go`:
- Around line 95-100: The directory creation in installer.go uses os.MkdirAll
for paths.EnvDir and paths.RootDir with mode 0o750 which may be too restrictive
for shared CI/multi-user setups; update the mode to 0o755 for both os.MkdirAll
calls (or, if intentional single-user behavior is required, add a clear comment
above the os.MkdirAll calls documenting the single-user assumption and
rationale) to avoid permission denied errors when multiple users/processes need
access.
- Around line 138-163: The loop over cfg.Packages/pkg.Commands currently
resolves and writes every command into manifest.Commands, allowing later
packages to overwrite earlier ones; update the logic in the loop (around where
whichController.Which is called and before createCommandShim/manifest.Commands
assignment) to detect existing entries in manifest.Commands for the same command
name and fail or surface an error instead of silently overwriting (include the
conflicting package info: pkg.Package and pkg.Version and the already-registered
entry), so duplicate declarations are rejected and the caller can handle the
conflict; ensure you reference and check manifest.Commands[command] before
creating the shim or assigning tools.Command.

In `@internal/tools/aqua/manifest.go`:
- Around line 19-21: Replace the direct os.WriteFile(path, data, 0o600) call
with an atomic replace: create a temp file in the same directory (os.CreateTemp
/ ioutil.TempFile), write the manifest bytes to that temp file, fsync the file
(via file.Sync and optionally syscall.Fsync on Unix), close it, set the desired
permissions (0600) and then atomically rename it into place with
os.Rename(pathTemp, path); ensure you remove the temp file on error. Update the
write logic in internal/tools/aqua/manifest.go where os.WriteFile is used so
readers never observe a partially written/truncated manifest.

In `@internal/tools/tools.go`:
- Line 79: The current replacement only handles "/" and leaves other path-unsafe
characters in the platform string; add a helper function named
sanitizePlatform(platform string) that uses strings.NewReplacer to replace "/",
"\\", ":", " " (and any other path-unsafe characters you deem necessary) with
"-" and call sanitizePlatform(platform) instead of
strings.ReplaceAll(strings.TrimSpace(platform), "/", "-") so the platform
variable is fully sanitized before being used in filesystem paths.

---

Nitpick comments:
In `@internal/core/spec/dag_test.go`:
- Around line 173-192: Extend TestLoadDAGToolsRejectsCommandPath to also assert
that shell-metacharacter fragments are rejected: call LoadYAML with a
tools/packages entry where commands contains a token like "jq;echo" (or "jq &&
echo") and require an error, then assert the error message contains the same
validation text (e.g. `must be an executable name` or mention of invalid
characters). Keep the test structure (t.Parallel(), use LoadYAML, require.Error
and assert.Contains) and place the new negative case alongside the existing
path-like command assertion in TestLoadDAGToolsRejectsCommandPath so both
rejects are covered.

In `@internal/tools/aqua/config.go`:
- Line 23: The current append into effective.Packages copies ToolPackage structs
shallowly which leaves nested slices (e.g., ToolPackage.Commands) shared and
susceptible to mutation; update the code that sets effective.Packages (replace
the append of cfg.Packages) to allocate a new slice and deep-copy each
ToolPackage: for each pkg in cfg.Packages create a new ToolPackage value, copy
primitive fields, and clone any slice or pointer fields (at minimum duplicate
Commands with a new []string and copy its elements) before appending to
effective.Packages so mutations to effective do not affect cfg.Packages.

In `@internal/tools/tools.go`:
- Line 137: The code builds the env manifest path using a hardcoded
manifestFileName joined to manifest.EnvDir (EnvManifest + "=" +
filepath.Join(manifest.EnvDir, manifestFileName)), which can mismatch how the
manifest was originally written; update the code to derive the file path from
the manifest's recorded path instead of hardcoding—either add a ManifestFile
field to the Manifest struct or use the existing CacheLayout.ManifestFile (or
other canonical ManifestFile property) when available, and replace uses of
filepath.Join(manifest.EnvDir, manifestFileName) with that manifest.ManifestFile
(or cacheLayout.ManifestFile); ensure Manifest creation/installer sets and
validates this field so the constructed EnvManifest is always consistent with
where the manifest was actually written.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3474b667-4f59-4de5-ae8e-f7b90fab841a

📥 Commits

Reviewing files that changed from the base of the PR and between 4638232 and 5f37110.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (23)
  • go.mod
  • internal/cmd/restart.go
  • internal/cmd/retry.go
  • internal/cmd/start.go
  • internal/cmd/start_tools.go
  • internal/cmd/start_tools_test.go
  • internal/cmn/schema/dag.schema.json
  • internal/core/dag.go
  • internal/core/spec/dag.go
  • internal/core/spec/dag_test.go
  • internal/core/tools.go
  • internal/runtime/builtin/command/command.go
  • internal/runtime/builtin/command/tools.go
  • internal/runtime/builtin/command/tools_test.go
  • internal/tools/aqua/config.go
  • internal/tools/aqua/config_test.go
  • internal/tools/aqua/installer.go
  • internal/tools/aqua/installer_integration_test.go
  • internal/tools/aqua/manifest.go
  • internal/tools/aqua/shim.go
  • internal/tools/aqua/shim_test.go
  • internal/tools/tools.go
  • internal/tools/tools_test.go

Comment thread internal/core/spec/dag.go
Comment thread internal/tools/aqua/installer.go
Comment thread internal/tools/aqua/installer.go Outdated
Comment thread internal/tools/aqua/manifest.go Outdated
Comment thread internal/tools/tools.go Outdated
@yohamta0
yohamta0 merged commit d886436 into main May 15, 2026
14 checks passed
@yohamta0
yohamta0 deleted the feature/aqua-dag-tools branch May 15, 2026 16:33
@coderabbitai coderabbitai Bot mentioned this pull request May 16, 2026
7 of 8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant