Retain verified tenant provenance and reconcile production blockers

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1046e-fdbf-7b93-a267-5f72b9b24526
This commit is contained in:
tegwick 2026-10-04 11:26:32 +02:00
parent 931e0b3249
commit c9ad5c2dd7
12 changed files with 274 additions and 25 deletions

View file

@ -102,12 +102,13 @@ systems. Consumption follows the assented `GH-DEC-2026-003` contract.
## Current State
- Status: **production-packaged spine, not yet live**. Authenticated HTTP
mutations, schema-v2 SQLite, outbox drain to audit-core, digest-pin-ready
image and single-writer StatefulSet, and a fail-closed PEP harness exist in
this repository. Live rollout, KeyCape audience/client proof, audit-core
sender registration, and a served secrets-engine consume binding remain
external gates on `APPROVAL-WP-0002`.
- Status: **deployed schema-v5 service; schema-v6 source ready**. Authenticated
HTTP mutations, durable SQLite, transactional outbox and restart/restore
evidence are implemented. Source now retains verified tenant provenance;
promotion needs a new release and approved migration/rollout. Native issuer
profile acceptance, lifecycle audit reconciliation and a named served PEP
remain blocked in `APPROVAL-WP-0002`; current owner requirements are in
`docs/production-dependencies.md`.
- Layer declaration: INTENT frontmatter + `layer.yaml`. Cadence declared in
`cadence.yaml`. No Tooling contacts.
- Consumption is public at `POST /v1/approvals/{id}/consume` under

View file

@ -147,6 +147,7 @@ class App:
assurance=json.dumps(identity.assurance, sort_keys=True),
evidence_ref=identity.evidence_ref,
principal_type=identity.principal_type,
tenant_source=identity.tenant_source,
)
return 200, obj.as_dict()
if rest == ["revoke"] and method == "POST":

View file

@ -28,6 +28,7 @@ class Identity:
scopes: frozenset[str]
assurance: dict[str, Any]
evidence_ref: str
tenant_source: str | None = None
def require(self, scope: str) -> "Identity":
if scope not in self.scopes:
@ -76,6 +77,12 @@ def identity_from_claims(claims: Mapping[str, Any], token: str) -> Identity:
raise Unauthenticated("token identity claims are invalid")
if not tenant or not isinstance(assurance, dict):
raise Unauthenticated("token tenant or assurance claim is invalid")
tenant_source = claims.get("tenant_source")
if "tenant_source" in claims and (
not isinstance(tenant_source, str)
or tenant_source not in {"directory", "registration", "default"}
):
raise Unauthenticated("token tenant_source claim is invalid")
audiences = _texts(claims.get("aud"), "aud")
roles = frozenset(_texts(claims.get("roles"), "roles", allow_empty=True))
scopes = frozenset(_texts(claims.get("scope", claims.get("scp")), "scope"))
@ -90,6 +97,7 @@ def identity_from_claims(claims: Mapping[str, Any], token: str) -> Identity:
scopes=scopes,
assurance=dict(assurance),
evidence_ref=f"jwt-sha256:{fingerprint}",
tenant_source=tenant_source,
)

View file

@ -42,7 +42,7 @@ EVENT_CLASSES = ("issuance", "use", "supersession", "revocation", "heartbeat")
# class and says nothing about the quiet one — here that is `revocation`, which
# is the class whose silence actually matters.
HEARTBEAT_CLASSES = ("issuance", "use", "supersession", "revocation")
LATEST_SCHEMA_VERSION = 5
LATEST_SCHEMA_VERSION = 6
SCHEMA = """
CREATE TABLE IF NOT EXISTS approvals (
@ -75,6 +75,7 @@ CREATE TABLE IF NOT EXISTS entries (
assurance TEXT,
evidence_ref TEXT,
principal_type TEXT,
tenant_source TEXT,
PRIMARY KEY (approval_id, subject_id)
);
CREATE TABLE IF NOT EXISTS outbox (
@ -258,6 +259,9 @@ class Engine:
# of principal bound it, and defaulting it to 'human' would
# manufacture approver evidence that was never presented.
conn.execute("ALTER TABLE entries ADD COLUMN principal_type TEXT")
if "tenant_source" not in entry_columns:
# v6: historical entries have no verified tenant provenance.
conn.execute("ALTER TABLE entries ADD COLUMN tenant_source TEXT")
outbox_columns = {
row["name"] for row in conn.execute("PRAGMA table_info(outbox)").fetchall()
}
@ -456,10 +460,11 @@ class Engine:
"assurance": e["assurance"],
"evidence_ref": e["evidence_ref"],
"principal_type": e["principal_type"],
"tenant_source": e["tenant_source"],
}
for e in self._conn()
.execute(
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type, tenant_source "
"FROM entries WHERE approval_id=? ORDER BY approved_at",
(row["id"],),
)
@ -520,9 +525,15 @@ class Engine:
assurance: str | None = None,
evidence_ref: str | None = None,
principal_type: str | None = None,
tenant_source: str | None = None,
) -> Approval:
if not subject_id:
raise Unprocessable("subject_id is required")
if tenant_source is not None and (
not isinstance(tenant_source, str)
or tenant_source not in {"directory", "registration", "default"}
):
raise Unprocessable("tenant_source is invalid")
conn = self._conn()
now = iso(self.now())
try:
@ -541,9 +552,9 @@ class Engine:
try:
conn.execute(
"INSERT INTO entries "
"(approval_id, subject_id, approved_at, assurance, evidence_ref, principal_type) "
"VALUES (?,?,?,?,?,?)",
(approval_id, subject_id, now, assurance, evidence_ref, principal_type),
"(approval_id, subject_id, approved_at, assurance, evidence_ref, principal_type, tenant_source) "
"VALUES (?,?,?,?,?,?,?)",
(approval_id, subject_id, now, assurance, evidence_ref, principal_type, tenant_source),
)
except sqlite3.IntegrityError as exc:
conn.rollback()
@ -852,7 +863,7 @@ class Engine:
a future schema that relaxes it.
"""
rows = conn.execute(
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type, tenant_source "
"FROM entries WHERE approval_id=? ORDER BY approved_at, subject_id",
(approval_id,),
).fetchall()
@ -866,6 +877,7 @@ class Engine:
**({"assurance": r["assurance"]} if r["assurance"] else {}),
**({"evidence_ref": r["evidence_ref"]} if r["evidence_ref"] else {}),
**({"principal_type": r["principal_type"]} if r["principal_type"] else {}),
"tenant_source": r["tenant_source"],
}
for r in seen.values()
]

View file

@ -10,7 +10,10 @@ database.
(registry tag `0.1.0-hfact-be1a388`, source `be1a388`). Both references MUST stay
identical immutable digests: migration and server share one database.
The new artifact carries `LATEST_SCHEMA_VERSION = 5`, matching this source:
The deployed artifact carries schema v5 and predates schema v6 in this source.
Schema v6 records verified `entries.tenant_source`; it requires a new scanned,
published image, identical migration/server pins, backup and an approved rollout.
The existing deployed image records:
`entries.principal_type` and explicit `approvals.human_control`. Its release scan
found zero HIGH/CRITICAL vulnerabilities. Disposable on-disk v4-to-v5 migration,
negative service/agent binding, human binding/consumption and restart persistence

View file

@ -1,6 +1,12 @@
# Requirements on the approver surface (`informed-decision`)
Status: issued by approval-engine 2026-09-09, against `APPROVAL-WP-0002-T01`.
Status: issued 2026-09-09; current acceptance reviewed 2026-10-04 against
`APPROVAL-WP-0002-T01`. Sections 3 and 4 retain the original questions:
KeyCape has since published approval:read and the assurance shape, registered
`informed-decision-approver`, and Informed Decision has recorded native human
acceptance. Current remaining receipts are in [production-dependencies.md](production-dependencies.md).
Schema-v6 source additionally preserves verified tenant_source; the deployed
schema-v5 image awaits separately approved promotion.
Addressed to `informed-decision` (`INFD-WP-0001`), copied to `key-cape`
(`KEY-WP-0013-T02`).

View file

@ -28,9 +28,12 @@ Consequences this engine accepts deliberately:
platform zone" is a fact about the person, and this claim cannot carry it.
No such doctrine exists today; if `gate-house` issues one, it needs a
directory-sourced claim and this check does not become that claim by matching.
- When key-cape emits provenance alongside the tenant, this engine records it on
the approver entry the way schema v4 records `principal_type` — an evidence
reader should not have to infer provenance from a string that cannot carry it.
- Schema v6 records verified `tenant_source` on approver entries and issuance/use
threshold evidence. KeyCape publishes `directory`, `registration`, or `default`.
An absent claim remains null; an explicit malformed or unknown value is refused.
Legacy entries remain null, never inferred from their subject or tenant.
Provenance does not change store isolation or establish zone membership.
The deployed schema-v5 image does not yet record this field.
| Route | Required scope |
|---|---|
@ -44,8 +47,8 @@ Consequences this engine accepts deliberately:
| explicit heartbeat | `approval:emit` |
Create additionally requires `binding.actor == sub`. Approval-entry subject,
assurance, evidence reference, and **principal type** are derived from the
verified JWT, never the request body.
assurance, evidence reference, **principal type**, and **tenant source** derive
from the verified JWT, never the request body.
`principal_type` is recorded from verified identity (since schema v4). Schema v5
implements GH-DEC-2026-016: when an approval declares `human_control: true`,

View file

@ -0,0 +1,62 @@
# Production dependency requirements
Reviewed 2026-10-04 for APPROVAL-WP-0002. Task status `wait` means blocked;
the workplan remains `blocked` after completing reachable source work.
| Task | Owner return required | Evidence that clears the wait |
| --- | --- | --- |
| T01 — authenticated approver evidence | key-cape and informed-decision; founder for release rollout | Exact deployed issuer revision and client/profile receipt for requester and human paths; human sub/type, tenant/source, resource audience, scopes and assurance captured as non-secret facts and correlated with a stored human-control entry. Promote scanned schema-v6 image under per-change approval, back up/migrate once, then verify native entry and issuance/use provenance. No tokens or secrets in receipts. |
| T04 — delivery and reconciliation | audit-core, with railiance-platform for deployed sender/custody acceptance | Accept current cadence.yaml inventory for source approval-engine and tenant tenant:platform, explicit redact policy and four transition heartbeat classes at the requested 172800s bound. Return actual lifecycle event ids/drain acknowledgment and independent accepted counts for the same bounded time window, with pending rows accounted separately. Prove startup beats, missing-class and lag findings (depth 100 / age 3600s), and scoped sender bearer revocation. |
| T05 — live PEP consumption | secrets-engine, with informed-decision for the act-specific human review and access-engine for decision binding | Name the served PEP route/workload, immutable component revision, engine image/endpoint, authenticated caller identities and exact binding. Prove claim-before-decision and CAS consume after ALLOW before protected side effect; same-digest retry, different-digest conflict, spent-on-failure, revoked/expired/superseded/wrong-binding/unavailable denial with no protected callback. Preserve the native approval and decision digests, declared human_control where required, and consumer-owned side-effect/refusal evidence. |
For live whitehat T-06 only, T05's named surface must additionally be covered by
a complete engagement and plane admission naming identities, bounded window
and technique. Neither an in-process client nor this dependency document
admits a test or authorizes a packet. WHITEHAT-WP-0008 remains blocked.
## Delivered prerequisites
- Approval Engine deployment, schema-v5 persistence, restart and restore:
[deployment receipt](evidence/2026-09-14-approval-engine-deployment.json).
Image sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49.
- KeyCape: KEY-WP-0013 publishes `informed-decision-approver`, callback
https://decisions.coulomb.social/auth/callback, scopes openid/approval:read/
approval:approve, S256 PKCE, MFA and tenant:platform. KEY-WP-0031 is finished
and publishes directory/registration/default provenance. A source fragment
is insufficient to pin the exact deployed issuer/profile used by an entry.
- Informed Decision: archived INFD-WP-0001-T08 reports real human acceptance.
Its docs/evidence/2026-09-27-tool-budget-approval-create.json records requester
actor informed-decision and human_control=true; the corresponding
2026-09-27-tool-budget-acceptance.json confirms approval
e9521804-8fce-4fbc-bfdf-8a1aa959fd61, subject
uid=platform-root,ou=people,dc=netkingdom,dc=local at 18:05:35Z.
These discharge the historical absence of a requester/human flow. They do
not prove the new v6 field or admit a different protected action. Standing
requester runtime composition remains INFD-WP-0005-T05's platform dependency.
- Secrets Engine: SECRETS-WP-0010's native reader acceptance records existing
CCR-2026-0019 delivery and separate native read/consume scopes, an engine 404
for an unknown approval and 403 for consume-only read. Credential delivery is
complete; that test explicitly created, bound and consumed no approval.
- Audit Core: AUDIT-WP-0009 is finished. Native producer/readback receipts from
2026-09-11 and engine startup heartbeat drain are delivered. Current class
admission, actual lifecycle reconciliation and observed findings still need
their own receipt; implementation completion is insufficient evidence.
## Approval Engine source completion and release boundary
Schema v6 retains verified tenant provenance without treating registration or
default provenance as directory membership. Entries and issuance/use evidence
carry null for absent provenance; historic rows/events are preserved. Signed
HTTP tests cover all three sources, absent/malformed claims and body spoofing;
on-disk tests cover migration, restart, binding preservation and rollback.
The production pin remains schema v5. Release preparation uses the existing
image-build/image-scan targets and immutable matching migration/server pins.
Production migration/rollout requires founder approval per change, a verified
backup and native readback; this review performs none of those live operations.
The [deployment runbook](../deploy/README.md) and
[storage operations](storage-operations.md) specify that boundary and recovery.
APPROVAL-IN-0001 also remains open for the unassigned shared Taxonomy schema
owner. It does not block this workplan's local claim contract and is not a
substitute for the three owner returns above.

View file

@ -78,3 +78,19 @@ and verify the existing backup before migration. The old v3 image must not serve
a v5 database; rollback requires the matching pre-migration backup and the
existing single-writer recovery procedure. No live database is migrated by the
source test run.
## Schema v6 — verified tenant provenance
`entries.tenant_source` is nullable TEXT, recording only the verified issuer claim
(`directory`, `registration`, or `default`). Missing claims and pre-v6 entries
stay NULL; migration never infers membership or changes bindings, quorum,
human-control declarations, or existing outbox evidence. New issuance/use
threshold evidence retains this field, including null for unknown provenance.
Migration is additive and must precede serving the v6 release. The deployed
schema-v5 image remains pinned; source changes do not migrate production.
Publish/scan and pin the same v6 artifact for both containers, take and verify
the pre-migration backup, and obtain per-change rollout approval. An old v5
server refuses a v6 store; rollback uses the verified pre-migration backup with
the matching release and the stopped single-writer restore procedure above.

View file

@ -144,7 +144,7 @@ def test_legacy_v4_rows_are_not_reclassified_from_human_entries(tmp_path):
assert survivor.entries[0]["principal_type"] == "human"
assert survivor.human_control is False
assert upgraded.claim(obj.id)["binding"]["human_control"] is False
assert upgraded.storage_status()["schema_version"] == LATEST_SCHEMA_VERSION == 5
assert upgraded.storage_status()["schema_version"] == LATEST_SCHEMA_VERSION == 6
upgraded.close()

View file

@ -0,0 +1,97 @@
"""Verified tenant origin remains evidence, including absence and legacy rows."""
import json
import sqlite3
import pytest
from cryptography.hazmat.primitives.asymmetric import rsa
from approval_engine.api import App, call
from approval_engine.auth import JWTAuthenticator
from approval_engine.errors import StoreUnavailable
from approval_engine.store import Engine, LATEST_SCHEMA_VERSION
from tests.conftest import FROZEN, binding, validity
from tests.test_auth import _JWKS, _jwt
def authenticator():
private = rsa.generate_private_key(public_exponent=65537, key_size=2048)
auth = JWTAuthenticator(
issuer="https://keycape.example", audience="approval-engine",
jwks_url="https://keycape.example/jwks", jwks_client=_JWKS(private.public_key()),
)
return private, auth
@pytest.mark.parametrize("source", ["directory", "registration", "default", None])
def test_verified_provenance_survives_http_entry_restart_and_use(tmp_path, source):
private, auth = authenticator()
claims = dict(sub="human", tenant="tenant:platform", principal_type="human",
scope="approval:approve")
if source is not None:
claims["tenant_source"] = source
token = _jwt(private, **claims)
path = tmp_path / "approval.sqlite"
engine = Engine(path, clock=lambda: FROZEN)
obj = engine.create(binding(), validity(), human_control=True)
digest = obj.binding_digest
status, approved = call(App(engine, auth), "POST", f"/v1/approvals/{obj.id}/entries",
{"tenant_source": "spoofed"}, authorization="Bearer " + token)
assert status == 200
assert approved["entries"][0]["tenant_source"] == source
engine.close()
engine = Engine(path, clock=lambda: FROZEN, auto_migrate=False)
assert engine.get(obj.id).entries[0]["tenant_source"] == source
assert engine.get(obj.id).binding_digest == digest
assert "tenant_source" not in json.dumps(engine.claim(obj.id))
engine.consume(obj.id, digest)
rows = engine._conn().execute("SELECT class, payload_json FROM outbox").fetchall()
for row in rows:
payload = json.loads(row["payload_json"])
assert payload["details"]["threshold"]["approvers"][0]["tenant_source"] == source
assert {row["class"] for row in rows} == {"issuance", "use"}
engine.close()
@pytest.mark.parametrize("source", [None, "unknown", "", {}, [], 1])
def test_explicit_invalid_provenance_refuses_http_bind_without_mutation(engine, source):
private, auth = authenticator()
token = _jwt(private, sub="human", tenant="tenant:platform", principal_type="human",
scope="approval:approve", tenant_source=source)
obj = engine.create(binding(), validity(), human_control=True)
status, _ = call(App(engine, auth), "POST", f"/v1/approvals/{obj.id}/entries", {},
authorization="Bearer " + token)
assert status == 401
assert engine.get(obj.id).entries == []
assert engine._conn().execute("SELECT COUNT(*) FROM outbox").fetchone()[0] == 0
def test_v5_migration_preserves_unknown_provenance_and_existing_evidence(tmp_path):
path = tmp_path / "approval.sqlite"
engine = Engine(path, clock=lambda: FROZEN)
obj = engine.create(binding(), validity(), human_control=True)
engine.add_entry(obj.id, "human", principal_type="human")
digest = obj.binding_digest
evidence = engine._conn().execute("SELECT payload_json FROM outbox").fetchall()[0][0]
engine.close()
with sqlite3.connect(path) as conn:
conn.execute("ALTER TABLE entries DROP COLUMN tenant_source")
conn.execute("PRAGMA user_version=5")
with pytest.raises(StoreUnavailable, match="migrate"):
Engine(path, auto_migrate=False)
engine = Engine(path, clock=lambda: FROZEN)
assert engine.storage_status()["schema_version"] == LATEST_SCHEMA_VERSION == 6
assert engine.get(obj.id).entries[0]["tenant_source"] is None
assert engine.get(obj.id).binding_digest == digest
assert engine.get(obj.id).human_control is True
assert engine._conn().execute("SELECT payload_json FROM outbox").fetchone()[0] == evidence
engine.close()
def test_provenance_rolls_back_with_failed_outbox(engine):
obj = engine.create(binding(), validity(), human_control=True)
engine.fail_outbox = True
with pytest.raises(StoreUnavailable):
engine.add_entry(obj.id, "human", principal_type="human", tenant_source="directory")
engine.fail_outbox = False
assert engine.get(obj.id).entries == []
assert engine.get(obj.id).status == "requested"

View file

@ -9,7 +9,7 @@ flavor: residual
owner: codex
topic_slug: netkingdom
created: "2026-09-01"
updated: "2026-10-03"
updated: "2026-10-04"
reviewed_at: "2026-09-01"
reviewed_against_commit: "ebce5abb276c01ab29ce2526f3b8abb332dc9e90"
reviewed_note: >-
@ -30,8 +30,9 @@ Move the completed first-cut engine spine into an authenticated, durable,
observable production service and prove one PEP integration end to end. This is
the residual production scope deliberately excluded from APPROVAL-WP-0001.
The workplan is proposed pending review against the deployment estate and the
current key-cape, access-engine, audit-core, and secrets-engine contracts.
Current status: blocked after the 2026-10-04 dependency review. T02, T03 and
T06 are complete. T01, T04 and T05 use `wait` for blocked task status; their
remaining owner receipts are specified in `docs/production-dependencies.md`.
Review completed 2026-09-01. The plan is active. Production mode will verify
KeyCape JWT signatures and exact issuer/audience/scopes; local caller-supplied
@ -50,7 +51,10 @@ id: APPROVAL-WP-0002-T01
status: wait
flavor: residual
priority: high
blocking_reason: "Await key-cape's exact requester/human client registrations and native issuer proof with a real human flow; repository implementation is complete."
blocked_on: message-from:key-cape
needs_human: true
blocking_reason: "Source records verified tenant provenance in schema v6. Existing native requester/human review evidence is delivered; await revision-pinned issuer/profile receipt from key-cape/informed-decision and founder-approved v6 release/migration with native entry/outbox readback."
intervention_note: "Founder per-change approval is required before promoting the schema-v6 image and migrating production; no live change is authorized by this review."
state_hub_task_id: "dc4523f5-e0af-5734-a0ef-07aa7f2b2a27"
```
@ -546,7 +550,8 @@ id: APPROVAL-WP-0002-T04
status: wait
flavor: residual
priority: high
blocking_reason: "Source declaration migrated and emitter mapping verified; sender/native custody delivered 2026-09-11; await canonical receiver declaration and heartbeat class admission, service admission and actual lifecycle outbox/cadence proof plus bearer revocation (synthetic accepted/duplicate/read-refusal/readback already verified 2026-09-11), exact-window reconciliation and observed per-class heartbeat/lag findings. Existing AUDIT-WP-0009 observer implementation is reused."
blocked_on: message-from:audit-core
blocking_reason: "Sender custody, native synthetic delivery/readback and service deployment are delivered. Await audit-core acceptance of current cadence/class inventory, actual lifecycle drain and same-window reconciliation, class-silence/lag findings and scoped bearer-revocation receipt. Existing AUDIT-WP-0009 implementation is complete and reused."
state_hub_task_id: "777a5cf5-c1db-5e07-8de6-ab109db5ccdc"
```
@ -610,6 +615,8 @@ id: APPROVAL-WP-0002-T05
status: wait
flavor: residual
priority: high
blocked_on: message-from:secrets-engine
blocking_reason: "Engine deployment and consumer read/consume credential delivery are delivered. Await secrets-engine named served PEP revision/route/identities and native exact-binding consume-before-side-effect/failure proof; requester/reviewer admission must match that act. Live T-06 additionally requires a complete engagement and plane admission with window/technique."
state_hub_task_id: "3196fb77-3df0-5358-ae12-2d07bac12041"
```
@ -805,3 +812,36 @@ approval.heartbeat alias while preserving the 86400s emission and requested
explicitly pending. See docs/emission-cadence.md for the bounded owner acceptance
requirements; T04 stays wait, and this workplan stays blocked. Source validation
does not admit a workload or close its real delivery proof.
## Dependency review and tenant provenance — 2026-10-04
Reviewed all tasks in all three workplans; only T01/T04/T05 in this workplan
remain open. The 2026-09-27 assertion that no repository work was reachable is
superseded: KEY-WP-0031 publishes `tenant_source`, and this engine had promised
to retain it but discarded it. Implemented schema v6: signature-verified
provenance survives entries and issuance/use threshold evidence; absent and
legacy provenance remains null, malformed explicit claims fail before mutation,
and HTTP body spoofing cannot supply it. Migration preserves bindings,
human-control declarations and historical outbox bytes.
Existing receipts remove old dependencies: T03 is done with deployed schema v5;
secrets-engine SECRETS-WP-0010 documents native consumer credential delivery;
informed-decision INFD-WP-0001-T08 is done and its 2026-09-27 requester/create
and human acceptance receipts are real. KEY-WP-0013 publishes the exact
`informed-decision-approver` registration and KEY-WP-0031 implements tenant
provenance. AUDIT-WP-0009 is finished; its implementation is reused, rather
than described as an unfinished predecessor. None of these supplies a named
served T-06 PEP or an engagement/admission. Whitehat reply
`4ebf1784-36a5-43c0-8685-1a8cccf49006` retains that blocking gap.
Current requirements, owner records and the evidence needed to clear each wait
are in `docs/production-dependencies.md`. No other repo was edited and no
production change or packet was performed. Deployed v5 remains pinned and the
runbook acknowledges that it predates v6. T03's historical deployment acceptance
is retained; promotion of the new evidence field is part of T01 acceptance.
Validation: 181 tests pass, including 12 new signed-HTTP, malformed-claim,
persistent migration/restart and transactional rollback cases. Repository
implementation is complete for these open tasks; native owner acceptance and
the per-change release/rollout gate remain. Workplan stays blocked.