Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a025c2-407a-7a32-b40a-f37a52f03f62
493 lines
26 KiB
Markdown
493 lines
26 KiB
Markdown
---
|
|
id: AUDIT-WP-0008
|
|
type: workplan
|
|
title: "Tenancy posture declaration and read-path enforcement"
|
|
domain: infotech
|
|
repo: audit-core
|
|
status: active
|
|
owner: claude
|
|
topic_slug: railiance
|
|
created: "2026-08-17"
|
|
updated: "2026-08-21"
|
|
depends_on:
|
|
- AUDIT-WP-0007
|
|
state_hub_workstream_id: "84c01552-4c65-45fd-bf6c-7daab0ecc9b6"
|
|
---
|
|
|
|
# AUDIT-WP-0008 — Tenancy posture declaration and read-path enforcement
|
|
|
|
## Goal
|
|
|
|
Answer the `rapp-postgres` review request against NetKingdom Tenancy Posture
|
|
v0.1, declare an accurate posture vector, and close the one gap the assessment
|
|
exposed in running code.
|
|
|
|
Canonical framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md`,
|
|
published by `policy-nexus` at `policy.coulomb.social`.
|
|
|
|
> **Framework moved twice under this workplan.** The assessment below was made
|
|
> against **draft-5**; the framework is now at **draft-8**. The assessment still
|
|
> holds — no finding was reversed — but three things changed underneath it:
|
|
> `R` moved 1 → 2 once `rapp-postgres` declared the window, a sixth axis **V
|
|
> (availability)** appeared that we had no position on, and the
|
|
> implemented-versus-evidenced distinction we used informally for `E` became a
|
|
> schema field. `tenancy.yaml` tracks draft-8 and validates clean against
|
|
> `net-kingdom/tools/tenancy-posture/validate.py`. T07 and T08 exist because of
|
|
> draft-8, not because of the original review.
|
|
|
|
## Assessment — what the ladders say about us
|
|
|
|
`rapp-postgres` guessed `audit-core I2 A3 E2 P1 R1`, and §5 carries that guess
|
|
as a worked example. Three of the five are too high.
|
|
|
|
| Axis | Guess | Actual | Why |
|
|
|---|---|---|---|
|
|
| I | 2 | **1** | Senders hold static bearer tokens (`audit_core/senders.py`); the tenant arrives in the request body (`audit_core/ingestion.py:379`) and is checked against an allowlist bound to the credential. No IdP, no verified claim. §4.1 names this case explicitly: I1 absorbs "the tenant is taken from the request rather than from a verified token". |
|
|
| A | 3 | **2** | No `flex-auth` call exists anywhere in the repo. Authorization is a single local boundary — `permits_source`, `permits_tenant`, `may_write`, `may_read`. That is the definition of A2. |
|
|
| E | 2 | **1** | The write path is E2-shaped. The read path applies **no tenant filter at all**. See below. |
|
|
| P | 1 | 1 | `platform-pg`, database per consumer. Correct. |
|
|
| R | 1 | 1 | No `backupRetentionDays` in `rapp-postgres/consumers/audit-core.yaml`; platform default 30 days applies. Correct. |
|
|
|
|
The I-axis correction is not a defect. The credential→tenant allowlist is a real
|
|
control; it is simply an **A**-axis control sitting where a reader would expect
|
|
an **I**-axis one. Decision 3.1 doing its job on its first live use.
|
|
|
|
### The E-axis finding
|
|
|
|
`_read` (`audit_core/ingestion.py:226-272`) gates on `identity.may_read` and
|
|
nothing else. `permits_tenant` is never called on the read path. A credential
|
|
with `may_read: true` can read **any** tenant's events through
|
|
`/v1/events?correlation_id=`, `/v1/events/<id>`, `/v1/dead-letters` and
|
|
`/v1/secret-findings`.
|
|
|
|
Exposure today is bounded: the production sender `user-engine` carries
|
|
`may_read: false`, and the read surface is ClusterIP behind a default-deny
|
|
NetworkPolicy. But bounded-by-deployment is not enforced-by-code, and under §6
|
|
declaring E2 on that basis would be the overclaim the framework exists to
|
|
prohibit. E1 is the honest reading until T04 lands.
|
|
|
|
This also corrects the premise of the review request. `rapp-postgres` wrote
|
|
"you are at E2, and E3 is more urgent for you than for anyone else". E3 is
|
|
indeed the right target for a service holding audit evidence, but §4.3 requires
|
|
the E2 evidence artifact alongside any E3 claim — so the first move is reaching
|
|
E2, not adopting RLS.
|
|
|
|
## Positions to take
|
|
|
|
### On the retention ceiling (their question 1)
|
|
|
|
**The 365-day ceiling is not wrong for us. Keep it.**
|
|
|
|
The ADR-0002 context line — "audit-core holds audit evidence and may need
|
|
years" — is right about the requirement and wrong about the mechanism. Physical
|
|
backup retention is not where multi-year audit custody belongs. It is already
|
|
routed: `data/capability/audit-core-operational.json` records `data.archive`
|
|
as an unmet requirement with a separate owner and an explicitly different
|
|
bucket and lifecycle from Barman.
|
|
|
|
Consequences we accept and want stated back:
|
|
|
|
- audit-core declares nothing above the 30-day platform default. We therefore
|
|
do **not** extend any co-resident's erasure horizon, now or under this plan.
|
|
- Multi-year retention is an archive procurement question, not a WAL-window
|
|
question. Raising the ceiling would buy us nothing and cost every co-resident
|
|
volume against 20Gi.
|
|
- Service class `batch` is correct. No correction wanted. Co-residency with
|
|
latency-critical `tenant-engine` is right to be reported under §8.3.3.
|
|
|
|
Ask of `rapp-postgres`: amend the ADR-0002 opening context to point at archive
|
|
rather than at backup retention, so the two statements agree.
|
|
|
|
**This position is conditional, and the condition is dated.** Routing multi-year
|
|
custody to `data.archive` is correct mechanically and unfunded practically:
|
|
the capability case records it as unmet, with no booked cost and no procurement
|
|
owner in motion. Taking this position without a date would read as "audit-core
|
|
is fine", and audit-core is not fine — it is correctly routed and unresourced.
|
|
|
|
Escalation: **reviewed 2026-12-31.** If no `data.archive` provision is procured
|
|
by then, audit-core reopens placement. §4.5 makes retention a placement trigger,
|
|
and P2 — a dedicated cluster with an independent window — becomes the fallback
|
|
worth arguing for. P2 still does not deliver WORM, so it is a worse answer than
|
|
archive; it is named here so the fallback exists rather than being improvised
|
|
under pressure. Recorded now so the review has something to fire against.
|
|
|
|
**Also taken now: R1 → R2.** R2 needs only that retention be declared per
|
|
dataset rather than inherited silently, and that the erasure horizon be
|
|
published. That is an explicit `backupRetentionDays: 30` in the consumer
|
|
declaration plus the horizon rendered in our operator surface — a change of
|
|
hours, not a future target. Deferring it as a "target" would have been the
|
|
framework's own §8.1 failure: a value in a file nobody re-reads.
|
|
|
|
### On erasure versus audit (their question 11)
|
|
|
|
Yes to the fact/payload split — retain the fact of an event, encrypt its
|
|
personal payload per subject, shred the key. But that resolution does not
|
|
survive contact with our integrity design as built, in two ways the framework
|
|
does not currently capture.
|
|
|
|
**One: it is not retrofittable.** `_record_hash`
|
|
(`audit_core/postgres_backend.py:583`) is SHA-256 over the canonical JSON of the
|
|
*cleartext* record, and that hash is what the chain commits to. Encrypting a
|
|
payload after the fact severs the binding between `payload_hash` and the stored
|
|
record. Shreddability has to be built as encrypt-then-hash at accept time, with
|
|
the chain committing to ciphertext. Retrofitting it onto existing events is not
|
|
possible without rewriting the chain, which is the one thing AUDIT-WP-0007 was
|
|
built to make detectable.
|
|
|
|
**Two, and sharper: the retained hash is a confirmation oracle.** A SHA-256 over
|
|
a canonical record whose fields are low-entropy (event type, actor, tenant,
|
|
subject, timestamp) lets anyone holding the hash guess the payload, hash the
|
|
guess, and confirm a match. Destroying the key does not make the content
|
|
unrecoverable while that hash survives. **Crypto-shredding does not reach R4
|
|
under this design.** It needs the integrity commitment to be an HMAC under a
|
|
per-subject key that dies with the key, or a per-record salt destroyed with it.
|
|
|
|
This is a genuine gap in §4.5, not just a fact about us: the key-destroyed route
|
|
treats key destruction as sufficient without requiring that no retained
|
|
commitment reveal the erased content. Recommend §4.5 gain that condition.
|
|
|
|
Our position: audit-core targets **R2**, not R4. Verified erasure of audit
|
|
evidence is a design tension we record rather than resolve, and a "R4
|
|
everywhere" fleet target must exempt this service explicitly.
|
|
|
|
**The exemption rests on a legal basis nobody in this estate has confirmed, and
|
|
that must be said out loud rather than assumed.** Declining to build erasure
|
|
means that if an Article 17 request arrives naming a data subject in the audit
|
|
trail, audit-core has no mechanism and must rely on audit evidence being
|
|
exempt — legal obligation, or legitimate interest in fraud and security
|
|
investigation. Those grounds are ordinary and widely relied on. They are also a
|
|
legal conclusion, and this repo is not competent to reach one. Nothing in
|
|
INTENT, SCOPE or the capability case records anyone having reached it either.
|
|
|
|
This workplan therefore **routes the question rather than absorbing it**: the
|
|
position is "we do not build erasure, on a legal basis we have named and not
|
|
validated", and the validation is somebody else's to own. An unnamed dependency
|
|
here is the difference between a recorded tension and an unnoticed liability —
|
|
and an audit service discovering in the middle of a regulator's question that
|
|
its exemption was assumed is the specific failure worth spending a paragraph to
|
|
avoid.
|
|
|
|
## Framework review findings
|
|
|
|
Sent under §20.2 — the framework is validated by whether it can describe us.
|
|
|
|
> **Outcome, 2026-08-17: all five were adopted in draft-7**, alongside reviews
|
|
> from `railiance-platform` and `flex-auth`. Finding 1 became **Decision 5.2**
|
|
> (declare per path, quote the minimum), finding 2 became **13.1a** (the floor
|
|
> needs a reason, not an artifact), finding 3 became **Decision 9.2**
|
|
> (consumer-facing credentials are named in), finding 4 became **Decision 4.5.3**
|
|
> (key destruction is not sufficient alone; the confirmation-oracle defect is
|
|
> recorded as general, with audit-core exempted from R4), and the §5 worked
|
|
> example now reads `I1 A2 E1 P1 R1` self-reported. Draft-7 also added
|
|
> **Decision 5.4**, which relocates the vector to `tenancy.yaml` in the repo
|
|
> root — see T01.
|
|
>
|
|
> Two of these now bind us rather than only the framework: 9.2 makes our static
|
|
> ingest tokens a declarable gap, and 5.5 requires a provider block. Both are in
|
|
> `tenancy.yaml`. T06 is therefore reduced to confirming receipt and correcting
|
|
> the one line draft-7 still has stale about us (E, once T05 lands).
|
|
>
|
|
> Draft-7's own status section still carries "Every correction so far was found
|
|
> by research or by relocation, not by review" and "Reviewed by nobody yet",
|
|
> three paragraphs below a list of eleven review-driven changes. Stale lines,
|
|
> worth flagging in T06 — a document about not overclaiming should not
|
|
> under-report its own review history.
|
|
|
|
1. **The E ladder cannot express asymmetric enforcement.** Our write path is E2
|
|
and our read path is E1. §4.3 assumes one level per service. This will recur
|
|
estate-wide, since most services enforce harder on write than on read, and
|
|
with no rule everyone will declare their best path.
|
|
|
|
Neither obvious fix is right alone. A bare **minimum across paths** is safe
|
|
to publish but destroys information: a service at E3-write/E1-read declares
|
|
identically to one at E1/E1, though the first is materially further along.
|
|
Bare **per-path declaration** (`E: {write: 3, read: 1}`) keeps the signal and
|
|
invites exactly the selective quoting §6 exists to stop — "our write path is
|
|
E3" is a sentence someone will say.
|
|
|
|
Recommend both, with the relationship stated: **declare per path, quote the
|
|
minimum.** The posture vector's `E` is the minimum across paths; the per-path
|
|
breakdown sits beside it and is where improvement is tracked. One sentence in
|
|
§4.3, and it is the only version that keeps honesty and signal at once.
|
|
|
|
2. **§13.1 collides with the ladder floor.** "A level is claimed only with its
|
|
evidence artifact present", but §13's table has no artifact for I0, I1, A0,
|
|
A1, E0, E1, R0 or R1. §5's own worked example of a conformant absorbed repo —
|
|
`I1 A1 E1 P0 R0` — cannot satisfy 13.1 on any axis. Either declaration alone
|
|
is the evidence at the floor rungs, or 13.1 binds only above them. Small, but
|
|
it makes the framework's flagship example non-conformant against its own rule.
|
|
|
|
3. **§9.1 omits consumer-facing credentials.** Decision 9.1 bans static
|
|
long-lived credentials for database access above E0, and we comply — leases
|
|
re-read at connection time, `audit_core/credentials.py`. But our *ingest*
|
|
credentials are static long-lived bearer tokens rotated by publishing a
|
|
second token alongside the first. §9 promotes leased credentials to "a
|
|
tenancy control"; the same argument applies with more force to the credential
|
|
that actually carries the tenant claim. The omission reads as accidental.
|
|
Name consumer-facing credentials in scope, or say deliberately that they are
|
|
not.
|
|
|
|
4. **§4.5 key-destroyed route** — add the condition in "On erasure versus audit"
|
|
above: no retained integrity commitment may reveal the erased content.
|
|
|
|
5. **§5 worked example** — correct `audit-core` to `I1 A2 E1 P1 R1`, and revise
|
|
its note. "Both E3 and R2 are urgent targets" is half right: E3 is, R2 is
|
|
nearly vacuous for us while the real requirement sits in `data.archive`.
|
|
|
|
Nothing here asks the framework to change shape. Four of five axes described us
|
|
without strain, and the one that did not (E) failed in a way that found a real
|
|
defect in our code. That is the framework working.
|
|
|
|
## Non-goals
|
|
|
|
- **E3 / row-level security is not in this plan.** ADR-0003 publishes the
|
|
contract and it is the right target, but it is blocked behind reaching E2
|
|
(§4.3) and needs the `EXPLAIN` comparison first. Declared as target with a
|
|
recorded gap in T01, not built here.
|
|
|
|
**The target carries a date: 2027-03-31.** The framework says services whose
|
|
cross-tenant exposure would be a reportable breach SHOULD target E3, and no
|
|
service on `platform-pg` fits that description better than this one. Every
|
|
quarter spent at E2-with-a-target is a quarter of stated, unclosed risk, and
|
|
an undated target is what §8.1 calls documentation rather than control. If the
|
|
date slips it slips as a decision, which is the whole point of §12.
|
|
- **No per-subject encryption is built.** T03 records a position; it does not
|
|
commit to an implementation.
|
|
- **No adversarial evidence facility is built here.** §19.3 gave that a home in
|
|
`whitehat-security`. T05 coordinates, it does not volunteer.
|
|
|
|
## Sequencing
|
|
|
|
**T04 runs before T01.** Task ids are hub-registered and not renumbered; the
|
|
execution order is T02 → T04 → T01 → T03 → T05 → T06.
|
|
|
|
The reason is narrow. A posture vector published in the repo is a canon-shaped
|
|
artifact that outlives the fortnight it takes to fix the read path, and `E1` on
|
|
an audit service is a sentence that will be quoted back long after it stopped
|
|
being true. Fixing first means the declaration is `E2` on both paths from its
|
|
first commit, and the E1 interval never enters the permanent record.
|
|
|
|
This is emphatically **not** withholding the finding. T02 goes first precisely
|
|
so the correction reaches `rapp-postgres` immediately: their guess of `E2` is
|
|
live in canon *now*, and it is wrong now. A message correcting the record is the
|
|
fast instrument; a published vector is the durable one. Deferring the durable
|
|
artifact by two weeks while the fast one goes out today is sequencing. Delaying
|
|
both would be concealment, and §6 would be right to call it that.
|
|
|
|
## Tasks
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "65f3109a-fa98-459a-9e59-d7ac211bd2bc"
|
|
```
|
|
Publish the posture vector. **Rewritten against draft-7**, which landed after
|
|
this task was written and moved the target: Decision 5.4 fixes the location at
|
|
**`tenancy.yaml` in the repo root**, not `docs/tenancy-posture.yaml`, and fixes
|
|
the schema — `current`, `target`, `reviewed`, `gap`, `placement_exceptions`,
|
|
`service_class`, per-path detail (5.2), and a provider block (5.5). Delivered at
|
|
that path and in that shape. Cross-linked from `SCOPE.md` and
|
|
`docs/interface-card.yaml`.
|
|
|
|
**Declared `I1 A2 E1 P1 R1`, and E is quoted at 1 on purpose.** T04 put the E2
|
|
mechanism on both paths, but §13.2 says a passing CI run is not E2 evidence:
|
|
the E2 artifact is adversarial, compares separate tenant contexts, and carries a
|
|
review date rather than a green build. Our cross-tenant tests are mechanical.
|
|
Under §13.1 the level is therefore not claimable until T05 produces the artifact,
|
|
so the vector declares 1 with the mechanism recorded in `paths.E` and the reason
|
|
in `gap.E`. Claiming E2 off unit tests would be the overclaim §6 prohibits —
|
|
and refusing exactly that kind of reasoning is what found the read-path defect
|
|
in the first place.
|
|
|
|
R stays at 1: R2 needs `backupRetentionDays: 30` in
|
|
`rapp-postgres/consumers/audit-core.yaml`, requested in T02. That file is
|
|
theirs, so R2 is not ours to declare unilaterally. The erasure horizon is
|
|
already published on `/readyz` as `recoverable_days`.
|
|
|
|
Two additions draft-7 forced that the original task did not anticipate:
|
|
|
|
- **A `credentials` gap under Decision 9.2.** Our own finding was adopted as a
|
|
rule, and it binds us: ingest credentials are static long-lived bearer tokens.
|
|
Declared as a stated gap with no dated remedy rather than a silent exclusion.
|
|
Asking for the rule and then exempting ourselves from it was not available.
|
|
- **A `provides` block under Decision 5.5.** audit-core provisions
|
|
`operations.audit`, so it declares what it makes *reachable* for a sender's
|
|
trail — E2 now, E3 pending ADR-0003, **E4 and R4 unreachable**. A sender
|
|
needing a structural cross-tenant guarantee cannot get it here and should be
|
|
told so rather than sold E2 in E4's language (§11.4).
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T02
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "224a12c0-d646-4c7f-9215-120e1ede4691"
|
|
```
|
|
Reply to `rapp-postgres` message `56b77e20-0152-4e09-8f76-99640269063b`.
|
|
**Runs first.** Carries four things: the retention position (ceiling stays 365,
|
|
audit-core declares nothing above the 30-day default, multi-year routes to
|
|
`data.archive`) with the **2026-12-31 escalation date and the P2 fallback named**;
|
|
the request for an explicit `backupRetentionDays: 30` in `consumers/audit-core.yaml`
|
|
to take us to R2; the ADR-0002 context amendment; and `batch` confirmed.
|
|
|
|
It also **corrects their posture guess immediately** — `I1 A2 E1 P1 R1`, not
|
|
`I2 A3 E2 P1 R1`, with the read-path defect stated plainly and T04 named as the
|
|
fix in flight. Their `E2` guess is live in canon and wrong today; the correction
|
|
does not wait on T01. Mark the message read on send.
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T03
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "0003954e-5033-457f-b9aa-97efdefe9beb"
|
|
```
|
|
Answer question 11 in `docs/erasure-and-audit.md`.
|
|
|
|
**Reduced by draft-8.** The framework half is already resolved upstream: the
|
|
confirmation-oracle finding landed as **Decision 4.5.3**, and §19.11 is now
|
|
marked *framework resolved*, recording that audit-core targets R2 and is
|
|
explicitly not a fleet R4 target. There is no amendment left to send. What
|
|
remains is audit-core's own position document — the fact/payload split, the
|
|
encrypt-then-hash constraint, why the retained cleartext hash defeats key
|
|
destruction, and the R2 ceiling — so the reasoning lives in this repo rather
|
|
than only in someone else's canon.
|
|
|
|
**Route the legal question to `risk-nexus`, not `the-custodian`.** §19.11 ends
|
|
"the legal basis for retaining audit facts remains a risk/legal question outside
|
|
this framework", and `policy-nexus` INTENT records that judging what an external
|
|
rule demands of the estate moved to `risk-nexus` on 2026-08-17. That is the
|
|
owner. The position is publishable with the dependency open; it is not
|
|
publishable with the dependency hidden.
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T04
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "e4440db3-6eb0-49f6-9aa9-930db8be9d18"
|
|
```
|
|
Bind the read path to permitted tenants. `permits_tenant` governs read as it
|
|
governs write, at the same choke point: tenant-scope `/v1/events`,
|
|
`/v1/events/<id>`, `/v1/dead-letters` and `/v1/secret-findings`. Decide and
|
|
record how an operator identity holding `tenants: ["*"]` is distinguished from a
|
|
scoped reader. Tests for cross-tenant read refusal. Update `docs/interface-card.yaml`
|
|
and the operator runbook. Raises E to 2 on both paths; update T01's declaration.
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T05
|
|
status: progress
|
|
priority: medium
|
|
state_hub_task_id: "30b56b30-e7eb-4873-aa7c-f4a3bf2fcb24"
|
|
```
|
|
Produce the §13 E2 evidence artifact — adversarial, with a review date: an
|
|
identity bound to tenant A demonstrably cannot read tenant B. Coordinate with
|
|
`whitehat-security` (WHITEHAT-WP-0001) on whether this is theirs to run or ours
|
|
to write and theirs to review. Record the review cadence as the exposure window
|
|
per §19.3.
|
|
|
|
Progress 2026-08-21: Whitehat's operating rules, attacker model, calibrated
|
|
differential harness, and `audit-core` probe pack now exist at commit `beab2a0`.
|
|
Audit-core reviewed the three-route pack and sent target-owner readiness packet
|
|
`6f2ae598-06bd-4089-a5fd-9de31688a733`. It fixes the synthetic-only scope,
|
|
excludes operator routes, caps the run at one concurrent request and ten per
|
|
minute, and requires two temporary read-enabled identities each scoped to one
|
|
fixture tenant. The production `user-engine` identity remains unchanged and is
|
|
not a test credential.
|
|
|
|
This is progress, not evidence and not live authorization. Whitehat still owes
|
|
a complete dated engagement and executable identity adapter; the operator must
|
|
approve that target/window, after which audit-core supplies the formal
|
|
post-approval owner acknowledgement. T05 becomes `done` only when the sanitized
|
|
target report exists and has been routed to `risk-nexus`.
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T06
|
|
status: done
|
|
priority: medium
|
|
state_hub_task_id: "ed58ca37-755b-4284-b1ac-2a02baaceb21"
|
|
```
|
|
Close the review loop with `net-kingdom`.
|
|
|
|
**Mostly overtaken.** All five findings were adopted across draft-7 and draft-8,
|
|
and the two stale status lines this task was going to flag are already fixed.
|
|
Nothing is outstanding as a *finding*.
|
|
|
|
What remains is confirmation rather than argument: acknowledge the adoptions,
|
|
confirm the §5 worked example `I1 A2 E1 P1 R2 V0` is accurate as written, and
|
|
state that `tenancy.yaml` validates clean against
|
|
`tools/tenancy-posture/validate.py`. Flag one thing forward — the E line becomes
|
|
wrong the day T05 lands, and under Decision 6.1 an *upgrade* has no notification
|
|
duty, so the correction is ours to send unprompted or it will simply sit stale.
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T07
|
|
status: progress
|
|
priority: high
|
|
state_hub_task_id: "e972a8f7-e6ee-4522-bc76-fb69d71e5ed5"
|
|
```
|
|
Reach **V1** on the availability axis, new in draft-8.
|
|
|
|
We declared `V0` because nothing exercises restart recovery for the complete
|
|
audit path. §13's V1 row asks for three things and they are all mechanical:
|
|
critical dependencies enumerated, restart/recreate recovery exercised, and the
|
|
interruption plus measured recovery time recorded. Decision 4.6.1 makes V
|
|
end-to-end, so the enumeration must include `platform-pg` and the credential
|
|
path — a receiver that restarts in 5s over a database that takes 90s is not V1
|
|
at 5s.
|
|
|
|
The 2026-08-16 reboot walk in the operator runbook is close but is not this: it
|
|
observed ~40s of unreadiness after a node reboot, without enumerating the
|
|
dependency set or recording a measured recovery time as an exercise. Decision
|
|
4.6.1 is explicit that a replica count or a status page is not evidence.
|
|
|
|
Delivered in `docs/availability-recovery.md`: the seven-dependency enumeration,
|
|
five exercise scenarios with integrity as a pass condition, and the measurement
|
|
rules. The exercise itself needs a live window on railiance01 and coordination
|
|
with `user-engine`, since two scenarios are visible to the sender.
|
|
|
|
One thing the enumeration settled: **V2 is not reachable from P1 as built.**
|
|
`platform-pg` runs `instances: 1`, and Decision 4.6.1 makes V the minimum across
|
|
synchronous providers — so no amount of replication on our Deployment raises it.
|
|
V1 is the ceiling here, not merely the next step.
|
|
|
|
```task
|
|
id: AUDIT-WP-0008-T08
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "50eed5e2-06d6-4bdc-913d-5a8cd41ce613"
|
|
```
|
|
Disclose the shared-substrate retention coupling in our **provider** block, and
|
|
notify `user-engine` under Decision 6.1.
|
|
|
|
Two defects in our own declaration, both found by reading draft-8 back against
|
|
`tenancy.yaml`:
|
|
|
|
- **`provider.R.available: 2` is misleading as written.** It tells a sender it
|
|
gets a 30-day horizon. At P1 the horizon is the *instance maximum* across
|
|
co-residents (§4.5), so a co-resident declaring longer silently extends what
|
|
we hand our senders. Decision 4.5.4 makes exactly this the reason a bare `R2`
|
|
minimum is insufficient. We are not a tier, but we are a provider quoting a
|
|
number we do not solely control, and the provider block must say so.
|
|
- **Our consumers were never told.** Decision 6.1: updating `tenancy.yaml`
|
|
without notifying dependants is declaration drift, not a completed change.
|
|
`user-engine` is the only sender and has not been told that `E4` and `R4` are
|
|
unreachable here, nor that current `E` is 1. This is not a downgrade — it is a
|
|
first accurate declaration replacing someone else's generous guess — but the
|
|
notification duty is the same and the guess was public.
|
|
|
|
## Done when
|
|
|
|
- `docs/tenancy-posture.yaml` declares an accurate vector with targets, per-path
|
|
E breakdown, and dated gaps.
|
|
- Read path enforces tenant scope, with a test that fails if it regresses.
|
|
- `rapp-postgres` has the retention answer, the corrected posture guess, and the
|
|
question 11 answer.
|
|
- NetKingdom has the five framework findings.
|
|
- The §5 worked example for `audit-core` is corrected upstream.
|
|
- Two dates are recorded and monitored per §8.1 rather than merely written:
|
|
`data.archive` review 2026-12-31, E3 target 2027-03-31.
|
|
- The erasure legal question has an owner outside audit-core (`risk-nexus`).
|
|
- `tenancy.yaml` validates clean against the normative validator, and keeps
|
|
doing so as the framework revises.
|
|
- V1 is evidenced by an exercise, not asserted from a restart policy (T07).
|
|
- The provider block discloses what it does not solely control, and every
|
|
consumer of `operations.audit` has been told what it declares (T08).
|