How the Sanitization Boundary Protects ai-memory Data: A Type-Safe Security Architecture
The Sanitization Boundary in ai-memory protects knowledge base integrity by enforcing that all external data passes through a type-safe Sanitizer before reaching the SQLite store, preventing secrets, malformed content, and malicious payloads from ever being persisted or sent to LLMs.
The ai-memory project, developed by Akita On Rails, implements a rigorous security model for ingesting untrusted text from hooks, HTTP requests, and user submissions. Rather than relying on ad-hoc validation, the codebase treats sanitization as a compile-time guarantee through Rust's type system. This article examines how the Sanitization Boundary works, where it is enforced, and why it matters for production AI systems.
What Is the Sanitization Boundary?
The Sanitization Boundary is the architectural edge where data transitions from untrusted external sources into ai-memory's trusted internal domain. Every component that accepts input from outside the codebase—wiki edits, webhook payloads, API requests—must cross this boundary through a single, controlled gateway: the Sanitizer type.
The boundary has three defining characteristics:
- Type-level enforcement: The
Sanitized<T>wrapper cannot be constructed without passing through the sanitizer - Single-writer validation: The SQLite store writer only accepts
Sanitized<Observation>objects - Comprehensive coverage: All ingestion points use the same sanitization pipeline
The Core Sanitizer Implementation
The heart of the boundary lives in crates/ai-memory-core/src/sanitize.rs. This file defines the Sanitizer struct and its companion Sanitized<T> wrapper.
// Creating a sanitizer with built-in security policies
let sanitizer = ai_memory_core::Sanitizer::builtin();
// The sanitizer applies multiple scrubbing rules:
// - Secret removal (API keys, tokens, passwords)
// - Line length limits
// - Whitespace normalization
// - Structural validation
The Sanitizer::builtin() method provides a default policy suitable for most deployments. For specialized needs, projects can extend this via Sanitizer::with_extra_patterns, as demonstrated in crates/ai-memory-wiki/src/wiki.rs (lines 166-170).
The Sanitized Type Contract
The Sanitized<T> wrapper is the load-bearing type that enforces the boundary. Its constructor in sanitize.rs performs three critical operations:
- Invokes
sanitizer.scrub()on the raw content - Applies a 2,000-character cap to prevent bloated observations
- Returns a sealed wrapper that downstream components trust
// Scrubbing untrusted input
let raw_payload = r#"my secret: sk-abc123xyz
normal observation text"#;
let cleaned = sanitizer.scrub(raw_payload);
// Result: "normal observation text" (secret removed, whitespace normalized)
// Wrapping for store submission
let observation = ai_memory_core::Observation::new(/* ... */);
let safe_observation = ai_memory_core::Sanitized::new(observation, &sanitizer);
// safe_observation now carries type-level proof of sanitization
Only Sanitized<T> instances can cross into the storage layer. Attempting to submit raw data triggers a compile-time error, not a runtime failure.
Store-Layer Enforcement
The SQLite writer actor in crates/ai-memory-store/src/writer.rs (line 806) finalizes the boundary by restricting its input type:
// Simplified signature from writer.rs
impl StoreWriter {
pub async fn submit(&mut self, obs: Sanitized<Observation>) -> Result<(), StoreError> {
// Persists to SQLite with guaranteed-clean data
}
}
This single-writer design means no bug can accidentally bypass sanitization. Even if a developer forgets to scrub data at an ingestion point, the compiler rejects the code. The type system becomes a security mechanism.
The character cap enforcement appears at multiple boundary crossings:
crates/ai-memory-hooks/src/router.rs(line 3382): Hook payload processingcrates/ai-memory-mcp/src/server.rs(line 801): MCP server request handling
Boundary Crossings Throughout the Stack
The Sanitization Boundary is consistently applied across every component that handles external text:
Wiki Content Ingestion
In crates/ai-memory-wiki/src/wiki.rs (lines 1131-1196), all page writes pass through the sanitizer before persistence. The wiki layer explicitly configures extended patterns for its specific content types.
Hook Payload Processing
The router in crates/ai-memory-hooks/src/router.rs (lines 437-4592) sanitizes incoming webhook payloads at the edge. This prevents malicious hooks from poisoning the knowledge base or triggering downstream exploits.
Markdown Rendering Pipeline
Even presentation-layer code enforces hygiene. crates/ai-memory-web/src/markdown.rs (line 240) applies sanitization to HTML events generated from user-authored markdown, preventing XSS-style attacks through rendered content.
MCP Server Requests
The Model Context Protocol server in crates/ai-memory-mcp/src/server.rs (lines 404-4204) injects the sanitizer into all request handling. This ensures that LLM-generated or LLM-consumed data remains within safe bounds.
Extended Sanitization Policies
Projects can customize boundary behavior without compromising core guarantees. The crates/ai-memory-wiki/src/wiki.rs configuration (lines 166-170) shows the extension pattern:
// Custom sanitizer with project-specific rules
let wiki_sanitizer = Sanitizer::builtin()
.with_extra_patterns(vec![
// Remove internal document markers
Regex::new(r"^\s*DRAFT\s*$").unwrap(),
// Strip version control annotations
Regex::new(r"\$Revision:[^\$]*\$").unwrap(),
]);
These extensions compose with built-in rules rather than replacing them. The core secret-scrubbing and structural protections remain active.
Security Guarantees Provided
The Sanitization Boundary delivers provable protection against several threat categories:
| Threat | Boundary Countermeasure |
|---|---|
| Secret leakage | Built-in regex patterns detect and remove API keys, tokens, passwords |
| Memory exhaustion | 2,000-character cap on all observations |
| Injection attacks | Structured output with normalized whitespace, no raw HTML passthrough |
| Type confusion | Sanitized<T> wrapper prevents accidental use of raw data |
| Bypass vulnerabilities | Single-writer store design eliminates alternative code paths |
Summary
- The Sanitization Boundary is a type-safe architectural boundary in ai-memory that controls all data flowing from external sources into the knowledge store
- The
Sanitizerstruct incrates/ai-memory-core/src/sanitize.rsprovides the only legal path for creatingSanitized<T>instances - The SQLite store writer in
crates/ai-memory-store/src/writer.rs(line 806) accepts onlySanitized<Observation>, making bypasses impossible at compile time - Built-in policies remove secrets, limit content length, and normalize structure; extensible patterns allow project-specific customization
- Every ingestion point—wiki writes, hook processing, markdown rendering, MCP requests—applies identical sanitization through
sanitizer.scrub()
Frequently Asked Questions
What happens if code tries to submit unsanitized data to the store?
The Rust compiler rejects the code. The store writer's submit() method explicitly requires Sanitized<Observation>, and there is no public constructor for Sanitized<T> that bypasses the Sanitizer. This type-level guarantee prevents entire categories of security bugs.
Can the default sanitization rules be disabled or weakened?
No. The Sanitizer::builtin() rules are always active. The with_extra_patterns() method only adds supplementary patterns—the core secret detection and structural protections cannot be removed. This design prevents accidental security degradation.
Where does the 2,000-character observation limit come from?
The cap is hardcoded at multiple boundary enforcement points: ai-memory-hooks/src/router.rs (line 3382) and ai-memory-mcp/src/server.rs (line 801). This limit prevents resource exhaustion attacks and keeps LLM context windows manageable.
Does sanitization affect performance?
The scrubbing operations are synchronous string processing applied once per ingestion. For typical observation sizes under the 2,000-character cap, overhead is negligible. The tradeoff favors security over raw throughput, appropriate for a knowledge base that persists data indefinitely.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →