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.
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].
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
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
- Topic keys — why hash-dedup needs the strip to happen first.
- Architecture / Braess #1 — the AST-level test that enforces the invariant.