pub struct ApprovalCache { /* private fields */ }Expand description
Session-scoped “approve for session” decision cache (§2.10). Keyed by
Self::key — (tool, subject) — so a repeated identical call (the
SAME canonical command, or the same path) skips re-prompting for the rest
of this agent’s lifetime, exactly like CC’s “don’t ask again”/oc’s
“always” (cc§4, oc§4). Cheap and unconditional to construct — an agent
that never enables capabilities.permissions simply never populates or
consults it (§1.13-style “zero cost when off”).
Implementations§
Source§impl ApprovalCache
impl ApprovalCache
Sourcepub fn new() -> ApprovalCache
pub fn new() -> ApprovalCache
A fresh, empty cache.
Sourcepub fn key(tool: &str, subject: Option<&str>) -> String
pub fn key(tool: &str, subject: Option<&str>) -> String
The cache key for a (tool, subject) pair — no hashing, so it
stays legible-ish in logs/debug output (see length_prefixed’s
doc comment for D-3’s length-prefixed encoding, which keeps this
readable while still being provably unambiguous); a session cache
has no untrusted-input DoS surface a hash would need to guard
against (bounded by how many distinct calls one session can make).
Only use this directly for a request that HAS a subject (bash’s
command, a file tool’s resolved path, apply_patch’s patch).
For the general case — including a request with NO subject — use
Self::key_for_request, which falls back to this exact function
when subject is Some (so every existing bash/file-tool caller
is unaffected) but does something different when it’s None — see
that method’s doc comment for why (F2, Fable-5 adversarial review).
Sourcepub fn key_for_request(req: &ApprovalRequest<'_>) -> String
pub fn key_for_request(req: &ApprovalRequest<'_>) -> String
F2 (Fable-5 adversarial review — HIGH, “‘allow for session’
over-grants tool-wide for subject-less tools”): the cache key
resolve_ask actually uses, for ANY request shape.
ApprovalRequest::subject is command.or(path).or(patch)
(Agent::permissions_gate_denial) — None for every MCP tool call
and any tool whose interesting content lives in richer JSON args
rather than a single command/path/patch string (e.g.
mcp_db_query {"sql": "…"}). Before this fix, Self::key alone
collapsed a subject-less request down to the bare tool name, so an
AllowForSession granted for ONE call’s args
({"sql":"SELECT 1"}) silently auto-allowed EVERY later call to
that tool regardless of args ({"sql":"DROP TABLE users"}) — an
over-grant the user never saw, let alone approved.
The fix: when there’s no subject, fold a canonical digest of
raw_args into the key too, so a session grant only ever
auto-allows the exact SAME args again — a call with different args
still reaches the handler. “Canonical” here means
canonical_json_string’s recursively-key-sorted rendering, NOT
Value’s own Display/to_string() — this crate’s own build
happens to render Value’s keys already-sorted (serde_json’s
default Map backing is a BTreeMap unless some dependency’s
build pulls in the preserve_order feature and Cargo’s feature
resolver unifies it into this target too), but a SECURITY-relevant
cache key has no business depending on an indirect, easily-
disturbed fact like that — so this re-sorts explicitly and is
correct regardless.
When subject IS Some (bash/file tools/apply_patch), this is
byte-identical to Self::key — those callers’ session-grant
breadth is completely unchanged.
D-3 (Fable-5 delta review — LOW hardening): the None branch used
to join tool and the args digest with a bare \u{0}args:
separator that wasn’t itself length-guarded — see
length_prefixed’s doc comment for the exact collision the
review proved constructible against Self::key’s Some branch,
and why length-prefixing every component (rather than trusting an
unlengthed separator no caller-controlled byte could ever
reproduce) closes it for good.
Sourcepub fn is_approved(&self, key: &str) -> bool
pub fn is_approved(&self, key: &str) -> bool
Has key previously been granted “for session”?
Sourcepub fn approve(&self, key: &str)
pub fn approve(&self, key: &str)
Record key as approved for the rest of this session. A poisoned
lock (a prior panic while held) is treated as “cache unavailable” —
silently drops the grant rather than panicking the caller; the next
identical call simply re-prompts, which is the fail-closed direction
(a lost cache entry costs an extra prompt, never a skipped one).