Memory Governance
Agents that persist context across calls write into a memory store. The governance guard caps what they can write, where they can write it, how long it survives, and how much per session. Source: crates/chio-guards/src/memory_governance.rs.
Surface
The guard claims two action shapes from extract_action:
ToolAction::MemoryWrite { store, .. }runs the full enforcement chain.ToolAction::MemoryRead { store, .. }runs the store allowlist only. A read against a denied store fails closed even when the write path is also blocked.
Anything else passes with Verdict::Allow. The guard does not enforce process resource limits (RSS, CPU time) directly; WASM tool servers cap those at the host through fuel metering. See Guard Platform Architecture for where the responsibility line sits.
Config
MemoryGovernanceConfig knobs:
| Knob | Type | Default | Purpose |
|---|---|---|---|
enabled | bool | true | Master switch. |
store_allowlist | Vec<String> | empty | Stores the agent may write or read. Layered on top of Constraint::MemoryStoreAllowlist. |
max_memory_entries | Option<u64> | None | Per-(agent, capability) write cap. |
max_retention_ttl_secs | Option<u64> | None | Hard ceiling on the retention TTL declared on a write. |
max_content_size_bytes | Option<u64> | None | Byte ceiling on the content payload of a single write. |
deny_patterns | Vec<String> | empty | Regex patterns that deny writes whose content text matches. |
Two policy sources
MemoryGovernanceConfig::store_allowlist and Constraint::MemoryStoreAllowlist on the matched grant. An empty union means "no store allowlist" and the check is skipped. Operators that want to forbid every write set the config to a non-empty list that excludes the offending store.Enforcement order
For a memory write the guard runs five gates in this order. The first gate that fails returns Deny; later gates do not consume quota.
- Store allowlist. Capability + config. Wildcards:
*matches any store;prefix*matches by prefix; otherwise exact match. - Retention TTL ceiling. When
max_retention_ttl_secsis set, the arguments must include a parseable TTL via one ofretention_ttl,retentionTtl,retention_ttl_secs,retentionTtlSecs,ttl,ttl_secs,expires_in, orexpiresIn. Missing TTL with a configured ceiling is treated as a request for indefinite retention and denied. - Content size. Reads
content_size/contentSize/content_bytes/sizefrom the arguments. Falls back to.len()on the content text. Over-size: Deny. - Deny patterns. Each regex is compiled at construction time. The text body of the write (
content,text,value,vector_text,payload) is scanned. Any match: Deny. - Per-session entry limit. Bumped only after the four prior gates pass. Counter key is
(agent_id, capability_id). Crossing the limit: Deny.
Failure modes
- Counter mutex poisoning produces
KernelError::Internal, which the kernel converts toKernelError::GuardDeniedvia the fail-closed wrapper. A poisoned counter denies every subsequent write under the same guard instance. - A regex in
deny_patternsthat does not compile fails construction withMemoryGovernanceError::InvalidPattern. Policy load rejects the config; runtime traffic never sees the guard. - A memory write whose store name is missing from the arguments is treated as having no store; the allowlist check matches the empty string against patterns and almost always denies. The action extractor is the source of truth for what counts as a store key.
Evidence
The guard records the matched store, the requested TTL, the byte size, and the per-session counter value alongside the verdict on the receipt. The denial reason is structured: store-not-allowed, retention-ceiling-exceeded, size-exceeded, deny-pattern-matched, or entry-limit-exceeded.
Resource model and WASM fuel
The memory guard is an application-level control: it caps the number, size, and lifetime of logical memory entries. It does not cap process RSS, CPU time, or syscall budget. Those caps live one layer down in the WASM host, where the kernel meters fuel per invocation and aborts on overrun. The guard pipeline does not see the abort directly; it sees the resulting KernelError::GuardDenied on the next call, or a tool-server error on the failing call.
Cross-link: Guard Platform Architecture covers how application-level guards compose with host-level sandboxing.
Per-instance counters do not survive a restart
MemoryGovernanceGuard keeps the counter map inside the guard struct. A kernel restart resets every counter. Operators that need durable per-session caps plumb them into the memory store itself rather than relying on the guard's in-process state.HushSpec snippet
hushspec: "0.1.0"
guards:
memory_governance:
enabled: true
store_allowlist:
- "agent-notes"
- "vector-*"
max_memory_entries: 500
max_retention_ttl_secs: 86400 # 24h
max_content_size_bytes: 65536 # 64 KiB
deny_patterns:
- "(?i)\bssn\b"
- "AKIA[0-9A-Z]{16}"
grants:
- id: "agent-knowledge"
tools: ["memory.write", "memory.read"]
constraints:
- memory_store_allowlist:
- "agent-notes"With this config, a write to agent-notes with {ttl: 3600, content: "..."} passes if it stays under 500 entries per session, the content fits in 64 KiB, and the body does not match either deny pattern. A write to incident-log denies (not in the union of allowlists). A write without an explicit TTL denies (indefinite retention requested under a 24h ceiling).
Performance class
Hot path: pattern scan over the action's arguments, HashMap bump on the counter, optional regex sweep on content text. Empty config short-circuits inside the match on action shape. Regex cost is bounded by the operator-supplied pattern set; the guard does not run a built-in PII detector. For a deployment that needs cross-cutting PII redaction on results, see Data-Layer Guards and the QueryResultGuard.
Next steps
- Data-Layer Guards for vector and SQL stores that the memory guard delegates to.
- Default Pipeline to see where memory governance sits in the conjunction.