feat(privacy): audited redaction that keeps history's shape and removes its content

Rows in append-only history are never deleted; their content can now be
made irrecoverable through one audited path. A redaction replaces the
content columns of an event, memory, message, model call, belief or
assertion with a fixed marker, keeps identifiers, links, timestamps,
scores and breakdowns, writes an append-only ledger row with a SHA-256 of
the removed content, appends a redaction event, optionally cascades to
derived rows and to the events that quote them, and scrubs stored snapshot
archives before re-signing them. noema_redact() is the only write the
append-only trigger admits, for UPDATE only, under a transaction-local flag
the application never sets; plain updates and all deletes stay refused.
Replay labels redacted stimuli instead of inventing them.

Cryptographic erasure was considered and deferred with reasons (ADR-0009);
docs/privacy.md defines retained, redacted, irrecoverable and deleted and
states what backups and earlier exports are not covered. Migration 0023.
This commit is contained in:
Andy Dixon
2026-08-23 23:52:07 +01:00
parent 9eed6e2a18
commit 7d608c2783
33 changed files with 1245 additions and 45 deletions
+8
View File
@@ -69,6 +69,7 @@ type Summary struct {
LLMLive int `json:"llm_live"`
LLMReplayed int `json:"llm_replayed"`
Cycles int `json:"cycles"`
Redacted int `json:"redacted"` // stimuli whose content was redacted and could not be replayed
}
// Step is the outcome of replaying one stimulus.
@@ -218,6 +219,13 @@ func (s *Service) Execute(ctx context.Context, r Replay) (Replay, error) {
}
orig, _ := s.Events.Query(ctx, evstore.Filter{MindID: src.ID, AfterSeq: stim.Seq, BeforeSeq: upper, Ascending: true, Limit: 2000})
st.Original = Digest(withoutPerception(orig))
if stim.RedactedAt != nil {
// The content is gone; replaying a marker would fabricate cognition.
st.Label, st.Note = "redacted", "stimulus content was redacted under a privacy policy and was not replayed"
r.Summary.Redacted++
steps = append(steps, st)
continue
}
startedAt := time.Now()
inj, err := drv.Inject(ctx, stim, map[string]any{"replay_of": stim.ID, "replay_id": r.ID})
if err != nil {