Chio/Docs
LOGIN · JOIN

Memory Governance

Writes to agent memory stay inside a store allowlist and set ceilings, and reads return only findings that are still live.

Concept chio@5f63e6e

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.

Memory governanceChoose an operation.
Admitted write: admitted to the store. Writes pass provenance, store, TTL, size, patterns, and entries in order; reads pass store, exact key, resolver pin, and live status.provenancestoreTTLsizepatternsentriesstoreexact keyresolver pinlive statusMemoryWriteMemoryReaddashed gates run only with the retraction profileaKsagent-notesAdmitted write: admitted to the store. Writes pass provenance, store, TTL, size, patterns, and entries in order; reads pass store, exact key, resolver pin, and live status.provenancestoreTTLsizepatternsentriesstorekeyresolverliveMemoryWriteMemoryReaddashed gates run only with the retraction profileaKsagent-notes

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.

The guard under the configuration from the wiring example, evaluated in your browser; the operations and the starting count of 41 entries are illustrative. The four writes run without the retraction profile and the read runs with it. Malformed actions and non-memory actions, which return before any gate, are left out.
  1. Finding provenance, with the retraction profile only. The arguments must carry a non-empty FINDING_DELIVERY_RECEIPT_ID_ARGUMENT string (finding_delivery_receipt_id), or the write is refused before any ceiling is read.
  2. Store allowlist. store_matches takes * as any store, prefix* as a prefix match, and anything else as an exact comparison.
  3. Retention TTL. extract_retention_ttl reads the first of retention_ttl, retentionTtl, retention_ttl_secs, retentionTtlSecs, ttl, ttl_secs, expires_in, or expiresIn that parses as a u64. A value above the ceiling is refused, and a missing TTL reads as a request for indefinite retention and is refused.
  4. Content size. extract_content_size_bytes reads content_size, contentSize, content_bytes, or size, then falls back to the byte length of the content text. A write whose size cannot be found is refused.
  5. Deny patterns. extract_content_text takes the first non-empty string among content, text, value, vector_text, and payload, and any pattern that matches it refuses the write. A write with none of those keys goes unscanned.
  6. Per-session entry limit. bump_counter increments the count for (agent_id, capability_id) and refuses once the new value passes max_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.

src/main.rs · embedding the kernelrust
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:

  1. Exact key. A MemoryRead whose key is None, which is what a similarity search over a collection produces, is refused.
  2. Resolver pin. When resolver_id() or feed_id() no longer matches the value pinned at construction, the read is refused instead of resolving against a substituted feed.
  3. Live status. resolve_live_finding must return a resolution whose feed_id is the pinned one and whose value is FindingStatusValue::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

Fields of MemoryGovernanceConfig, defined in crates/guards/chio-guards/src/memory_governance.rs.

enabled

boolDefault true

When false, every evaluation returns allow before any action is extracted.

store_allowlist

Vec<String>Default empty

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

Option<u64>Default None

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

Option<u64>Default None

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

Option<u64>Default None

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

Vec<String>Default empty

Regexes compiled once at construction. A write whose content text matches any of them is refused.

Exampler"AKIA[0-9A-Z]{16}"

finding_retraction

Option<FindingRetractionGuardConfig>Default None

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

MemoryGovernanceErrorConstruction

The config names a policy and no resolver was supplied: "finding retraction policy requires an injected resolver".

UnexpectedFindingRetractionResolver

MemoryGovernanceErrorConstruction

A resolver was supplied while the profile stayed off: "finding retraction resolver supplied without an enabled policy".

FindingRetractionResolverIdentity

MemoryGovernanceErrorConstruction

The resolver's resolver_id() differs from the one the policy pinned.

FindingRetractionResolverFeed

MemoryGovernanceErrorConstruction

The resolver's feed_id() serves a different status feed than the policy pinned.

InvalidPattern

MemoryGovernanceErrorConstruction

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_counter maps a poisoned mutex to KernelError::Internal("memory-governance guard counter mutex poisoned"). The kernel's guard loop treats any guard error as a refusal and reports guard "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_quarantined returns true when 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.

Next: govern the stores beneath a memory tool

Memory Governance · Chio Docs