diff --git a/SCOPE.md b/SCOPE.md index dbf7663..c48c920 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -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 diff --git a/approval_engine/api.py b/approval_engine/api.py index 77c0f43..78bbb2f 100644 --- a/approval_engine/api.py +++ b/approval_engine/api.py @@ -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": diff --git a/approval_engine/auth.py b/approval_engine/auth.py index a6ee5cc..b91e752 100644 --- a/approval_engine/auth.py +++ b/approval_engine/auth.py @@ -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, ) diff --git a/approval_engine/store.py b/approval_engine/store.py index 53ac4f2..c546d68 100644 --- a/approval_engine/store.py +++ b/approval_engine/store.py @@ -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() ] diff --git a/deploy/README.md b/deploy/README.md index 6b370cc..a21806e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -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 diff --git a/docs/approver-surface-requirements.md b/docs/approver-surface-requirements.md index 78f3736..2665458 100644 --- a/docs/approver-surface-requirements.md +++ b/docs/approver-surface-requirements.md @@ -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`). diff --git a/docs/caller-authentication.md b/docs/caller-authentication.md index 9dd8d3d..c019e67 100644 --- a/docs/caller-authentication.md +++ b/docs/caller-authentication.md @@ -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`, diff --git a/docs/production-dependencies.md b/docs/production-dependencies.md new file mode 100644 index 0000000..ab1ec6e --- /dev/null +++ b/docs/production-dependencies.md @@ -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. diff --git a/docs/storage-operations.md b/docs/storage-operations.md index c0c53df..6ca81ee 100644 --- a/docs/storage-operations.md +++ b/docs/storage-operations.md @@ -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. diff --git a/tests/test_human_control.py b/tests/test_human_control.py index 925ba43..467c518 100644 --- a/tests/test_human_control.py +++ b/tests/test_human_control.py @@ -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() diff --git a/tests/test_tenant_provenance.py b/tests/test_tenant_provenance.py new file mode 100644 index 0000000..9f3769b --- /dev/null +++ b/tests/test_tenant_provenance.py @@ -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" diff --git a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md index c43fa78..1758bb3 100644 --- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md @@ -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.