feat(acp): add OpenCode harness - #6777
Conversation
- add native OpenCode ACP spawn, resume, and model handling - preserve Kiro-only capability and sandbox boundaries - pin backend selection and protocol behavior with tests
- project only fully exposed managed MCP servers into OpenCode sessions - use portable ACP local-server fields and pooled stub replacement - recover trusted MCP identity from sanitized server prefix with fail-closed collisions - accept rawInput from OpenCode permission payloads for display and deny checks - route session/new and session/load through backend-aware server helper - update MCP and ACP client specs alongside behavior
- add OpenCode to AgentBackendTab picker with server-derived enablement - route /api/models to advertised OpenCode configOptions when acp_backend is opencode - never fall through to kiro catalog for opencode model namespaces - document backend selection and model routing in README
- frontend gate bypasses entirely when acp_backend is opencode and shows switch button for kiro-blocked users - backend /api/kiro-prerequisite returns ready when opencode is selected so CLI and non-owner checks also pass
- route /api/models to advertised OpenCode session first, then cold CLI fallback via opencode models with strict id validation and size cap - centralize Kiro-identity checks behind selected_backend_uses_kiro_identity so usage/kiro-prerequisite and sessions bypass cleanly for OpenCode - expose acp_backend on providers via positive identity, let _advertised_opencode_models filter on harness id - harden KiroPrerequisiteGate switch: async invalidation, external doc link and private-error redaction
|
Kiro Crew [operator: chenmingwei23#de330d0c]: This PR has been inactive for 7+ days. I reviewed the blockers but they require your input:
Once you have rebased onto current main and settled the baseline-backend contract, the pipeline will re-assess on its next cycle. If you would prefer no automation on this PR, add the pr-no-autofix label. |
bolichen97
left a comment
There was a problem hiding this comment.
Description / code mismatch
Four claims in the Description are contradicted by the code on f04a43160: the harness-parity approach it promises is inverted in the diff, the parity-gate pass it reports cannot hold against that same diff, and the OpenCode setup link it describes references an i18n key that exists in no catalog.
1. Kiro-specific gates are bypassed with not is_kiro_backend, the exact form the Description promises never to use
The Description says —
Bypass Kiro-specific gates only when
acp_backend == "opencode"(positive check, nevernot is_kiro).
The code does — four added lines express harness identity negatively:
src/kiro_crew/providers/acp.py:1484—not is_kiro_backendsrc/kiro_crew/providers/acp.py:1208—not is_claude_backendsrc/kiro_crew/acp/client.py:2444— a negative identity test the branch's own widened rule matchessrc/kiro_crew/dashboard/handlers/agents.py:1081—!= ACP_BACKEND_OPENCODE
Risk — H5 is the invariant this change is most directly governed by (AGENTS.md, Harness parity: "Identity is positive... Never a bare string literal, an inequality, or a negation"), and it fails toward the permissive answer: not is_kiro_backend and != ACP_BACKEND_OPENCODE silently hand a fifth harness whatever branch they guard, with nothing going red until an operator who never opted into that harness pays for it. scripts/check_harness_parity.py does pin all four — the negative-identity rule catches the three in providers/acp.py and acp/client.py, and the negative-constant rule (!=\s*ACP_BACKEND_[A-Z_]+) catches agents.py:1081. All four are added lines under SCAN_ROOTS = ("src/kiro_crew/",) with no harness-ok suppression and no exempt= entry, so the diff-scoped gate reports four H5 violations (see finding 2). The harness-parity.md H6 row this PR edits cites test_acp_backend_opencode.py::TestOpenCodeProviderBoundaries, which pins H6's capability-set exclusions and is not a waiver for H5.
providers/acp.py:1484 additionally changes existing KAS behaviour that the Description does not disclose: cleanup_session no longer deletes ~/.kiro/sessions/cli/<sid>.json* for the KAS backend, which previously reached that code.
Required change — rewrite all four as positive tests or named-set membership: self.is_kiro_backend, provider.acp_backend == ACP_BACKEND_OPENCODE, self._is_opencode. For providers/acp.py:1208, the positive restoration of the early-return guard is if self.is_acp_runtime_backend or self.is_opencode_backend: return, or invert the block onto if self.is_claude_backend: — not a bare negation of either. If the KAS cleanup skip is intended, state it in the Description and in docs/system-specs/modules/harness-parity.md.
2. The reported check_harness_parity.py pass cannot hold — the gate matches four added lines
The Description says —
HARNESS_BASE_REF=origin/main python scripts/check_harness_parity.py✓
The code does — the gate matches the four lines listed in finding 1. Three of them fail under main's unmodified rules: dashboard/handlers/agents.py:1081 hits the untouched negative-constant rule, and providers/acp.py:1208 and providers/acp.py:1484 hit main's existing is_(?:kiro|claude|kas)_backend alternation. Only src/kiro_crew/acp/client.py:2444 depends on this branch's own widening of _HARNESS_LITERAL and negative-identity to include opencode — so the branch adds a line that its own tightening then catches.
Risk — the Harness Parity Gate job (.github/workflows/ci.yml:405-429, which exports HARNESS_BASE_REF from the PR's base.sha, runs --test, then the enforcing invocation) did not execute on this head SHA; the check-runs present are limited to the fork workflow guards, Fork PR Description, and readiness signals, which is a consequence of the fork plus the dirty merge-ref state rather than anything the author did. That leaves the reported ✓ as the only evidence a reviewer has for the invariant most central to this change, and it does not hold.
Required change — fix the four lines per finding 1, then re-run HARNESS_BASE_REF=origin/main python3 scripts/check_harness_parity.py and update the Tests section with the real result.
3. The OpenCode setup link has no catalog key, so it renders a raw dotted path and the PR's own new test cannot pass
The Description says —
KiroPrerequisiteGatequeriesacp_backend;opencoderenderschildrendirectly, otherwise showsOr use OpenCode → Continue with OpenCode(patchConfig('agent.acp_backend','opencode'),opencode.ai/docslink,opencode auth logincopy block, private-error redaction)
The code does — website/src/components/KiroPrerequisiteGate.tsx:1016 calls i18nT('components.kiroPrerequisiteGate.open_opencode_setup'), and that key is defined in no catalog, including en.json. Separately, the diff to this component is purely additive — the switchBackendMutation plus the card — and contains no acp_backend query and no early return children; the opencode bypass is served only by the backend returning ready: true, not by the component. The Description's third claimed new test, bypass when opencode, is absent from the test diff, which adds only switches to OpenCode and keeps first-run setup visible.
Risk — three concrete failures. (1) The opencode.ai/docs link renders as the literal text components.kiroPrerequisiteGate.open_opencode_setup on the first-run setup screen. (2) The new test switches to OpenCode from first-run setup and renders the dashboard opens with await screen.findByRole('link', { name: 'Open OpenCode setup' }), and website/integration/setup.ts — which is the setupFiles entry for the entire vitest project (website/vite.config.ts:591), not just integration tests — calls initI18n('en') against the real catalog, so that query never resolves. (3) frontend-lint runs npm run i18n:check (.github/workflows/ci.yml:1420), whose runner website/scripts/i18n-check.mjs spawns check-i18n-keys.mjs; its key-refs row is declared scope: 'repo', enforce: 'hard-zero' at website/scripts/lib/i18n-gate-table.mjs:111, and a t() naming a key that does not exist fails the step.
Required change — add open_opencode_setup under components.kiroPrerequisiteGate with the intended copy, then run cd website && npm run i18n:check and the vitest file. Either implement the component-side acp_backend query and early return children the Description describes, or correct the Description to say the bypass is backend-only, and either add the bypass when opencode test or drop it from the Tests section.
4. New English catalog keys with none of the shipped target catalogs updated
The Description says —
AgentBackendTab.tsx4th row backed byselectable_backend_values(),en.json+ tests
The code does — six keys are added to the English catalog only: five under the hunk @@ -4349,7 +4349,12 @@ at website/src/i18n/locales/en.json:4353-4357 (line 4352 is the modified your_crew_is_almost_ready), plus "opencode": "OpenCode" at website/src/i18n/locales/en.json:6354. No target catalog is touched. A seventh key, components.kiroPrerequisiteGate.open_opencode_setup, is referenced but added to no catalog at all (finding 3).
Risk — this contradicts the checklist item "[x] Existing tests pass and new tests added for new functionality": frontend-test reds on the catalog-parity cases, which cover 12 non-en codes rather than 11, because en-XA is itself a SUPPORTED_LANGUAGES entry (devOnly) and the parity loop includes it. The pseudolocale check (gen-pseudolocale.mjs --check, another hard zero) derives en-XA.json from en, which is unchanged here, so it fails as well.
Required change — add the new keys, translated, to every target catalog; place hand-authored strings in en.manual.json rather than the generated en.json; regenerate en-XA.json; then run cd website && npm run i18n:check && npm run test.
Audit note — three open PRs are building the same ACP-harness seamThis is a consolidation request, not a duplicate finding: #5349, #6307 and #6777 What the audit verified by reading all three merge-base diffs:
Why this needs a decision rather than a merge orderThe three collide inside the same functions, not merely in the same files, and each pins its Two things must not be lost whichever design winsFrom #5349, carry before consolidating: (1) website/src/pages/settings/AiBackendPanel.tsx plus website/src/test/AiBackendPanel.test.tsx -- the Settings -> System -> AI Backend picker with per-backend descriptions and the restart/new-chat disclosure (aiBackendPanel.restart_new_chats_description), together with its command-palette wiring in website/src/components/commandPalette/settingsRegistry.gen.ts and settingsTabLabel.ts; this is closer to the maintainer's 'the provider switch has its own page, keep consistent with the internal version' than #6307's preview-gated Developer -> ACP Adapters tab (AcpBackendCard.tsx). (2) src/kiro_crew/dashboard/kiro_readiness.py::running_backend_requires_kiro_prerequisite -- keep its fail-closed rule (an absent or unknown backend still demands kiro readiness) as the behaviour #6307's live resolver must preserve. (3) src/kiro_crew/cli_doctor.py's actionable codex rows ('Codex ACP adapter not found', Fix: npm i -g @agentclientprotocol/codex-acp), folded into acp/doctor.py::_report_adapter, and reused as the basis for the missing agent_sdk/backend_install.py codex probe that landed main names as codex's selectability blocker. (4) From test/test_acp_backend_codex.py, the design-independent cases only: codex advertised-model gating, session/new MCP descriptor wiring, session resume, and the auth-required / readiness boundary. Do NOT carry CodexAcpClient, CodexAcpProvider / create_codex_provider_factory, the create_factory codex branch, the ACP_BACKENDS_SELECTABLE edit, CODEX_EFFORT_CONFIG_ID = 'reasoning_effort', or the restart-only SessionManager.acp_backend snapshot -- each is superseded by, or contradicted by, landed main. One security-relevant noteNothing needs harvesting for a closure - neither PR is closed. For the consolidation, two items must not be lost whichever design wins. From #6307: src/kiro_crew/acp/opencode.py's ensure_routed_settings() with PERMISSION_ASK / PERMISSION_BYPASS_VALUES, which writes Suggested next step: a maintainer picks the seam design (the registry-dispatch shape vs. the From a repository-wide duplicate/overlap audit of every pull request open against |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
|
@knowlet Thanks for this, and sorry for the long wait. OpenCode is still absent from Still unique to this PR: the sanitized Please rebase and narrow to that remaining scope. Two blockers regardless: #9013 (@atomsbaza) registers the same backend on the landed registry but keeps it known-and-not-selectable, citing a live probe showing OpenCode's ACP surface exposes no permission mode. I suggest landing that registration first, then rebasing this PR's OpenCode-specific work on top. Could you two settle selectability here? Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong. |
Problem / Motivation
Kiro Crew is KiroACP-only (
agent.provider = acp, harness selected viaagent.acp_backend), but the only selectable harnesses in the public build werekiro(default) andkas. Users without a signed-inkiro-cliare blocked by the full-screenKiroPrerequisiteGate(“Set up Kiro”) and cannot reach Developer → Agent Backend, so there is no way to switch to an alternative harness. The dashboard’sGET /api/modelsalso unconditionally spawnskiro chat --list-models, so an OpenCode session’s provider-qualified model IDs (opencode/big-pickle,sakana/fugu) never appear in the picker.Issue #6622 tracks the orthogonal governance question (“what may this deployment select”) that the registry from #6593 (“what can this build serve”) left open. Adding a third selectable harness makes that governance scope necessary and provides a concrete harness to bound.
Why it matters
configOptions(provider-qualified IDs). Falling through to the Kiro catalog shows Kiro model IDs thatopencode acpwill reject (AcpModelUnavailable) and hides the models the backend can actually run.SCOPE_CATALOGrow;agent_backendhad none, so an enterprise cannot say “only run the harness we qualified” (Add an agent_backend governance scope to bound which ACP harness a fleet may select #6622).If left undone, OpenCode users must maintain a Kiro login they never use, and fleet operators cannot bound the harness set.
What changed (motivation → approach → change)
Motivation → Keep Kiro as the unconditional first-class harness; any new harness must adapt to existing seams, not generalize the Kiro path (harness-parity H5–H13). A harness that cannot land without changing the Kiro construction path must not land.
Approach → Add OpenCode as an
AcpProviderbackend at theacp_backendseam (not a secondagent.providervalue), keep all capability membership positive/opt-in, and make the dashboard backend-aware at the same registry that feeds the PATCH allowlist and config schema. Bypass Kiro-specific gates only whenacp_backend == "opencode"(positive check, nevernot is_kiro).Change →
feat(acp): add opencode transport):ACP_BACKEND_OPENCODE = "opencode"inacp_backends.py(leaf, re-exported byacp/types.py),BASELINE_SELECTABLE_BACKENDS += opencode,_resolve_opencode_bin(OPENCODE_BIN/mise which/~/.opencode/bin/ PATH),PROTOCOL_VERSION_OPENCODE = 1, per-sessionAcpClient(excluded fromACP_BACKENDS_SESSION_SHARING/STEER/INTERNAL_SANDBOX/ACP_RUNTIME/KIRO_IDENTITY_STORE),provider_label = "opencode"(fix(session): read agent specs through the hardened, size-capped gate #5423-style hardenedmaterialized_agent_spec).feat(acp): integrate opencode mcp tools): Portable projection viamanaged_session_servers(onlyMANAGED_MCP_SERVER_NAMESwith whole@serverexposure, pooled-stub replacement), sanitized wire-keysanitize(server)+_+sanitize(tool)with fail-closedmcp_identity_trusted,rawInputaccepted.feat(dashboard): expose opencode acp backend+feat(acp): polish ...):AgentBackendTab.tsx4th row backed byselectable_backend_values(),en.json+ tests;GET /api/models—acp_backend == "opencode"→_advertised_opencode_models(ACPconfigOptions) + cold CLI fallbackopencode models(1M cap,^[^\s/]+/[^\s]+$, dedup,autosentinel), never spawnskiro-clifor OpenCode;GET /api/sessions/usageandGET /api/kiro-prerequisitebypass viaselected_backend_uses_kiro_identity()(ACP_BACKENDS_KIRO_IDENTITY_STORE).fix(dashboard): allow opencode to bypass kiro setup gate):KiroPrerequisiteGatequeriesacp_backend;opencoderenderschildrendirectly, otherwise showsOr use OpenCode → Continue with OpenCode(patchConfig('agent.acp_backend','opencode'),opencode.ai/docslink,opencode auth logincopy block, private-error redaction); backendapi_kiro_prerequisite_statusmirrors with_opencode_ready_snapshot+asyncio.to_thread(KiroCrewConfig.load).Tests
test_acp_backend_opencode.py(16 cases): spawn isopencode acp --cwd, protocol1,configOptionsmodel capture, model switch +AcpModelUnavailableon unadvertised id, load withoutmodes, MCP projection keeps only fully exposed managed servers, pooled-stub replacement,rawInputpermission recovery, collision/unknown/overlap fail-closed,_advertised_opencode_modelsisolation;test_acp_backend_kaspredicateexactly one holdsnow includesopencode.test_acp_runtime.py::test_every_session_request_builder_consults_pooled_serversupdated to assert backend-aware helper still reaches pooled path; 299 acp-runtime tests green.test_agent_backend_editable(9),test_harness_parity(27),KiroPrerequisiteGate.test.tsx(addsswitches to OpenCode+keeps visible on failure+bypass when opencode),AgentBackendTab.test.tsx(4th backendoffers all four+reflects persisted OpenCode+saves public OpenCode).test_api_models_entitlement/retrystill green;HARNESS_BASE_REF=origin/main python scripts/check_harness_parity.py✓,scripts/docs-lint.sh✓,black✓.Manual verification
opencode 1.18.23+SAKANA_API_KEYon macOS gateway withKIROCREW_HOME=$(mktemp -d):AcpClient(backend=opencode).ensure_ready()→ses_*,available_models10xsakana/*+opencode/*,stream_events("hi")→Hi/end_turn,ls -latool-call →tool_result→emptydir response.KiroPrerequisiteGatebypass: withacp_backend=""shows “Set up Kiro” + new “Or use OpenCode” card; clickingContinue with OpenCodepatches config and gate disappears; withacp_backend="opencode"gate never appears even whenkiro-cliunauthenticated,GET /api/kiro-prerequisitereturnsready: true(SHA-pinned probe not spawned).Screenshots / video
OpenCode session running as
opencode/big-pickle(live test on this branch,sakana/fuguprovider available):Full dashboard context
Same prompt via the Kiro path still uses
kiro-cli; footer now correctly shows the active harness’s model (opencode/big-pickle).Related Issues
Related to #6622 — Adds the third selectable harness that the future
agent_backendgovernance scope will bound. No newSCOPE_CATALOGrow yet; the allow-vs-exclusively / H1-H3 /installed_context()recursion discussion from #6622 remains open. The helperselected_backend_uses_kiro_identity()introduced here is the resolution the future scope can reuse atprovider_factory,PATCH /api/config/kirocrew, andGET /api/config/schema.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...) — N/A — this feature is intentionally split into 5 logical phases (feat(acp): add opencode transport,feat(acp): integrate opencode mcp tools,feat(dashboard): expose opencode acp backend,fix(dashboard): allow opencode to bypass kiro setup gate,feat(acp): polish opencode model fallback and unified kiro gate) per the task spec “Conventional Commit after each logical phase”. Squashing would hide the harness-parity boundaries.README.md,docs/system-specs/modules/acp-client.md,docs/system-specs/modules/providers.md,docs/architecture/mcp.md,harness-parity.md)Contribution License Agreement