Problem statement
Two different problems are currently conflated under "redaction":
- Secret redaction (lossy, one-way). Drop or mask API keys/tokens before they reach the model or the logs. This is what the open PRs #1249, #1305, #1513, #1812 all implement. All four are still open and have had no maintainer engagement since 2026-08-16.
- PII de-identification (reversible). For a personal assistant, the sensitive payload is mostly not credentials — it is third-party personal data: contact names, phone numbers, street addresses, order/contract numbers, and other participants in group chats. This data must stay usable by the model while never leaving the host in plaintext. One-way masking destroys the value (the agent can no longer reason about "the person" or produce a usable draft reply), and deleting it after the fact does not help, because the value already left the machine.
nanobot's memory model makes case (2) sharper than in a chatbot:
- conversation summaries are appended to
memory/history.jsonl; - Dream consolidates them into
SOUL.md/USER.md/memory/MEMORY.md; - those durable files are fed back into the system context every turn.
So a single leaked identifier does not stay in one turn — it gets consolidated, versioned in GitStore, and re-sent to the provider indefinitely.
Proposed design
A config-driven, reversible de-identification filter at the provider boundary:
- Policy — entity classes (person name, phone, email, postal address, national ID, plus user-supplied regex), and scope flags so each surface can be filtered independently:
llm_egress,persistence(session store / history.jsonl / durable memory files),tool_output,logs. - Deterministic reversible map —
Alice <phone> → PERSON_7f3a1c / PHONE_7f3a1c, stable within a scope so the model can still track that two mentions refer to the same entity. The map store lives outside the agent-readable workspace (same placement principle already used forsessions/<workspace-id>/sessions.sqlite3) with0600permissions, and is never sent anywhere. - Restore on the return path — model output and any persisted artifact are mapped back, so the user always sees real values. Only the placeholder form crosses the network.
- Fail-closed option — if the map store is unavailable, block egress rather than silently sending plaintext.
- Zero-loss constraint — masking must never drop or re-serialize content. Existing explicit truncation markers (read/grep/exec) must survive untouched; the filter is a pure token-for-token text substitution with no compression.
- Auditability — per-turn counter of substituted/restored spans, so a user can verify the filter actually ran instead of trusting a "I understood your preference"-style claim.
Where it needs to hook (the real blocker)
We run a private in-tree patch for this today, and the reason it has to be a patch is that there is currently no supported extension point for it:
- an outbound text transform over request messages (including tool results before they are persisted), and
- an inbound inverse transform over assistant text before it is persisted or delivered to a channel.
nanobot/agent/plugins.py covers skills/MCP packaging. agent/hook.py already offers useful seams — finalize_content(context, content) -> str | None for assistant output and after_execute_tool(...) for tool results — which covers roughly half of what is needed. What is missing is the symmetric pair:
- no hook sees the outbound request messages (prior turns + tool results) before they are handed to the provider, which is exactly where de-identification has to happen;
- no hook sits on the persistence boundary (session store,
memory/history.jsonl, durable memory files), so filtered text and stored text cannot be kept consistent.
Hooks are also in-process code rather than something an installed Agent Plugin package can register today.
Question for maintainers: would you rather see this as core config (a privacy section next to tools/gateway), or as an Agent Plugin with two new hook points added? We are happy to build either.
Suggested consolidation
The four open PRs would land more cleanly if unified under one egress filter contract with two filter kinds sharing the same plumbing:
| kind | direction | use |
|---|---|---|
redact | one-way, lossy | credentials, tokens (PRs #1249 / #1305 / #1513 / #1812) |
deidentify | two-way, reversible | third-party PII in personal-assistant workloads (this request) |
Willingness to contribute
We operate this in production as a private patch and would like to upstream it — as core config or as an agent plugin, whichever the maintainers prefer. Happy to open the PR if the shape above is acceptable.
Activity
0No activity yet.