feat: multi-agent support (milestone 1) — agent abstraction + default drop-in provisioning - #46
feat: multi-agent support (milestone 1) — agent abstraction + default drop-in provisioning#46neurolabs wants to merge 21 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #46 +/- ##
==========================================
+ Coverage 88.83% 90.63% +1.80%
==========================================
Files 88 99 +11
Lines 5553 6014 +461
==========================================
+ Hits 4933 5451 +518
+ Misses 431 365 -66
- Partials 189 198 +9 ☔ View full report in Codecov by Harness. |
|
Relates to #37 |
| DaemonKillCmd() string | ||
| DaemonHealthCmd() string | ||
| DaemonHealthParse(stdout string) (bool, error) | ||
| WorktreeListCmd() string |
There was a problem hiding this comment.
worktree and daemon don't depend on each other, split.
There was a problem hiding this comment.
Split DaemonProvider into two independent capabilities in internal/agent/capabilities.go: DaemonProvider (daemon lifecycle: start/kill/health) and WorktreeProvider (worktree list/create/parse), each with its own type-assertion helper (AsDaemonProvider / AsWorktreeProvider). --serve-only now requires DaemonProvider; --worktree requires WorktreeProvider, so an agent can implement either independently. opencode implements both, so default behavior is unchanged.
|
|
||
| func (opencodeProfile) ImageSpec() ImageSpec { | ||
| return ImageSpec{ | ||
| VersionArg: "OPENCODE_VERSION", |
There was a problem hiding this comment.
this could be toUpper($name)_VERSION
There was a problem hiding this comment.
Derived via versionArgFor(name) = strings.ToUpper(name) + "_VERSION" in internal/agent/image.go. opencode still yields OPENCODE_VERSION.
| func (opencodeProfile) ImageSpec() ImageSpec { | ||
| return ImageSpec{ | ||
| VersionArg: "OPENCODE_VERSION", | ||
| VersionLabel: "org.opencode-sandbox.opencode-version", |
There was a problem hiding this comment.
also derivable from name
There was a problem hiding this comment.
Derived via versionLabelFor(name) = "org.opencode-sandbox." + name + "-version". opencode still yields org.opencode-sandbox.opencode-version.
| return ImageSpec{ | ||
| VersionArg: "OPENCODE_VERSION", | ||
| VersionLabel: "org.opencode-sandbox.opencode-version", | ||
| DisableUpdateEnv: "OPENCODE_DISABLE_AUTOUPDATE", |
There was a problem hiding this comment.
Should we add a generic map for environment changes? Maybe multiple vars must be set.
There was a problem hiding this comment.
ImageSpec.AgentEnv (renamed from the single-string DisableUpdateEnv) is now a generic map[string]string, and DockerfileFromImageSpec emits one ENV k=v line per entry. Multiple vars can now be set; opencode renders identically (ENV OPENCODE_DISABLE_AUTOUPDATE=true).
| } | ||
|
|
||
| func (opencodeProfile) LatestVersion(ctx context.Context) (string, error) { | ||
| return opencode.LatestVersion(ctx) |
There was a problem hiding this comment.
Should internal/opencode and opencode.go be moved&merged to internal/agent/opencode?
There was a problem hiding this comment.
internal/opencode was merged into the opencode profile: the release checker now lives in internal/agent/opencode_release.go (latestOpenCodeVersion, newerOpenCodeThan) in the same package, and the internal/opencode package is deleted. Kept in the same package rather than an internal/agent/opencode subpackage to avoid an import cycle, since the subpackage would need to import the parent agent package.
| // fall back to the requested (or empty) version. | ||
| func resolveAgentVersion(ctx context.Context, a agent.Agent, requested string) (string, error) { | ||
| if _, ok := agent.AsUpgradeChecker(a); ok { | ||
| return resolveOpenCodeVersion(ctx, requested) |
There was a problem hiding this comment.
This should use the agent param, not hardcode opencode
There was a problem hiding this comment.
resolveAgentVersion(ctx, a agent.Agent, requested) now resolves the latest version via the agent's own UpgradeChecker (falling back to the requested/empty version when the agent implements none), instead of hardcoding opencode's resolver. The vm upgrade path (agentLatestVersion) was updated the same way.
| // error. Agents that implement ConfigMerger use their own snippet pattern and | ||
| // VM config path; other agents fall back to the opencode snippet behavior. | ||
| func buildMergedConfig(a agent.Agent, vmHome string) (string, []byte, bool, error) { | ||
| if cm, ok := agent.AsConfigMerger(a); ok { |
There was a problem hiding this comment.
Don't hardcode opencode in the body
There was a problem hiding this comment.
The hardcoded opencode auth.json removal path was replaced with an agent-driven walk: provisionDestinations derives the removal set from the agent's own ProvisionRules via agent.EvalProvisionRules. For opencode this still yields .local/share/opencode/auth.json (and the config-file family), but no opencode-specific path is hardcoded.
| boot vmBoot, | ||
| ) (string, error) { | ||
| ui.Verbosef("expected config files: %v", cfs.Keys) | ||
| a, _ := agent.Lookup("") |
There was a problem hiding this comment.
Why not use the defined agent here?
There was a problem hiding this comment.
setUpSandbox now resolves the agent from opts.Agent via agent.Lookup(opts.Agent) (with an explicit "unknown agent" error) instead of agent.Lookup(""), so the configured agent is threaded through VM orchestration.
| ) (*Session, error) { | ||
| projectSlug := git.ProjectSlug() | ||
|
|
||
| a, _ := agent.Lookup("") |
There was a problem hiding this comment.
Why not use the ocnfigured agent here?
There was a problem hiding this comment.
Same fix in run_orchestrate.go: PrepareSandbox resolves the agent from opts.Agent instead of agent.Lookup("").
| opencode-sandbox provisions a single opencode config into the VM at `/home/dev/.config/opencode/opencode.json`. No embedded | ||
| provider or permission config is shipped with opencode-sandbox. Instead, opencode config is assembled from snippet files | ||
| under `~/.config/opencode-sandbox/opencode/` (user) and `.opencode-sandbox/opencode/` (project): | ||
| opencode-sandbox is agent-aware. A `--agent <name>` flag on `run`, `build`, and the `volume` subcommands selects the |
There was a problem hiding this comment.
Next to flag, also config.yaml and ENV VAR
There was a problem hiding this comment.
The agent is now also selectable via the agent launcher config key and the OPENCODE_SANDBOX_AGENT env var, in addition to the --agent flag, with precedence flag > env > config > default. Documented in docs/configuration.md (config-key and env-var tables) and CHANGELOG.md.
neurolabs
left a comment
There was a problem hiding this comment.
config subcommand needs respec and rework.
|
How can a user switch to provisioning from opencode-sandbox/ configuration instead of using the config? |
Address all review comments on PR #46 for multi-agent milestone 1: - Thread the configured agent through VM orchestration and version resolution instead of hardcoding agent.Lookup("") / opencode's resolver - Split DaemonProvider into daemon and worktree capabilities so an agent can implement either independently - Make ImageSpec fields (version arg/label) derivable from the agent name; generalize DisableUpdateEnv to a map - Merge internal/opencode into the opencode profile - Rename internal/opencodeconfig to generic internal/configmerge - Allow selecting the agent via the agent config key and OPENCODE_SANDBOX_AGENT env var - Replace config show with agent-aware config agent <name>, which lists merged snippet config and host drop-in files (merged/not merged) - Fix hardcoded opencode auth.json removal via an agent-driven walk - Update docs and changelog Behavior for the default opencode agent is unchanged except the intended breaking CLI rename (config show -> config agent) and the additive agent config/env surface.
|
All review comments addressed in commit a310c29 (multi-agent milestone 1 review cleanup). Reviewer replies (inline)
PR-level: config subcommand respecReworked 'config show' into an agent-aware 'config agent [name]' command. It prints the agent's merged snippet config AND the host drop-in files, each marked 'merged' or 'not merged'. [name] defaults to the configured agent. 'config home' is unchanged. How to switch to snippet provisioningCreating snippet files that match the agent's snippet pattern (e.g. opencode-.json) makes the snippet merge win over the host config: when snippets exist, the drop-in copy of the config-file family is skipped and the merged snippet config is provisioned. To disable the native-config drop-in entirely (so only the snippet merge and home.yaml mappings apply), set 'provision-host-config: false' in the launcher config. This is documented in the 'Default drop-in provisioning' section of docs/configuration.md. |
Register built-in pi (@earendil-works/pi-coding-agent) and claude-code (@anthropic-ai/claude-code) agent profiles alongside opencode. Both run interactively (no daemon), so --worktree/--serve-only are rejected at flag-parse time via the existing capability check. Both implement an UpgradeChecker (pi via pi.dev, claude-code via the npm registry latest dist-tag) and a ConfigMerger writing settings*.json* snippets to ~/.pi/agent/settings.json / ~/.claude/settings.json. - Move Node.js install before the agent install block so npm-based agents install with npm install -g. - Parameterize reprovision config-family detection via ConfigMerger.ConfigFileNames and ConfigFiles.MergedPath so merged-config detection and 'config agent' output are correct for pi/claude. - Share the semver compare and JSON latest-version fetch helpers.
Summary
Introduces the
internal/agentabstraction of built-in coding-agent profiles with optional capabilities discovered by type assertion (DaemonProvider, UpgradeChecker, ConfigMerger, AttachRunner, Provisioner), plus gitignore-based default drop-in provisioning that copies the active agent's config + credential files from the host into the VM. Adds a--agentCLI flag, renames--opencode-versionto--agent-version(deprecated alias kept), and rejects--worktree/--serve-onlyfor agents lacking a daemon.Milestone 1 registers only
opencode(fully backward compatible); the seam paves the way for future agents (pi, claude).What changed
internal/agent(new): registry (Register/Lookup/Names), capability interfaces,ImageSpec, opencode profile, and a gitignore-based provisioning manifest evaluator (go-git).--agentflag (run/build/volume), defaultopencode, validated againstagent.Names().--agent-versionreplaces--opencode-version(kept as a deprecated alias).auth.jsonis now copied by default; use the env-secret mechanism (env.secret) to opt out. The env-secret channel is unchanged.opencode-*.json*(a bareopencode.jsonno longer merges by default); YAML snippet patterns supported.configpaths.Breaking change
--opencode-versionrenamed to--agent-version(deprecated alias retained).Test
make checkgreen (fmt, lint, tests).