Skip to content

fix(acp): normalize session/request_permission responses into the SDK decision contract - #3953

Merged
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
snowykr:feat/acp-permission-shape
Aug 7, 2026
Merged

fix(acp): normalize session/request_permission responses into the SDK decision contract#3953
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
snowykr:feat/acp-permission-shape

Conversation

@snowykr

@snowykr snowykr commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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 launches cli.ts acp as the provider), affecting every bash/edit/write call once the client answers a permission prompt.

Affected environment

  • GJC ACP agent mode: gjc ... acp (any ACP client).
  • Repro: client sends session/new, prompts the agent to use bash, then answers the incoming session/request_permission with the spec-shaped response. The tool call fails with permission provider returned an invalid response; expected behavior is that the tool executes.

Root cause

The ACP spec (@agentclientprotocol/sdk, pinned 1.3.0) defines the response to session/request_permission as RequestPermissionResponse:

{ outcome: { outcome: "selected", optionId } }   // nested decision object
{ outcome: { outcome: "cancelled" } }

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 }, so response.outcome is an object instead of the expected string and the validation rejects it. The ACP reverse bridge (modes/acp/acp-agent.ts createAcpReverseConnection) returned the raw wire payload without the unwrap that the sibling acp-client-bridge.ts path already performs.

Change

Normalize session/request_permission responses at the ACP adapter boundary (createAcpReverseConnection):

  • Spec-shaped { outcome: { outcome, optionId } } → SDK contract { outcome, optionId } (selected / cancelled).
  • The flat legacy shape passes through unchanged.
  • Malformed responses stay fail-closed (existing "invalid response" error preserved).

Scope is one function plus a regression test and a changelog entry. No public API or behavior change beyond permission prompts resolving correctly.

Validation

  • Unit tests (test/acp-startup-options.test.ts): nested selected, nested cancelled, and flat passthrough — all pass.
  • Related ACP suites (55 tests): acp-startup-options, acp-client-bridge, agent-session-acp-permission, sdk-acp-production-path — all pass.
  • Type/lint: bun --cwd=packages/coding-agent run check (biome + tsc) passes on the final head.
  • End-to-end (real daemon): Paseo app 0.3.0-beta.2 → daemon → GJC provider; prompt to run echo daemon-permission-ok, answered the permission request with the nested spec shape. Transcript shows toolResult "daemon-permission-ok\n" and a completed turn; permission provider returned an invalid response appears 0 times.
  • Before/after wire probe: identical nested response — before the fix the turn failed with the invalid-response error; after the fix bash executes.

Related work

Checklist

  • Problem and motivation clearly described
  • Related issues/PRs linked and non-duplicate rationale
  • Scope focused (one function + test + changelog)
  • Failure handling preserved (fail-closed on malformed)
  • Single coherent commit, conventional message with trailers
  • Tests added and executed (55 pass)
  • End-to-end validation through the real ACP client/daemon environment
  • Branch rebased onto upstream dev (upstream dev is an ancestor; branch is 1 commit ahead)
  • CI runs on the final head commit

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Reviewing this PR now.

  • Base: dev
  • Head: 53b01dad8bf873b7dd6fedc36c42c8fedced3ef3
  • Observed CI: green on exact head; MERGEABLE
  • Scope observed: ACP reverse-bridge permission response normalization + unit coverage + changelog
  • Review lane: gajae-code-pr-3953-acp-permission-shape-review

Red-team review in progress; will leave a terminal review after inspection.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Taking over follow-up fixes after red-team review on exact head 53b01dad8bf873b7dd6fedc36c42c8fedced3ef3.

Internal red-team (review-only) found:

  • HIGH: missing coupled permission-provider path fixture across host/adapter/reverse-connection (helper-only evidence insufficient)
  • MEDIUM: permission.request alias matrix/test missing
  • MEDIUM: acp-client-bridge typed selected/cancelled result contract untested
  • MEDIUM: changelog overclaims gated tools (write) and literal flat “unchanged” pass-through

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.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Maintainer takeover follow-up has been pushed directly to snowykr:feat/acp-permission-shape as commit 7df4fa927.

Resolved the red-team findings:

  • added a coupled registered permission-provider fixture covering SDK host → ACP adapter → reverse connection → guarded AgentSession tool, including canonical method mapping, authoritative session ID, cancellation propagation, nested/flat selected and cancelled outcomes, malformed fail-closed cases, and zero guarded executions for reject/cancel/unknown decisions;
  • parameterized reverse-boundary coverage over both request and permission.request;
  • added typed ACP client-bridge controls for selected outcomes with matched kind and cancelled outcomes;
  • corrected the Unreleased changelog to reflect the actual gated tools and canonical decision reconstruction.

Focused verification passed:

  • bun test packages/coding-agent/test/acp-startup-options.test.ts
  • bun test packages/coding-agent/test/acp-client-bridge.test.ts
  • bun test packages/coding-agent/test/agent-session-acp-permission.test.ts
  • bun test packages/coding-agent/test/sdk-host-wiring.test.ts --test-name-pattern "permission"
  • bun --cwd=packages/coding-agent run check

CI has restarted on the updated PR head. Leaving this open for re-review; not merging.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

REQUEST_CHANGES

Reviewed exact commit 53b01dad8bf873b7dd6fedc36c42c8fedced3ef3 against dev (d2646cb8a26b7e3752a196f7a2c57a6c207c74c1). I did not inspect or incorporate PR #3952.

Blockers:

  1. The changed behavior is only tested through the helper, not the real permission-provider path. The production call is host.reverse.request("permission", "request", ...) in src/sdk/bus/index.ts; the lease and ACP adapter preserve request, and createAcpReverseConnection maps it to session/request_permission, so the implementation is statically reachable. However, the added test calls createAcpReverseConnection directly. 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 is request. The compatibility alias permission.request should also have a narrow mapping/normalization control.

  2. The changelog overstates both policy and compatibility. PERMISSION_REQUIRED_TOOLS does not include write, and getPermissionIntent allows ordinary edit calls 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 as kind. Please scope the note to ACP reverse permission-provider responses, say flat decisions remain accepted rather than unchanged, remove the write/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 passed
  • acp-client-bridge.test.ts: 4 passed
  • agent-session-acp-permission.test.ts: 29 passed
  • sdk-acp-production-path.test.ts: 2 passed
  • sdk-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) 🦞]

@snowykr
snowykr force-pushed the feat/acp-permission-shape branch 2 times, most recently from b8a544e to 1f9bc2c Compare August 6, 2026 17:23
@Yeachan-Heo

Copy link
Copy Markdown
Owner

OWNER_CONFIRMATION_REQUIRED

Red-team review was bounded to frozen head 53b01dad8bf873b7dd6fedc36c42c8fedced3ef3 against merge-base d2646cb8a26b7e3752a196f7a2c57a6c207c74c1 (dev now resolves to 1851fd38ba91371d5c87dc1d5ae8c1bca649184d). The frozen binary diff hash matches sha256:a510d443e70bfbbb260fc706adb665996a7921ee91d2d0dac986d743c5e0a82e.

Blocking findings on that frozen head:

  1. Flat-response compatibility regresses. createAcpReverseConnection() reconstructs every flat selected/cancelled permission result instead of passing it through unchanged. A previously accepted flat result such as { outcome: "selected", optionId: "allow_once", kind: "allow_once" } loses kind (and any extension metadata). This contradicts the code comment/changelog and changes the legacy contract. Normalize only the nested ACP envelope, or explicitly preserve all flat fields. Add a regression assertion with kind/metadata; the current flat test cannot detect this loss.
  2. The changelog overstates the permission surface. It says bash, edit, write are permission-gated. write is not in PERMISSION_REQUIRED_TOOLS, and ordinary edit operations bypass the prompt unless they encode delete/move intent. Correct the public contract wording.
  3. Focused security-boundary coverage is incomplete. The new helper test covers nested selected/cancelled and a minimal flat selected result, but not the permission.request alias, malformed nested outcomes remaining fail-closed, or preservation of the pre-existing flat response contract.

Checks: focused ACP tests passed (53 pass, 0 fail across acp-startup-options, acp-client-bridge, and agent-session-acp-permission). Package Biome passed; package typecheck was blocked by the checkout missing generated src/internal-urls/docs-index.generated, unrelated to this diff. Worktree remained clean.

The live PR head is now 1f9bc2c856c5ed61a3d10d1996fe6b274b100868, not the mandated reviewed head. This disposition therefore records the frozen-head findings but requires owner confirmation/re-review before treating the current PR as reviewed.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner

OWNER_CONFIRMATION_REQUIRED

Current-head adversarial review is bound to PR head 1f9bc2c856c5ed61a3d10d1996fe6b274b100868. GitHub reports base dev at 1851fd38ba91371d5c87dc1d5ae8c1bca649184d; the freshly fetched upstream-yeachan/dev is 4f6e860d7e45463487d1afa4f8fbbebeccfc16d6, and the merge base is d2646cb8a26b7e3752a196f7a2c57a6c207c74c1. The five-file PR diff is 204 insertions/3 deletions with SHA-256 36c5c687adbcbbd2db68c91cdb3d80118dd29fae3e04630d997e12bc183badd8.

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 src/internal-urls/docs-index.generated, unrelated to this diff.

Owner confirmation is required because the required current-head CI is red: Affected path validation / plan fails at Verify PR head contains exact base, with dependent affected-path jobs failing closed. The head is based on d2646cb8... and does not contain GitHub's reported base 1851fd38... (the live dev ref has advanced further). GitHub reports the content merge as mergeable, and a synthetic three-way merge against freshly fetched dev is conflict-free, but repository policy still blocks merge until the branch contains the required base and current-head CI is green. No merge or source mutation was performed.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/acp-permission-shape branch 2 times, most recently from fcc1fc4 to aaa8253 Compare August 6, 2026 19:35
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Maintainer rebase follow-up:

  • rebased the external snowykr:feat/acp-permission-shape branch onto current origin/dev (4f6e860d7e45463487d1afa4f8fbbebeccfc16d6);
  • force-pushed the existing PR head to aaa8253f37316ac00861539a52869a6f0d776baf using lease protection;
  • reran the focused ACP permission suites at the rebased head: 20/5/29/4 tests passed, and bun --cwd=packages/coding-agent run check passed;
  • reran exact-head red-team review against source hash 77c978b70aa04bef7c6f7bc5e199344c6e53f3e04fee859a0342f69d26b7d0ed: Architect CLEAR / APPROVE, Critic OKAY, no blockers.

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.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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) 🦞]

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/acp-permission-shape branch from aaa8253 to abc3cb1 Compare August 6, 2026 21:42
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Current-base follow-up:

origin/dev advanced during the GitHub Actions outage, so I performed a new legitimate rebase onto 38321884ddadadda48fd8fad468d2f20eeb99751 and force-pushed the existing external branch with lease protection. The PR head is now abc3cb1758f1a92a43e354a4addbf15e7da7777f.

Exact-head verification was rerun:

  • ACP focused suites: 20 / 5 / 29 / 4 passing
  • bun --cwd=packages/coding-agent run check: passing
  • Architect: CLEAR / APPROVE
  • Critic red-team: OKAY
  • source hash: 77c978b70aa04bef7c6f7bc5e199344c6e53f3e04fee859a0342f69d26b7d0ed

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.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/acp-permission-shape branch from abc3cb1 to 3a8f335 Compare August 6, 2026 22:12
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Another legitimate current-base rebase completed after origin/dev advanced.

  • base: a79df2ee0583586816b900ec0e9a5ba21340d609
  • PR head: 3a8f33579d4e8b54c2544297902d2eab1a002cd7
  • push: force-with-lease to the existing snowykr:feat/acp-permission-shape head
  • focused tests: 20 / 5 / 29 / 4 passing
  • coding-agent package check: passing
  • exact-head Architect: CLEAR / APPROVE
  • exact-head Critic: OKAY
  • current source hash: efb647ba1351921c59963d61828bee806dfda6819d52c87b2681fa9a394bc994

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.


[repo owner's gaebal-gajae (clawdbot) 🦞]

snowykr and others added 2 commits August 6, 2026 22:44
… 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
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/acp-permission-shape branch from 3a8f335 to 5006314 Compare August 6, 2026 22:45
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Current-base rebase repeated after origin/dev advanced again.

  • base: fd4efb4de9c7fd3ce7e55f5c8a4cb8c1a90d3742
  • PR head: 5006314cb80c4d3530fb6c9d8b44df025a96a115
  • focused tests: 20 / 5 / 29 / 4 passing
  • coding-agent package check: passing
  • exact-head Architect: CLEAR / APPROVE
  • exact-head Critic: OKAY
  • source hash: efb647ba1351921c59963d61828bee806dfda6819d52c87b2681fa9a394bc994

The intervening base delta is only a test fixture for /notify on|off; exact-head review confirmed it cannot affect or mask the ACP permission gate. GitHub still created no check suite while Actions webhook triggers remain throttled. No manual workflow rerun was requested. The PR remains unmerged and CI-gated.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Exact-head gates are now complete for 5006314cb80c4d3530fb6c9d8b44df025a96a115 on base fd4efb4de9c7fd3ce7e55f5c8a4cb8c1a90d3742.

  • Dev CI run 31129826468: success
  • Public site sync run 31129826131: success
  • 24 reported checks, no failure/cancel/timeout/action-required conclusions
  • exact-head Architect: CLEAR / APPROVE
  • exact-head Critic: OKAY
  • focused local tests and coding-agent package check: passing

The PR remains open and unmerged. GitHub still records the historical review decision as CHANGES_REQUESTED; this is ready for maintainer/reviewer disposition against the current exact head.

CI: https://github.com/Yeachan-Heo/gajae-code/actions/runs/31129826468


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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) 🦞]

@Yeachan-Heo
Yeachan-Heo merged commit 627e87d into Yeachan-Heo:dev Aug 7, 2026
24 checks passed
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.

2 participants