Repository navigation
Delegate MCP tasks to opencode - #2218
Sanne-ai-bot wants to merge 3 commits into
Conversation
b3290c7 to
fe11d04
Compare
|
Site preview: https://a4b074f3.isx-5ks.pages.dev |
fe11d04 to
5cf28fa
Compare
|
infra rerun: unit-tests-macos: "The job was not acquired by Runner of type hosted" (cancelled at 15 min, no steps ran; https://github.com/Sanne/incus-spawn/actions/runs/43497607085/job/132134915812) |
|
reviewing in mcp-isx-agent-2213-review-yhdnc |
Review verdict: changestest claim: verified (the new
What we askFrom the PR's worker (before ready):
|
Sanne-ai-bot
left a comment
There was a problem hiding this comment.
Changes requested: see What we ask in #2218 (comment).
| case PI -> "command -v pi >/dev/null && command -v node >/dev/null" | ||
| + " && timeout 10 pi --help | grep -q -- --session-id"; | ||
| case OPENCODE -> "command -v opencode >/dev/null && command -v node >/dev/null" | ||
| + " && timeout 10 opencode run --help 2>&1 | grep -q -- --auto"; |
There was a problem hiding this comment.
Blocking, for the PR's worker: read Codex's and Pi's --help through 2>&1 too (lines 99, 100 and 102 above this one) - same stdout-only | grep shape that refused every opencode delegate; 2>&1 is harmless if they print on stdout.
| /** opencode's yargs prints {@code run --help} on stderr, so a probe reading stdout never matches (live-VM finding). */ | ||
| @Test | ||
| void opencodeProbeReadsHelpFromStderr() { | ||
| assertTrue(DelegateAgent.OPENCODE.probe().contains("opencode run --help 2>&1 | grep -q -- --auto")); |
There was a problem hiding this comment.
Blocking, for the PR's worker: run probe() under bash against a stub on PATH that prints its help on stderr, for all three agents, and assert exit 0 - asserting the string contains 2>&1 only restates the change.
Sanne#2212 made opencode a built-in tool, but isx mcp could not hand it a task: delegate's agent was claude, codex or pi, list_templates listed no delegate agent for an opencode-only template, and a coordinator's opencode never got the isx server. opencode is now a DelegateAgent on the Codex adapter's seams, last in the default order so no template that delegated before changes its default. A guest Node runner (opencode-runner.cjs) runs `opencode run --format json --auto`, the prompt on stdin and the brief opening run 1's prompt, as for Codex, since opencode has no system-prompt option; the primer reaches it through opencode.json's instructions. opencode names its own session, so run 1 saves the sessionID of the first event and send_message recovers it from events-1.jsonl as for Codex. opencode refuses a missing --session (exit 1, nothing created), so a lost session fails the run and needs none of Pi's checks. The report is the last assistant message's text; step_finish gives turns, tokens and cost. A bare model gets the provider of opencode.json's model, since opencode takes provider/model. Presence finds a person's opencode by --session/-s and its directory, and isx shell --task joins with `opencode --session <id>`. A coordinator's opencode gets mcp.isx in ~/.config/opencode/opencode.json on each start, from its own marker and config test. opencode has no removal command and its mcp add cannot set oauth: false, so Node run as the agent edits the file; one that is not JSON is left alone. The secret header is {file:/run/isx/instance-secret}, read once per process. Part of Sanne#2213 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
opencode prints its help on stderr, so the probe's grep saw nothing and every delegate to a real opencode was refused (found on a live VM). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…by running a6437b5 found on a live VM that opencode prints its help on stderr, so a probe piping stdout alone into grep refused every delegate to it. The Codex and Pi probes had the same shape: a CLI release that moved its help to stderr would have had every delegate to it refused the same way. Each now reads both streams. DelegateAgentProbeTest no longer matches the probe's text. It runs every agent's probe under bash against stand-ins on the PATH that print the help on stderr, then on stdout, and expects exit 0; and one without the options, or without Node, fails it. Without the fix, the stderr case fails for Codex and Pi. Part of Sanne#2213 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a6437b5 to
f58c73a
Compare
|
Answer to the review above, at f58c73a.
What we askFrom Sanne (decisions):
|
|
infra rerun: host-distro (arch): "HTTP 429 (Server: cloudflare) for https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.pom" (https://github.com/Sanne/incus-spawn/actions/runs/43507246163/job/132169935454) |
|
reviewing in mcp-isx-agent-2213-review-32hkr |
Review verdict: cleantest claim: verified (
What we askFrom Sanne (decisions):
|
Part of #2213
Remaining: one delegate turn against a real model, which needs a host account that accepts opencode (the Claude subscription here and on the VM answers "Third-party apps now draw from your extra usage"). Next: blocked on Sanne.
What
isx mcpcan now hand a task to opencode, as it does to Claude Code, Codex and Pi.list_templates.delegate_agentslistsopencodefor a template that installs it. Without anagent, opencode comes last (Claude, then Codex, then Pi), so no template that delegated before changes its default.delegate(agent: "opencode")runsopencode run --format json --autoin a guest systemd unit, through a new Node runner (mcp/opencode-runner.cjs) on the sharedrunner-io.cjs.task_resultgives the last assistant message as the report, with turns, tokens and cost from opencode'sstep_finishevents.send_messageresumes with--session <id>.isx shell --taskjoins withopencode --session <id>, andPresencesees a person's opencode by its--session/-sand its directory.--mcp-client) getsmcp.isxin~/.config/opencode/opencode.jsonon each start, and a copy loses it on its first start.Design
Chosen: opencode is a fourth
DelegateAgenton the Codex seams. It names its own session, so the runner saves thesessionIDof the first event andsend_messagerecovers it fromevents-1.jsonlas for Codex (DelegateAgent.recoversSession). The brief opens run 1's prompt, as for Codex, becauseopencode runhas no system-prompt option. The primer already reaches opencode throughopencode.json'sinstructions.Alternatives: Pi's pattern (isx chooses the ID up front), which opencode's
runcannot do. Session-file checks before a resume as for Pi: not needed, because opencode refuses a--sessionit cannot find (exit 1, nothing created; verified with 1.19.5, the TUI's too).Why: least new mechanism. The things that differ are the model form (opencode takes
provider/model, so a bare ID gets the provider ofopencode.json'smodel) and the coordinator registration. opencode has no command that removes a server, and itsmcp addcannot setoauth: false, so Node, run as the agent, edits the JSON config. A file that is not JSON (a person's comments) is left alone, and the edit is tried again on the next start.What it gives up: the MCP header is
{file:/run/isx/instance-secret}, which opencode reads once per process. While that file is missing, opencode refuses its whole config and so does not start. That happens only in an instance Incus restarted after a host reboot, before isx delivered the secret again; every way in through isx orsshdelivers it first (design/mcp.md).Regression test
New behaviour, so the tests below fail on main simply because
DelegateAgent.OPENCODEdoes not exist:TaskScriptsTest:opencodeRunsAndResumesTheSessionItNamedWithPromptsOnStdin,opencodeIsGivenTheModelAsProviderAndModel,anOpencodeSessionThatIsGoneFailsTheRunAndOpensNoOther,opencodeRequiresAZeroExitAStepAndNoError,opencodeNeverReportsTheRestOfAMessageWhosePartWasTooLargeToKeep,aLinkedOpencodeConfigStillNamesTheProvider,anOpencodeSessionIdCutOffBeforeItWasSavedIsRecovered,aPersonsOpencodeIsFoundByTheSessionItResumes, plus opencode intheJoinCommandIsWhatPresenceTakesForAPersonInTheConversation. These run the real runner under bash and Node against an opencode stand-in that keeps opencode's session rule.DelegationToolsTest#anOpencodeOnlyTemplateDelegatesToOpencode: discovery, default order, results, continuation and recovery.McpClientRegistrationTest: the opencode registration on a coordinator's first start, removal on a copy, a non-JSON config left alone, and re-registration after a rebuild rewrote the config.CHARACTER
It touches what a coordinator instance can reach and how a worker runs. The opencode registration carries no credential: it names the existing per-start secret by its path, and the proxy still checks address, secret and stamp, so a leftover entry reaches nothing. opencode runs with
--auto, which is #1857's full access inside the instance, the instance being the boundary. Credentials are unchanged (the template'sprovideraccount, injected by the proxy).Docs
CLAUDE.md,
.claude/rules/mcp.md,.claude/rules/config.md, design/mcp.md, README.md, docs/MCP.md, docs/MCP-PROTOCOL.md, docs/AGENT-INSTALL.md (one line; #2214 also edits that page), new docs/dev/MCP-OPENCODE-DELEGATION.md. DESIGN.md needed no change: no new section.Verified on
mvn testfor common, cli and proxy, withXDG_CONFIG_HOMEset as on GitHub's runners (all pass); the docs gates.list_templateslists opencode;delegateruns through the guest unit and gets ases_...session;send_messagecontinues it in one session;isx shell --taskopens opencode's TUI on that session under the lock,task_statussaysattachedandsend_messageis refusedwrong_state; a coordinator reachesisx mcp(17 tools) withopencode mcp listshowingisxconnected, and a copy has nomcp.isx. The hop found the probe bug fixed in c6d4119.cost_usd, a file written by opencode'swritetool) was checked against a mock of Anthropic's API, here and on the VM. No real model turn has run: seeRemaining:./AGENTS.mdreaching the model, a missing session refused with exit 1.mvn package -Dnative -DskipTestsbuilt the CLI. Itsisx mcpservesopencodeindelegate'sagentenum, and the binary carries the runner and the probes reading2>&1.NativeImageInitializationTestpasses.Handoff
vm hop 1/3 done (result on #2213); no further hop.
Self-review
Self-review: review medium: 8 findings, applied 2 (shared outcome builder and snippet with
PiEvents; message count ignores parts without an ID), plus 2 applied by hand afterwards (a text part dropped for size keeps the rest of that step from passing for the report; a symlinkedopencode.jsonstill names the provider), reverted 0, not applied 4.Review findings not applied:
opencode-runner.cjs: a final step that ends withlengthcounts as success. Same rule as the Pi and Codex runners, which fail only on error or aborted; changing it would be a policy for every agent.McpClientRegistration.java: the{file:}header stops opencode from starting while the secret file is missing. Accepted and documented trade-off (Design above, design/mcp.md); opencode has no per-connection header command.Tasks.java: the recovery branch picks its padding and wording by== CODEX. Minor style; two agents.Presence.java: an opencode run through its Node launcher is not recognised.OpencodeSetup.linkBinaryreplaces the stub with the native binary and fails the build otherwise, so the guest always runs the binary.Second pass (CI fix): review medium: 10 findings, applied 4 (a linked
opencode.jsonkeeps its link;McpClientCheck's message names opencode; a run-1 error wording;.claude/rules/proxy.mdnames opencode), plus 1 by hand (docs and themodeldescription say a bare ID needs the template's model), not applied 5:opencode-runner.cjs: a bare model with nomodelinopencode.jsonfails. The fix would beOpencodeSetuprecording the provider in the config, which changes opencode's config; the docs now say what happens instead.opencode-runner.cjs: a final step without text leaves an earlier message as the report. Which message is "the report" then is a judgement; the earlier text is often the summary.opencode-runner.cjs: 64 KiB cap onopencode.json. isx writes a few hundred bytes.opencode-runner.cjs: the runner followsXDG_CONFIG_HOME, as opencode does, so it reads the config opencode reads.InitCommand,McpTools,OutputSchemasandPresence: a refactor beyond this change (DelegateAgentis package-private inmcp).Third pass (review answer, 5cf28fa..HEAD only, the rest was reviewed twice): review medium: 6 findings, applied 2 (
DelegateAgentProbeTestruns the probe with only its stand-ins on the PATH, so the no-node case runs on a host with node, and its time bound applies), not applied 4:DelegateAgentProbeTest: Claude's probe reads no help, so its two cases cover onlycommand -v. Kept: the review asked for every agent's probe.McpClientRegistrationTest: hiding the system node matchesonAgentPath's text. A changed format fails loudly on a host with node.DelegateAgent.java:2>&1lets an option named in a stderr warning pass the probe. The runner then fails the run with its stderr, which is reported; refusing every delegate was the worse failure.McpClientRegistration.java: without node, a copy keeps its marker and the entry. That is how a missing binary is already handled, and the entry reaches nothing without the stamp.CI fix
The first CI run failed
TaskScriptsTest.opencodeIsGivenTheModelAsProviderAndModelandaLinkedOpencodeConfigStillNamesTheProviderwith exit 125. GitHub's Ubuntu runners setXDG_CONFIG_HOME, and the runner, as opencode itself does, looks foropencode.jsonthere instead of in the test's home. The test harness now unsets it, as it already unsets Pi's location variables. Reproduced locally withXDG_CONFIG_HOMEset, and fixed.The second self-review pass (below) also made a linked
opencode.jsonkeep its link when the coordinator registration edits it, and madeisx branch --mcp-client's check name opencode. It narrowed the bare-model wording: a bare ID needs the template to set a model.Review answer
Review #2218 (comment): the Codex and Pi probes read
--helpthrough2>&1(f58c73a);DelegateAgentProbeTestruns every probe under bash against stand-ins printing the help on stderr and on stdout (f58c73a; the stderr case fails for Codex and Pi without the fix); the opencode registration needsnodeon the agent's PATH (folded into 369d0ab, withanOpencodeWithoutNodeIsLeftToTryAgain, which fails without it).Follow-ups
Fixed: none. Filed: none. Commented on: none.
Noted, not filed:
Risk
Medium-low. The new code is opencode-only: an agent's dispatch is a
switchor an explicit check. One exception is the coordinator registration script, which every start runs. Its opencode line costs one[ -x ]per PATH directory when opencode is absent, and runs nothing as the agent in that case (pinned byMcpClientRegistrationTest). The reviewer should look hardest atOPENCODE_EDIT(JS run as the agent on its own file) and at the runner's success rule.Timing
Build (JVM) about 1 m; unit tests: common 2 m 18 s, cli 2 m 24 s, proxy 4 m 45 s; native CLI build about 1 m.
What we ask
From Sanne (decisions):
provider: openai) for one VM turn. Recommend: (a), because everything that is isx's runs on real Incus, and a model turn adds only the provider's answer.🤖 Generated with Claude Code