Skip to content

Commit 3bd8206

Browse files
committed
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
1 parent 4e9a2c5 commit 3bd8206

4 files changed

Lines changed: 75 additions & 70 deletions

File tree

docs/design/agent-workflows/projects/pi-approval-parking/open-questions.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@ state a working default. #1 still gates slice order.
3030
explicitly on the resume branch before `sandbox_agent.ts:1389`. Details in plan.md
3131
slice 2.
3232

33-
4. **Flag coupling. OPEN, decide at slice 2 review.** `AGENTA_RUNNER_PI_DIALOG_GATE`
34-
(sandbox-side `AGENTA_AGENT_PI_DIALOG_GATE` via `buildPiExtensionEnv`) works without
35-
keep-alive but produces a half-state (better gates, no parking). Working default:
36-
independent flags, documented advice to enable the dialog gate only where keep-alive is
37-
on. Alternative: check keep-alive runner-side and refuse the half-state.
33+
4. **Flag coupling. RESOLVED (2026-07-10): there is no flag.** The dialog gate shipped as
34+
the unconditional Pi behavior and the relay permission plumbing was deleted with it, so
35+
the half-state question dissolved: without keep-alive the gate still decides instantly
36+
with real card identity, and an ask degrades to the cold durable-decision path.
3837

3938
5. **Deny-warm UX. OPEN, a product call, not a mechanism question.** On a warm deny the hook
4039
returns `blockReason` and Pi continues its loop on the live session; the model sees the

docs/design/agent-workflows/projects/pi-approval-parking/plan.md

Lines changed: 36 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -135,49 +135,28 @@ non-matching title takes today's path untouched.
135135
browser-fulfilled pause semantics through the relay (`dispatch.ts:239`, `relay.ts:214`);
136136
permission-gating a client tool via the dialog would be wrong. The dialog gate applies to
137137
`callback` (and `code`) specs; `client` specs keep today's path untouched.
138-
- Double-gate handling: the relay watcher still runs `permissions.decide` on the execution
139-
request (`relay.ts:240-264`). After a dialog allow, that second check must pass. The
140-
premise holds: the responder and the relay share the SAME `ConversationDecisions` object,
141-
built once per turn (`sandbox_agent.ts:1218`, consumed at `:1225` and `:1268`). But
142-
`ConversationDecisions` exposes only `take`/`peek` over private `decisionQueues`
143-
(`responder.ts:209-237`); there is no public write. The mechanism therefore is:
144-
- Add a FIFO append API to `ConversationDecisions` (a decision pushed onto the queue for a
145-
`(toolName, canonical args)` key, consumed by the next matching `take`).
146-
- On the COLD dialog path (a stored decision answers the dialog instantly), the dialog's
147-
`decide` call consumed one queued decision; append exactly one back for the relay
148-
execution check. Consume-1-append-1, so no stale decision survives to a LATER identical
149-
gate in the same turn.
150-
- The key-parity invariant, stated as a tested invariant, not an assumption:
151-
`envelope.toolName === spec.name` and `envelope.input` is the exact `execute` params
152-
object, so the approved-call key matches what the relay reads (`dispatch.ts:84` writes
153-
`args: params ?? {}`; `relay.ts:245` reads `req.args`).
154-
- 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`.
174152
- 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.
178157

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).
181160

182161
### Slice 3: park and resume
183162

@@ -214,31 +193,30 @@ regression tests.
214193

215194
| Area | Removed (flag on) | Kept | Added |
216195
|---|---|---|---|
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 |
219198
| 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 |
220199
| Reply mapping (`responder.ts`) | nothing | everything (`decisionToReply` is already correct; the daemon maps `{once, always, reject}` to the dialog option by kind) | nothing |
221200
| 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) |
224203
| Wire contract | nothing | everything (`interaction_request` shape unchanged; card payload uses existing fields) | nothing (assert with the existing wire-contract test) |
225204

226205
## The warm/cold behavior matrix
227206

228207
"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.
230210

231211
| Scenario | Behavior |
232212
|---|---|
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. |
234214
| 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. |
235215
| 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. |
236216
| Deny after TTL (cold) | Same path; the stored deny blocks the re-issued call. |
237217
| 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). |
238218
| 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. |
242220
| 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. |
243221

244222
## Rollout and compatibility
@@ -247,9 +225,10 @@ is cold. Flag names: KA = `AGENTA_RUNNER_SESSION_KEEPALIVE`, DG = `AGENTA_RUNNER
247225
bundle (research.md §6); runner and extension deploy atomically. The only mixed state is a
248226
session created before a deploy and resumed after it; the pool's config fingerprint and the
249227
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.
253232
- **pi-acp is pinned** (0.0.29). The dialog reaper behavior and the extension-UI translation
254233
are version-load-bearing; a Pi or pi-acp upgrade must re-run the spike's hold scenario
255234
(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
258237

259238
1. Upstream a structured-metadata field to `pi-acp` (maintainer Sergii Kozak, svkozak/pi-acp)
260239
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.
263241

264242
## Test inventory (summary)
265243

266244
- Unit: envelope build/parse round-trip (incl. the spike's hostile probe string), request
267245
classification (envelope vs non-matching title), tool-call id normalization everywhere the
268246
id is read, runner-side spec lookup parity (author-allow instant, author-deny instant,
269-
read-only builtin auto-allow), malformed-envelope reject (fail closed), decisions FIFO
270-
append + consume-1-append-1 accounting, client-spec exclusion from the dialog gate.
247+
read-only builtin auto-allow), malformed-envelope reject (fail closed), unknown builtin and
248+
unresolved custom-tool reject (fail closed), client-spec exclusion from the dialog gate,
249+
the Claude title-collision passthrough.
271250
- Dispatch (fake session): Pi ask parks; resume-approve runs original call once (instant AND
272251
warm-resume decision seeding for the relay's second check); resume-deny blocks; TTL expiry
273252
-> cold; approval-mismatch evicts; multi-gate refuses the park and degrades cold

docs/design/agent-workflows/projects/pi-approval-parking/research.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Everything here is verified against `services/runner/src` as of 2026-07-09 (post
44
#5183 merges) unless a package path says otherwise. A reader should be able to implement from
55
this file plus plan.md without re-deriving the mechanics.
66

7+
Landed note (2026-07-10, PR #5185): the mechanics in §1 describe the code this feature
8+
REPLACED. The relay permission plane (§1's `relayPermissionCheck`, the watcher's permission
9+
handling) is deleted; both Pi gates now ride `ctx.ui.confirm` unconditionally, with no flag
10+
(the §7 flag paragraph is historical).
11+
712
## 1. How the two Pi gates pause today (the code being replaced)
813

914
Pi tools and gates ride a file relay because the in-sandbox Pi process cannot reach Agenta.
@@ -157,8 +162,8 @@ and both transports coexist in one extension during rollout.
157162
- `AGENTA_AGENT_TOOLS_RELAY_TIMEOUT` sets the relay poll deadline (`relay.ts:61-63`). The
158163
dialog path never touches it.
159164
- The extension's env is built by `buildPiExtensionEnv` (`pi-assets.ts:67-78`), which is
160-
where `AGENTA_AGENT_BUILTIN_GATING` and the relay dir are set; the new transport flag
161-
follows that pattern (runner `AGENTA_RUNNER_PI_DIALOG_GATE` -> sandbox
162-
`AGENTA_AGENT_PI_DIALOG_GATE`), not `run-plan.ts`.
163-
- Whether a run starts the relay at all is `useToolRelay` (`run-plan.ts:447`); a
164-
builtin-only run under the dialog flag no longer needs it.
165+
where `AGENTA_AGENT_BUILTIN_GATING` is set. (Historical: the plan draft routed the new
166+
transport through a flag pair here; the landed change has no flag, the dialog transport is
167+
unconditional for Pi.)
168+
- Whether a run starts the relay at all is `useToolRelay` (`run-plan.ts`); with the dialog
169+
transport a builtin-only run starts no relay (custom tools only).

docs/design/agent-workflows/projects/pi-approval-parking/status.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,30 @@ open-questions.md the same day. All findings were code-verified by the reviewer:
7878
concurrency (safe degrade documented), warm-deny mechanics (sound after B2; the UX product
7979
call stays open as #5).
8080

81+
## Implementation (2026-07-09/10, lane feat/pi-approval-parking, PR #5185)
82+
83+
Slices 0-3 implemented; slice 4 (live QA) runs as a separate stage.
84+
85+
- Slice 0 PASSED both criteria live through the sandbox-agent daemon on the dev box: the
86+
envelope arrived byte-exact at a runner-side `onPermissionRequest` handler (hostile probe
87+
string included), and a plain `respondPermission(id, "once")` resolved the held dialog to
88+
allow so the original `park_probe` ran with its original token, immediately and after a
89+
180-second hold with no reaper. The daemon exposed `availableReplies: ["once","reject"]`,
90+
confirming B1 (no reply mapping); the synthetic `pi-ui-<uuid>` id on the ACP request
91+
confirmed B2 (normalization required).
92+
- Slices 1-3 landed per plan.md, then two review rounds (an internal Opus review, a Codex
93+
xhigh review, CodeRabbit) tightened the fail-closed identities: unknown builtin AND
94+
unresolved custom-tool names reject; envelope detection is scoped to Pi runs structurally.
95+
- Scope change (Mahmoud, 2026-07-10): the dialog gate shipped WITHOUT a feature flag as the
96+
only Pi permission path, and the relay permission plane was deleted (extension
97+
`relayPermissionCheck` poll, `handlePermissionRelayRequest`, the `kind: "permission"`
98+
record protocol, `RelayPermissions` enforcement, the planned double-gate FIFO bridge).
99+
The relay carries tool execution and results only; a builtin-only run starts no relay.
100+
`ParkedApproval.gateType` for the Pi plane is `"pi-acp-permission"` (symmetric with
101+
`"claude-acp-permission"`).
102+
81103
## Next steps
82104

83-
1. Slice 0: the live daemon confidence run (reuse the committed spike assets).
84-
2. Coordination check with JP on where the pool/park machinery lives before slice 3.
85-
3. Slices 1-4 per plan.md.
105+
1. Slice 4: the live warm/cold matrix on the dev box (separate QA stage).
106+
2. JP coordination: the park record moves with the pool wherever the backend warm-session
107+
work lands (checked 2026-07-09: no active migration in flight).

0 commit comments

Comments
 (0)