Chio/Docs

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:

KnobTypeDefaultPurpose
enabledbooltrueMaster switch.
store_allowlistVec<String>emptyStores the agent may write or read. Layered on top of Constraint::MemoryStoreAllowlist.
max_memory_entriesOption<u64>NonePer-(agent, capability) write cap.
max_retention_ttl_secsOption<u64>NoneHard ceiling on the retention TTL declared on a write.
max_content_size_bytesOption<u64>NoneByte ceiling on the content payload of a single write.
deny_patternsVec<String>emptyRegex patterns that deny writes whose content text matches.

Two policy sources

The effective store allowlist is the union of 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.

  1. Store allowlist. Capability + config. Wildcards: * matches any store; prefix* matches by prefix; otherwise exact match.
  2. Retention TTL ceiling. When max_retention_ttl_secs is set, the arguments must include a parseable TTL via one of retention_ttl, retentionTtl, retention_ttl_secs, retentionTtlSecs, ttl, ttl_secs, expires_in, or expiresIn. Missing TTL with a configured ceiling is treated as a request for indefinite retention and denied.
  3. Content size. Readscontent_size / contentSize / content_bytes / size from the arguments. Falls back to .len() on the content text. Over-size: Deny.
  4. 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.
  5. 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 to KernelError::GuardDenied via the fail-closed wrapper. A poisoned counter denies every subsequent write under the same guard instance.
  • A regex in deny_patterns that does not compile fails construction with MemoryGovernanceError::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

policy.yaml
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

Memory Governance · Chio Docs