Retention and redaction lifecycle#
Regulated deployments need destructive and time-bound operations to be governed: two people for an irreversible erasure, an explicit retention schedule per workflow, and a legal hold that overrides both. BitExact makes each act tamper-evident evidence in itself.
Dual-control (two-person) redaction#
Redaction destroys committed values irreversibly (the field's commitment is kept, so the run still verifies, but the value is gone). Beyond the RBAC and CSRF gates, a redaction takes two people:
- Request — one principal names the steps/fields and a reason.
bitexact redaction request <run> --steps 5 --fields 5:result.ssn --reason "PII"(or the run-detail Redact drawer). The request is recorded on the run's tamper-evident custody chain asredaction-requested. - Approve — a different principal approves; only then is anything
destroyed.
bitexact redaction approve <run> --by <approver>. Self-approval is refused, and the refusal names the requester. The approval records, on the custody chain, the requester, the reason, the field count, and the pre → post head — the explicit re-seal. - Reject clears the request without redacting, and takes a reason of
its own (
--reason, orreasonin the API body - required), recorded on the custody chain asredaction-rejectedbeside the request it answers.
bitexact redaction status <run> shows what is pending on one run, and
GET /api/redactions every pending request on the runs the caller may see -
its run, steps, fields, reason, requested_by and requested_at - so
an approver can find them. The run's own GET /api/runs/<run>/redaction
names its requester, who may not approve it, and both say in dual_control
whether this inspector's identities can complete a two-person redaction at
all; so does the answer to a request.
The erasure itself is the existing on-chain redact_in_place: the redaction
lands as a chained audit entry before any value is destroyed (write-ahead), and
the run is re-verified after. The single-control bitexact redact remains as an
admin break-glass primitive; the two-person lifecycle is the governed path.
Making dual-control mandatory. Set BITEXACT_REQUIRE_DUAL_CONTROL=1 to
disable the immediate redact path entirely (CLI and API) so every redaction
must go through request → approve. On the command line the actor is whatever
--by is typed, so with dual control set bitexact redaction request and
bitexact redaction approve refuse as well: both decisions are taken in the
viewer, where each principal is authenticated. status and reject destroy
nothing and stay available on the CLI. The two-person guarantee is only as
strong as the identities your deployment asserts: it is meaningful under
reverse-proxy/SSO auth, where the requester and approver are distinct
proxy-asserted principals, and under named viewer tokens
(<token>:<name>:<role>, principal token:<name> - docs/identity-access.md).
An unnamed token names a role, not a person (the principal is
token:<role>), so two holders of the same role's unnamed token are one
principal and the approval is refused as self-approval.
Retention policy per workflow#
A workflow declares a retention schedule as a governance act on its append-only ledger:
bitexact workflow set-retention claims-triage --schedule adverse=7y,standard=3y
Each run commits its own retention_deadline to its chain (in a
decision_record); that committed deadline — not the mutable policy — is the
floor bitexact purge honors (see [#76]). bitexact retention <workflow> (and
the workflow page's Retention lifecycle panel) reconcile the two: which runs
are past their committed deadline and purge-eligible, which carry no committed
deadline at all (a policy-application gap), and which are on legal hold. A
declared policy with runs missing a deadline exits non-zero — a CI signal that
the policy was not applied everywhere.
BitExact reports the gap between the declared schedule and the committed
deadlines; it never rewrites a committed deadline, which stays evidence. The
report's run_ids names the runs behind each summary count
(past_due_purgeable, missing_deadline, on_hold).
What a purge would take. GET /api/retention/purge-preview?older_than=<days>&mode=delete|archive
answers what bitexact purge --older-than <days> --delete (or --archive)
would do now, over the runs the caller may see, and changes nothing: the runs
it would remove (would_purge), and the ones it would refuse (blocked, each
with blocked_by - hold, deadline, recording or unreadable - and the
reason the command prints). It applies the command's own rule, so the preview
and the purge agree on every run. purge reclaims the embedded store only; on
the PostgreSQL backend the preview answers available: false and points at
bitexact archive.
Legal hold#
A legal hold (bitexact hold <run> --reason ... --by ... / the run header
control) blocks purge and overrides the retention deadline: a held run is never
purge-eligible regardless of its committed deadline. The hold is a chained
hold audit entry on the run itself - permitted after the seal like a
redaction claim, covered by a fresh recorder checkpoint when the recorder
signs - and a release (bitexact release-hold <run> --reason ... --by ...)
is a chained hold_release entry. Placing and releasing each require a
reason - the matter or ticket the hold is for, and why it can end - recorded
in the chained entry and, from the viewer, on the run's custody chain; an empty
one is refused before anything is written. Nothing outside the
chain places or lifts a hold: there is no marker file to delete, the hold
travels with the run through export, migration and archive, and the standalone
verifier verifies it with the rest of the chain. A hold does not make a run
read as recent: retention ages a run by its evidence, not by the audit entries
placed on it. A hold can be placed and released whatever the license's
state, expired included, and so can a redaction be requested, decided and
applied: legal hold and erasure are obligations owed to a court or a data
subject, and a late renewal must never be the reason one is missed. Releasing
a hold deletes nothing - bitexact purge itself waits for a valid license.