--- status: accepted date: 2026-07-09 --- # 0006 — Grant-scope selection on forwarded approvals ## Status Accepted. ## Context A subagent with no UI escalates an `ask` up the tree; the serving (parent/root) session drains its inbox and a human decides ([#557], ADR-0005). When that human approves "for this session," the ruling could land only on the **requesting subagent**: the response rides back to the child, whose `GateRunner` records the pattern into the child's own `SessionRules`. The human had no way to record the ruling at the **serving scope**, so a grant meant to cover the parent and all its subagents had to be re-approved per child ([resolved direction](../architecture/architecture.md#resolved-direction) 4, Phase 9 Step 4). ## Decision Offer the human a scope when approving a forwarded request "for this session," and record a whole-session grant on the serving node. 1. **The child rides its suggestion along.** `GateRunner` already computes a `SessionApproval` (surface + one-or-more patterns) for the ask. It now flows on `PromptPermissionDetails.sessionApproval` (via `SessionApproval.toForwardedData()`) into the `ForwardedPermissionRequest.sessionApproval` field the child writes. The field is optional and read tolerantly, so an older child (no suggestion) simply offers no scope choice. 2. **A two-step dialog.** The base four-option prompt is unchanged. Choosing "for this session" opens a second `select` — subagent-only (listed first, the least-privilege default) vs the whole session — but only for a forwarded ask that carries a suggestion. A cancelled scope select defaults to subagent-only. `LocalUserAuthorizer` builds the scope labels (`buildForwardedScopeLabels`) and is still the single `permissions:ui_prompt` emit site; the emit fires once before the first select, so the [#292] non-degraded broadcast is unaffected. 3. **Whole-session grants record on the serving node only.** The dialog returns a serving-node-internal `approved_for_serving_session` state. `ForwardedRequestServer.applyGrantScope` records the child's suggested pattern into the serving session's `SessionRules` — the same instance the resolver and gate runner read — then translates the response to a plain `approved`. The child records nothing; its next identical action re-forwards and resolves as recorded authority (the [#557] serve-time evaluation auto-approves it). A subagent-only grant (`approved_for_session`) passes through untouched — the child records, exactly as before. The serving node is the single source of truth for a whole-session grant. Because the serving `SessionRules` is shared, the grant governs the parent's own actions immediately and future forwarded resolutions for free. ### The `approved_for_serving_session` state It is serving-node-internal: produced by the dialog, consumed by `ForwardedRequestServer`, and translated to `approved` before any response is written, so it never reaches disk or the child. It is a member of `PermissionDecisionState` (and `isPermissionDecisionState`, for guard completeness); the on-disk `ForwardedPermissionResponse.state` stays within the four legacy values. ### Rejected alternatives - **Record on both the serving node and the requesting child.** Rejected: two copies blur the scope, and the subagent-only vs whole-session distinction collapses to "does the parent also hold a copy." Serving-node-only keeps a single source of truth; the child re-forwards and auto-approves. - **A `grantScope` marker on an `approved_for_session` decision.** Rejected: the server must translate the response to `approved` for the whole-session case anyway (so the child does not double-record), and a state that says "subagent" while a marker says "serving" is less honest than a distinct state. - **Inline scope options in the base dialog (a five-option prompt).** Rejected in favor of the operator's two-step choice: the base prompt stays byte-identical for every local ask, and the scope question appears only when it applies. ## Consequences - A human can grant a forwarded request for the whole serving session; the parent and its subagents then resolve it without a second prompt. - The default (subagent-only, pre-selected) preserves today's behavior exactly; this ships as `feat:`, not a breaking change. - The forwarded request and response formats gain one optional field each, read tolerantly — an upgrade needs no config edit and tolerates version skew. ### Accepted limitations - **Cross-cwd / cross-surface re-resolution is best-effort.** A recorded whole-session path grant matches a child's later forward only when cwd and surface align — the pre-existing single-surface/cross-cwd limitation from ADR-0005 (`docs/decisions/0005-serving-authorizer-provenance.md`), tracked in [#565]. An imperfect match lands on `ask` → prompt, never a silent grant. - **Three-way scope (root / parent / requesting subagent) is not shipped.** The tree is depth-2 today, so "parent" and "root" coincide and the dialog offers two scopes. The three-way split waits on multi-hop escalation — admitted-not-shipped, the same shape as the escalation chain. [#292]: https://github.com/gotgenes/pi-packages/issues/292 [#557]: https://github.com/gotgenes/pi-packages/issues/557 [#565]: https://github.com/gotgenes/pi-packages/issues/565