Add AUDIT-WP-0006 for honest custody against live canon.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Review of info-tech-canon 0.6.0, resource-control, fin-hub, and
railiance-master found production still claims archive custody after
the platform backup and ITC-CAP catalog made that claim false.
This commit is contained in:
tegwick 2026-08-15 20:45:13 +02:00
parent c2af842c55
commit 5433562d7a
3 changed files with 353 additions and 6 deletions

View file

@ -23,7 +23,13 @@ audit-core exists to provide the capability described in INTENT.md.
## Current State
- Status: active; implementation and stability should be verified by the repo agent.
- Status: active. AUDIT-WP-0001…0005 finished.
- Production receiver on railiance01 (`namespace audit-core`), Postgres
append-only store on `platform-pg`, sender `user-engine`.
- Recovery is the platform `data.backup` window (30 days, RESOURCE-WP-0002
live), not an audit-core deletion window. Production still reports
`custody_class=archive`; AUDIT-WP-0006 is the honesty/canon-join pass.
- Open workplan: `workplans/AUDIT-WP-0006-honest-custody-and-canon-join.md`.
## Getting Oriented

View file

@ -12,7 +12,7 @@
| workplan | AUDIT-WP-0002 | finished | — | workplans/AUDIT-WP-0002-pluggable-audit-backend.md |
| workplan | AUDIT-WP-0003 | finished | — | workplans/AUDIT-WP-0003-user-engine-event-ingestion-service.md |
| workplan | AUDIT-WP-0004 | finished | — | workplans/AUDIT-WP-0004-receiver-correctness-and-hardening.md |
| workplan | AUDIT-WP-0005 | active | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| workplan | AUDIT-WP-0005 | finished | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0001-T01 | done | — | workplans/AUDIT-WP-0001-statehub-bootstrap.md |
| task | AUDIT-WP-0001-T02 | done | — | workplans/AUDIT-WP-0001-statehub-bootstrap.md |
| task | AUDIT-WP-0001-T03 | done | — | workplans/AUDIT-WP-0001-statehub-bootstrap.md |
@ -29,8 +29,8 @@
| task | AUDIT-WP-0004-T06 | done | — | workplans/AUDIT-WP-0004-receiver-correctness-and-hardening.md |
| task | AUDIT-WP-0004-T07 | done | — | workplans/AUDIT-WP-0004-receiver-correctness-and-hardening.md |
| task | AUDIT-WP-0005-T01 | done | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T02 | progress | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T03 | progress | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T02 | done | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T03 | done | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T04 | done | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T05 | progress | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T06 | progress | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T05 | done | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |
| task | AUDIT-WP-0005-T06 | done | — | workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md |

View file

@ -0,0 +1,341 @@
---
id: AUDIT-WP-0006
type: workplan
title: "Honest operational custody against ITC-CAP and the live platform backup"
domain: infotech
repo: audit-core
status: ready
owner: grok
topic_slug: railiance
created: "2026-08-15"
updated: "2026-08-15"
depends_on:
- AUDIT-WP-0005
- RESOURCE-WP-0002
- ITC-WP-0014
- ITC-WP-0015
---
# AUDIT-WP-0006 — Honest operational custody against ITC-CAP and the live platform backup
## Goal
Stop claiming a capability the live catalog and the live backup provision
contradict, and join the spine neighbours already use.
After AUDIT-WP-0005 the receiver is in production on railiance01, append-only
in `platform-pg`, fail-closed on `AUDIT_CORE_REQUIRE_CUSTODY_CLASS=archive`.
Since then three things landed that change what that claim means:
1. **info-tech-canon 0.6.0 / ITC-CAP 0.4.0** names distinct abilities:
`operations.audit` (attributable historical evidence), `data.archive`
(long-term retention under lifecycle/integrity/retrieval), `data.backup`
(recover a store), `governance.evidence`, `governance.lifecycle`.
Consumption must carry an EvidenceBasis. Maturity attaches to a
*provision*, not an abstract capability.
2. **resource-control RESOURCE-WP-0002 is finished.** Off-host Barman to
Scaleway (`resource:platform:audit-storage`) is live. Restore evidence
used `audit_core.events` as the checksum. The provision is scored
`data.backup` D4 against a D5 requirement. The 30-day window is real.
3. **fin-hub / railiance-master** tightened ownership: booked cost stays
in fin-hub; usage and inventory stay in resource-control; `rapp.yaml`
is only valid in a `rapp-*` repo; default exposure is private.
audit-core still reports `custody_class=archive`, documents archive as
"planned WORM + manifests", leaves `retention_days` unset (read as
indefinite), and keeps a reuse-surface card at A2 / `source module` /
`helix_forge`. The operator runbook still says production Barman is
fail-closed. Those statements are now false against the catalog and
against the platform.
This workplan makes the **operating claim** match the **live store and
the live backup**, and publishes the join artefacts neighbours can
require the same way they already require `data.backup`.
## Why this changes how audit-core should work
Not documentation-only. Three operating surfaces currently encode the
wrong claim:
| Surface | Today | After this workplan |
| --- | --- | --- |
| `RetentionPolicy.custody_class` + `/readyz` | `archive` | An honest class for durable operational custody. `archive` reserved for a future `data.archive` sink. |
| `AUDIT_CORE_REQUIRE_CUSTODY_CLASS` | `archive` | The honest production class. Still fail-closed: a development backend cannot serve. |
| Recovery promise | "no deletion; Barman fail-closed; do not promise an RPO" | Recoverable history = platform `data.backup` window (30 days, cited, EvidenceBasis `measured` from resource-control T05). Unbounded retention is **unknown**, not a fact. |
`None` retention is not infinite archive. Canon: unknown carries no
quantity and must name the gap and owner. The gap owner for off-host
recovery is rapp-postgres + resource-control. The gap owner for a
WORM / object-lock archive beyond that window is **this repo**, as an
unmet `data.archive` requirement — not a silent `retention_days=None`.
## Three names that must stay distinct
| Name | What it is | Owner |
| --- | --- | --- |
| `capability.audit.event-retain` / ITC-CAP `operations.audit` | Product: collect and retain audit events | **audit-core** |
| `platform:audit-core` | Postgres consumer join key for database `audit_core` | rapp-postgres (`consumers/audit-core.yaml`) |
| `platform:audit-storage` / `resource:platform:audit-storage` | Scaleway bucket for **all of** `platform-pg` Barman | resource-control + reef-storage |
Do not restatement `data.backup` here. Cite the existing capability
case at `resource-control/data/capability/platform-audit-storage.json`.
## Boundaries
**This workplan owns**
- Honest `RetentionPolicy` and the production fail-closed gate.
- Recovery language in the runbook, backend contract, and `/readyz`.
- An ITC-CAP requirement + provision record for `operations.audit`.
- A Canon Interface Card.
- Refresh of the reuse-surface card so it no longer says A2 / packaging
in progress.
- Persisting the non-secret user-engine sender tenant scope `["*"]`.
- SCOPE.md current state.
**This workplan does not own**
- Procuring or operating S3 / Barman / WAL (`resource-control`,
`rapp-postgres`, `reef-storage`).
- Booked Scaleway invoices or a second usage stream for
`platform:audit-storage` (`fin-hub` FIN-WP-0004-T05).
- Emitting `UsageObservation` or `booked_cost`. Keep the existing
`platform:audit-core` key on the postgres consumer.
- A `declarations/rapp.yaml` in this repo. The schema requires
`rapp_id: ^rapp-…`. Extraction to `rapp-audit-core` is a
railiance-master first-wave decision, not this file.
- Public ingest. ClusterIP + default-deny already matches ADR-0008
private-by-default. Do not add Ingress.
- Hash-chain / tamper evidence, hot search, object-store archive
sink, v1 nested envelope, control-plane APIs, OpenBao/K8s
collectors. Those remain INTENT residuals, not this optimisation.
- OpenBao AppRole-as-ESO-parent for `database/creds`. Residual from
AUDIT-WP-0005; different problem.
**Founder decision this workplan records, not executes:** if audit
events must survive beyond the 30-day platform backup window, that is
a `data.archive` requirement for resource-control to procure (different
bucket/lifecycle than Barman). T03 writes the requirement as unmet. Do
not build the sink here.
## T01 — Restate custody vocabulary in the running service
```task
id: AUDIT-WP-0006-T01
status: todo
priority: high
```
`CustodyClass` is currently `development | archive | hot_search`. The
backend contract defines `archive` as "long-term evidence (planned)…
durable object storage, batch manifests". The Postgres backend reports
`archive` anyway, with `tamper_evidence=False` and `retention_days=None`.
Production will not start unless the class string is `archive`.
That string is now a catalog collision with ITC-CAP `data.archive`.
Make `RetentionPolicy` say what the store actually is:
1. Add an honest production class (suggested `operational`) meaning:
durable, append-only against the runtime role, recoverable through
the platform `data.backup` provision. Keep `archive` in the enum
for a future object-store backend that can satisfy `data.archive`
hooks (`retention_policy`, `integrity_verification`, `retrieval_test`).
2. Postgres reports the honest class. `immutable=True` stays (trigger
0002). `tamper_evidence=False` stays. Do not inflate.
3. Add explicit recovery fields so `/readyz` can publish them:
recoverable window (days), source of that number, EvidenceBasis.
The 30-day figure is **cited** from the platform provision, basis
`measured` against resource-control T05 / the 2026-08-13 walk —
not re-measured here unless the walk is re-run.
4. Change `AUDIT_CORE_REQUIRE_CUSTODY_CLASS` in
`deploy/audit-core.yaml` to the honest class. Fail-closed remains:
a `development` backend still cannot serve. Accept the old `archive`
value as an alias during one deploy so a mixed rollout cannot
refuse to start.
5. Update `docs/audit-backend-contract.md`. Delete "Production archive
policy (planned)" as if the durable backend did not exist.
user-engine does not parse `custody_class`. Still treat `/readyz` as a
sender-visible contract: keep `custody_class` in the JSON and add the
new fields rather than removing the old one.
Done when: unit tests cover the new class and the alias; production
manifest requires the honest class; contract and `/readyz` no longer
call Postgres `data.archive`.
## T02 — Bind the recovery promise to the live platform backup
```task
id: AUDIT-WP-0006-T02
status: todo
priority: high
```
`docs/operator-runbook.md` still says production Barman is fail-closed
and "do not promise an RPO until that target exists". RESOURCE-WP-0002
closed that gap on 2026-08-14.
Rewrite the Restore section to:
- Recoverable history is the platform `data.backup` window (30 days,
prefix `platform-pg/`, bucket owned by `resource:platform:audit-storage`).
- Cite, do not copy, `resource-control/data/capability/platform-audit-storage.json`
and `rapp-postgres/docs/restore.md`.
- State the provision honesty: requirement D5, provision D4; RPO/RTO
numbers are theirs (`measured`, single observation). audit-core does
not claim a better grade.
- audit-core still does not expire rows. That is a **lifecycle policy
statement**, not a recovery guarantee. Rows older than the backup
window are not promised after a restore.
- Physical restore remains instance-wide; single-consumer path remains
logical export. Do not invent a second restore procedure.
Leave the 2026-08-13 walk in place as historical evidence. Add a short
dated note that the fail-closed sentence is superseded by
RESOURCE-WP-0002-T05 (30 events, 65 s full / 65 s PITR).
Done when the runbook and the T06 evidence note no longer contradict
the live backup provision.
## T03 — Publish an ITC-CAP case for the live provision
```task
id: AUDIT-WP-0006-T03
status: todo
priority: high
```
Neighbours already restate real provisions against the live catalog
(`resource-control/data/capability/platform-audit-storage.json`).
audit-core's reuse-surface card uses a private id
(`capability.audit.event-retain`), puts maturity on the capability
(violates CAP-R2), and still says availability A2.
Add a capability record valid against
`info-tech-canon/infospace/schemas/capability-record.schema.yaml` and
the live `capabilities.yaml`. Suggested path:
`data/capability/audit-core-operational.json` (or equivalent under
`registry/` if a `data/` tree is not wanted — pick one and index it).
Minimum content:
- **requires**
- `operations.audit` (profile `administrative` or `security`),
production dependency.
- `data.archive` as an **unmet** requirement if INTENT's unbounded
WORM archive is still wanted. Do not invent a provision for it.
- Do **not** re-require `data.backup`. Record it under
`uses_provisions` pointing at the existing platform provision.
- **provision**
- provider: the live receiver (landscape id, e.g.
`audit-core.railiance01`).
- capability: `operations.audit`.
- maturity: **D4** (approved production dependency). Not D5:
integrity is trigger-enforced, not proven (`tamper_evidence=False`);
reliability is not actively controlled; one replica.
- `uses_provisions`:
- platform-pg / CNPG as the transactional store (`depends_on`
only if the catalog pair exists; otherwise `may_use` and say so).
- `data.backup` provided by `rapp-postgres/platform-pg CNPG
barmanObjectStore` (`may_use` or `depends_on` per catalog).
- OpenBao / ESO as `security.secrets` (`may_use`, never class `P`).
- `consumes` in native units with EvidenceBasis (CAP-R8, CAP-R10):
- `S` stored events — `measured` from `pg_total_relation_size` or
`/v1/stats`, or `unknown` with owner if not yet queried.
- `H` operator hours — `unknown` (owner: audit-core; start a
time record later).
- `I` tokens — `unknown`.
- Do not invent euros. `invoiced` is a fin-hub fact.
- `evidence` hooks:
- `audit_records` → existing failure-matrix / live accept evidence
(`measured`).
- `integrity_verification` → gap, owner audit-core (trigger is
not a proof). Do not borrow the restore drill for this hook.
Refresh `registry/capabilities/capability.audit.event-retain.md` and
`registry/indexes/capabilities.yaml`: map to `operations.audit`, move
maturity onto the provision, status reflecting production, domain
`infotech`, consumption mode no longer "source module" only. Do not
delete the reuse-surface id; join it.
Validate with `info-tech-canon capability-review` against the live
catalog. Gaps are allowed when they name owner and disposition.
Done when the record validates and the reuse-surface card no longer
contradicts the live receiver.
## T04 — Publish a Canon Interface Card
```task
id: AUDIT-WP-0006-T04
status: todo
priority: medium
```
ITC-GOV / ITC-SEC / ITC-DATA say subsystems that produce Evidence
SHOULD publish a Canon Interface Card. Schema requires `id`, `title`,
`consumer`, `canon_surfaces`. Use the template at
`info-tech-canon/infospace/agent/templates/canon-interface-card.template.yaml`.
The card is how senders and reviewers join this repo to the canon
without reading INTENT. It is not a second capability catalog.
Declare at least:
- Produced: Evidence / AuditRecord (stored events). This service is
**not** an independent Audit-as-assessment.
- Consumed: ITC-CAP `operations.audit`, ITC-GOV Evidence / EvidenceBasis,
rapp-postgres as store, resource-control `data.backup` as recovery.
- Known deviations: no `data.archive` sink; `tamper_evidence=False`;
no hash-chain; single sender (`user-engine`); no `rapp.yaml` (not a
`rapp-*` repo); `/readyz` custody class historically overclaimed
(closed by T01).
- purpose_fit + gap rule: a gap is acceptable only with owner and
disposition.
Done when the card exists in-repo and validates against
`interface-card.schema.yaml`.
## T05 — Persist sender tenant scope and refresh SCOPE
```task
id: AUDIT-WP-0006-T05
status: todo
priority: medium
```
Inbox 2026-08-13 from net-kingdom (NK-WP-0024): live user-engine sender
was limited to `tenant:friendly:binky`; six legitimate platform events
for other tenants returned 400. The live Secret was corrected to
`tenants: ["*"]` (source `user-engine`, `may_write=true`,
`may_read=false`). That scope is **not a secret**. Persist it in the
authoritative senders document / deployment source so an ExternalSecret
refresh cannot revert it. Do not write token values into Git.
`docs/senders.example.json` already shows `["*"]`. Confirm the live
Secret and the OpenBao KV / wrap-migrate source match. If the
workstation `~/.audit-core-senders.json` was later tightened for a
matrix run, do not restore that narrow list.
Refresh `SCOPE.md` Current State: production receiver on railiance01,
Postgres operational custody, recovery bounded by platform 30-day
backup, AUDIT-WP-0001…0005 finished, this workplan active. Out of
scope stays: Barman/S3 procurement, booked cost, rapp extraction.
Done when the live sender document cannot revert to a single-tenant
list on refresh, SCOPE matches the repo, and the net-kingdom message
is answered.
## Acceptance
- Production fail-closed gate no longer keys off the word `archive`.
- `/readyz` and the runbook agree with RESOURCE-WP-0002: 30-day
recoverable window, cited, graded.
- An ITC-CAP case for `operations.audit` validates; `data.archive` is
either unmet-with-owner or explicitly not required.
- Interface card and reuse-surface card no longer describe a draft
module.
- user-engine sender tenant scope `["*"]` survives credential refresh.
- No new cost stream, no `rapp.yaml` in this repo, no public ingest,
no second backup procedure.