The validators, every check.

Defense in depth at the API boundary. 23 validator functions, every limit explicit, every closed set enumerated. The same checks run on HTTP, MCP, CLI, and inbound federation sync — there is no privileged path that bypasses them.

23 functions 9 limits 3 closed sets red-team #235 / #240 hardened
Hard limits at a glance

Every numeric ceiling.

Constants live in src/validate.rs at module top. Bumping any of these is a wire-format change — clients should treat them as the contract.

512
MAX_TITLE_LEN (chars)
65 536
MAX_CONTENT_SIZE (bytes)
512
MAX_NAMESPACE_LEN (chars)
8
MAX_NAMESPACE_DEPTH (segments)
64
MAX_SOURCE_LEN (bytes)
128
MAX_TAG_LEN (bytes/tag)
50
MAX_TAGS_COUNT (per memory)
128
MAX_AGENT_ID_LEN (bytes)
64
MAX_RELATION_LEN (bytes)
128
MAX_ID_LEN (bytes)
64
MAX_AGENT_TYPE_LEN (bytes)
65 536
MAX_METADATA_SIZE (bytes)
32
MAX_METADATA_DEPTH (nesting)
1 yr
MAX TTL_SECS (= 31 536 000)
100
MAX consolidation IDs

Field-level validators — strings

validate_title(title: &str) → Result<()>CreateMemory · UpdateMemory · Consolidate
Memory title. Surfaces in lists, recall hits, and the dashboard. Trimmed length ≤ 512 characters.
Rules
  • Trimmed value must be non-empty
  • Character count ≤ 512
  • No control characters (except \n, \t)
OK "Q3 OKR review" // 14 chars OK "Multi-line\ntitle" // \n is allowed ERR "" // title cannot be empty ERR " " // trim → empty ERR "x".repeat(513) // exceeds max length ERR "bad\x00null" // invalid characters
validate_content(content: &str) → Result<()>CreateMemory · UpdateMemory · Consolidate (summary)
Memory body. The size cap protects HNSW (vectors are computed from content) and SQLite WAL.
Rules
  • Trimmed value must be non-empty
  • Byte length (not chars) ≤ 65 536
  • No control characters (except \n, \t)
validate_namespace(ns: &str) → Result<()>all write paths · sync_push receive
Flat or hierarchical (Task 1.4). / is the segment delimiter; flat namespaces ("global", "ai-memory") remain valid — hierarchy is opt-in.
Rules
  • Non-empty after trim
  • Length ≤ 512 characters
  • No backslash, no null byte, no space, no control char
  • No leading /, no trailing /, no empty segment (//)
  • Segments . and .. rejected — red-team #240 (path-traversal-style scope leak)
  • Depth (segment count) ≤ 8
OK "global" OK "ai-memory-mcp-dev" // flat with hyphens OK "alphaone/engineering/platform" // 3 levels OK "alphaone/engineering/platform/team/squad/pod/role/agent" // 8 levels — at cap ERR "/global" // leading / ERR "alphaone//eng" // empty segment ERR "alphaone/.." // path-traversal blocked ERR "a/b/c/d/e/f/g/h/i" // depth 9 > 8
helper: normalize_namespace(input) trims, strips leading/trailing slashes, collapses //, lowercases. Not auto-applied by write paths — opt-in to preserve case sensitivity on existing flat namespaces.
validate_source(source: &str) → Result<()>CreateMemory · sync_push receive
Closed-set discriminator stored on each memory. Adding a value here is a wire-format change — peers running older versions will reject the new value on sync.
Accepted values
  • user — human-typed
  • claude — Claude Code or other Claude client
  • hook — emitted by a Claude Code hook
  • api — generic HTTP API
  • cli — local ai-memory CLI
  • import — bulk import path
  • consolidation — output of memory_consolidate
  • system — daemon-internal stamps
  • chaos — chaos-engineering test harness
  • notify — v0.6.2 (S32) handle_notify inbox stamps
validate_agent_id(agent_id: &str) → Result<()>register agent · CreateMemory.agent_id · approval votes
NHI-hardened identifier. Permits prefixed/scoped forms; rejects shell metacharacters.
Rules
  • Non-empty
  • Length ≤ 128 bytes
  • Allowed chars: ASCII alphanumeric + _ - : @ . /
  • No whitespace, no null, no control char, no shell metacharacter
OK "ai:claude-code@host-1:pid-123" OK "host:dev-1:pid-9-deadbeef" OK "anonymous:req-abcdef01" OK "spiffe://example.org/ns/team/pod" // future SPIFFE ERR "alice bob" // space ERR "alice;rm -rf /" // shell metachar
validate_scope(scope: &str) → Result<()>CreateMemory.scope (Task 1.5)
Closed set — drives hierarchical visibility matching. Memories without explicit scope are treated as private by the query layer.
Accepted values
  • private — visible only to the originating agent
  • team — visible to agents at the same namespace prefix (one level)
  • unit — visible up the hierarchy two levels
  • org — visible across the whole organization namespace
  • collective — federation-wide visibility
validate_agent_type(agent_type: &str) → Result<()>register agent
Open ai:<name> namespace + curated short-list. Red-team #235: the original closed whitelist blocked future agents — now operators can register ai:claude-opus-4.8, ai:gpt-5, ai:gemini-2.5 without a code release.
Rules
  • Non-empty, length ≤ 64 bytes
  • Curated set always wins: human, system, ai:claude-opus-4.6, ai:claude-opus-4.7, ai:codex-5.4, ai:grok-4.2
  • Open ai:<name> form: name 1–60 chars, ASCII alphanumeric + _ - .
validate_relation(relation: &str) → Result<()>LinkBody · sync_push links receive
Closed set — KG relationship type. New relations are a wire-format change.
Accepted values
  • related_to — generic association (default for memory_link)
  • supersedes — newer memory replaces older (typed cognition)
  • contradicts — explicit disagreement (Task: contradiction detection)
  • derived_from — provenance (consolidation outputs)
validate_id(id: &str) → Result<()>memory_get · memory_update · LinkBody · consolidate
Generic memory identifier — accepts UUIDs and any other clean string ≤ 128 bytes. Format-agnostic so legacy/imported ids continue to work.
Rules
  • Non-empty after trim
  • Length ≤ 128 bytes
  • No control characters

Field-level validators — numeric & structural

validate_priority(priority: i32) → Result<()>CreateMemory · UpdateMemory
Recall ranks by priority * confidence * recency. Out-of-band values are rejected so the ranking math stays predictable.
Rules
  • 1 ≤ priority ≤ 10 (inclusive)
validate_confidence(confidence: f64) → Result<()>CreateMemory · UpdateMemory
How certain a memory is. NaN/Inf are explicitly rejected.
Rules
  • Finite (no NaN, no Inf)
  • 0.0 ≤ confidence ≤ 1.0
validate_tags(tags: &[String]) → Result<()>CreateMemory · UpdateMemory · register agent (capabilities)
Free-form tags. Used by recall + list filters; stored as a comma-joined column.
Rules
  • Vec length ≤ 50
  • Each tag (after trim) non-empty
  • Each tag length ≤ 128 bytes
  • Each tag has no control characters
validate_capabilities(caps: &[String]) → Result<()>register agent
Delegates entirely to validate_tags. Same rules.
validate_expires_at(expires_at: Option<&str>) → Result<()>CreateMemory (write path)
Strict — rejects past dates. Used when accepting fresh user input that should expire in the future.
Rules
  • If present, must be valid RFC3339
  • If present, must be in the future (now < expires_at)
  • None passes through
validate_expires_at_format(ts: &str) → Result<()>UpdateMemory
Lenient — format-only check. Updates allow past dates so callers can use it for programmatic TTL management and GC testing without round-tripping through clock manipulation.
validate_ttl_secs(ttl: Option<i64>) → Result<()>CreateMemory
Convenience input — server computes expires_at = now + ttl_secs. Capped at 1 year so a typo can't accidentally make a memory effectively immortal.
Rules
  • If present, must be > 0
  • If present, must be ≤ 31 536 000 (1 year)
validate_metadata(metadata: &serde_json::Value) → Result<()>CreateMemory · UpdateMemory
Cap on size + nesting depth — protects the JSON column from runaway payloads. governance, agent_id, scope, taxonomy fields all live here, so the cap has to be high enough for real-world use.
Rules
  • Must be a JSON object (not array, not scalar)
  • Serialized size ≤ 65 536 bytes
  • Nesting depth ≤ 32 — guards against stack overflow on recursive walk

Governance validators

validate_governance_policy(policy: &GovernancePolicy) → Result<()>set_namespace_standard write
Closed-set tags handled by serde on deserialize; this adds semantic bounds beyond serde's reach.
Rules
  • ApproverType::Agent(id) → must pass validate_agent_id
  • ApproverType::Consensus(n) → quorum must be ≥ 1
  • If any of write/promote/delete is Approve, approver must be meaningful (not Consensus(0))
  • ApproverType::Human always valid
OK { write: Any, promote: Any, delete: Owner, approver: Human } // the default OK { write: Approve, promote: Approve, delete: Owner, approver: Consensus(3) } ERR { write: Any, promote: Any, delete: Owner, approver: Consensus(0) } // quorum < 1 ERR { write: Approve, promote: Any, delete: Owner, approver: Consensus(0) } ERR { write: Any, promote: Any, delete: Owner, approver: Agent("alice;rm") } // bad agent_id

Composite validators — full payload checks

validate_create(mem: &CreateMemory) → Result<()>POST /memories · memory_store
The top-level write-path gate. Composes all single-field validators and short-circuits on first failure.
validate_title(&mem.title)?; validate_content(&mem.content)?; validate_namespace(&mem.namespace)?; validate_source(&mem.source)?; validate_tags(&mem.tags)?; validate_priority(mem.priority)?; validate_confidence(mem.confidence)?; validate_expires_at(mem.expires_at.as_deref())?; // strict — rejects past validate_ttl_secs(mem.ttl_secs)?; validate_metadata(&mem.metadata)?;
validate_memory(mem: &Memory) → Result<()>import path · sync_push receive
Like validate_create but for fully-realized Memory records. Permits past expires_at (importing historical data).
Adds beyond validate_create
  • validate_id(&mem.id)
  • access_count >= 0
  • created_at, updated_at are RFC3339
  • last_accessed_at, expires_at RFC3339 if present (no chronological check)
validate_update(update: &UpdateMemory) → Result<()>PUT /memories/:id · memory_update
Validates only the fields that are present (every field on UpdateMemory is Option). Uses validate_expires_at_format not validate_expires_at — past dates allowed for programmatic TTL management.
validate_consolidate(ids, title, summary, namespace) → Result<()>memory_consolidate
Bulk consolidation — collapses N memories into 1 derived summary. The cap of 100 keeps the LLM call bounded.
Rules
  • 2 ≤ ids.len() ≤ 100
  • No duplicate ids in the input set
  • Each id passes validate_id
  • Title passes validate_title
  • Summary passes validate_content
  • Namespace passes validate_namespace
Defense in depth

Where these validators run.

There is no privileged path that bypasses the validators. Every entry that creates or mutates state on the server runs the same set of checks.

HTTP handlersvalidate_create / validate_update / validate_link at request decode
MCP handlers → same validators, ahead of the daemon dispatch
CLI → calls into HTTP daemon → same validators
Federation receive (POST /sync/push)validate_memory on every received row before insert; rejects malformed peer payloads
Hooks (Claude Code) → calls HTTP daemon → same validators
Import (memory_import)validate_memory on every row

A failed validator returns a 400-class HTTP error with the exact bail! message. AI clients can surface the reason verbatim — there is no "validation failed" placeholder.