Skip to content

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

Closed
snowykr wants to merge 13 commits into
devfrom
feat/acp-permission-shape
Closed

fix(acp): normalize session/request_permission responses into the SDK decision contract#23
snowykr wants to merge 13 commits into
devfrom
feat/acp-permission-shape

Conversation

@snowykr

@snowykr snowykr commented Aug 6, 2026

Copy link
Copy Markdown
Owner

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

  • Related existing PR: feat(acp): bridge workflow-gate asks to the ACP permission channel #22 "feat(acp): bridge workflow-gate asks to the ACP permission channel" (open). That PR routes workflow-gate asks through the same session/request_permission channel and would hit the same shape bug; this change is complementary (fixes the permission response contract) and not a duplicate.
  • No existing issue/PR addresses the "permission provider returned an invalid response" shape mismatch (searched permission provider, request_permission).

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 dev; CI runs on the final head commit

Dev CI run 31102063678 (dev @473eab944) failed because coding-agent shard
7 timed out in "Smithery env trust boundary > uses the built-in endpoints
and no key by default" after 5004ms (Bun's default per-test timeout).

Root cause: each test spawns a child Bun process (smithery-env-probe.ts)
that eagerly parses 9 env/dotfiles (5 shell rc files + 4 .env paths) at
module load via $credentialEnv → env.ts:39-50. Locally each test runs
~800ms, but under CI's 7-shard parallel load the child-process spawn +
TS compilation + synchronous file I/O can exceed the 5s default.

This is a CI flake (resource contention), not a deterministic regression
— reproduced 5 pass / 0 fail consistently at dev 473eab9 across 3 local
runs (~4s total for all 5 tests). The fix adds a 30s per-test timeout
matching the established codebase pattern (agent-session-resilient-retry
.test.ts:589,1283 use 30_000 for child-process tests). No production code
or test logic changed — only the timeout budget.

Lore-id: 7d2e8c1f
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: 3x reproduction at dev 473eab9 (5 pass / 0 fail each, ~4s total);
post-fix verification (5 pass / 0 fail, ~6s total)
Not-tested: CI shard-7 parallel contention (requires CI runner)
Supersedes: dev CI run 31102063678 failure evidence
The issue fixture treated one configured model's limits as provider-wide defaults for unrelated discovered ids. Cover same-id YAML precedence, exact-id model overrides, and unknown fallback independently.

Lore-id: issue-3954-yaml-discovery-contract
Constraint: model configuration remains identity-scoped
Rejected: provider-wide cross-model limit inheritance | not supported by schema or runtime merge semantics
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: focused issue regression and three nearby model-registry discovery checks
@snowykr
snowykr force-pushed the feat/acp-permission-shape branch from 7df4fa9 to b8a544e Compare August 6, 2026 15:57
…eachan-Heo#3948)

Red-team CLEAR at f9c0fe0. All CI checks success/skipped. Test-only timeout stabilization for Dev CI flake 31102063678.
@snowykr
snowykr force-pushed the feat/acp-permission-shape branch from b8a544e to 1f9bc2c Compare August 6, 2026 17:23
…Heo#3961)

Red-team MERGE_READY at 460285d. Test-only contract correction for post-Yeachan-Heo#3927 discovery limits (same-id YAML, modelOverrides, UNK fallbacks). No runtime change. Local public surfaces failed pre-checkout on GitHub Actions 5xx only.
@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
gaebal-gajae and others added 3 commits August 6, 2026 20:11
…ntract

Dev CI at 4f6e860 fails provider-onboarding-wizard-redteam on a stale
assertion: it expected "At least one model id is required" while
addApiCompatibleProvider throws the post-Yeachan-Heo#3927 public message
"At least one model id or model discovery is required."

Update the empty-models red-team expectation to the exact intended
substring of that contract. No runtime change.

Lore-id: 9c4e1a02
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: bun test packages/coding-agent/test/provider-onboarding-wizard-redteam.test.ts (7 pass / 0 fail)
Not-tested: full Dev CI shard matrix
…ntract (Yeachan-Heo#3966)

Red-team MERGE_READY. Test-only: assert post-Yeachan-Heo#3927 public error 'At least one model id or model discovery is required'. Dev CI 31127515306 success. No runtime change.
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/acp-permission-shape branch from aaa8253 to abc3cb1 Compare August 6, 2026 21:42
gaebal-gajae and others added 2 commits August 6, 2026 21:45
Dev CI run 31128319216 (Yeachan-Heo#3855 exact head) failed only when
"uses the built-in endpoints and no key by default" hit 30004ms —
exactly the 30s per-test cap from Yeachan-Heo#3948. Assertions and provider
behavior were green; this is CI contention at the previous budget.

Raise each child-process it() timeout from 30_000 to 60_000, matching
existing 60s child-process patterns (e.g. acp-session-delete-wire).
No assertion or production change.

Lore-id: a3f8c201
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: sequential 3x (5 pass/~4s); 6-way parallel contention (6x 5 pass/~4s)
Not-tested: full 8-shard Dev CI load shape
…chan-Heo#3967)

Red-team MERGE_READY at accba29. Test-only 30s→60s timeout for smithery-env-trust child-process CI contention flake (run 31128319216 @30004ms). Unblocks Yeachan-Heo#3855.
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/acp-permission-shape branch from abc3cb1 to 3a8f335 Compare August 6, 2026 22:12
gaebal-gajae and others added 4 commits August 6, 2026 22:13
The builtin notify handler always returns {prompt: command.text} for on/off so
it cannot shadow the extension-owned per-session control — whether or not a
lazy/native command is installed. A stale fixture on the startup-cost PR
expected {consumed:true} + "unavailable" for the no-command path and failed
exact-head shard-5.

Add the contract on dev as a minimal baseline so the performance PR does not
absorb unrelated expectation rewrites into its diff.

Lore-id: ntfy-pass-1
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: bun test packages/coding-agent/src/slash-commands/notify-command.test.ts
Not-tested: full coding-agent shard matrix
…nly-pass-through

test(coding-agent): lock /notify on|off always-pass-through contract
… 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5006314cb8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,56 @@
import { describe, expect, test } from "bun:test";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Split unrelated test maintenance from the ACP fix

This ACP response-normalization commit also adds /notify routing coverage and changes unrelated provider-discovery, onboarding-wizard, and Smithery-timeout tests. Bundling these independent changes makes the ACP fix harder to review, revert, or cherry-pick safely; move them into separate logical commits as required by the repository contract.

AGENTS.md reference: AGENTS.md:L152-L155

Useful? React with 👍 / 👎.

@snowykr

snowykr commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Closing: identical change merged upstream via Yeachan-Heo#3953.

@snowykr snowykr closed this Aug 7, 2026
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