mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
feat(permission): integrate auto-review and hard guards
This commit is contained in:
@@ -605,40 +605,30 @@ Like yolo it is deny-preserving (only `allow` is touched) and applied at composi
|
||||
Global is excluded — it is the lowest precedence, so nothing more permissive is inherited when it fails.
|
||||
The two overlays stack in order: fail-closed floors `allow`→`ask` first, then yolo (if enabled) rewrites `ask`→`allow`, so an explicit yolo opt-in still wins.
|
||||
|
||||
### Discriminating delegation: a model `Authorizer`
|
||||
### Discriminating delegation: the built-in model `Authorizer`
|
||||
|
||||
Nothing constrains an `Authorizer` to be deterministic.
|
||||
`LocalUserAuthorizer` is already a non-deterministic oracle — the human — and the determinism principle governs *recorded* authority (`evaluate()`), never the live-authority layer.
|
||||
A model (e.g. Claude Haiku) can hold an `Authorizer` role on the same terms: it is live authority, so it never touches `evaluate()` or the deterministic core.
|
||||
Nothing constrains an `Authorizer` to be deterministic. `LocalUserAuthorizer` is already a non-deterministic oracle — the human — and the determinism principle governs *recorded* authority (`evaluate()`), never the live-authority layer.
|
||||
|
||||
The design is settled in [ADR 0007](../decisions/0007-model-judge-authorizer-chain-adr.md); the essentials follow.
|
||||
This package now owns the default model reviewer as the named `auto-review` authorizer. It is still downstream of deterministic policy: only an ordinary `ask` enters the authorizer chain, and a hard-guard `deny` never reaches it. The reviewer returns `allow`, `deny`, or `defer`; only `defer` falls through to the terminal human authority. Its transcript boundary admits direct user messages and recognized non-cancelled structured answers, never assistant/tool/compaction content.
|
||||
|
||||
**The live-authority layer is a Chain of Responsibility.**
|
||||
Each link returns `allow | deny | defer`; on `defer` the next link decides.
|
||||
The chain ends at a **terminal that cannot defer** — today the human (`LocalUserAuthorizer`), the headless `DenyingAuthorizer`, or `ParentAuthorizer` (terminal for its node, forwarding up to the parent node's chain — the [recursion](#the-recursion) above).
|
||||
The invariant is type-level: a terminal returns only `allow | deny`, so a deferring link cannot occupy the terminal slot.
|
||||
`selectAuthorizer` becomes the terminal-selection step of `composeAuthorizerChain` — registered non-terminal links, then the context-selected terminal.
|
||||
The composition root registers the authorizer on the same `LocalPermissionsService` instance it publishes. `installBuiltinAutoReview()` returns lifecycle methods instead of registering a second set of Pi session handlers. The root's single `session_start` and `session_shutdown` handlers coordinate service publication and reviewer state; `turn_start` resets the denial circuit breaker, and `agent_settled` is the safe boundary for optional count-based analysis.
|
||||
|
||||
**One chain per node.**
|
||||
An ask is adjudicated by exactly one node's chain: the node whose terminal decides it (ADR 0007 §7).
|
||||
A subagent node's terminal relays the ask to a serving node, which escalates it through *its* chain over the same child-fixed facts — so a relaying node resolves no links, and records `authorizer_chain_delegated` rather than reporting each configured name as a fail-safe skip.
|
||||
An adjudicating node records `authorizer_chain_resolved` with the names it consulted, since a deferring link decides nothing and otherwise leaves no evidence it ran.
|
||||
A separate `HardGuardPolicyLoader` supplies a global-only manager to `PermissionResolver`. Resolution is deliberately asymmetric:
|
||||
|
||||
```text
|
||||
ask -> [ model-judge link ] --defer--> … --defer--> [ terminal: human | Parent | Denying ]
|
||||
├─ deny (with teaching reason) -> denied
|
||||
├─ allow (slice 2, if not excluded) -> permitted
|
||||
└─ defer -> next link
|
||||
hardGuardManager.check(intent, no session rules)
|
||||
deny -> final
|
||||
allow -> continue
|
||||
ordinaryManager.check(intent, session rules)
|
||||
allow/deny -> final
|
||||
ask -> authorizer chain -> human terminal fallback
|
||||
```
|
||||
|
||||
**The model judge is a non-terminal link**, not a decorator or a fourth channel.
|
||||
It reviews an `ask`, decides the ones it is confident about, and defers the rest to its successor — a middle rung between prompt-everything and allow-everything.
|
||||
Denies are decided by recorded authority and structurally never reach an `Authorizer`, so a model link cannot grant a hard deny; the safeguard for a sensitive resource stays an explicit `deny` rule, which survives the model just as it survives the yolo rewrite.
|
||||
Thus hard-file `allow` means “not blocked by hard guards,” not “approved overall.” Project config, per-agent config, session grants, yolo mode, and model authority cannot loosen a hard deny.
|
||||
|
||||
The verdict range is `allow | deny | defer` — a superset of the earlier allow-or-escalate framing — because the first use case is **deny-first**.
|
||||
A light model reviews `external_directory` asks, denies an errant "typo" path with a teaching `reason` (wrong path; correct location) so the invoking model self-corrects, and defers everything else.
|
||||
A second use case adjudicates **opaque bash**: the model decomposes a `bash -c "…"` / `eval` command and queries the deterministic engine per sub-command through an injected, narrow `PermissionQuery` (never a reach-through to `PermissionsService`), allowing only what the engine already grants for the pieces it identifies.
|
||||
The two are one link on a **capability gradient**: the deny/defer reviewer is strictly more restrictive and ships first; the allow-capable adjudicator loosens privilege and is gated behind the full envelope (hard exclusions, audit `origin: "authorizer:model"`, non-persistence, off by default), because its safety property holds only if the model's decomposition is faithful.
|
||||
Completed AutoReview verdicts flow to an owner-only count ledger. The reviewer callback strips rationale/transcript/raw payload and persists bounded fingerprints. Reaching the configured checkpoint marks analysis pending. At `agent_settled`, a tool-free, extension-free, in-memory AgentSession receives only aggregated records. Its strict JSON proposal is validated against the evidence, persisted, and injected into the same session as a follow-up. The checkpoint advances only after a valid proposal is stored; shutdown leaves pending work durable. No analysis path mutates either policy file.
|
||||
|
||||
The public authorizer registry remains available for other named authorities. Configuration order still determines chain order, unregistered names are skipped fail-safe, and the delegation envelope remains the final boundary on model `allow` decisions.
|
||||
|
||||
Registration mirrors `registerToolAccessExtractor`: a downstream extension offers a **named** capability (`registerAuthorizer("model-judge", …)`) on `permissions:ready`, and this package makes no LLM call itself.
|
||||
Three invariants govern the seam: config order (not registration order) fixes the security-relevant chain order; a missing configured link is skipped fail-safe (more prompting, never less); and **registration alone grants no authority** — a link decides nothing until the operator names it in the `authorizerChain` config (opt-in).
|
||||
|
||||
Reference in New Issue
Block a user