Capability taxonomy, derivation functions, and the pure gate decision
engine (#587, #591).
One tool call requires a set of [Capability]s (derived from its spec's
MCP-style annotations plus its registry provenance) and is granted a set
(derived from the agent's policy plus the conversation's provenance/taint
state at that moment). [decide] compares them and returns the single
[GateOutcome] for the call — the one decision path that replaces the
previous OR of an argument-aware policy check, a sandbox-denial escalation,
and a runtime "untrusted content + egress" override.
The containment invariants live here as pure logic, testable exhaustively:
- Untrusted content in context removes [
Capability::ArbitraryEgress] and [Capability::MutateExternal] from the granted set — a message body or an issue title carries attacker-steered bytes out as surely as a fetch does. - An operator-declared taint-resilient set survives that subtraction, and
only when the policy declares it ([
GrantPolicy::taint_resilient]). - A tool whose spec cannot be classified requires the full privileged set
([
CapabilitySet::all]) — fail closed. - The model is monotonic: under a fixed policy, adding taint never adds a capability.
Everything here is a pure function over its inputs. No gate wiring, no IO,
no clock: the executor surface (#592) derives the inputs and the agent's
per-call gate (#593) is a thin adapter over [decide].