feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -0,0 +1,74 @@
---
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