Memory Governance
Writes to agent memory stay inside a store allowlist and set ceilings, and reads return only findings that are still live.
On this page
What an agent writes to memory becomes input to its later decisions. The memory guard governs both directions: which writes enter a memory store, and which stored values the agent may read back.
MemoryGovernanceGuard, registered as memory-governance in chio-guards, checks each write against a store allowlist, a retention ceiling, a size cap, deny patterns, and a per-session entry count. With the retraction profile on, a read returns a value only while the finding it came from is still live. The host routes memory operations to the kernel as memory write and read actions.
A write passes its gates in a fixed order, and the count moves last
evaluate_write runs the gates in order and returns at the first refusal. Each ceiling applies only when it is configured, and the entry counter moves at the final gate, so a write refused before that gate uses no quota.
A write that clears every gate reaches the store
agent-notes is in the allowlist, a ttl of 3600 is under the 86,400-second ceiling, the content fits in 64 KiB and matches no pattern, and the entry counter moves from 41 to 42 of 500.
- Finding provenance, with the retraction profile only. The arguments must carry a non-empty
FINDING_DELIVERY_RECEIPT_ID_ARGUMENTstring (finding_delivery_receipt_id), or the write is refused before any ceiling is read. - Store allowlist.
store_matchestakes*as any store,prefix*as a prefix match, and anything else as an exact comparison. - Retention TTL.
extract_retention_ttlreads the first ofretention_ttl,retentionTtl,retention_ttl_secs,retentionTtlSecs,ttl,ttl_secs,expires_in, orexpiresInthat parses as au64. A value above the ceiling is refused, and a missing TTL reads as a request for indefinite retention and is refused. - Content size.
extract_content_size_bytesreadscontent_size,contentSize,content_bytes, orsize, then falls back to the byte length of the content text. A write whose size cannot be found is refused. - Deny patterns.
extract_content_texttakes the first non-empty string amongcontent,text,value,vector_text, andpayload, and any pattern that matches it refuses the write. A write with none of those keys goes unscanned. - Per-session entry limit.
bump_counterincrements the count for(agent_id, capability_id)and refuses once the new value passesmax_memory_entries. It counts evaluations that reach this gate, which can differ from the writes the memory store confirms.
The guard claims memory actions and passes every other action
Evaluation starts with extract_action_checked over the tool name and arguments. ToolAction::MemoryWrite { store, .. } takes the write path, ToolAction::MemoryRead { store, key } takes the read path, which applies the same store allowlist, and every other action returns GuardDecision::allow(). An extraction that fails with MalformedAction returns GuardDecision::deny(Vec::new()) before the branch, so a malformed argument shape is refused.
The store is whatever the extractor found. It probes collection, index, namespace, and store in that order and falls back to the tool name through unwrap_or_else(|| self.tool.clone()). A bare vector_upsert call with no store argument reaches the guard as store == "vector_upsert" and is matched like any store name, so a pattern that covers the tool name admits the write.
Read more in Requests & Action Classification.
Operator config and grant constraints form one allowlist
Policy comes from two places. The operator sets MemoryGovernanceConfig once for the deployment, and an issuer can mint Constraint::MemoryStoreAllowlist onto one ToolGrant inside a ChioScope. The effective store allowlist is their union, and every other ceiling comes from the config alone. HushSpec has no memory section (the Rules type in chio-policy has no memory field), so the Rust host constructs and registers the guard.
use chio_guards::{MemoryGovernanceConfig, MemoryGovernanceGuard};
use chio_core::capability::scope::Constraint;
// Deployment-wide ceilings. A struct literal must name every field, so
// spread the default and set only the ones this deployment changes.
let guard = MemoryGovernanceGuard::with_config(MemoryGovernanceConfig {
store_allowlist: vec!["issue-memory".to_string(), "sku-*".to_string()],
max_memory_entries: Some(500),
max_retention_ttl_secs: Some(86_400),
max_content_size_bytes: Some(64 * 1024),
deny_patterns: vec![
r"(?i)\bssn\b".to_string(),
r"AKIA[0-9A-Z]{16}".to_string(),
],
..MemoryGovernanceConfig::default()
})?;
// The per-grant surface. The effective store allowlist is the union of
// this list and the one in the config above.
grant.constraints.push(Constraint::MemoryStoreAllowlist(vec![
"issue-memory".to_string(),
]));
// Register on an existing mutable kernel.
kernel.add_guard(Box::new(guard));Under this configuration a write from triage to issue-memory carrying {"ttl": 3600, "content": "..."} is admitted while the session stays under 500 entries, the content fits in 64 KiB, and the body matches neither pattern. A write to incident-log is refused because it is in neither allowlist. A write with no TTL key is refused because an unstated lifetime under a 24-hour ceiling reads as indefinite retention.
An empty union makes effective_store_allowlist return None, and the store check is skipped. To forbid a store, set a non-empty list that leaves it out. MemoryGovernanceConfig is deny_unknown_fields, so a misspelled key fails to deserialize instead of silently disabling a ceiling.
Under the retraction profile, a read returns only live findings
Without the profile, evaluate_read checks the store allowlist alone. With finding_retraction configured and a pinned resolver injected at construction, three more gates run on every read:
- Exact key. A
MemoryReadwhosekeyisNone, which is what a similarity search over a collection produces, is refused. - Resolver pin. When
resolver_id()orfeed_id()no longer matches the value pinned at construction, the read is refused instead of resolving against a substituted feed. - Live status.
resolve_live_findingmust return a resolution whosefeed_idis the pinned one and whosevalueisFindingStatusValue::Live.Pending,Retracted, and a resolver error are refused.
A key that fails to resolve live joins the quarantine set for (store, key). A later read asks the resolver again, and a live answer removes the marker while the set is unsaturated. The set is bounded by MAX_QUARANTINED_FINDING_KEYS: one more unresolved key at capacity sets saturated, every later resolution is refused with "Finding memory quarantine state is saturated", and no live answer clears it. A store or key longer than MAX_QUARANTINE_COMPONENT_BYTES counts as quarantined without being stored, so an oversized key cannot grow the set.
The profile also rechecks after admission. requires_dispatch_revalidation is true, and revalidate_before_dispatch reruns the read evaluation immediately before dispatch, turning a fresh refusal into KernelError::GuardDenied("memory-governance dispatch revalidation denied"). required_finding_status_feed_id names the feed a governed write is admitted against, and returns "Finding memory write resolver identity changed" when the resolver has drifted off its pin. validate_output_before_release resolves the finding once more and refuses output whose canonical SHA-256 differs from the memory_content_sha256 in the verified write provenance.
Quarantine acts at read time. Entries derived from a finding before its retraction stay in the store, and conclusions an agent already acted on stand; Cognition Market covers the purchase path and Finding Status the retraction and status feed.
A refusal names the guard, and a retraction refusal names its stage
Every gate returns GuardDecision::deny(Vec::new()), and that empty vector is the evidence array. The decision carries no reason label, matched store, requested TTL, byte size, or counter value. A caller that needs the gate reads the kernel's Deny reason, which names the guard. The retraction paths raise KernelError::GuardDenied with a specific message, so a refusal at dispatch revalidation or output release reads differently from one at evaluation.
Counters and the quarantine set live inside the guard struct, and a kernel restart resets both. A deployment that needs durable entry caps enforces them in the memory store.
The guard governs logical entries: how many, how large, how long they live, and whether their source finding is live. Process memory, CPU time, and syscalls are bounded one layer down, in the WASM host, where the kernel meters fuel per invocation.
Read more in Fail-Closed Semantics.
Memory governance fields, errors, and cost
Search for store_allowlist, finding_retraction, or InvalidPattern.
Fields of MemoryGovernanceConfig, defined in crates/guards/chio-guards/src/memory_governance.rs.
enabled
When false, every evaluation returns allow before any action is extracted.
store_allowlist
Stores the agent may write or read, unioned with Constraint::MemoryStoreAllowlist on the matched grant. An empty union skips the store check.
Example["issue-memory", "sku-*"]
max_memory_entries
Write cap per (agent_id, capability_id). With Some(n), the write that takes the counter past n is refused.
ExampleSome(500)
max_retention_ttl_secs
Ceiling on the retention TTL a write may declare. Under a set ceiling, a write that declares none is refused.
ExampleSome(86_400)
max_content_size_bytes
Byte ceiling on one write's content. Under a set ceiling, a write whose size cannot be determined is refused.
ExampleSome(64 * 1024)
deny_patterns
Regexes compiled once at construction. A write whose content text matches any of them is refused.
Exampler"AKIA[0-9A-Z]{16}"
finding_retraction
Turns on the retraction profile. Carries resolver_id and feed_id, and changes both the write path and the read path.
Retraction profile construction
Setting finding_retraction alone builds no guard. The profile needs a synchronous, deployment-pinned FindingRetractionResolver handed in through MemoryGovernanceGuard::with_config_and_retraction_resolver; the trait is synchronous because guard evaluation cannot park a memory read while a network lookup completes. Both constructors route through the same private build, which checks configuration and resolver against each other and refuses with one of four MemoryGovernanceError variants.
MissingFindingRetractionResolver
The config names a policy and no resolver was supplied: "finding retraction policy requires an injected resolver".
UnexpectedFindingRetractionResolver
A resolver was supplied while the profile stayed off: "finding retraction resolver supplied without an enabled policy".
FindingRetractionResolverIdentity
The resolver's resolver_id() differs from the one the policy pinned.
FindingRetractionResolverFeed
The resolver's feed_id() serves a different status feed than the policy pinned.
InvalidPattern
A deny_patterns entry failed to compile. Carries the offending pattern and the underlying regex::Error, so traffic never reaches a half-configured guard.
Failure modes
- A poisoned counter refuses.
bump_countermaps a poisoned mutex toKernelError::Internal("memory-governance guard counter mutex poisoned"). The kernel's guard loop treats any guard error as a refusal and reportsguard "memory-governance" error (fail-closed), so every later write that reaches the counter on that guard instance is refused too. - A bad regex never runs. Construction fails with
MemoryGovernanceError::InvalidPattern. - A poisoned quarantine map reports quarantined.
is_finding_quarantinedreturnstruewhen the lock cannot be taken, so observability never upgrades an indeterminate state to live.
Run cost
The hot path is a handful of serde_json lookups over the argument object, one HashMap bump behind a mutex, and an optional regex sweep over the content text. A disabled guard returns before the extractor runs, and an action that is neither a memory read nor a memory write returns from the match arm. Regex cost is bounded by the operator's pattern set, since the guard ships no built-in detector; cross-cutting redaction on results is QueryResultGuard in chio-data-guards, on Data-Layer Guards.
The retraction profile changes that shape. Every governed read makes a synchronous resolver call, and the resolver composes a provenance lookup, a verified lineage edge, and an authenticated status cache. Its latency sets the read path's cost.