Skip to main content

Privacy

iLAB Memory has one non-negotiable privacy contract: any text inside <private>...</private> is replaced with [REDACTED] before the content is hashed, deduped, or written to disk.

This means you can include API keys, PII, or any other sensitive material in content — wrap it in <private> and the secret will never reach the SQLite store.

note

This is Braess #1, the most rigorously protected invariant in the library. AST-level tests assert that compute_normalized_hash is only ever called on the output of strip_private. The pipeline is enforced inside upsert_observation, the single call site for every mem_save / mem_session_summary.

The pipeline​

1. Caller passes raw content​

Your code calls mem_save(..., content="...sensitive..."). The raw string includes one or more <private>...</private> regions.

2. strip_private replaces tagged regions​

strip_private(content) substitutes every <private>...</private> region with the literal string [REDACTED]. Case-insensitive, supports attributes on the opener, handles nested tags via a fixed-point loop.

3. Hash on the stripped content​

normalized_hash = sha256(stripped_content). The raw input is never fed to the hash function. Two saves whose only difference is a <private> block will both produce the same hash and dedup correctly.

4. Persist the stripped content​

The Observation row stored on disk has the stripped content and title. There is no path that writes the raw input.

Example​

from ilab_memory import ILabMemory

mem = ILabMemory.from_path("./mem.db")

result = mem.mem_save(
user_id="alice",
type="preference",
title="Preferred greeting",
content="Alice prefers being greeted as 'Ali'. <private>API key: sk-ABC123</private>",
topic_key="user/alice/greeting",
)

obs = mem.mem_get_observation(user_id="alice", observation_id=result.id)
print(obs.content)
# → "Alice prefers being greeted as 'Ali'. [REDACTED]"

The string sk-ABC123 is gone — not just from the read response, but from disk. Open the SQLite file in any inspector and you will only find [REDACTED].

tip

Defense in depth. Wrap any sensitive content in <private> even if you think it is safe. The cost is a few characters; the benefit is that one careless print(obs.content) cannot leak a secret.

What strip_private will and will not match​

Matched (redacted)
  • <private>secret</private>
  • <PRIVATE>SECRET</PRIVATE> (case-insensitive)
  • <private foo="bar">attributed</private>
  • <private>multi\nline\nbody</private> (DOTALL)
  • Nested tags: <private>outer <private>inner</private> outer</private> (collapsed to a single [REDACTED])
  • Unclosed openers: <private>oops… redacts everything from that opener to end-of-string (strict-privacy default).
NOT matched (left as-is)
  • <private-section> — different tag name (hyphen breaks the opener pattern).
  • <private matters> — no = in the attribute area, so not treated as an attributed opener.
  • An isolated </private> with no preceding opener — left as literal text.

Where the strip happens​

The strip runs at the library boundary — inside upsert_observation, which is the single function every save path goes through. Both mem_save and mem_session_summary route through it. There is no path that bypasses the strip.

The HTTP and MCP API layers serialize the post-strip observation. Network responses, API logs, and database files all see [REDACTED] only.

Limits to keep in mind​

warning

The strip is regex-based on the literal <private> / </private> tag syntax. It does not detect PII semantically.

  • SSNs, credit cards, emails outside <private> blocks are stored as-is.
  • Alternative tag syntax (HTML comments, custom tags, JSON fields named "secret") is not recognized.
  • Tag-bombing attacks (deeply nested malformed tags) are bounded — the fixed-point loop caps at 64 iterations and Phase 2 redacts from the first residual opener to end-of-string. Strict privacy wins over preservation.

v0.2 may add structured PII detection. For v0.1, wrap deliberately.

Configurability — none, by design​

The replacement token is [REDACTED], full stop. It is a module-level constant with no runtime override. Why?

  • Determinism: the dedup hash includes [REDACTED]. Letting callers swap the token would break cross-process dedup.
  • Audit: a fixed token makes redacted regions trivially greppable in dumps.
  • Safety: one less knob means one less footgun.

Next​