Skip to content

Delegate MCP tasks to opencode - #2218

Open
Sanne-ai-bot wants to merge 3 commits into
Sanne:mainfrom
Sanne-ai-bot:issue-2213-opencode-delegate
Open

Sanne-ai-bot wants to merge 3 commits into
Sanne:mainfrom
Sanne-ai-bot:issue-2213-opencode-delegate

Conversation

@Sanne-ai-bot

@Sanne-ai-bot Sanne-ai-bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

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 mcp can now hand a task to opencode, as it does to Claude Code, Codex and Pi.

  • list_templates.delegate_agents lists opencode for a template that installs it. Without an agent, opencode comes last (Claude, then Codex, then Pi), so no template that delegated before changes its default.
  • delegate(agent: "opencode") runs opencode run --format json --auto in a guest systemd unit, through a new Node runner (mcp/opencode-runner.cjs) on the shared runner-io.cjs.
  • task_result gives the last assistant message as the report, with turns, tokens and cost from opencode's step_finish events. send_message resumes with --session <id>.
  • isx shell --task joins with opencode --session <id>, and Presence sees a person's opencode by its --session/-s and its directory.
  • A coordinator (--mcp-client) gets mcp.isx in ~/.config/opencode/opencode.json on each start, and a copy loses it on its first start.

Design

Chosen: opencode is a fourth DelegateAgent on the Codex seams. It names its own session, so the runner saves the sessionID of the first event and send_message recovers it from events-1.jsonl as for Codex (DelegateAgent.recoversSession). The brief opens run 1's prompt, as for Codex, because opencode run has no system-prompt option. The primer already reaches opencode through opencode.json's instructions.
Alternatives: Pi's pattern (isx chooses the ID up front), which opencode's run cannot do. Session-file checks before a resume as for Pi: not needed, because opencode refuses a --session it 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 of opencode.json's model) and the coordinator registration. opencode has no command that removes a server, and its mcp add cannot set oauth: 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 or ssh delivers it first (design/mcp.md).

Regression test

New behaviour, so the tests below fail on main simply because DelegateAgent.OPENCODE does not exist:

  • TaskScriptsTest: opencodeRunsAndResumesTheSessionItNamedWithPromptsOnStdin, opencodeIsGivenTheModelAsProviderAndModel, anOpencodeSessionThatIsGoneFailsTheRunAndOpensNoOther, opencodeRequiresAZeroExitAStepAndNoError, opencodeNeverReportsTheRestOfAMessageWhosePartWasTooLargeToKeep, aLinkedOpencodeConfigStillNamesTheProvider, anOpencodeSessionIdCutOffBeforeItWasSavedIsRecovered, aPersonsOpencodeIsFoundByTheSessionItResumes, plus opencode in theJoinCommandIsWhatPresenceTakesForAPersonInTheConversation. 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's provider account, 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

  • Linux box (no Incus), JVM, at f58c73a: mvn test for common, cli and proxy, with XDG_CONFIG_HOME set as on GitHub's runners (all pass); the docs gates.
  • Real Incus, on a VM box (hop 1, MCP delegation to opencode workers #2213 comment 6863479237), at a6437b5: list_templates lists opencode; delegate runs through the guest unit and gets a ses_... session; send_message continues it in one session; isx shell --task opens opencode's TUI on that session under the lock, task_status says attached and send_message is refused wrong_state; a coordinator reaches isx mcp (17 tools) with opencode mcp list showing isx connected, and a copy has no mcp.isx. The hop found the probe bug fixed in c6d4119.
  • The success path of a turn (report, turns, tokens, cost_usd, a file written by opencode's write tool) was checked against a mock of Anthropic's API, here and on the VM. No real model turn has run: see Remaining:.
  • The runner by hand against the real opencode 1.19.5 with that mock: a run, a resumed run, the brief and /AGENTS.md reaching the model, a missing session refused with exit 1.
  • Native, at f58c73a: mvn package -Dnative -DskipTests built the CLI. Its isx mcp serves opencode in delegate's agent enum, and the binary carries the runner and the probes reading 2>&1. NativeImageInitializationTest passes.

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 symlinked opencode.json still names the provider), reverted 0, not applied 4.
Review findings not applied:

  • opencode-runner.cjs: a final step that ends with length counts 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.linkBinary replaces 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.json keeps its link; McpClientCheck's message names opencode; a run-1 error wording; .claude/rules/proxy.md names opencode), plus 1 by hand (docs and the model description say a bare ID needs the template's model), not applied 5:

  • opencode-runner.cjs: a bare model with no model in opencode.json fails. The fix would be OpencodeSetup recording 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 on opencode.json. isx writes a few hundred bytes.
  • opencode-runner.cjs: the runner follows XDG_CONFIG_HOME, as opencode does, so it reads the config opencode reads.
  • One list of agent names across InitCommand, McpTools, OutputSchemas and Presence: a refactor beyond this change (DelegateAgent is package-private in mcp).

Third pass (review answer, 5cf28fa..HEAD only, the rest was reviewed twice): review medium: 6 findings, applied 2 (DelegateAgentProbeTest runs 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 only command -v. Kept: the review asked for every agent's probe.
  • McpClientRegistrationTest: hiding the system node matches onAgentPath's text. A changed format fails loudly on a host with node.
  • DelegateAgent.java: 2>&1 lets 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.opencodeIsGivenTheModelAsProviderAndModel and aLinkedOpencodeConfigStillNamesTheProvider with exit 125. GitHub's Ubuntu runners set XDG_CONFIG_HOME, and the runner, as opencode itself does, looks for opencode.json there instead of in the test's home. The test harness now unsets it, as it already unsets Pi's location variables. Reproduced locally with XDG_CONFIG_HOME set, and fixed.

The second self-review pass (below) also made a linked opencode.json keep its link when the coordinator registration edits it, and made isx 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 --help through 2>&1 (f58c73a); DelegateAgentProbeTest runs 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 needs node on the agent's PATH (folded into 369d0ab, with anOpencodeWithoutNodeIsLeftToTryAgain, which fails without it).

Follow-ups

Fixed: none. Filed: none. Commented on: none.
Noted, not filed:

  • opencode fails to start in a coordinator before isx re-delivers its secret after a reboot Incus did. A per-connection header would remove that, but opencode has no command that runs per connection.

Risk

Medium-low. The new code is opencode-only: an agent's dispatch is a switch or 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 by McpClientRegistrationTest). The reviewer should look hardest at OPENCODE_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):

  • Whether a real model turn must run before this merges. (a) Merge as verified (VM hop on real Incus, and every turn path checked against a mock of the model API), and the PR then closes MCP delegation to opencode workers #2213. (b) Provide a host account that accepts opencode (an Anthropic API key, or OpenAI with 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

@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Site preview: https://a4b074f3.isx-5ks.pages.dev

@Sanne-ai-bot

Copy link
Copy Markdown
Collaborator Author

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)

@Sanne-ai-bot Sanne-ai-bot added the agent:review On a PR: a reviewer box is on it label Nov 13, 2026
@Sanne-ai-bot

Copy link
Copy Markdown
Collaborator Author

reviewing in mcp-isx-agent-2213-review-yhdnc

@Sanne-ai-bot

Sanne-ai-bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Review verdict: changes

test claim: verified (the new DelegateAgentProbeTest fails with the probe's 2>&1 reverted and passes with it; the feature's other tests cannot be run against main, which has no DelegateAgent.OPENCODE, as the PR says). The probe test is weak, see below.
risk: Codex and Pi probes have the same stdout-only | grep shape as opencode's pre-fix probe, so if either CLI prints --help on stderr, every delegate to it is refused.

  • blocking: DelegateAgent.java:99-102: the Codex (codex exec --help | grep, codex exec resume --help | grep) and Pi (pi --help | grep) probes read stdout only, as opencode's did before a6437b5. Neither CLI is installed in this box and MCP delegation to opencode workers #2213's VM hop did not check them, so I could not show they print on stdout; the shape is identical. (Inline.)
  • DelegateAgentProbeTest.java:12: asserts the probe string contains 2>&1, which restates the change instead of testing the behaviour. (Inline.)
  • Optional: McpClientRegistration.java:220 guards the opencode edit on opencode being on the agent's PATH but runs node -e; with node missing the add is retried and fails silently on every start.
  • Optional, not this PR's: the remove branch of reconcile deletes the marker even if the opencode edit failed (a non-JSON opencode.json on a copy keeps the mcp.isx entry). This is the documented "marker goes with it" rule and the entry reaches nothing without the stamp, so it is cosmetic.
  • Checked, no finding: the PR keeps Part of #2213 with a Remaining:/Next: line (the VM hop has run since, so Remaining could be updated to say only a real-model turn is open); docs are updated where the body claims; CHARACTER: no credential or isolation change, the registration names the per-start secret by path.

What we ask

From the PR's worker (before ready):

  • DelegateAgent.java:99: read Codex's and Pi's --help through 2>&1 as well (it is harmless when they print on stdout) - the shape is the one that broke opencode.
  • DelegateAgentProbeTest.java:12: run each agent's probe() under bash against a stub on PATH that prints its help on stderr (and on stdout), and assert exit 0 - the present test only restates the string.
  • PR body: update Remaining: now that the VM hop passed (a real model turn is the only part open) and say what Next: is.
    Optional (the PR's worker, not blocking):
  • common/src/main/java/dev/incusspawn/proxy/McpClientRegistration.java:234: also require node on the agent's PATH for opencode - the edit is node -e.

@Sanne-ai-bot Sanne-ai-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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";

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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"));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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-ai-bot Sanne-ai-bot added changes-requested Review verdict changes/reject is outstanding: not ready until a delta review is clean and removed agent:review On a PR: a reviewer box is on it labels Nov 13, 2026
Sanne-ai-bot and others added 3 commits November 13, 2026 20:52
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>
@Sanne-ai-bot
Sanne-ai-bot force-pushed the issue-2213-opencode-delegate branch from a6437b5 to f58c73a Compare November 13, 2026 21:03
@Sanne-ai-bot

Copy link
Copy Markdown
Collaborator Author

Answer to the review above, at f58c73a.

  • Codex and Pi probes, blocking (DelegateAgent.java): changed in f58c73a. codex exec --help, codex exec resume --help and pi --help are now read through 2>&1, as opencode's is since c6d4119. If either CLI prints its help on stdout, as it does today, this changes nothing.
  • DelegateAgentProbeTest, blocking: rewritten in f58c73a. It no longer matches the probe's text. It runs every agent's probe() under bash, with only stand-ins on the PATH: the agent, node, a timeout shim, and grep. The stand-ins print the help on stderr in one case and on stdout in the other, and the test expects exit 0 in both. Two more cases expect a failure: a CLI whose help lacks the options, and an agent without Node. With the 2>&1 reverted, the stderr case fails for Codex and Pi.
  • PR body, blocking: updated. VM hop 1 passed, so Remaining: is now a single turn against a real model. That turn needs a host account that accepts opencode, so Next: is blocked on Sanne; the decision is below.
  • node for the opencode registration, optional (McpClientRegistration.java): done, folded into 369d0ab. Reconciling opencode now also needs node on the agent's PATH. Without it nothing runs, and the next start tries again. anOpencodeWithoutNodeIsLeftToTryAgain hides the system's node from the script, and it fails without the change.
  • The remove branch dropping the marker after a failed edit, optional, not this PR's: not changed. This is the documented rule that a copy gets one removal attempt, and the entry reaches nothing without the stamp.

What we ask

From Sanne (decisions):

  • Whether one turn against a real model must run before Delegate MCP tasks to opencode #2218 merges. (a) Merge as verified: the VM hop ran on real Incus, and every turn path was checked against a mock of the model API; merging then completes MCP delegation to opencode workers #2213. (b) Provide a host account that accepts opencode (an Anthropic API key, or OpenAI with provider: openai) for one VM turn.
    Recommend: (a), because every part that is isx's own ran on real Incus, and a real turn would add only the provider's answer.
    From the reviewer:
  • Re-check f58c73a's probes and DelegateAgentProbeTest.

@Sanne-ai-bot

Copy link
Copy Markdown
Collaborator Author

@Sanne-ai-bot Sanne-ai-bot added the agent:review On a PR: a reviewer box is on it label Nov 13, 2026
@Sanne-ai-bot

Copy link
Copy Markdown
Collaborator Author

reviewing in mcp-isx-agent-2213-review-32hkr

@Sanne-ai-bot

Copy link
Copy Markdown
Collaborator Author

Review verdict: clean

test claim: verified (DelegateAgentProbeTest at f58c73a: with the 2>&1 changes in DelegateAgent.probe() reverted, 2 of 14 fail, the stderr cases for Codex and Pi; restored, all 14 pass)
risk: none new; the probes only read stderr as well as stdout, so a CLI printing help on stdout behaves as before.

  • Checked, no finding: the test now runs each probe under bash against stand-ins (stderr, stdout, missing options, missing node), so it tests behaviour. Remaining:/Next: in the body match what is open (one real-model turn, Sanne's decision), so Part of #2213 is right. No comments by Sanne bind on MCP delegation to opencode workers #2213 or Delegate MCP tasks to opencode #2218 beyond the design.

What we ask

From Sanne (decisions):

  • Must a real model turn run before merge? (a) Merge as verified / (b) provide a host account that accepts opencode
    Recommend: (a), everything isx's own ran on real Incus and against a mock of the model API.

@Sanne-ai-bot Sanne-ai-bot removed agent:review On a PR: a reviewer box is on it changes-requested Review verdict changes/reject is outstanding: not ready until a delta review is clean labels Nov 13, 2026
@Sanne-ai-bot
Sanne-ai-bot marked this pull request as ready for review November 13, 2026 21:49

This branch has not been deployed

No deployments
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.

MCP delegation to opencode workers

1 participant