You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(design): pi-approval-parking landed with no flag; relay permission plane removed
The dialog gate shipped as the unconditional Pi behavior (Mahmoud, 2026-07-10): no
AGENTA_RUNNER_PI_DIALOG_GATE / AGENTA_AGENT_PI_DIALOG_GATE pair, gateType
pi-acp-permission (symmetric with claude-acp-permission), the relay permission
plumbing and the planned double-gate FIFO bridge deleted, builtin-only runs start
no relay. plan.md carries the final mechanism and the reduced warm/cold matrix
(keep-alive is the only remaining switch); research.md marks its §1/§7 relay
permission mechanics as the replaced code; open-questions #4 (flag coupling)
resolved by there being no flag; status.md records slice-0 evidence, the review
rounds, and the scope change. Implementation: PR #5185.
Claude-Session: https://claude.ai/code/session_01CSTSEXSe4DDhoXCFjZpZ5W
- The WARM-RESUME path bypasses the responder entirely (the resume calls
155
-
`respondPermission` directly, `sandbox_agent.ts:1389`), so nothing on that path appends
156
-
to the queue by construction. Slice task: verify whether `extractApprovalDecisions`
157
-
already seeds the turn's stored map from the resume request's transcript (the FE folds
158
-
the decision into the resume request). If it does, the relay check consumes that seeded
159
-
decision and nothing more is needed; if it does not, add an explicit append on the
160
-
resume branch just before `:1389`. Either way, cover it with the
161
-
dialog-allow-then-relay-execute dispatch test on the RESUME path, not only the instant
162
-
path.
163
-
This keeps the relay as defense-in-depth with one source of truth. The alternative (skip
164
-
relay enforcement when the dialog plane is on) removes the second check entirely; rejected
165
-
because a bug in the extension flag plumbing would then leave zero gates.
166
-
- Flag: `AGENTA_RUNNER_PI_DIALOG_GATE` (runner side, default off), exported into the sandbox
167
-
as `AGENTA_AGENT_PI_DIALOG_GATE` by `buildPiExtensionEnv` (`pi-assets.ts:67-78`, the same
168
-
place `AGENTA_AGENT_BUILTIN_GATING` is set), which is where the extension env is actually
169
-
built. One flag controls both sides coherently because the runner installs the extension
170
-
per run (research.md §6).
171
-
- Relay scope note: once the flag is on, a builtin-only run (no custom tools) no longer needs
172
-
the relay at all; `useToolRelay` (`run-plan.ts:447`) can be tightened to skip it. Document
173
-
now, tighten in this slice if trivial, otherwise record as part of the deletion follow-up.
138
+
- Single enforcement point (final, 2026-07-10): the dialog gate is the ONLY Pi permission
139
+
check. The relay's permission plane (the watcher's `permissions.decide` on execution
140
+
requests, `handlePermissionRelayRequest`, `relayPermissionCheck`, the `kind: "permission"`
141
+
record protocol) is DELETED, and the relay carries tool execution and results only. The
142
+
earlier draft kept the relay check as flag-gated defense-in-depth and bridged the double
143
+
gate with a FIFO re-append into `ConversationDecisions`; with the dialog gate unconditional
144
+
there is no flag plumbing whose failure the second check would catch, so the check guarded
145
+
nothing and the bridge was its only cost. Both are gone.
146
+
- No flag: the dialog gate is the unconditional behavior for every Pi run. Envelope detection
147
+
on the runner is scoped to Pi runs structurally (the responder receives the resolved-specs
148
+
map only on Pi runs; its presence turns detection on), so a Claude gate whose ACP title
149
+
collides with the dialog title still takes the base path.
150
+
- Relay scope: a builtin-only run (no custom tools) starts no relay at all; `useToolRelay`
151
+
(`run-plan.ts`) is `toolSpecs.length > 0`.
174
152
- Tests: extension-level unit tests run through the existing extension test seams (dialog
175
-
raised for callback specs, NOT raised for client specs, fail-closed false on cancel); a
176
-
dispatch-level test that a dialog-allowed custom tool executes exactly once through the
177
-
relay (instant path and resume path); consume-1-append-1 accounting.
153
+
raised for callback specs, NOT raised for client specs, fail-closed false on cancel, args
154
+
validated before the dialog); classification tests for the fail-closed identities (unknown
155
+
builtin, unresolved custom tool, malformed envelope) and the Claude title-collision
156
+
passthrough.
178
157
179
-
Deliverable: with the flag on, both Pi gates ride the dialog plane end to end; asks still
180
-
pause-and-destroy (parking arrives in slice 3). Behavior with the flag off is byte-identical.
158
+
Deliverable: both Pi gates ride the dialog plane end to end; asks still pause-and-destroy
159
+
(parking arrives in slice 3).
181
160
182
161
### Slice 3: park and resume
183
162
@@ -214,31 +193,30 @@ regression tests.
214
193
215
194
| Area | Removed (flag on) | Kept | Added |
216
195
|---|---|---|---|
217
-
| Extension (`agenta.ts`) | the `relayPermissionCheck` call in the builtin hook; the naked `runResolvedTool` for gated NON-CLIENT custom tools | tool registration; `runResolvedTool` relay EXECUTION (results still flow over the relay files); client tools' browser-fulfilled path untouched; the old permission path behind the flag for rollback | the dialog gate (`ctx.ui.confirm` + envelope) at both gates, non-client specs only |
218
-
| Relay (`relay.ts`, `dispatch.ts`, `run-plan.ts`) |nothing yet (`relayPermissionCheck` and `handlePermissionRelayRequest` become dead when the flag is on; delete after a bake period, recorded follow-up; a builtin-only run can stop starting the relay, `useToolRelay``run-plan.ts:447`) | the watcher, execution dispatch, `permissions.decide` defense-in-depth| none |
196
+
| Extension (`agenta.ts`) | the `relayPermissionCheck` call in the builtin hook; the naked `runResolvedTool` for gated NON-CLIENT custom tools | tool registration; `runResolvedTool` relay EXECUTION (results still flow over the relay files); client tools' browser-fulfilled path untouched| the dialog gate (`ctx.ui.confirm` + envelope) at both gates, non-client specs only, unconditional|
197
+
| Relay (`relay.ts`, `dispatch.ts`, `run-plan.ts`) |the whole permission plane: `relayPermissionCheck`, `handlePermissionRelayRequest`, the `kind: "permission"` record protocol, `RelayPermissions` and the watcher's `permissions.decide` enforcement; builtin-only runs stop starting the relay (`useToolRelay`= custom tools only) | the watcher and execution dispatch (execute requests + results, client-tool pass-through)| none |
219
198
| Responder seam (`acp-interactions.ts`) | nothing | all pause/park/reply mechanics | envelope detection + tool-call id normalization at the top of `handleRequest` + `GateDescriptor` from envelope with runner-side spec lookup + card payload synthesis + malformed-envelope reject |
220
199
| Reply mapping (`responder.ts`) | nothing | everything (`decisionToReply` is already correct; the daemon maps `{once, always, reject}` to the dialog option by kind) | nothing |
221
200
| Park record (`sandbox_agent.ts`, `server.ts`) | nothing | everything |`gateType` union widened to include `"pi-dialog-permission"` (`sandbox_agent.ts:368`) + the `server.ts:628` gate-type guard accepts it + envelope identity in `parkedApproval`|
222
-
| Stored decisions (`responder.ts``ConversationDecisions`) | nothing | keying (name + canonical args), `take`/`peek`| a FIFO append API; consume-1-append-1 on the cold dialog path; warm-resume seeding (verify `extractApprovalDecisions`, else an explicit resume-branch append) |
223
-
| Config | nothing |`AGENTA_RUNNER_SESSION_KEEPALIVE`, TTLs, pool cap |`AGENTA_RUNNER_PI_DIALOG_GATE` -> sandbox `AGENTA_AGENT_PI_DIALOG_GATE` via `buildPiExtensionEnv` (`pi-assets.ts:67-78`); default off, flip after slice 4 greens|
201
+
| Stored decisions (`responder.ts``ConversationDecisions`) | nothing | keying (name + canonical args), `take`/`peek`(the dialog gate's cold decision map) | nothing (the planned double-gate FIFO append died with the relay permission plane) |
202
+
| Config | nothing |`AGENTA_RUNNER_SESSION_KEEPALIVE`, TTLs, pool cap |nothing (no flag: the dialog gate is the unconditional Pi behavior)|
"Warm" means keep-alive on, session parked, answer inside the approval TTL. Every other cell
229
-
is cold. Flag names: KA = `AGENTA_RUNNER_SESSION_KEEPALIVE`, DG = `AGENTA_RUNNER_PI_DIALOG_GATE`.
208
+
is cold. The dialog gate itself has no flag (always on for Pi); KA =
209
+
`AGENTA_RUNNER_SESSION_KEEPALIVE` still gates the pool and the parking.
230
210
231
211
| Scenario | Behavior |
232
212
|---|---|
233
-
| Warm approve (KA+DG on, within TTL) | The resume answers the held dialog; the hook returns allow; the original call runs with its original arguments inside the original `prompt()`; call N+1 carries the real result for the original id. Byte-exact. |
213
+
| Warm approve (KA on, within TTL) | The resume answers the held dialog; the hook returns allow; the original call runs with its original arguments inside the original `prompt()`; call N+1 carries the real result for the original id. Byte-exact. |
234
214
| Warm deny | The resume answers `no`; the hook returns `blockReason`; the tool call reports failed; the turn continues live on the same session (Pi handles the block in-loop). Nothing executes. |
235
215
| Approve after TTL (cold) | The park expired and the session was destroyed (the held dialog died with it, fail-closed). The decision lands on today's cold path: cold replay, the model re-issues the call, `decide` consumes the stored decision by name plus canonical args. After harness session resume lands, the same but with full structured history (rubric B); either way the decision map absorbs drift by re-firing the gate on mismatch. |
236
216
| Deny after TTL (cold) | Same path; the stored deny blocks the re-issued call. |
237
217
| ACP transport drop mid-pending | The spike's drop scenario: `pi-acp` and Pi die cleanly, nothing executes. The pool's parked-promise rejection evicts the slot; the next message runs cold. Degradation target is tier-2 session resume once that project lands (the pending call is already on Pi's disk). |
238
218
| TTL expiry racing an approval | The pool's existing race handling: expiry destroys and the late decision misses the pool (`approval-mismatch`/pool-miss path) and degrades to the cold decision map. The durable row was written at pause time, so the answer always lands. No new code; covered by an existing-pattern test. |
239
-
| KA on, DG off | Exactly today: relay-poll gates, pause destroys the session, cold decision-map resume. |
240
-
| KA off, DG on | The dialog gate still works (instant allow/deny from the responder; better card identity), but an ask pauses and destroys the session (no pool), and the dialog dies with it, fail-closed. Cold resume as today. Acceptable, but flip DG on only where KA is on to avoid a confusing half-state; state this in the rollout note. |
241
-
| Both off | Byte-identical to today. |
219
+
| KA off | The dialog gate still decides instantly (allow/deny from the responder, real card identity), but an ask pauses and destroys the session (no pool), and the held dialog dies with it, fail-closed. Cold decision-map resume: the durable path. |
242
220
| Daytona (any flags) | The pool does not park Daytona sandboxes (keep-alive slice 3 deferred), so every Daytona ask is the "KA off" row: pause, destroy, cold decision-map resume. The dialog transport itself works on Daytona (the extension and env flow are identical, research.md §6), so when slice 3 lands, Daytona parking needs no Pi-specific work. |
243
221
244
222
## Rollout and compatibility
@@ -247,9 +225,10 @@ is cold. Flag names: KA = `AGENTA_RUNNER_SESSION_KEEPALIVE`, DG = `AGENTA_RUNNER
247
225
bundle (research.md §6); runner and extension deploy atomically. The only mixed state is a
248
226
session created before a deploy and resumed after it; the pool's config fingerprint and the
249
227
restart-drains-pool behavior make that a cold resume, which both transports handle.
250
-
-**Flag order.** Ship slices 1-3 dark, then enable `AGENTA_RUNNER_PI_DIALOG_GATE` on the dev
251
-
stack with keep-alive already on, run slice 4, then default it on. The old relay permission
252
-
path stays in the code one release as the rollback lever, then gets deleted (follow-up).
228
+
-**On by default, no flag.** The dialog gate is the only Pi permission path; the relay
229
+
permission plumbing is deleted in the same change (the runner installs the extension per
230
+
run, so both sides switch atomically). Keep-alive off still degrades every ask to the cold
231
+
durable-decision path, so the fail-closed story does not depend on the pool.
253
232
-**pi-acp is pinned** (0.0.29). The dialog reaper behavior and the extension-UI translation
254
233
are version-load-bearing; a Pi or pi-acp upgrade must re-run the spike's hold scenario
255
234
(this is in the risks of the parkable-gates design; repeat it in the upgrade checklist).
@@ -258,16 +237,16 @@ is cold. Flag names: KA = `AGENTA_RUNNER_SESSION_KEEPALIVE`, DG = `AGENTA_RUNNER
258
237
259
238
1. Upstream a structured-metadata field to `pi-acp` (maintainer Sergii Kozak, svkozak/pi-acp)
260
239
or carry a pnpm patch, retiring the envelope encoding.
261
-
2. Delete `relayPermissionCheck` / `handlePermissionRelayRequest` after the bake period.
262
-
3. Daytona parking (keep-alive slice 3) picks up Pi parking for free; verify then.
240
+
2. Daytona parking (keep-alive slice 3) picks up Pi parking for free; verify then.
0 commit comments