feat(acp): advertise clientCapabilities in the initialize handshake - #512
Conversation
Both ACP transports omitted `clientCapabilities` entirely, so the agent applied the all-false default -- indistinguishable from declining every optional capability. Adds one shared `ACP_CLIENT_CAPABILITIES` dict in `acp/types.py`, sent by `AcpClient._initialize_session` and `AcpRuntime`. `fs` and `terminal` stay false (we serve no handler for those). `elicitation` is declared as a forward-bet: kiro-cli 2.14.0 compiles the `elicitation/create` schema and gates it on this capability, but does not yet route an MCP server's elicitation out over ACP -- verified by probe, which returns `-32601 method not found`. A shared dict rather than two literals because the transports build their initialize params independently, which is how a capability added to one silently stays dark on the other. A source-level test asserts both. Split out of #464 to keep that PR's review surface on its own feature.
Design Review (Fable 5) — 🟡 CONCERNS · blast radius: mediumAdvisory design-level review of Design-Verdict: CONCERNS Advertising Watch
[DESIGN-REVIEWED] d126f87 |
Arbiter — ✅ no blocking findingsArbiter found no unresolved long-term items that require action before merging Second-order review for Review detailsBoth files read. Judging the three sub-threshold themes against the narrow bar. 1. Weak source-grep transport test (Claude MEDIUM, GPT MEDIUM). A test that asserts a string appears in source can go green while the wire key is misspelled or removed from the params dict. This is a test-coverage-quality gap — the shipped behavior is correct, and strengthening the test is fully reversible in a later change with no migration, rollout, or contract cost. Fails both tests; follow-up. 2. Advertising 3. Nothing else in the findings approaches the bar. Arbiter-Verdict: PASS No sub-threshold finding meets the long-term-impact bar. Suggested follow-ups (open as issues — non-blocking)
[ARBITER-REVIEWED] d126f87 False positive or not applicable? The PR author or a repository writer can comment: For a broader accepted-risk deferral, apply |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsReview pass 1Severity: MEDIUM -- src/kiro_crew/acp/types.py:76 -- Review pass 2No issues found - LGTM. Review pass 3No issues found - LGTM. False positive or not applicable? The PR author or a repository writer can comment: |
Opus 5 Review — ✅ no blocking findingsReviewed No blocking findings — 2 advisory items. MEDIUM — MEDIUM — Verdict recorded via the action's structured output for commit False positive or not applicable? The PR author or a repository writer can comment: |
Problem
KiroCrew's ACP
initializerequest omitsclientCapabilitiesentirely. Bothtransports (
AcpClient._initialize_sessionandAcpRuntime) send onlyprotocolVersion+ client name/version, so the agent applies the all-falsedefault and treats us as a client that supports no optional capability at all.
Why it matters
Capability negotiation is the mechanism by which the agent decides whether it
may issue a richer client-bound request. Sending nothing is indistinguishable
from declining everything, so any capability that lands upstream stays dark for
us until someone notices and edits two separate call sites.
The concrete instance is ACP elicitation — a general-purpose form prompt
(
elicitation/create,mode: form|url, arequestedSchemasupportingenum/oneOflabelled single-select andtype: arraymulti-select). kiro-cli 2.14.0already compiles that schema and gates it on
clientCapabilities.elicitation.Fix (symptom → root cause → change)
Symptom: the agent never offers elicitation, and there is no way to tell
whether that is an upstream gap or our own omission.
Root cause, established by probe rather than by reading code. I drove
kiro-cli over ACP with a stub MCP server whose tool issues an
elicitation/createback to the host, declaredclientCapabilities.elicitation, and had the model call it. The agent replied:So there are two independent gaps: the MCP→ACP forwarding path is
unimplemented in 2.14.0 (upstream), and we advertise nothing (ours). This PR
fixes only the second — the part we own.
Change: one shared
ACP_CLIENT_CAPABILITIESdict inacp/types.py, sent byboth transports.
fs.readTextFile/fs.writeTextFilefalsefs/*handler; advertising them would invite requests that hit_reject_unknown_server_request.terminalfalseelicitation{form: {}, url: {}}Shared dict rather than two literals because the two transports build their
initializeparams independently — that is precisely how a capability added toone silently stays dark on the other.
Consequence stated plainly, not buried: once the bridge lands, inbound
elicitation/createwill be rejected by_reject_unknown_server_requestuntil ahandler is wired. That is the same failure mode as today, but then attributable
to us rather than upstream. Declaring the capability is what makes the work
visible instead of silent.
Tests
test/test_acp_client_capabilities.py(3 tests):test_elicitation_is_declared— the exact shape kiro-cli gates on. Revertingthe dict fails it.
test_fs_and_terminal_stay_false— guards against advertising a handler we donot serve, which would turn silence into inbound errors.
test_both_acp_transports_send_capabilities— asserts both call sites, soadding a capability to one transport only cannot pass. Asserted on source
because neither params dict is reachable without spawning a real agent
subprocess;
encoding="utf-8"is explicit becauseread_text()defaults tothe locale codec (cp1252 on the Windows shards) and these files contain em
dashes.
The existing ACP suite (900 tests) passes unchanged — no test asserted an exact
initializeparams shape, so adding the key breaks nothing.Manual verification
Done, and it is what produced the root cause above: a stub MCP server plus an
ACP driver script confirmed the handshake accepts
clientCapabilities.elicitationcleanly, and that an MCP server'selicitation/createis still answered-32601 method not foundby kiro-cli2.14.0. Worth re-running when kiro-cli updates, to detect the day the bridge
lands.
Screenshots
N/A — no user-visible UI change. This is a wire-protocol field in the
initializerequest.Notes
Split out of #464 (
ask_question), where this shipped as an unrelatedforward-bet. Separating it keeps that PR's review surface to the feature itself
and lets this stand on its own merits. There is no functional coupling between
the two.