Files
my-pi/pi-permission-system/docs/decisions/0006-forwarded-grant-scope-selection.md

5.3 KiB

status, date
status date
accepted 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 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.