fix(acp): normalize session/request_permission responses into the SDK decision contract - #3953
Conversation
|
Reviewing this PR now.
Red-team review in progress; will leave a terminal review after inspection. — |
|
Taking over follow-up fixes after red-team review on exact head Internal red-team (review-only) found:
Maintainer fix lane started:
Will push follow-up commits (or a maintainer successor PR if fork push is unavailable) and re-request review after focused verification. — |
|
Maintainer takeover follow-up has been pushed directly to Resolved the red-team findings:
Focused verification passed:
CI has restarted on the updated PR head. Leaving this open for re-review; not merging. — |
Yeachan-Heo
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES
Reviewed exact commit 53b01dad8bf873b7dd6fedc36c42c8fedced3ef3 against dev (d2646cb8a26b7e3752a196f7a2c57a6c207c74c1). I did not inspect or incorporate PR #3952.
Blockers:
-
The changed behavior is only tested through the helper, not the real permission-provider path. The production call is
host.reverse.request("permission", "request", ...)insrc/sdk/bus/index.ts; the lease and ACP adapter preserverequest, andcreateAcpReverseConnectionmaps it tosession/request_permission, so the implementation is statically reachable. However, the added test callscreateAcpReverseConnectiondirectly. It does not prove a registered SDK permission provider traverses host → reverse lease →AcpSdkAdapter→ the new normalizer → SDK validation → guarded-tool authorization. For this fail-closed boundary, add focused coupled coverage showing nested selected/cancelled and flat selected/cancelled responses work, malformed values are rejected, empty/unknown option IDs and cancellation keep the guarded tool at zero executions, and the canonical runtime method isrequest. The compatibility aliaspermission.requestshould also have a narrow mapping/normalization control. -
The changelog overstates both policy and compatibility.
PERMISSION_REQUIRED_TOOLSdoes not includewrite, andgetPermissionIntentallows ordinaryeditcalls without a permission request; only destructive edit delete/move intents are gated. The normalizer also reconstructs flat selected/cancelled outcomes rather than passing them through unchanged, dropping optional fields such askind. Please scope the note to ACP reverse permission-provider responses, say flat decisions remain accepted rather than unchanged, remove thewrite/blanket-edit claim, and retain the named-client assertion only with reproducible support.
Fail-closed code inspection is otherwise sound: malformed nested/null/array responses fall through to the SDK validator, and unknown string option IDs are rejected by PERMISSION_OPTIONS_BY_ID before execution. acp-client-bridge.ts already unwraps the typed direct requestPermission() response, but it is a separate path and is not evidence for this SDK reverse bridge.
Focused verification in the worktree:
acp-startup-options.test.ts: 20 passedacp-client-bridge.test.ts: 4 passedagent-session-acp-permission.test.ts: 29 passedsdk-acp-production-path.test.ts: 2 passedsdk-host-wiring.test.ts: 79 passed
The PR head moved to 7df4fa927b3c2578d7cf2556b70174ea7852cda0 while this exact-head review was running. This review is intentionally attached to the requested 53b01dad... commit; the newer head needs a fresh review before merge.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
b8a544e to
1f9bc2c
Compare
|
OWNER_CONFIRMATION_REQUIRED Red-team review was bounded to frozen head Blocking findings on that frozen head:
Checks: focused ACP tests passed ( The live PR head is now — |
|
OWNER_CONFIRMATION_REQUIRED Current-head adversarial review is bound to PR head No blocking ACP permission-shape defect reproduced on this head. The historical findings are resolved here: the changelog now describes the real gated-tool surface, flat responses are explicitly canonicalized rather than claimed to pass through unchanged, both reverse aliases are covered, and the live registered-provider test exercises nested/flat selection, cancellation, malformed/unknown fail-closed cases, session authority, abort propagation, and zero guarded execution on rejection. Focused current-head checks passed: 4 selected ACP boundary/provider tests, 45 assertions, 0 failures; Biome checked 2,536 files with no findings. Package typecheck remains unavailable because this checkout lacks generated Owner confirmation is required because the required current-head CI is red: — |
fcc1fc4 to
aaa8253
Compare
|
Maintainer rebase follow-up:
GitHub accepted both force-push synchronization events, but it has not created check suites for the new head yet. GitHub Status currently reports a major outage for Actions with workflow runs failing or delayed and webhook deliveries potentially delayed. No workflow rerun was requested. The PR remains unmerged; fresh exact-head CI is being monitored and remains required before merge. — |
Yeachan-Heo
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES
Code review of exact head 1f9bc2c856c5ed61a3d10d1996fe6b274b100868 found no new ACP permission-shape blocker. The historical ACP findings do not reproduce on this head: permission scope wording is corrected, both reverse aliases are covered, and the registered-provider path exercises nested/flat decisions, malformed and unknown fail-closed outcomes, cancellation, session authority, abort propagation, and guarded-tool non-execution.
Merge remains blocked because the required Affected path validation / plan check is red at Verify PR head contains exact base. The author must update/rebase this branch against the required base and current-head CI must pass before merge.
Focused evidence: 4 selected ACP boundary/provider tests passed with 45 assertions and 0 failures. Biome checked 2,536 files with no findings. The package typecheck could not complete because this checkout lacks generated src/internal-urls/docs-index.generated; that limitation is unrelated to this PR diff.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
aaa8253 to
abc3cb1
Compare
|
Current-base follow-up:
Exact-head verification was rerun:
GitHub still has not created a check suite for this exact head. The active GitHub Actions incident now explicitly states that webhook triggers are throttled and many push and pull-request events are not triggering workflow runs. No manual workflow rerun was requested. The PR remains unmerged; fresh current-head CI is still required. — |
abc3cb1 to
3a8f335
Compare
|
Another legitimate current-base rebase completed after
The new base only changes Smithery timeout coverage plus its changelog entry; the current exact-head review confirmed no ACP boundary, fixture, or changelog regression. GitHub still created no exact-head check suite while its Actions incident reports throttled push/PR webhooks. No manual workflow rerun was requested. The PR remains unmerged and CI-gated. — |
… decision contract
ACP clients answer `session/request_permission` with the spec-shaped
`RequestPermissionResponse` (`{ outcome: { outcome, optionId } }`), but the SDK
permission-provider contract is the flat decision (`{ outcome, optionId }`).
The ACP reverse bridge returned the raw nested payload, so every
permission-gated tool call (bash, edit, write) under a standards-compliant
client such as Paseo failed with "permission provider returned an invalid
response". Unwrap the outer outcome at the adapter boundary; the flat legacy
shape passes through unchanged.
Lore-id: 2b18a5e0
Constraint: permission decisions must stay fail-closed -- malformed responses still surface the existing invalid-response error
Rejected: accepting both shapes in the SDK bus | duplicates ACP wire knowledge in the generic SDK contract
Confidence: high
Scope-risk: narrow
Reversibility: revert-commit
Tested: nested selected/cancelled and flat shapes via createAcpReverseConnection unit test; live ACP probe against cli.ts acp with a Paseo-shaped nested response
Not-tested: other ACP client implementations beyond Paseo-shaped responses
The response-shape fix was only proven at the reverse helper boundary, leaving the registered provider path, alias, and typed bridge contracts exposed to regression. Add a coupled fail-closed fixture and correct the release note to match the actual guarded tools. Lore-id: pr3953-followup Constraint: keep ACP envelope parsing at createAcpReverseConnection Constraint: SDK permission providers remain flat and fail closed Tested: ACP startup, client bridge, agent-session permission, SDK host permission, coding-agent package check Not-tested: full repository test suite
3a8f335 to
5006314
Compare
|
Current-base rebase repeated after
The intervening base delta is only a test fixture for — |
|
Exact-head gates are now complete for
The PR remains open and unmerged. GitHub still records the historical review decision as CI: https://github.com/Yeachan-Heo/gajae-code/actions/runs/31129826468 — |
Yeachan-Heo
left a comment
There was a problem hiding this comment.
MERGE_READY
Terminal adversarial review is bound to exact PR head 5006314cb80c4d3530fb6c9d8b44df025a96a115 and current dev f1eb7b59419a2ce7c94cd7cb7e419e62dc6b65cf.
Verified:
- exact-head Dev CI
31129826468: success - exact-head Public site sync
31129826131: success - 18 successful checks, 6 intentional skips, no failures
- coupled registered provider path and malformed fail-closed matrix remain exact
- both permission aliases, session authority, cancellation, typed bridge outcomes, guarded zero-execution cases, and changelog wording remain covered
- current dev delta does not overlap the touched ACP contract; no rebase churn is warranted
- no linked closing issue is declared
No blocking findings remain.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
Problem
Permission-gated tool calls (
bash,edit,write) fail with "permission provider returned an invalid response" whenever GJC runs as an ACP agent under a standards-compliant ACP client. Reproduced with Paseo 0.3.0-beta.2 (the app's daemon launchescli.ts acpas the provider), affecting every bash/edit/write call once the client answers a permission prompt.Affected environment
gjc ... acp(any ACP client).session/new, prompts the agent to usebash, then answers the incomingsession/request_permissionwith the spec-shaped response. The tool call fails withpermission provider returned an invalid response; expected behavior is that the tool executes.Root cause
The ACP spec (
@agentclientprotocol/sdk, pinned1.3.0) defines the response tosession/request_permissionasRequestPermissionResponse:Paseo (and the official SDK's own fixtures) send exactly this nested shape. GJC's SDK permission provider (
sdk/bus/index.ts) validates the flat decision{ outcome: "selected", optionId }, soresponse.outcomeis an object instead of the expected string and the validation rejects it. The ACP reverse bridge (modes/acp/acp-agent.tscreateAcpReverseConnection) returned the raw wire payload without the unwrap that the siblingacp-client-bridge.tspath already performs.Change
Normalize
session/request_permissionresponses at the ACP adapter boundary (createAcpReverseConnection):{ outcome: { outcome, optionId } }→ SDK contract{ outcome, optionId }(selected / cancelled).Scope is one function plus a regression test and a changelog entry. No public API or behavior change beyond permission prompts resolving correctly.
Validation
test/acp-startup-options.test.ts): nested selected, nested cancelled, and flat passthrough — all pass.acp-startup-options,acp-client-bridge,agent-session-acp-permission,sdk-acp-production-path— all pass.bun --cwd=packages/coding-agent run check(biome + tsc) passes on the final head.echo daemon-permission-ok, answered the permission request with the nested spec shape. Transcript showstoolResult "daemon-permission-ok\n"and a completed turn;permission provider returned an invalid responseappears 0 times.Related work
devand supersedes the fork PR for upstream review.session/request_permissionchannel and would hit the same shape bug; this change is complementary (fixes the permission response contract) and not a duplicate.Checklist
dev(upstreamdevis an ancestor; branch is 1 commit ahead)