Skip to content

feat(acp): add opencode as a selectable ACP backend - #9013

Open
atomsbaza wants to merge 1 commit into
kirodotdev:mainfrom
atomsbaza:feat/opencode-acp-backend
Open

feat(acp): add opencode as a selectable ACP backend#9013
atomsbaza wants to merge 1 commit into
kirodotdev:mainfrom
atomsbaza:feat/opencode-acp-backend

Conversation

@atomsbaza

@atomsbaza atomsbaza commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

Kiro Crew currently supports {kiro, claude, kas, codex} as selectable ACP backends. Users who run opencode have no way to point agent sessions at it: the backend registry, spawn arms, install probes, and capability sets are closed over the existing four ids, so opencode never appears under Developer → Agent Backend even when the binary is installed.

Why it matters

opencode ships an ACP-compatible stdio agent (opencode acp) with session fork/list/resume and image support. Without an adapter, users standardizing on opencode cannot use Kiro Crew as their dashboard/orchestration layer for it, and must keep a separate client open for those sessions.

What changed (motivation → approach → change)

Goal: make opencode a first-class selectable backend with the smallest possible surface change.

Approach: follow the codex backend template (PR #7963) rather than a new integration path, because the current main has refactored backend registration into a leaf module with capability membership sets — a new backend is now mostly data, not branches:

  • acp_backends.py: added ACP_BACKEND_OPENCODE to the known/baseline-selectable/policy sets; deliberately not added to ACP_BACKENDS_SESSION_MCP_ARRAY, steer, compaction, model-selection, member-dispatch, or seed-local-settings sets (v1 is chat-only — per-session mcpServers support on opencode is unproven, and joining silently would drop Crew tools with no error).
  • acp/client.py: _resolve_opencode_acp_bin() (OPENCODE_ACP_BIN env → mise → PATH), spawn arm [opencode, "acp"], _is_opencode helper.
  • agent_sdk/backend_install.py: _probe_opencode() registered in _PROBES (reports opencode auth login as a user-side prerequisite).
  • Capability gating: unsupported calls (steer/compact/set-model/startup-model) fail fast with explicit errors instead of silently misbehaving; existing backends' behavior is unchanged.
  • Content serialization for text / image / embedded context per the wire format observed in a live opencode acp probe (v1.18.x).
  • Docs: agent-host-contract and acp-client module specs updated.

Tests

New test/test_acp_opencode_backend.py (29 tests) mirrors the codex twins: registry snapshot/invariants across the 14 capability sets, spawn-resolution order, probe state, capability gating fail-fast behavior, serialization, and member-dispatch exclusion. Existing kas / editable-backend / backend-install / harness-parity tests updated where they enumerate backends.

Local validation: twin suite 29/29; targeted run across the 18 touched test files = 1,210 passed; mypy + flake8 clean on touched files.

Manual verification

Live E2E against a real gateway booted from this branch (isolated KIROCREW_HOME, port 5588), full round completed:

  1. Developer → Agent Backend probes and lists opencode (Experimental); selecting it persists as the session backend (acp_backend=opencode in the instance config).
  2. A real chat prompt ("Reply with exactly one short sentence: what backend are you running on?") was answered through the opencode adapter — the gateway spawned opencode acp, and the reply rendered in the dashboard. Tools used in the session were opencode's own built-ins, matching the v1 chat-only scope (no per-session MCP array).

Screenshots / video

Developer → Agent Backend on the test gateway, with opencode present and selected:

Agent Backend page showing opencode selected

A real prompt answered in chat through the opencode adapter:

Chat prompt answered over the opencode backend

Related Issues

N/A — no tracking issue yet.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@atomsbaza
atomsbaza requested a review from a team as a code owner September 6, 2026 11:46
@atomsbaza
atomsbaza requested a review from CrysisDeu September 6, 2026 11:46
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Sep 6, 2026
@atomsbaza
atomsbaza force-pushed the feat/opencode-acp-backend branch from 82114e6 to 041d037 Compare September 6, 2026 12:27
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — 🔴 changes requested (blocking)

Reviewed b640e3d36619a7324ed873e049dbecf681b9d087 via the fork AI-review pipeline; updated in place on each push.

1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands.

BLOCKING -- src/kiro_crew/acp_backends.py:130 -- selectable OpenCode sessions skip fail-closed permission routing

ACP_BACKEND_OPENCODE,
Selecting OpenCode -> initialization ignores its UNVERIFIED routing verdict -> prompts run without positively arming deny rules, sensitive-path checks, or governance.
Anchor: backend-security-controls
Fix: Keep OpenCode non-selectable until an enforced, fail-closed routing mechanism is implemented.
FINDING -- src/kiro_crew/acp_backends.py:51 -- "a session on it mounts no Crew tools" contradicts the pooled MCP stubs always appended during session creation/load -> Fix: qualify the claim to exclude pooled broker stubs.
[BLOCK-MERGE] b640e3d
[GPT-REVIEWED] b640e3d

Adjudication (Opus 4.8) — is blocking on each finding proportionate?

I have enough to rule. Let me confirm the enforcement flow once more: is_enforced("opencode") = routing_for("opencode") in {SESSION_CONFIG}. Since opencode is not in ACP_BACKEND_ROUTING, routing_for returns UNVERIFIED (acp_backends.py:574), so is_enforced is False (acp_tool_gate.py:274-276), and enforce_runtime_routing returns without refusing the session for a non-enforced backend (acp_tool_gate.py:353-360).

This is a fenced, annotate-only finding. My verdict set is {UPHOLD-FENCED, FLAG}.

Harm rung: UNBOUNDED (security-class — governance ceiling / denied-command / sensitive-path bypass). Conditions confirmed: opencode is baseline-selectable on a plain public build (acp_backends.py BASELINE_SELECTABLE_BACKENDS, diff:415-424); routing_for("opencode") falls through to UNVERIFIED (acp_backends.py:574) and enforce_runtime_routing does NOT refuse a non-enforced backend (acp_tool_gate.py:353-360), so a selected opencode session proceeds and its own built-in tool calls never reach HookManager.on_tool_call. Recovery path: none — a self-approved destructive/credential-reading tool call on the harness leaves no gate to catch it.

FLAG requires a rarity argument that the reaching conditions are extreme. They are not: selecting a baseline-selectable backend and letting its agent run a tool is the ordinary operating path, not a contradictory-timing or unproducible-input corner. I cannot complete the rarity record, and this is exactly the class where a wrong clearance is unrecoverable — so UPHOLD-FENCED.

[ADJUDICATION] b640e3d total=0 uphold=0 downgrade=0
[GPT-ADJUDICATED] b640e3d
[ADJUDICATION-FENCED] b640e3d fenced=1 flagged=0
UPHOLD-FENCED F1 src/kiro_crew/acp_backends.py:130 -- opencode is baseline-selectable yet routing_for returns UNVERIFIED (acp_backends.py:574) and enforce_runtime_routing does not refuse a non-enforced backend (acp_tool_gate.py:353-360), so its self-approved tool calls bypass the denied-command, sensitive-path and governance gate on the ordinary use path — an unbounded, unrecoverable security-class harm with no extreme-condition rarity argument to justify a FLAG.
[GPT-ADJUDICATED-FENCED] b640e3d

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — 🟡 CONCERNS

Design-level review of b640e3d36619a7324ed873e049dbecf681b9d087 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

"Chat-only" scopes Crew's tools, not opencode's — its self-approving built-ins ship with no gate, no credential mask, and no sandbox-floor refusal.

Watch

  • The v1 security posture is below the bar codex set, and its stated rationale is a false premise. The spawn arm says "there is no compensating control to arm and nothing for the mask to cover" — but adapter_hidden_credential_dirs exists to fence the child process's passive reads, which happen regardless of Crew tool mounting; the PR's own E2E shows opencode's built-in tools executing. So a selected opencode session runs a self-approving harness whose reads reach the credential homes the standard sandbox tier deliberately leaves open (the exact hole the codex mask closed), with none of the denied-command/sensitive-path/ceiling controls consulted, and — unlike codex — no enforce_sandbox_floor refusal when even the OS sandbox is off. Relatedly, this is the first selectable backend to land on Routing.UNVERIFIED, whose base docstring says it "always resolves INDETERMINATE, which refuses" — it does not refuse here, and that contract text isn't updated. The honest-UNVERIFIED reporting is good; the un-compensated execution behind it deserves an explicit human security sign-off.
  • Two binary PNGs under temp-screenshots/opencode-e2e/ are committed to the tree solely so the PR description can hotlink them; merged, they live in git history permanently under a directory named "temp". Drop them from the diff and attach via GitHub upload.

Suggestions

  • Give opencode the codex treatment in this PR — floor-derived credential mask (excluding its own auth store) plus the sandbox-floor check — or hold it out of BASELINE_SELECTABLE_BACKENDS until permission routing is verified on the wire, the same evidence bar the PR already applies to per-session MCP.

[DESIGN-REVIEWED] b640e3d

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5, fork) — 🟡 CONCERNS

Premise-level review of b640e3d36619a7324ed873e049dbecf681b9d087 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

First-Principles-Verdict: CONCERNS

A clean, data-driven backend addition that follows the codex template, but ships a dormant _opencode_session_mcp_servers() seam that only ever returns [] — speculative surface deferred to a proof that may never come.

What this change ships

Intent: let users point agent sessions at their installed opencode harness. ADDITION.

  1. opencode appears/selectable under Developer → Agent Backend — justified
  2. Selecting it persists and spawns opencode acp (resolver: env → mise → PATH) — justified
  3. Install probe reports missing binary + opencode auth login remedy — justified
  4. steer / manual compact / set_model / startup-model fail fast on opencode — justified, declared
  5. client.steer() now returns False for any non-ACP_BACKENDS_STEER backend (codex too) — rides along, but derived from H6 (identity by membership)
  6. _opencode_session_mcp_servers() + two splice sites, always returns [] — zero content-consumer, speculative
  7. Two E2E screenshots committed to temp-screenshots/ — declared; repo convention (.gitignore:97 exempts it)

Watch

  • Dormant MCP seam. _opencode_session_mcp_servers (client.py:3221) returns [] unconditionally and opencode is in no capability set, so both splices (session/new line 5330, session/load line 5477) are provable no-ops today; _pooled_mcp_servers() supplies any real servers regardless. Its only justification is "filling this hook later proves the capability" — inherited from the codex seam by symmetry, with no edition/mirror story of its own (unlike codex's NO_MIRROR entry). Zero consumers of non-empty output.

Subtractions

  • Drop _opencode_session_mcp_servers and its two if self._is_opencode splices; opencode mounts no tools without them, and re-adding a splice when per-session MCP is proven is a one-line edit — not surface to carry now.

[FIRST-PRINCIPLES-REVIEWED] b640e3d

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed b640e3d36619a7324ed873e049dbecf681b9d087 via the fork AI-review pipeline; updated in place on each push.

Review details

Both candidates rest on opencode's ACP wire behavior, which cannot be verified from this repository. Let me confirm the key facts I've established before concluding.

I've verified the load-bearing code paths:

Candidate 1 (opencode ships with no credential mask / no gate routing): Grounded facts — opencode's routing is UNVERIFIEDis_enforced is False → adapter_hidden_credential_dirs returns () and enforce_sandbox_floor is a no-op; opencode is wrapped by Crew's OS sandbox (not in ACP_BACKENDS_INTERNAL_SANDBOX); the standard tier deliberately leaves ~/.aws/~/.ssh readable (sandbox.py:6372-6374). But the observable credential exposure requires opencode to self-approve its own file reads without sending session/request_permission — the standard ACP mechanism through which the sensitive-path block would otherwise fire. Whether opencode acp gates its own reads is external adapter behavior I cannot verify from this repo, and the safe case (it routes → gated) is at least as plausible as the unsafe one. The outcome (c) reduces to a "might." Below the 80 bar.

Candidate 2 (warm-pool set_model raises AcpError and hard-kills a claim): The new guard does raise for opencode, and the warm-claim path (session_allocation.py:1346→1357-1359) does _dispatch_hard_kill + raise. But reaching it requires opencode configured as backend + a populated warm pool + a concrete non-auto model pin whose resolved value differs from the pool default — an "if a user configures" condition for a backend that supports no model switching and advertises no models in its picker. The outcome is a recoverable turn failure, not data loss. The candidate is self-rated LOW confidence; reachability doesn't clear the 80 bar. (KAS, which the guard also excludes, routes through AcpSessionHandle.set_model/set_config_option, not the modified AcpClient.set_model — so no regression there.)

Neither survives falsification, and I found no grounded 80+ defect to add.

No findings.

[OPUS-REVIEWED] b640e3d

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Sep 6, 2026
Register `opencode` in the baseline selectable backend registry and add
its spawn arm (stdio JSON-RPC via `opencode acp`), install probe, session
lifecycle with capability gating, and content serialization, mirroring
the codex backend template.

v1 scope: chat-only (no per-session MCP array), no steer/compaction, no
model-via-config; unsupported capability calls fail fast with explicit
errors so existing backends are unaffected.

Adds twin tests (test_acp_opencode_backend.py, 29 tests) and updates the
agent-host-contract and acp-client docs.
@atomsbaza
atomsbaza force-pushed the feat/opencode-acp-backend branch from 041d037 to b640e3d Compare September 6, 2026 12:55
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant