pub struct PolicyGate { /* private fields */ }Expand description
The tool authorizer a long-lived host installs on each of its sessions:
the same filter basis’s ApprovalGate applies, plus
the one answer that has to be terminal.
§Why the refusal is here and the rest is not
ApprovalGate answers nothing — every consequential call comes back as a
Prompt — and that is what lets the policy be read per call, one layer up,
where session/set_mode arrives. mentra resolves a Prompt against the
conversation’s remembered rules before the approver is consulted, so a
durable Global- or Project-scope allow seeded through the session’s
permission handle answers ahead of the policy. For Always and
Prompt that is unremarkable: both permit consequential work, and a
standing allow is a host saying so in advance. For Never it is a
standing override of the one thing that mode promises.
So this gate answers Never itself, with a Deny mentra returns
unchanged — no rule is read, no PermissionRequested is emitted, and the
approver is never reached. Every other policy still surfaces the call
exactly as ApprovalGate did, remembered rules and all.
The policy is read from the shared SessionApproval on each call, so one
installed gate follows a session across every switch. It is deliberately
read per call rather than per turn: a gate that cached the policy it was
installed under would be the stale answer this whole module exists to
prevent.
§What it does not close
A durable rule still answers ahead of the client under Always and
Prompt, because under those policies the gate still says Prompt.
Revoking one is mentra’s business
(mentra#43), not something a
posture can express. And a rule remembered against a read-only tool is
never consulted under any policy — this gate allows a non-consequential
call outright, exactly as ApprovalGate does, for the reason written on
basis::approval::is_consequential.
§And why it carries no timeout
ApprovalGate has a with_timeout, and installing
this gate replaces the runtime’s, so the obvious thing is to mirror the knob
so a host can restate a bound it would otherwise lose. That knob does not
do what its name suggests, so this gate does not offer one.
A ToolAuthorizer’s timeout is mentra’s bound on mentra’s own wait. When it
fires, mentra drops the authorization, denies that call, and lets the turn
carry on. It does not touch basis’s side: basis’s event forwarder is
parked inside Approver::approve for the same request
(basis/src/run/prepared/forward.rs), and nothing wakes it, so a run whose
approver never answers never returns, bound or no bound — measured, not
assumed. What a bound really buys is an earlier refusal for an approver that
answers late; what it is reached for is the approver that never answers,
and there it buys nothing.
So the recourse for an unanswered request is the one that actually unparks
the approver, and every host that installs this gate already needs it: over
ACP the client sends session/cancel — which the protocol requires of a
client abandoning a session/request_permission, and which basis-acp
implements and pins (tests/acp/permission.rs) — and a host on another
transport bounds its own approver, which is the layer that is genuinely
waiting. Adding a knob here would have offered a guarantee that reads like
that one and is not.
Cloneable for the same reason SessionApproval is, and not Debug for
the same reason either: what it holds is one session’s live policy, shared.
Implementations§
Source§impl PolicyGate
impl PolicyGate
Sourcepub fn new(approval: SessionApproval) -> Self
pub fn new(approval: SessionApproval) -> Self
Gates a session on approval, reading it live.