authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-28 18:26:13-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
logafad39d125a319e5fa22e15bbc8615b3a6267aa4
treef3c649bd5f2415bb47e7208b2e77941611341b88
parentf97916f371ab2480a47f62efa17d42c64a253501

Revise Cursor provider plan for ACP JSON-RPC integration

- Switch plan from `agent -p` stream-json to `agent acp` JSON-RPC transport - Document ACP session, permission, event-mapping, and interrupt/recovery behavior - Update implementation phases, tests, and delivery checklist for ACP-first design

1 files changed, 227 insertions(+), 210 deletions(-)

.plans/18-cursor-agent-provider.md+227-210
......@@ -1,248 +1,260 @@
1# Plan: Cursor CLI (`agent`) Provider Integration
1# Plan: Cursor ACP (`agent acp`) Provider Integration
22
33## Goal
44
5Add Cursor as a first-class provider in T3 Code using Cursor CLI stream JSON mode (`agent -p --output-format stream-json`), with robust session lifecycle handling and canonical `ProviderRuntimeEvent` projection.
5Add Cursor as a first-class provider in T3 Code using ACP (`agent acp`) over JSON-RPC 2.0 stdio, with robust session lifecycle handling and canonical `ProviderRuntimeEvent` projection.
66
77---
88
9## 1) Exploration Findings (from live CLI runs)
9## 1) Exploration Findings (from live ACP probes)
1010
11### 1.1 Core invocation shape
11### 1.1 Core invocation and transport
1212
13131. Binary is `agent` on PATH (`2026.02.27-e7d2ef6` observed).
142. Non-interactive streaming mode:
15 - `agent -p --trust --output-format stream-json --stream-partial-output ...`
163. Session continuity:
17 - `agent create-chat` returns a chat UUID.
18 - `--resume <chatId>` and `--continue` preserve history.
19 - Stream `session_id` equals chat UUID.
20
21### 1.2 Stream JSON event families observed
22
231. `system/init`
242. `user`
253. `thinking/delta`, `thinking/completed`
264. `assistant` (partial and final)
275. `tool_call/started`, `tool_call/completed`
286. `result/success` (`is_error: false` in all successful process exits)
297. Previously observed under load/rate-limit:
30 - `connection/reconnecting`, `connection/reconnected`
31 - `retry/starting`, `retry/resuming`
32 - followed by non-JSON terminal line `v: [resource_exhausted] Error` and process exit code `1`
33
34### 1.3 Tool call variants observed
35
361. `shellToolCall`
372. `readToolCall`
383. `editToolCall`
394. `grepToolCall`
405. `globToolCall`
41
42Tool completion outcomes observed:
43
441. `result.success`
452. `result.failure` (non-zero exit, stderr)
463. `result.rejected` (permission denied by Cursor approval layer)
47
48### 1.4 Critical protocol quirks
49
501. `tool_call.call_id` contains embedded newline characters (must sanitize for IDs).
512. Startup failures are plain text, not JSON:
52 - invalid model -> plain error + exit `1`
53 - invalid API key -> plain warning + exit `1`
543. `result.subtype` can be `success` even when tool calls failed/rejected inside the turn.
554. Running multiple `agent` commands concurrently can race on `~/.cursor/cli-config.json` writes (`ENOENT rename ...cli-config.json.tmp` observed).
56
57Fixtures recorded in repo:
58
591. `.tmp/cursor-observations/*.ndjson`
602. `.tmp/cursor-observations/startup-invalid-*.txt`
142. ACP server command is `agent acp`.
153. Transport is newline-delimited JSON-RPC 2.0 over stdio.
164. Messages:
17 - client -> server: requests and responses to server-initiated requests
18 - server -> client: responses, notifications (`session/update`), and server requests (`session/request_permission`)
19
20### 1.2 Handshake and session calls observed
21
221. `initialize` returns:
23 - `protocolVersion`
24 - `agentCapabilities` (`loadSession`, `mcpCapabilities`, `promptCapabilities`)
25 - `authMethods` (includes `cursor_login`)
262. `authenticate { methodId: "cursor_login" }` returns `{}` when logged in.
273. `session/new` returns:
28 - `sessionId`
29 - `modes` (`agent`, `plan`, `ask`)
304. `session/load` works and requires `sessionId`, `cwd`, `mcpServers`.
315. `session/prompt` returns terminal response `{ stopReason: "end_turn" | "cancelled" }`.
32
33Important sequence note:
341. ACP currently allows `session/new` even without explicit `initialize`/`authenticate` when local auth already exists.
352. For adapter consistency and forward compatibility, we should still send `initialize` and `authenticate` during startup.
36
37### 1.3 `session/update` event families observed
38
39Observed `params.update.sessionUpdate` values:
40
411. `available_commands_update`
422. `agent_thought_chunk`
433. `agent_message_chunk`
444. `tool_call`
455. `tool_call_update`
46
47Observed payload behavior:
48
491. `agent_*_chunk` provides `content: { type: "text", text: string }`.
502. `tool_call` may be emitted multiple times for same `toolCallId`:
51 - initial generic form (`title: "Terminal"`, `rawInput: {}`)
52 - enriched form (`title: "\`pwd\`"`, `rawInput: { command: "pwd" }`)
533. `tool_call_update` statuses observed:
54 - `in_progress`
55 - `completed`
564. `tool_call_update` on completion may include `rawOutput`:
57 - terminal: `{ exitCode, stdout, stderr }`
58 - search/find: `{ totalFiles, truncated }`
59
60### 1.4 Permission flow observed
61
621. ACP server sends `session/request_permission` (JSON-RPC request with `id`).
632. Request shape includes:
64 - `params.sessionId`
65 - `params.toolCall`
66 - `params.options` (`allow-once`, `allow-always`, `reject-once`)
673. Client must respond on same `id` with:
68 - `{ outcome: { outcome: "selected", optionId: "<one-option-id>" } }`
694. Reject path still results in tool lifecycle completion events (`tool_call_update status: completed`), typically without `rawOutput`.
70
71### 1.5 Error and capability quirks
72
731. `session/cancel` currently returns:
74 - JSON-RPC error `-32601` Method not found
752. Error shape examples:
76 - unknown auth method: `-32602`
77 - `session/load` missing/invalid params: `-32602`
78 - `session/prompt` unknown session: `-32603` with details
793. Parallel prompts on same session are effectively single-flight:
80 - second prompt can cause first to complete with `stopReason: "cancelled"`.
814. `session/new` accepts a `model` field (no explicit echo in response).
82
83Probe artifacts:
841. `.tmp/acp-probe/*/transcript.ndjson`
852. `.tmp/acp-probe/*/summary.json`
863. `scripts/cursor-acp-probe.mjs`
6187
6288---
6389
6490## 2) Integration Constraints for T3
6591
661. T3 requires adapter contract support for:
921. T3 adapter contract still requires:
6793 - `startSession`, `sendTurn`, `interruptTurn`, `respondToRequest`, `readThread`, `rollbackThread`, `stopSession`, `listSessions`, `hasSession`, `stopAll`, `streamEvents`.
682. Orchestration depends on canonical runtime events (`ProviderRuntimeEvent`) only.
693. Current `ProviderCommandReactor` ignores `thread.turn-start-requested.payload.provider` and prefers existing provider/session; this must be fixed for reliable provider selection (including Cursor).
704. Cursor CLI currently has no external approval callback API; approvals are internal to CLI behavior.
942. Orchestration consumes canonical `ProviderRuntimeEvent` only.
953. `ProviderCommandReactor` provider precedence fix remains required (respect explicit provider on turn start).
964. ACP now supports external permission decisions, so Cursor can participate in T3 approval UX via adapter-managed request/response plumbing.
7197
7298---
7399
74100## 3) Proposed Architecture
75101
76## 3.1 New server components
102### 3.1 New server components
77103
781. `apps/server/src/provider/Services/CursorAdapter.ts` (service contract/tag only for DI parity)
792. `apps/server/src/provider/Layers/CursorAdapter.ts` (single implementation unit; owns Cursor process lifecycle + stream parsing + runtime projection)
803. No separate `cursorCliManager.ts` abstraction in v1.
1041. `apps/server/src/provider/Services/CursorAdapter.ts` (service contract/tag + ACP event schemas).
1052. `apps/server/src/provider/Layers/CursorAdapter.ts` (single implementation unit; owns ACP process lifecycle, JSON-RPC routing, runtime projection).
1063. No manager indirection; keep logic in layer implementation.
81107
82108### 3.2 Session model
83109
841. T3 `ProviderSessionId` = synthetic UUID managed by adapter (stable for T3 APIs).
852. Cursor chat UUID stored as:
86 - `ProviderSession.resumeCursor = { chatId: string }`
87 - `ProviderSession.threadId = ProviderThreadId(chatId)` for adapter contract compatibility.
883. Adapter keeps runtime map:
89 - `sessionId -> { chatId, cwd, model, activeChildProcess?, activeTurnId?, turnLog }`
1101. One long-lived ACP child process per T3 Cursor provider session.
1112. Track:
112 - `providerSessionId` (T3 synthetic ID)
113 - `acpSessionId` (from `session/new` or restored via `session/load`)
114 - `cwd`, `model`, in-flight turn state
115 - pending permission requests by JSON-RPC request id
1163. Resume support:
117 - persist `acpSessionId` in provider resume metadata and call `session/load` on reattach.
90118
91119### 3.3 Command strategy
92120
931211. `startSession`:
94 - call `agent create-chat` unless `resumeCursor.chatId` exists.
95 - validate provider is `cursor` when provided.
122 - spawn `agent acp`
123 - `initialize`
124 - `authenticate(cursor_login)` (best-effort, typed failure handling)
125 - `session/new` or `session/load`
961262. `sendTurn`:
97 - spawn `agent -p --trust --output-format stream-json --stream-partial-output --resume <chatId>`.
98 - add `--model` when provided.
99 - map runtime mode:
100 - `approvalPolicy = never` -> include `--force`
101 - otherwise omit `--force` (commands may be rejected by CLI).
102 - use `--workspace <cwd>` when available.
127 - send `session/prompt { sessionId, prompt: [...] }`
128 - consume streaming `session/update` notifications until terminal prompt response
1031293. `interruptTurn`:
104 - terminate active child process for session (`SIGINT` then hard kill fallback).
130 - no native `session/cancel` today; implement fallback:
131 - terminate ACP process + restart + `session/load` for subsequent turns
132 - mark in-flight turn as interrupted/failed in canonical events
1334. `respondToRequest`:
134 - map T3 approval decision -> ACP `optionId`
135 - reply to exact JSON-RPC request id from `session/request_permission`
105136
106137### 3.4 Effect-first implementation style (required)
107138
1081. Keep adapter logic inside `CursorAdapterLive` layer constructor (no manager indirection).
1092. Use Effect process primitives first:
110 - `ChildProcessSpawner` + `ChildProcess.make` from `effect/unstable/process` for `agent` execution.
1113. Use Effect concurrency/state primitives:
112 - `Queue` for adapter event queue, `Stream.fromQueue` for `streamEvents`
113 - `Ref` / `Ref.Synchronized` for session maps and active-turn process handles
114 - `Effect.scoped` + `Effect.forkScoped` for worker fibers and cleanup
1154. Use Effect stream parsing path for stdout/stderr:
116 - decode bytes -> line buffer -> JSON parse -> typed projection
117 - keep non-JSON lines on a fallback branch that emits `runtime.error`
1185. Keep errors in typed adapter error algebra (`ProviderAdapter*Error`) via `Effect.mapError` boundaries, not ad-hoc exceptions.
1391. Keep logic inside `CursorAdapterLive`.
1402. Use Effect primitives:
141 - `Queue` + `Stream.fromQueue` for event fan-out
142 - `Ref` / `Ref.Synchronized` for session/process/request state
143 - scoped fibers for stdout/stderr read loops
1443. Typed JSON decode at boundary:
145 - request/response envelopes
146 - `session/update` union schema
147 - permission-request schema
1484. Keep adapter errors in typed error algebra with explicit mapping at process/protocol boundaries.
119149
120150---
121151
122## 4) Canonical Event Mapping Plan
123
124For each parsed NDJSON line, emit zero or more `ProviderRuntimeEvent`s:
125
1261. `system/init`
127 - emit `session.started` (once per adapter session lifecycle)
128 - emit `thread.started` if thread not yet emitted
1292. `assistant` partial chunks
130 - emit `message.delta` with stable per-turn synthetic `itemId`
1313. final `assistant` message
132 - emit `message.completed`
1334. `tool_call/started`
134 - emit `tool.started`
135 - map kind:
136 - shell -> `command`
137 - edit -> `file-change`
138 - read/grep/glob -> `other`
1395. `tool_call/completed`
140 - emit `tool.completed`
141 - include summary detail from nested payload (`command`, `path`, stderr fragment, rejection reason)
1426. `thinking/*`
143 - initial version: ignore for canonical events (optional later: map to `tool.*` “Thinking”)
1447. `result/success`
145 - emit `turn.completed` with `status: completed` unless unrecoverable parse/process error already set failure
1468. non-JSON stdout/stderr protocol line
152## 4) Canonical Event Mapping Plan (ACP -> ProviderRuntimeEvent)
153
1541. `session/update: agent_message_chunk`
155 - emit `message.delta` for assistant stream
1562. prompt terminal response (`session/prompt` result `stopReason: end_turn`)
157 - emit `message.completed` + `turn.completed`
1583. `session/update: agent_thought_chunk`
159 - initial mapping: emit thinking activity (or ignore if we keep current canonical surface minimal)
1604. `session/update: tool_call`
161 - first-seen `toolCallId` emits `tool.started`
162 - subsequent `tool_call` for same ID treated as metadata update (no duplicate started event)
1635. `session/update: tool_call_update`
164 - `in_progress`: optional progress activity
165 - `completed`: emit `tool.completed` with summarized `rawOutput` when present
1666. `session/request_permission`
167 - emit `approval.requested` with mapped options
168 - when client decision sent, emit `approval.resolved`
1697. protocol/process error
147170 - emit `runtime.error`
148 - emit `turn.completed` with `status: failed`
1499. `connection/*` + `retry/*` (when present)
150 - emit `tool.started/completed` with `toolKind: other`, title like “Connection retry”
151 - if final failure follows, also emit `runtime.error`
171 - fail active turn/session as appropriate
152172
153173Synthetic IDs:
154
1551. `turnId`: adapter-generated UUID per `sendTurn`.
1562. `itemId`: `${turnId}:assistant` for assistant stream; `${turnId}:${sanitizedCallId}` for tool calls.
1573. `call_id` sanitization: replace whitespace (including newline) with `_`.
1741. `turnId`: T3-generated UUID per `sendTurn`.
1752. `itemId`:
176 - assistant stream: `${turnId}:assistant`
177 - tools: `${turnId}:${toolCallId}`
158178
159179---
160180
161## 5) Approval and Checkpoint Behavior (explicit limitations)
181## 5) Approval, Resume, and Rollback Behavior
162182
163183### 5.1 Approvals
164184
1651. Cursor CLI approval is internal; adapter cannot accept/reject mid-turn via `respondToRequest`.
1662. Plan for v1:
167 - `respondToRequest` returns `ProviderAdapterRequestError` (“Cursor CLI does not expose external approval response API”).
168 - approval-required runtime uses no `--force`, so dangerous commands become `tool_call.result.rejected`.
1693. Future:
170 - if Cursor exposes approval RPC/hooks, implement `approval.requested` + `approval.resolved` fully.
1851. Cursor ACP permission requests are externally controllable; implement full `respondToRequest` path in v1.
1862. Decision mapping:
187 - allow once -> `allow-once`
188 - allow always -> `allow-always`
189 - reject -> `reject-once`
190
191### 5.2 Resume
171192
172### 5.2 Rollback / thread read
1931. `session/load` is available and should be first-class for adapter restart/reconnect.
1942. Must send required params: `sessionId`, `cwd`, `mcpServers`.
173195
1741. Cursor CLI has no observed equivalent to `thread/read` or `thread/rollback`.
196### 5.3 Rollback / thread read
197
1981. ACP currently has no observed rollback API.
1751992. Plan for v1:
176 - `readThread` returns adapter-maintained in-memory turn snapshot.
177 - `rollbackThread` returns `ProviderAdapterRequestError` unsupported.
200 - `readThread`: adapter-maintained snapshot projection
201 - `rollbackThread`: explicit unsupported error
1782023. Product guard:
179 - disable checkpoint revert for Cursor threads until rollback is implemented.
203 - disable checkpoint revert for Cursor threads in UI until rollback exists.
180204
181205---
182206
183207## 6) Required Contract and Runtime Changes
184208
185## 6.1 Contracts
186
1871. `packages/contracts/src/orchestration.ts`
188 - add `cursor` to `ProviderKind`.
1892. `packages/contracts/src/provider.ts`
190 - add `CursorProviderStartOptions` under `providerOptions.cursor`:
191 - `binaryPath?`
192 - `apiKey?` (optional; default login auth)
193 - `trust?` (default true for headless)
1943. `packages/contracts/src/model.ts`
195 - add `cursor` provider model options baseline.
196 - initial set should mirror observed supported IDs that are stable enough for UX.
1974. Update related contract tests.
198
199## 6.2 Server orchestration and registry
200
2011. Add `CursorAdapter` to provider registry and server layer wiring.
2022. Update provider-kind decoding in:
203 - `apps/server/src/provider/Layers/ProviderSessionDirectory.ts`
204 - persistence schemas that currently literal-match provider kinds.
2053. Fix provider selection in `ProviderCommandReactor`:
206 - honor `thread.turn-start-requested.payload.provider` as highest precedence.
207 - fallback to existing session provider, then default.
208
209## 6.3 Web
210
2111. Enable Cursor in provider selector (`apps/web/src/session-logic.ts`).
2122. Add Cursor label/icon treatment.
2133. Add optional Cursor settings fields if using per-user overrides (binary path/api key).
2144. Ensure thread/provider display and legacy mapping code handles `cursor`.
209### 6.1 Contracts
210
2111. Add `cursor` to `ProviderKind`.
2122. Add Cursor provider start options (`providerOptions.cursor`), ACP-oriented:
213 - optional `binaryPath`
214 - optional auth/mode knobs if needed later
2153. Extend model options for Cursor list and traits mapping.
2164. Add schemas for ACP-native event union in Cursor adapter service file.
217
218### 6.2 Server orchestration and registry
219
2201. Register `CursorAdapter` in provider registry and server layer wiring.
2212. Update provider-kind persistence decoding for `cursor`.
2223. Fix `ProviderCommandReactor` precedence to honor explicit provider in turn-start command.
223
224### 6.3 Web
225
2261. Cursor in provider picker and model picker (already partially done).
2272. Trait controls map to concrete Cursor model identifiers.
2283. Surface unsupported rollback behavior in UX.
215229
216230---
217231
218232## 7) Implementation Phases
219233
220### Phase A: Protocol-safe adapter skeleton
234### Phase A: ACP process and protocol skeleton
221235
2221. Implement `CursorAdapter` service/layer with all runtime logic in `CursorAdapterLive` (no CLI manager), plus session map + event queue.
2232. Implement robust NDJSON parser with:
224 - line buffering
225 - tolerant non-JSON handling
226 - explicit process-exit mapping.
2273. Wire into registry and server layers.
2361. Implement ACP process lifecycle in `CursorAdapterLive`.
2372. Implement JSON-RPC request/response multiplexer.
2383. Implement `initialize`/`authenticate`/`session/new|load` flow.
2394. Wire `streamEvents` from ACP notifications.
228240
229### Phase B: Event projection completeness
241### Phase B: Runtime projection and approvals
230242
2311. Map assistant/tool/result events to canonical runtime events.
2322. Add synthetic ID strategy and item lifecycle consistency.
2333. Add runtime error and retry handling.
2431. Map `session/update` variants to canonical runtime events.
2442. Implement permission-request bridging to `respondToRequest`.
2453. Implement dedupe for repeated `tool_call` on same `toolCallId`.
234246
235### Phase C: Provider selection and UX
247### Phase C: Turn control and interruption
236248
2371. Fix `ProviderCommandReactor` provider precedence.
2382. Enable Cursor provider in web UI.
2393. Add Cursor model defaults/options.
2491. Implement single in-flight prompt protection per session.
2502. Implement interruption fallback (process restart + reload) because `session/cancel` unavailable.
2513. Ensure clean state recovery on ACP process crash.
240252
241### Phase D: Safeguards and unsupported surfaces
253### Phase D: Orchestration + UX polish
242254
2431. Explicit unsupported errors for `respondToRequest` and `rollbackThread` (v1).
2442. Guard checkpoint-revert path for Cursor threads.
2453. Document limitations in UI/help text.
2551. Provider routing precedence fix.
2562. Cursor-specific UX notes for unsupported rollback.
2573. End-to-end smoke and event log validation.
246258
247259---
248260
......@@ -252,59 +264,64 @@ Follow project rule: backend external-service integrations tested via layered fa
252264
253265### 8.1 Unit tests (`CursorAdapter`)
254266
2551. stream JSON parse and mapping:
256 - assistant partial/final
257 - each tool type
258 - shell success/failure/rejected
259 - non-JSON line failure
2602. process lifecycle:
261 - start/send/interrupt/stop
262 - stale session errors
2633. ID sanitization:
264 - `call_id` with newline.
2671. JSON-RPC envelope parsing:
268 - response matching by id
269 - server request handling (`session/request_permission`)
270 - notification decode (`session/update`)
2712. Event projection:
272 - `agent_message_chunk` / `agent_thought_chunk`
273 - `tool_call` + `tool_call_update` dedupe/lifecycle
274 - permission request -> approval events
2753. Error mapping:
276 - unknown session
277 - method-not-found (`session/cancel`)
278 - invalid params
265279
266280### 8.2 Provider service/routing tests
267281
2682821. Registry resolves `cursor`.
2692. Session directory persistence reads/writes `cursor` provider.
2703. ProviderService fan-out/order with Cursor events.
2832. Session directory persistence reads/writes `cursor`.
2843. ProviderService fan-out ordering with Cursor ACP events.
271285
272286### 8.3 Orchestration tests
273287
2742881. `thread.turn.start` with `provider: cursor` routes to Cursor adapter.
2752. checkpoint revert on Cursor thread returns controlled failure activity.
2763. approval response command on Cursor session surfaces “unsupported approval API”.
2892. approval response command maps to ACP permission response.
2903. checkpoint revert on Cursor thread returns controlled unsupported failure.
277291
278### 8.4 Smoke/integration harness
292### 8.4 Optional live smoke
279293
2801. Optional local smoke behind env flag (`CURSOR_SMOKE=1`) to run real `agent`:
281 - create session
282 - send simple turn
283 - verify message delta/completion + turn completion.
2941. Env-gated ACP smoke:
295 - start session
296 - run prompt
297 - observe deltas + completion
298 - exercise permission request path with one tool call
284299
285300---
286301
287302## 9) Operational Notes
288303
2891. Do not run concurrent `agent` processes per workspace by default; serialize per adapter session to avoid config races.
2902. Keep native event NDJSON logs for Cursor similar to Codex adapter logs.
2913. Treat startup plain-text failures as process-level errors and surface full message.
3041. Keep one in-flight turn per ACP session.
3052. Keep per-session ACP process logs/NDJSON artifacts for debugging.
3063. Treat `session/cancel` as unsupported until Cursor ships it; avoid relying on it.
3074. Preserve resume metadata (`acpSessionId`) for crash recovery.
292308
293309---
294310
295311## 10) Open Questions
296312
2971. Should Cursor provider expose full model list dynamically (`agent --list-models`) or use curated static list in contracts?
2982. Should T3 keep strict approval UX parity with Codex, or accept Cursor’s current non-interactive approval limitation in v1?
2993. Is checkpoint revert required for initial Cursor release, or can it ship as explicitly unsupported?
3131. Should we call `authenticate` always, or only after auth-required errors?
3142. Should model selection be passed at `session/new` only, or can/should we support model switching mid-session if ACP adds API?
3153. For interruption UX, do we expose “hard interrupt” semantics (process restart) explicitly?
300316
301317---
302318
303319## 11) Delivery Checklist
304320
3051. Contracts updated (`ProviderKind`, provider start options, models).
3062. Cursor adapter + layer implemented and registered.
3073. Provider selection precedence fixed in `ProviderCommandReactor`.
3084. Cursor enabled in web provider picker.
3095. Tests added for adapter mapping, routing, and orchestration behavior.
3106. Lint + test suite green.
3211. Plan/documentation switched from headless `agent -p` to ACP `agent acp`.
3222. Contracts updated (`ProviderKind`, Cursor options, model/trait mapping).
3233. Cursor ACP adapter layer implemented and registered.
3244. Provider precedence fixed in orchestration router.
3255. Approval response path wired through ACP permission requests.
3266. Tests added for protocol decode, projection, approval flow, and routing.
3277. Lint + tests green.