# How the Sanitization Boundary Protects ai-memory Data: A Type-Safe Security Architecture

> Discover how the ai-memory Sanitization Boundary shields your knowledge base. Learn how type-safe sanitization prevents malicious data from reaching your SQLite store or LLMs.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: architecture
- Published: 2026-09-01

---

**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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/sanitize.rs)**. This file defines the `Sanitizer` struct and its companion `Sanitized<T>` wrapper.

```rust
// 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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs)** (lines 166-170).

### The Sanitized<T> Type Contract

The `Sanitized<T>` wrapper is the **load-bearing type** that enforces the boundary. Its constructor in [`sanitize.rs`](https://github.com/akitaonrails/ai-memory/blob/main/sanitize.rs) performs three critical operations:

1. Invokes `sanitizer.scrub()` on the raw content
2. Applies a **2,000-character cap** to prevent bloated observations
3. Returns a sealed wrapper that downstream components trust

```rust
// 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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs)** (line 806) finalizes the boundary by restricting its input type:

```rust
// 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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs)** (line 3382): Hook payload processing
- **[`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs)** configuration (lines 166-170) shows the extension pattern:

```rust
// 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 **`Sanitizer`** struct in [`crates/ai-memory-core/src/sanitize.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/sanitize.rs) provides the only legal path for creating **`Sanitized<T>`** instances
- The SQLite **store writer** in [`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs) (line 806) accepts only `Sanitized<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`](https://github.com/akitaonrails/ai-memory/blob/main/ai-memory-hooks/src/router.rs) (line 3382) and [`ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/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.