Skip to content

fix(privacy): redact credentials on every write path and mask them when shown - #569

Open
kevintseng wants to merge 6 commits into
mainfrom
fix/redact-every-write-path
Open

kevintseng wants to merge 6 commits into
mainfrom
fix/redact-every-write-path

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

What this fixes

Limits

  • Names, tags, relations and agent message payloads are identifiers or protocol data, and are stored as given.
  • Text stored before this change is not rewritten in the database. It is masked when shown. forget and the memory tool's str_replace select a line by its exact stored text, which export returns.
  • There is no data invariant check for this change.

Tests

npm run verify is green on the final tree. New tests cover each write path, the display paths (including the dashboard routes), the SessionStart hook with a 300 KB legacy row and handoff, and the redactor's linear time on eyJ runs. Each was checked to fail without its fix.

Refs #523, #464, #554, #565, #567

…lines

Credential-shaped text (a connection string's password, `token=…`, API keys,
PEM private keys including truncated and JSON-escaped ones, a `Bearer`
token on the next line) is replaced with `***REDACTED***` before it is
stored, on every write path:

- `remember`, `learn` (including the derived lesson name), `task_state`,
  every direct graph write (entity create, batch, metadata updates,
  accepted dream proposals, staged product improvements); metadata is
  redacted in the form it is serialised in, and a guard keeps only its
  pattern as written;
- hook captures (commit, session summary, pre-compact, handoff), redacted
  before a title is shortened;
- `import` (create, append, overwrite) and the text values of the metadata
  an import may bring in, including any `replaced_history`;
- `replace`: the version it files into `replaced_history` is redacted
  before the history's size limits are applied; a version too large to
  keep is left out on its own, the earlier versions stay, and the result
  says so (`previousVersionDropped`);
- the Anthropic memory tool (create, str_replace, insert), on the whole file
  text; its size limit is checked after redaction.

Redaction is idempotent, so editing a file through the memory tool no
longer removes lines that follow a redaction marker.

`forget` and memory-tool `str_replace` act only on an exact match of the
stored text. When only the redacted spelling of the selector is stored,
they refuse with one line and change nothing, instead of acting on a line
that another secret may also have produced.

Text printed into an agent's context — the briefing's ranked sections,
index, handoff and task state, the SessionStart injection, pre-edit recall
and its guard warnings — and `task_state` over MCP, HTTP and the CLI (reads
and write responses), and `/v1/doctor`, are redacted. `recall` and the
memory tool's `view` return memories stored before this change as stored.

Refs #523, #464, #554
…glued and escaped credentials

A memory whose text holds a BEGIN or END line of a private key without the rest no longer takes new metadata text in the same write: the new text cannot be told apart from the rest of the key, so the whole write is refused and nothing is written. memesh pin and memesh unpin add no text, so they are allowed, and they mask every part of a key in that memory's metadata, including history entries that hold one; after that the memory takes new text again.

A key or name=value credential written directly after another credential is masked in full, a credential written in JSON escapes inside a duplicate key counts as secret-shaped, and a history entry with a missing or non-text title still masks a key split across its observations.
…art of a private key

A write is refused when the memory's stored title, observations, metadata or history hold a BEGIN or END line of a private key without the rest and the write adds any text, including a new metadata key name, whatever its timestamps are. memesh unpin clears such a line in the metadata, also on a memory that was never pinned, and memesh forget --observation removes one from the observations. A task state update is one write again: when its metadata cannot be stored, its new observation is not kept either.

Credentials glued into a long chain are masked in time proportional to the text, and text with escapes nested more than eight levels deep is masked on its own without affecting the strings stored with it.
A run of keys glued together, or deeply nested JSON text, no longer makes redaction slow: a search that comes back to where its last match ended skips ahead, and nested levels do not repeat a check already made for the whole text. memesh pin, unpin and task print a refused write as one line (or a JSON error) and exit 1, and the message names how to clear a lone key line in a memory's title as well as in its metadata and observations.
…e-path

The history a replaced memory keeps is bounded in one place (replaced-history.ts) for both replace and import overwrite: a version too large for the cap keeps as many observations, then tags, as fit, and its title is cut when even that does not fit, so a replaced version is always kept and the previousVersionDropped result field is gone.

Also in this merge:
- recall (CLI, MCP, HTTP), the memory tool's view and the dashboard (GET /v1/entities and /v1/entities/:name, history included) mask a credential an older version stored, without changing the stored row; export keeps the stored text; insert numbers lines as view shows them.
- forget and str_replace refusals say that shown text is masked and a line is selected by its stored text.
- redacting text with long runs of `eyJ` is linear: the JWT search takes each token run's first `eyJ` once instead of rescanning the run from every `eyJ` (300 KB: about 45 s before, milliseconds now; same masking), so the SessionStart hook and the handoff view redact full text within their time budget (#567).
- import overwrite redacts the replaced version before bounding the history, so the history stays within its cap.
- memesh learn prints one error line and exits 1 when a write is refused, as pin, unpin and task already do.
- hooks redact the metadata they store (post-commit file names) and print a guard message with the home directory shown as ~.
Comment thread tests/core/redact-secrets.test.ts Fixed
…the CRLF fixture

The briefing test expected `~/runbook.md`, which is `~\runbook.md` on Windows. The CRLF fixture now replaces the line ending with an anchored pattern.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants