Files
my-pi/pi-permission-system/docs/decisions/0005-serving-authorizer-provenance.md

73 lines
5.5 KiB
Markdown

---
status: accepted
date: 2026-07-09
---
# 0005 — Serving a forwarded permission is resolution; provenance rides the ask
## Status
Accepted.
## Context
A subagent with no UI escalates an `ask` decision up the tree by writing a forwarded-permission request file; the parent (serving) session drains its inbox and answers each request.
Before this decision, the serving node answered with bespoke logic — its own yolo check (the last one outside the composed ruleset) and a direct UI dialog — and never ran `evaluate()`.
So a parent `allow`/`deny` rule could not govern a child's escalation: the parent was prompted for actions its own policy had already decided ([#557], Phase 9 resolved direction 1).
Rerouting the prompt naively through the serving session's selected `Authorizer` (`LocalUserAuthorizer`) would have silently re-degraded the `permissions:ui_prompt` broadcast to `forwarding: null`, reversing the deliberate [#292] contract hardening (`docs/cross-extension-api.md`: "Forwarded prompts are not degraded"), which no in-monorepo consumer exercises and a green test suite would not catch.
## Decision
Serving a forwarded request is identical to resolving a local action:
1. **Recorded authority first.**
A request carrying a concrete `(surface, value)` display projection resolves against the serving node's composed ruleset via a narrow `ServingPolicy.check(surface, value)` seam (an access-intent build plus `resolver.resolve`, the same primitives `LocalPermissionsService` composes).
`allow` (including a yolo-rewritten `allow`) auto-approves; `deny` auto-denies.
2. **Escalate `ask`.**
An `ask`, or a request without display fields, escalates through the `AskEscalator` seam to the serving session's selected `Authorizer` — the human at the root today, a further hop up once multi-hop lands.
3. **Provenance is data on the ask, not a second emission path.**
The escalated ask carries its forwarded provenance (requester agent/session, the child's original `source`/`surface`/`value`) as fields on `PromptPermissionDetails`.
`LocalUserAuthorizer` — now the single `permissions:ui_prompt` emit site — renders it (populated `forwarding` context, the child's display projection, the "(Subagent)" dialog title), so the broadcast stays non-degraded ([#292]) with no server-side emission.
The serving node's yolo check and its `ConfigReader` dependency are removed; yolo inheritance falls out of the yolo-rewritten ruleset for free (a yolo `ask``allow` rewrite auto-approves at step 1, an explicit `deny` survives it).
### Base ruleset (agent-neutral resolution)
`ServingPolicy.check` resolves with `agentName` undefined — the serving node's own base policy.
The requesting subagent's agent name is display-only.
Rationale: the child already applied its own per-agent overrides before forwarding, and cross-session agent-name semantics are undefined until principal identity lands in the access-intent domain.
Revisited post-ship in [#565].
### Rejected alternatives
- **Server-side event emission with a decision-only `Authorizer` call.**
Rejected: it splits the emit-then-dialog pairing `LocalUserAuthorizer` owns, needs an emit-suppressed `authorize` variant (a genuine control flag), and keeps two `permissions:ui_prompt` emit sites that can drift.
- **A per-request decorator `Authorizer` that adds forwarding presentation.**
Rejected: authorizers are selected once per session; a per-request decorator is the same data flow dressed in object ceremony.
- **Route `ask` through the `Authorizer` and accept the degraded broadcast.**
Rejected: it reverses the [#292] hardening for the exact consumer (notification extensions) it was built for, undocumented as a considered trade-off.
## Consequences
- Parent `allow`/`deny` rules govern children's escalations; a recorded `allow` suppresses the prompt, a recorded `deny` auto-denies.
- An explicit `deny` now wins under yolo on the serving path (previously the bespoke yolo check approved everything), matching documented yolo semantics.
- A legacy/version-skew request without `(surface, value)` escalates to a prompt instead of auto-approving under yolo — the fail-safe direction; the fields have been carried since [#292].
- A request the recorded policy decides emits no `permissions:ui_prompt`; the event fires only when a human is about to be asked (the notify-now contract's intent).
- An escalated forwarded ask now also flows through the `PermissionPrompter` bracketing (`permission_request.waiting`/`approved`/`denied`) alongside the serving lifecycle's `forwarded_permission.*` entries — the uniform-escalation shape [#556] chose.
### Accepted limitations
- **Single-surface re-resolution is best-effort.**
A forwarded request carries one `(surface, value)` pair, so the serving node cannot perfectly reproduce a child decision that layered multiple surfaces (e.g. an `external_directory` check over a `path`).
An imperfect resolution lands on `ask` → prompt, never a silent grant.
- **No real notification consumer exercises the non-degraded broadcast in-repo.**
The [#292] fidelity is pinned by unit tests (the `LocalUserAuthorizer` forwarded-details render plus the server's details mapping) but not an end-to-end consumer.
Post-ship validation of all three — base-agent-scope, single-surface fidelity, and real-consumer fidelity — is tracked in [#565].
[#292]: https://github.com/gotgenes/pi-packages/issues/292
[#556]: https://github.com/gotgenes/pi-packages/issues/556
[#557]: https://github.com/gotgenes/pi-packages/issues/557
[#565]: https://github.com/gotgenes/pi-packages/issues/565