From 8231b2a8f5a9d4c1bcf64406ee3d600a8db1862b Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 1 Nov 2026 23:15:58 +0100 Subject: [PATCH] docs: preserve reviewed NetKingdom session artifacts --- .../platform-confirmation-seam.patch | 779 ++++++++++++++++++ .../platform-owner-review.json | 41 + .../test-evidence.txt | 9 + .../platform-owner-review.md | 58 ++ .../platform-wave5.patch | 434 ++++++++++ .../test-evidence.json | 46 ++ .../protected-execution/manifest.json | 48 ++ .../platform-cleanup.patch | 595 +++++++++++++ .../platform-owner-review.json | 22 + .../key40-reconciliation.patch | 59 ++ .../recovery-attempt-r1/README.md | 29 + .../admission.template.json | 37 + .../recovery-attempt-r1/attempt-metadata.json | 96 +++ .../recovery-attempt-r1/candidate.patch | 752 +++++++++++++++++ .../public-verifier-image-readback.json | 19 + .../recovery-attempt-r1/review.json | 85 ++ .../recovery-attempt-r1/source-closure.json | 19 + .../t35-t36-source-next/NEXT-STEPS.md | 25 + .../public-tls-metadata.py | 54 ++ .../registration-selector-proposal.json | 36 + .../t35-t36-source-next/source-review.json | 184 +++++ .../test_public_tls_metadata.py | 34 + .../tls-owner-review-request.json | 41 + .../recovery-attempt-r1/test-root/Makefile | 396 +++++++++ .../bootstrap/apply-service-secrets.py | 253 ++++++ .../sso-mfa/bootstrap/creds-apply.sh | 128 +++ .../bootstrap/creds-bootstrap-agent.sh | 413 ++++++++++ .../sso-mfa/bootstrap/creds-rotate.sh | 292 +++++++ .../sso-mfa/bootstrap/creds-status.sh | 124 +++ .../sso-mfa/bootstrap/creds-verify.sh | 96 +++ .../sso-mfa/bootstrap/decrypt-secrets.sh | 65 ++ .../sso-mfa/bootstrap/emergency-bundle.sh | 45 + .../sso-mfa/bootstrap/encrypt-secrets.sh | 100 +++ .../sso-mfa/bootstrap/gen-secrets.sh | 158 ++++ .../sso-mfa/bootstrap/openbao-init-unseal.sh | 7 + .../sso-mfa/bootstrap/openbao-lab.py | 148 ++++ .../sso-mfa/bootstrap/pack-bundle.sh | 61 ++ .../sso-mfa/bootstrap/rotation-env.py | 83 ++ .../sso-mfa/bootstrap/sops-custody-fd.py | 54 ++ .../sso-mfa/bootstrap/sops-custody-unlock.sh | 210 +++++ .../sso-mfa/bootstrap/sops-private.py | 228 +++++ .../test-root/sso-mfa/bootstrap/sops-write.py | 188 +++++ .../sso-mfa/k8s/authelia/create-secrets.sh | 7 + .../k8s/authelia/tests/probe_claims.py | 214 +++++ .../k8s/keycape/approval-clients-rollout.py | 318 +++++++ .../keycape/authentication-policy-rollout.py | 83 ++ .../k8s/keycape/configure-openbao-oidc.sh | 5 + .../sso-mfa/k8s/keycape/create-pi-token.sh | 6 + .../sso-mfa/k8s/keycape/create-secrets.py | 137 +++ 49 files changed, 7321 insertions(+) create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave4/platform-confirmation-seam.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave4/platform-owner-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave4/test-evidence.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/platform-owner-review.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/platform-wave5.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/test-evidence.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-cleanup.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-owner-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-acceptance-next/key40-reconciliation.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/admission.template.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/attempt-metadata.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/candidate.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/public-verifier-image-readback.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/source-closure.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/t35-t36-source-next/NEXT-STEPS.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/t35-t36-source-next/public-tls-metadata.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/t35-t36-source-next/registration-selector-proposal.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/t35-t36-source-next/source-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/t35-t36-source-next/test_public_tls_metadata.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/t35-t36-source-next/tls-owner-review-request.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/Makefile create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/apply-service-secrets.py create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/creds-apply.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/creds-bootstrap-agent.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/creds-rotate.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/creds-status.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/creds-verify.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/decrypt-secrets.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/emergency-bundle.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/encrypt-secrets.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/gen-secrets.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/openbao-init-unseal.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/openbao-lab.py create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/pack-bundle.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/rotation-env.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/sops-custody-fd.py create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/sops-custody-unlock.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/sops-private.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/bootstrap/sops-write.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/authelia/create-secrets.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/authelia/tests/probe_claims.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/keycape/approval-clients-rollout.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/keycape/authentication-policy-rollout.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/keycape/configure-openbao-oidc.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/keycape/create-pi-token.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/keycape/create-secrets.py diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave4/platform-confirmation-seam.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave4/platform-confirmation-seam.patch new file mode 100644 index 00000000..bcece797 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave4/platform-confirmation-seam.patch @@ -0,0 +1,779 @@ +diff --git a/docs/task-execution-mediator-seam.md b/docs/task-execution-mediator-seam.md +new file mode 100644 +index 0000000..964688b +--- /dev/null ++++ b/docs/task-execution-mediator-seam.md +@@ -0,0 +1,107 @@ ++# Strict task handoff to independent consumption confirmation ++ ++RPF-WP-0035-T12 source preparation, 2026-10-16. This seam calls only the existing ++fixed disposable native adapter. It installs no production origin, identity, ++permission, real grant, generic broker or backend default. ++ ++## Actual starting gaps and reused owner interfaces ++ ++At Platform source base d042d468 and Secrets c80a944 the native fixture used a ++synthetic direct `consume-confirmed` journal transition. No actual strict Secrets ++task handoff existed. Generic Secrets consume accepted a substituted approval ID, ++coerced idempotent values and did not require the original exact decision. Those ++are Secrets-owned defects; Platform did not copy its authorization policy or edit ++that repository. The Secrets owner prepared a separate strict module under ++SECRETS-WP-0013, referenced by exact hashes in this package's manifest. ++ ++Approval Engine already provides `ExecutionConfirmationClient.consume_exact` and ++`confirm_consumption`. The latter performs an independently authenticated GET of ++the durable original approval object and filters it to the original consumption ++triplet/time. It is historical PIP evidence, not authorization. It neither validates ++an Access decision nor makes a supplied receipt authoritative. Platform's adapter ++previously assumed its first native call was journal ordinal zero, conflicting ++with a preceding durable CAS/confirmation sequence. ++ ++`scripts/task_execution_mediator.py` now performs the narrow owner join: ++ ++1. Require the actual `secrets_engine.task_execution_handoff` dependency; absence ++ refuses before provider credentials, journal reservation or native effects. ++ The mediator itself calls `authorize_task_execution`. It accepts neither an ++ inbound authorization object nor a caller-selected authorizer/transport callback. ++2. Independently compile the exact local catalogue/descriptor/grant and compare the ++ strict owner's detached handoff descriptor byte-for-byte. Only authorization_id ++ may be added to approval reference metadata; purpose and source semantics cannot ++ be substituted through an inbound catalogue object. Secrets owns signed Check, ++ claim/native-act/full and approval-free digest joins, caller/subject correspondence ++ and the strict prepared object. Platform does not render or duplicate that decision. ++3. Bind the actual returned full canonical digest into the owner Plan. Reserve the ++ approval/request/operation/Plan tuple and the precommitted cleanup obligations ++ once while holding the same invocation lock through cleanup. ++4. Commit a CAS call intent, invoke Secrets `consume_task_execution` once and require ++ its actual typed result with exact original approval/request/decision/time and ++ `idempotent is False`. A historical idempotent response cannot open native work. ++5. Commit a separate read intent and invoke the actual Approval owner's default ++ isolated transport, with independently installed read credential and exact same ++ owner-admitted origin. Require `confirm_consumption` to match the native act, ++ approval-free digest and original exact triplet/time. Raw approval entries and ++ provider credentials never enter journal evidence or receipts. ++6. Recheck original approval/decision/operation bounds using the existing Secrets ++ time adapter, then persist `consume-confirmed`. Any CAS/read failure, timeout, ++ denied read or mismatch records unknown and causes zero native execution. A ++ subsequent invocation cannot retry the consumed approval or persisted admission. ++7. Only then enter the fixed native grant/metadata adapter. Its next business-call ++ ordinal comes from the same live journal, so CAS and GET occupy ordinals0/1 and ++ native calls continue at2. Pending/unknown/cleanup-closed state cannot supply a ++ next ordinal. Each native request retains its own durable intent and outcome. ++8. Cleanup stays under the original distinct obligations even if business work ++ fails. Returned evidence explicitly says native apply is unverified: six successful ++ native calls do not establish total approved baseline/readback normalization, ++ atomic CAS, service-login provenance or native production owner acceptance. ++ ++The owner configuration selects provider route/read credential and the existing ++Secrets configuration. These are independently installed runtime inputs, never ++part of an HTTP body or authority derived from a caller string. Both providers use ++real existing transports. Python import/file checks do not establish runtime ++credential isolation; production custody/enforcement remains an external gate. ++ ++## Whole disposable source-server proof ++ ++Tests run the actual Access `serve` binary built from the owner source, with its ++real Ed25519 signing implementation and enforce-mode TokenReview client. A local ++synthetic reviewer independently admits only the explicitly fixed service token, ++principal/audience and expiry. This is not actual Kubernetes identity proof. ++Signing keys are generated in private temporary files; the public key and real ++policy digest are independently installed in the Secrets trust file. The policy ++digest comes from an owner-local actual source CLI before any network response. ++ ++The positive policy is a separately named local synthetic exact-descriptor Rego ++fixture with a real deny fixture and Rego test. It never edits or enables the ++actual task policy. The actual proposed service task package is exercised unchanged ++and returns genuine signed DENY. There is no fabricated DecisionEnvelope or ++model-only approval. Real Approval WSGI/SQLite plus distinct independently admitted ++synthetic service/human identities create, approve, consume and serve the object. ++The strict Secrets module uses its actual mandatory producer-type signature verifier, ++retained request/claim/envelope and one-shot strict CAS, followed by the independent ++Approval GET and real disposable OpenBao grant/token/metadata/cleanup requests. ++ ++Tests also prove actual independent read-scope denial after successful consume: ++the durable approval is consumed, Platform remains unknown with pending read intent, ++no target native objects exist and reinvocation cannot consume again. The native ++journal is checked for known synthetic token leakage. A source purpose substitution ++refuses before authorization/CAS, and a missing strict module refuses before any ++credentials. Actual-policy denial leaves the approval approved and journal empty. ++All identity, policy/registry admission, source-bundle/baseline digests and grant ++approval in these tests are synthetic fixture inputs. These tests do not admit an ++estate caller, real grant, production transport or actual protected custody. ++ ++## Remaining production work ++ ++Adopt/publish the exact strict Secrets dependency and this owner seam together; ++admit the existing task-execution-grant source projection and actual identity, ++policy package, provider origin/custody and complete source revisions. The current ++actual policy remains hard-closed. Finish target CCR immutable semantics, total ++native expected-state normalization/conflict control, CCR38 provenance, actual ++per-call served-time/use enforcement, audit/outbox and protected uncertain/crash ++recovery. Then obtain and execute the exact founder-scoped attended trial. ++Approval replay prevention and uncertain native-effect reconciliation remain ++separate guarantees. T12, Secrets WP13 and native adoption stay open. +diff --git a/scripts/task_execution_journal.py b/scripts/task_execution_journal.py +index a16291b..8f1b9a2 100644 +--- a/scripts/task_execution_journal.py ++++ b/scripts/task_execution_journal.py +@@ -231,6 +231,21 @@ class Journal: + return {"state": row[3], "evidence_digest": row[4], "dispatch_allowed": False, + "external_outcome_unknown": bool(pending)} + ++ def next_call_ordinal(self, binding: Binding): ++ """Continue the same admitted invocation after owner auth/consume calls. ++ ++ This never grants dispatch or resumes a persisted admission. The actual ++ before_call transaction remains the final serialization/intent guard. ++ """ ++ if not self._held or self._admission != binding: ++ raise Refused("next call requires same live admission") ++ count,pending=self.db.execute("SELECT COUNT(*),COALESCE(SUM(outcome='pending'),0) FROM calls WHERE operation=?",(binding.operation_id,)).fetchone() ++ cleanup=self.db.execute("SELECT COUNT(*) FROM cleanup WHERE operation=? AND outcome!='owed'",(binding.operation_id,)).fetchone()[0] ++ state=self.db.execute("SELECT state FROM operations WHERE operation=?",(binding.operation_id,)).fetchone()[0] ++ if pending or cleanup or state in {'failed','unknown','cleanup-confirmed'} or count>63: ++ raise Refused("call sequence is uncertain, closed or exhausted") ++ return count ++ + def before_call(self, binding: Binding, ordinal: int, call_digest: str): + """Persist one exact plan-call intent before the mediator issues it. + +diff --git a/scripts/task_execution_mediator.py b/scripts/task_execution_mediator.py +new file mode 100644 +index 0000000..d42b55f +--- /dev/null ++++ b/scripts/task_execution_mediator.py +@@ -0,0 +1,180 @@ ++"""Owner mediator seam for the existing strict Secrets handoff and Approval GET. ++ ++Only the disposable NativeFixtureAdapter is callable here. No production backend, ++caller-supplied authorizer/transport, approval boolean or portable receipt exists. ++The strict Secrets module is an explicit external dependency: absence refuses ++before credential-bearing requests or native effects, never a permissive fallback. ++""" ++from __future__ import annotations ++from dataclasses import dataclass ++from datetime import datetime,timezone ++import importlib ++import json ++from pathlib import Path ++from urllib.parse import urlsplit ++ ++from task_execution_cleanup import run_cleanup ++from task_execution_journal import Binding,Cleanup,Journal,Refused ++from task_execution_plan import Compiler,encoded,content_hash ++from task_execution_native_fixture import FixtureAdmission,NativeFixtureAdapter ++ ++ ++@dataclass(frozen=True) ++class ConfirmationConfig: ++ """Owner-installed independent read credential/route, never request fields.""" ++ origin: str ++ token_file: Path ++ timeout_seconds: float = 3 ++ ++ ++_HANDOFF_FIELDS={'execution_contract','approval_id','request_digest','decision_id', ++ 'native_binding','approval_binding_digest','submitted_request_digest', ++ 'decision_not_before','decision_expires_at','approval_expires_at'} ++ ++ ++def _strict_dependency(): ++ try: ++ module=importlib.import_module('secrets_engine.task_execution_handoff') ++ for name in ['TaskExecutionAuthorization','TaskExecutionConsumption', ++ 'authorize_task_execution','consume_task_execution']: ++ if not hasattr(module,name):raise ImportError() ++ return module ++ except ImportError: ++ raise Refused('strict Secrets task execution handoff dependency unavailable') from None ++ ++ ++def _fresh(cfg,handoff,descriptor): ++ from secrets_engine.application_time import read_window,validity_bounds ++ lower,upper=validity_bounds(window=read_window(cfg)) ++ try: ++ parse=lambda value:datetime.fromisoformat(value.replace('Z','+00:00')) ++ deadline=parse(descriptor['operation_deadline']) ++ approval=parse(handoff['approval_expires_at']);decision=parse(handoff['decision_expires_at']) ++ start=parse(handoff['decision_not_before']) if handoff['decision_not_before'] else None ++ if any(value.tzinfo is None for value in [deadline,approval,decision]) or (start is not None and start.tzinfo is None):raise ValueError() ++ if upper>=min(deadline,approval,decision) or deadline>min(approval,decision) or (start is not None and lowerverified with a value-free snapshot commitment digest and returns +native_apply=verified. All three separate cleanup obligations must confirm before +cleanup-confirmed. A native denial or metadata mismatch transitions unknown even +if a direct caller catches the exception; the next write cannot proceed. + +Acceptance is sequential per target. Policy may already be applied when role +baseline conflicts. A readback is a statement about that observation, without +native CAS, external-writer exclusion or simultaneous final-state guarantee. +There is no retry, rollback or reauthorization after uncertainty. SQLite reopening +refuses reserve/dispatch, even while the disposable native effect survives. +Cleanup handles remain memory-only: restart refusal is not recovery or proof that +cleanup can finish after process loss. TSA03 remains open. + +## Native admission packet still required + +No actual fresh approved CCR/catalog/grant or production operation is selected. +CCR-2026-0042 remains in_flight and lacks the corresponding Secrets catalogue; +the fixture uses synthetic approved copies of CCR-2026-0002 and issue-core +catalogue, random operation IDs and ephemeral loopback OpenBao. Do not promote +its source paths, before/after defaults, synthetic identity or signing keys into +production admission. + +Owners must provide the chosen fresh metadata-only CCR/catalog, exact source and +install manifests, exact immutable grant projection/body/hash, final compiled Plan +and descriptor, owner-installed backend origin+cluster UID and before/after bytes +with their hashes, and independently scoped owner custody. Access owns actual +key/trust distribution, enforced caller mapping and exact reviewed runtime-policy +admission; current actual policy stays DENY. Secrets owns bounded dispatcher +adoption and trust/package configuration. Approval owns actual human approval, +strict CAS and independently scoped read confirmation. Platform still needs +independent scoped recovery authority and recoverable protected token/accessor +custody, external-writer conflict control/native baseline guarantees where +required, attended owner-native proof and cleanup-after-process-loss acceptance. +These are exact remaining inputs/guarantees, not reasons to withhold this source +slice. No cross-owner source edits were made. + +The package records initial failing fixture-interface/teardown checks and their +corrections, rather than suppressing them. Test success is disposable fixture +source proof only. Public keys and metadata may be generated in ephemeral tests; +secret values/handles are excluded from journal evidence and this packet. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/platform-wave5.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/platform-wave5.patch new file mode 100644 index 00000000..59b5cf8d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/platform-wave5.patch @@ -0,0 +1,434 @@ +diff --git a/docs/task-execution-mediator-seam.md b/docs/task-execution-mediator-seam.md +index 964688b..463c056 100644 +--- a/docs/task-execution-mediator-seam.md ++++ b/docs/task-execution-mediator-seam.md +@@ -54,9 +54,10 @@ with a preceding durable CAS/confirmation sequence. + native calls continue at2. Pending/unknown/cleanup-closed state cannot supply a + next ordinal. Each native request retains its own durable intent and outcome. + 8. Cleanup stays under the original distinct obligations even if business work +- fails. Returned evidence explicitly says native apply is unverified: six successful +- native calls do not establish total approved baseline/readback normalization, +- atomic CAS, service-login provenance or native production owner acceptance. ++ fails. Wave 5 requires exact committed before/after snapshots before reporting ++ native apply verified; six HTTP successes alone remain insufficient. This is ++ sequential fixture acceptance, without atomic CAS, service-login provenance or ++ native production owner acceptance. + + The owner configuration selects provider route/read credential and the existing + Secrets configuration. These are independently installed runtime inputs, never +@@ -105,3 +106,10 @@ per-call served-time/use enforcement, audit/outbox and protected uncertain/crash + recovery. Then obtain and execute the exact founder-scoped attended trial. + Approval replay prevention and uncertain native-effect reconciliation remain + separate guarantees. T12, Secrets WP13 and native adoption stay open. ++ ++Wave 5 adds owner-installed exact metadata snapshots committed through the ++existing expected_before/expected_after descriptor fields, per-target baseline ++and complete role/policy readback comparison, durable readback/verified evidence, ++and a separate cleanup-confirmed transition. The sequence remains sequential, ++without native CAS or independently recoverable cleanup handles. See ++[fixture acceptance](task-execution-native-fixture.md#exact-disposable-metadata-acceptance-2026-10-16-wave-5). +diff --git a/docs/task-execution-native-fixture.md b/docs/task-execution-native-fixture.md +index 7096f1a..1dd6b91 100644 +--- a/docs/task-execution-native-fixture.md ++++ b/docs/task-execution-native-fixture.md +@@ -33,7 +33,7 @@ metadata calls use independently recompiled source/method/path/body. A task use + reserved for revoke; native token limits also enforce the ceiling. Business calls + check wall and monotonic deadlines before effect, with a five-second cleanup reserve. + No retry/remint, caller HCL/JSON/path, mount/AppRole/KV/SecretID or shell API exists. +-The adapter cannot claim complete baseline CAS or normalized readback acceptance. ++Wave 5 below adds exact sequential snapshot acceptance; native baseline CAS remains absent. + + Every business/grant request records durable intent and closed status evidence. + Native cleanup has its own durable per-request intents beneath the original +@@ -89,3 +89,35 @@ native token, then performs explicit synthetic owner recovery. No blind retry oc + No umbrella broker, general executor, new credential catalogue or standing service + admin entitlement is introduced. Approval CAS prevents approval replay; it cannot + make uncertain native effects exactly-once. RPF T12 and Secrets WP13 remain open. ++ ++### Exact disposable metadata acceptance (2026-10-16, wave 5) ++ ++The owner-installed `FixtureAdmission` now carries canonical JSON before/after ++snapshot bytes. Their SHA-256 values must equal the existing descriptor's ++`expected_before` and `expected_after`, and both snapshots must name exactly the ++two compiled metadata paths. Explicit JSON null means absence. Only the actual ++OpenBao empty-errors 404 is accepted as absence; denied/malformed reads fail. ++These inputs contain metadata, never KV values or private token handles. ++ ++Policy comparison retains name, HCL, version and cas_required. Only the ++modification timestamp is excluded, after requiring an aware timestamp and the ++closed observed policy field set. Role comparison retains every returned field, ++including aliases, token defaults and unexpected additions, using canonical JSON ++bytes (therefore false cannot equal zero). The disposable Kubernetes fixture pins ++the complete OpenBao 2.5.5 response shape. Another plugin/version/default requires ++an independently reviewed owner snapshot; nothing infers new accepted defaults. ++ ++Each baseline gates its corresponding write. Policy readback occurs before role ++baseline/write/readback. A role conflict can therefore leave the policy applied; ++that operation remains unknown. This sequence has no native CAS, aggregate ++baseline gate, concurrent-writer exclusion, or simultaneous final-state proof. ++After all six calls match the committed snapshots, the same-live adapter records ++readback and verified with a value-free evidence digest. The mediator reports ++native_apply verified; cleanup-confirmed requires all separate cleanup receipts. ++Readback success with unconfirmed cleanup cannot reach cleanup-confirmed. ++ ++Reopening SQLite cannot dispatch, reconsume, restore token/accessor handles or ++retry cleanup. Native effects can survive process loss. Cleanup handles remain ++memory-only: no independently authorized recovery mechanism is implemented here. ++This closes disposable sequential apply acceptance only, not TSA03 or native ++production recovery/admission. +diff --git a/scripts/task_execution_mediator.py b/scripts/task_execution_mediator.py +index d42b55f..6614974 100644 +--- a/scripts/task_execution_mediator.py ++++ b/scripts/task_execution_mediator.py +@@ -169,12 +169,16 @@ def run_native_fixture_task(*,compiler:Compiler,journal:Journal,plan_request:dic + _fresh(secrets_cfg,handoff,descriptor) + native.metadata_call(ordinal) + result['native_calls_completed']+=1 ++ result['readback_evidence_digest']=native.verify_metadata() ++ result['native_apply']='verified' + except Exception: + _unknown(journal,binding) + result['native_apply']='unknown' + finally: + result['cleanup_confirmed']=run_cleanup(journal,binding,native)['confirmed'] +- # Native total expected-state normalization/acceptance remains explicitly +- # absent. Six successful calls cannot be relabelled a verified owner trial. ++ if result['native_apply']=='verified' and result['cleanup_confirmed']: ++ journal.transition(binding,'verified','cleanup-confirmed',result['readback_evidence_digest']) ++ # Readback and cleanup are independent outcomes; no recovery authority ++ # or native compare-and-swap is implied by either fixture result. + result['business_state']=journal.observe(binding)['state'] + return result +diff --git a/scripts/task_execution_native_fixture.py b/scripts/task_execution_native_fixture.py +index c9ee244..caa7da1 100644 +--- a/scripts/task_execution_native_fixture.py ++++ b/scripts/task_execution_native_fixture.py +@@ -32,6 +32,8 @@ class PrivateSession: + class FixtureAdmission: + """Independently installed owner fixture inputs, not a request schema.""" + descriptor: bytes ++ expected_before: bytes ++ expected_after: bytes + owner_token: str = field(repr=False) + identity_session: PrivateSession = field(repr=False) + server_process: subprocess.Popen = field(repr=False) +@@ -78,6 +80,22 @@ class NativeFixtureAdapter: + self.hcl+='path "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n' + if self.meta['execution_grant']['openbao']['policy']!={'name':self.policy_name,'hcl':self.hcl}: + raise Refused('native grant body differs from bound owner projection') ++ # Installed owner snapshots use the existing descriptor commitments. No ++ # request-supplied expectation or response may authorize itself. ++ self.expected={} ++ for phase in ('before','after'): ++ raw=getattr(admission,'expected_'+phase) ++ if type(raw) is not bytes or 'sha256:'+content_hash(raw)!=descriptor['expected_'+phase]: ++ raise Refused('owner expected-state digest mismatch') ++ value=json.loads(raw) ++ if type(value) is not dict or set(value)!=set(paths) or encoded(value)!=raw: ++ raise Refused('canonical exact target snapshot required') ++ if any(item is not None and type(item) is not dict for item in value.values()): ++ raise Refused('metadata snapshot object or explicit absence required') ++ if phase=='after' and any(item is None for item in value.values()): ++ raise Refused('present final metadata required') ++ self.expected[phase]=value ++ self._observed={'before':{},'after':{}} + self.cleanup=tuple(Cleanup(item['kind'],item['call_digest']) for item in cleanup_specs(binding.operation_id)) + if self.meta['cleanup'] != cleanup_specs(binding.operation_id): raise Refused('cleanup plan mismatch') + self.task=None; self._mint_attempted=False; self._policy_attempted=False; self._ordinal=0; self._next_metadata=0 +@@ -183,11 +201,53 @@ class NativeFixtureAdapter: + call=self.compiler.before_call(self.plan,ordinal) + status,data=self._business(call.method,call.path,self.task.token,call.payload,task=True) + self._next_metadata+=1 +- self._ok(status,data,{200,204,404} if ordinal in {0,3} else {200,204}) +- # Metadata only. The trusted owner caller must compare native normalized +- # baseline/readback to separately admitted expected state. Not KV values. ++ try: ++ self._ok(status,data,{200,204,404} if ordinal in {0,3} else {200,204}) ++ if ordinal in {0,2,3,5}: ++ phase='before' if ordinal in {0,3} else 'after' ++ value=self._metadata_snapshot(call.path,status,data) ++ if encoded(value)!=encoded(self.expected[phase][call.path]): ++ raise Refused('native exact expected-state mismatch') ++ self._observed[phase][call.path]=value ++ except Exception: ++ state=self.journal.observe(self.binding)['state'] ++ if state!='unknown': self.journal.transition(self.binding,state,'unknown') ++ raise Refused('native baseline or readback unconfirmed') from None + return status,data + ++ @staticmethod ++ def _metadata_snapshot(path,status,response): ++ # HTTP errors/denials are never absence. OpenBao's exact empty 404 ++ # metadata response is the only absent representation admitted here. ++ if status==404 and response=={'errors':[]}: ++ return None ++ if status!=200 or response.get('errors') or type(response.get('data')) is not dict: ++ raise Refused('metadata read not confirmed') ++ value=dict(response['data']) ++ if path.startswith('sys/policies/acl/'): ++ if set(value)!={'cas_required','modified','name','policy','version'}: ++ raise Refused('unrecognized policy metadata fields') ++ modified=value.pop('modified') ++ if type(modified) is not str or datetime.fromisoformat(modified.replace('Z','+00:00')).tzinfo is None: ++ raise Refused('invalid policy modification timestamp') ++ # All role fields, aliases, defaults and unknown additions participate in ++ # exact canonical JSON comparison. True never equals integer 1 here. ++ return value ++ ++ def verify_metadata(self): ++ self._fresh() ++ if self._next_metadata!=6 or self.journal.observe(self.binding)['state']!='applying': ++ raise Refused('complete same-live metadata sequence required') ++ for phase in ('before','after'): ++ if encoded(self._observed[phase])!=encoded(self.expected[phase]): ++ raise Refused('complete expected metadata evidence required') ++ evidence=content_hash(encoded({'expected_before':'sha256:'+content_hash(self.admission.expected_before), ++ 'expected_after':'sha256:'+content_hash(self.admission.expected_after), ++ 'scope':'per-target-sequential-readback-no-native-cas'})) ++ self.journal.transition(self.binding,'applying','readback',evidence) ++ self.journal.transition(self.binding,'readback','verified',evidence) ++ return evidence ++ + def _cleanup_admitted(self, kind, digest): + self._live() + expected=next(item for item in self.cleanup if item.kind==kind) +diff --git a/tests/test_task_execution_mediator.py b/tests/test_task_execution_mediator.py +index 8ad9045..52024f1 100644 +--- a/tests/test_task_execution_mediator.py ++++ b/tests/test_task_execution_mediator.py +@@ -99,7 +99,9 @@ def test_whole_strict_consume_confirmation_precedes_real_native_effect(owner,tmp + with whole_fixture(owner,tmp_path) as fixture: + result=invoke(fixture) + assert result['native_calls_completed']==6 and result['cleanup_confirmed'] +- assert result['native_apply']=='unverified' # total owner baseline/readback acceptance remains separate ++ assert result['native_apply']=='verified' ++ assert result['business_state']=='cleanup-confirmed' ++ assert len(result['readback_evidence_digest'])==64 + obj=fixture.approval.engine.get(fixture.approval_id) + assert obj.status=='consumed' + assert obj.consumed_digest==result['request_digest'] +diff --git a/tests/test_task_execution_native_fixture.py b/tests/test_task_execution_native_fixture.py +index 2a8e419..4c54313 100644 +--- a/tests/test_task_execution_native_fixture.py ++++ b/tests/test_task_execution_native_fixture.py +@@ -78,6 +78,19 @@ def owner(compiler,bao_dev,tmp_path): + 'backend_origin':origin,'cluster_uid':'disposable-fixture-only', + 'operations':req['operations'],'operation_deadline':(datetime.now(timezone.utc)+timedelta(seconds=240)).isoformat(), + 'backend_budget':{'ttl_seconds':120,'uses':12},'expected_before':'sha256:'+'0'*64,'expected_after':'sha256:'+'1'*64} ++ # Independently installed synthetic expected state, before any native write. ++ policy_call=compiler.before_call(plan,1);role_call=compiler.before_call(plan,4) ++ body=json.loads(role_call.payload) ++ before=m.encoded({policy_call.path:None,role_call.path:None}) ++ after=m.encoded({policy_call.path:{'cas_required':False,'name':policy_call.path.rsplit('/',1)[1], ++ 'policy':json.loads(policy_call.payload)['policy'],'version':1},role_call.path:{ ++ 'alias_name_source':'serviceaccount_uid','bound_service_account_names':body['bound_service_account_names'], ++ 'bound_service_account_namespace_selector':'','bound_service_account_namespaces':body['bound_service_account_namespaces'], ++ 'policies':body['policies'].split(','),'token_bound_cidrs':[],'token_explicit_max_ttl':0, ++ 'token_max_ttl':0,'token_no_default_policy':False,'token_num_uses':0,'token_period':0, ++ 'token_policies':body['policies'].split(','),'token_strictly_bind_ip':False, ++ 'token_ttl':900,'token_type':'default','ttl':900}}) ++ descriptor.update(expected_before=m.tagged(before),expected_after=m.tagged(after)) + # Exercise the actual consumer closed descriptor parser as well; source pins, + # grant/expected-state digests remain deliberately synthetic owner fixtures. + from secrets_engine.execution_contract import ExecutionContract +@@ -107,7 +120,7 @@ def owner(compiler,bao_dev,tmp_path): + plan=compiler.bind_execution(compiler.compile(req),descriptor,installed) + binding=Binding('SYNTHETIC-APPROVAL-NOT-PRODUCTION',req['request_digest'],req['operation_id'],plan.content_digest) + state=tmp_path/'private-journal';state.mkdir(mode=0o700);journal=Journal(state) +- admission=FixtureAdmission(installed,root,identity,process) ++ admission=FixtureAdmission(installed,before,after,root,identity,process) + native=NativeFixtureAdapter(compiler,plan,journal,binding,admission) + try: yield native,journal,binding,call,compiler,descriptor + finally: journal.close() +@@ -140,6 +153,7 @@ def test_native_fixed_grant_metadata_denials_and_confirmed_cleanup(owner): + assert role[1]['data']['bound_service_account_names']==expected['bound_service_account_names'] + assert role[1]['data']['bound_service_account_namespaces']==expected['bound_service_account_namespaces'] + assert role[1]['data']['token_policies']==expected['policies'].split(',') ++ assert len(native.verify_metadata())==64 + # Root probes native capabilities without spending the limited task token. + for path in ['sys/policies/acl/sibling','auth/kubernetes/role/sibling','secret/data/sibling','auth/token/create','sys/mounts/secret','auth/approle/role/sibling/secret-id']: + status,data=call('POST','sys/capabilities',{'token':native.task.token,'paths':[path]}) +@@ -152,7 +166,7 @@ def test_native_fixed_grant_metadata_denials_and_confirmed_cleanup(owner): + assert status==400 and data.get('errors') in (['invalid accessor'],['1 error occurred:\n\t* invalid accessor\n\n']) + assert result['confirmed'] + assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404 +- assert journal.observe(binding)['state']=='applying' # native success is not full owner verification ++ assert journal.observe(binding)['state']=='verified' + with pytest.raises(Refused): native.metadata_call(6) + with pytest.raises(Refused): run_cleanup(journal,binding,native) + +@@ -261,7 +275,7 @@ def test_nonloopback_estate_origin_is_refused_before_native_transport(owner): + installed=m.encoded(changed) + plan=changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed,installed) + changed_binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest) +- admission=FixtureAdmission(installed,native.admission.owner_token,native.admission.identity_session,native.admission.server_process) ++ admission=replace(native.admission,descriptor=installed) + with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission) + + +@@ -294,7 +308,7 @@ def test_proposed_source_grant_never_materializes_even_with_matching_digest(owne + installed=m.encoded(changed_descriptor) + plan=changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed_descriptor,installed) + changed_binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest) +- admission=FixtureAdmission(installed,native.admission.owner_token,native.admission.identity_session,native.admission.server_process) ++ admission=replace(native.admission,descriptor=installed) + with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission) + assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404 + +diff --git a/tests/test_task_execution_readback.py b/tests/test_task_execution_readback.py +new file mode 100644 +index 0000000..5bc0b5b +--- /dev/null ++++ b/tests/test_task_execution_readback.py +@@ -0,0 +1,127 @@ ++"""Exact disposable native acceptance, partial effects and restart refusal.""" ++import json ++from dataclasses import replace ++import pytest ++from test_task_execution_native_fixture import owner,bao_dev,compiler,admit ++from task_execution_native_fixture import NativeFixtureAdapter ++from task_execution_journal import Refused,Journal ++from task_execution_cleanup import run_cleanup ++ ++ ++def start(native,journal,binding): ++ admit(native,journal,binding);native.materialize_grant() ++ journal.transition(binding,'grant-issued','applying') ++ ++ ++def test_expectation_digest_pin_rejected_before_reserve(owner): ++ n,j,b,c,compiler,*_=owner ++ with pytest.raises(Refused,match='expected-state digest'): ++ NativeFixtureAdapter(compiler,n.plan,j,b,replace(n.admission,expected_after=b'{}')) ++ assert c('GET','sys/policies/acl/'+n.policy_name)[0]==404 ++ ++ ++@pytest.mark.parametrize('target',[0,3]) ++def test_actual_conflicting_baseline_gates_its_target_write(owner,target): ++ n,j,b,c,compiler,*_=owner ++ call=compiler.before_call(n.plan,target+1) ++ existing={'policy':'path "unrelated" { capabilities = ["read"] }'} if target==0 else json.loads(call.payload) ++ assert c('PUT',call.path,existing)[0] in {200,204} ++ with j.locked(): ++ start(n,j,b) ++ for i in range(target): n.metadata_call(i) ++ with pytest.raises(Refused,match='baseline or readback'):n.metadata_call(target) ++ assert j.observe(b)['state']=='unknown' ++ if target==3: ++ # Policy effect already happened: this is deliberately no aggregate CAS. ++ assert c('GET',compiler.before_call(n.plan,1).path)[1]['data']['policy']==json.loads(compiler.before_call(n.plan,1).payload)['policy'] ++ assert run_cleanup(j,b,n)['confirmed'] ++ assert j.observe(b)['state']=='unknown' ++ ++ ++@pytest.mark.parametrize('target',[2,5]) ++def test_actual_readback_drift_never_verifies(owner,target): ++ n,j,b,c,compiler,*_=owner ++ with j.locked(): ++ start(n,j,b) ++ for i in range(target):n.metadata_call(i) ++ call=compiler.before_call(n.plan,target-1) ++ body=json.loads(call.payload) ++ if target==2:body['policy']+='\npath "sibling/*" { capabilities = ["read"] }' ++ else:body['policies']+=',unexpected-policy' ++ assert c('PUT',call.path,body)[0] in {200,204} ++ with pytest.raises(Refused,match='baseline or readback'):n.metadata_call(target) ++ with pytest.raises(Refused):n.verify_metadata() ++ assert run_cleanup(j,b,n)['confirmed'] ++ assert j.observe(b)['state']=='unknown' ++ ++ ++@pytest.mark.parametrize('status,response',[(403,{'errors':[]}), (404,{}),(404,{'errors':['permission denied']}), (200,{'data':None})]) ++def test_only_exact_backend_absence_counts(status,response): ++ with pytest.raises(Refused):NativeFixtureAdapter._metadata_snapshot('auth/kubernetes/role/x',status,response) ++ ++ ++def test_json_types_and_unknown_role_fields_remain_significant(owner): ++ n,j,b,c,compiler,*_=owner ++ original=n._http ++ def changed(method,path,token,body=b''): ++ status,response=original(method,path,token,body) ++ if method=='GET' and path==compiler.before_call(n.plan,5).path and status==200: ++ response['data']['token_num_uses']=False # False == 0 in Python; never canonical JSON. ++ return status,response ++ n._http=changed ++ with j.locked(): ++ start(n,j,b) ++ for i in range(5):n.metadata_call(i) ++ with pytest.raises(Refused):n.metadata_call(5) ++ assert run_cleanup(j,b,n)['confirmed'] ++ assert j.observe(b)['state']=='unknown' ++ ++ ++def test_reopened_sqlite_cannot_resume_or_recover_memory_handles(owner): ++ n,j,b,c,compiler,*_=owner ++ with j.locked(): ++ start(n,j,b) ++ n.metadata_call(0);n.metadata_call(1) ++ # Model loss of the live admission; handles deliberately are not reconstructed. ++ # The still-running disposable server proves native effect survives reopening. ++ location=j.directory ++ assert c('GET',compiler.before_call(n.plan,1).path)[0]==200 ++ j.db.close() ++ # Owner journal retains the exact DB path; this is a real reopened SQLite. ++ reopened=Journal(location) ++ try: ++ with reopened.locked(): ++ assert reopened.observe(b)['dispatch_allowed'] is False ++ with pytest.raises(Refused):reopened.reserve(b,n.cleanup) ++ with pytest.raises(Refused):reopened.next_call_ordinal(b) ++ finally:reopened.close() ++ # No recovery/cleanup claim: only same-process fixture teardown retains handles. ++ ++ ++def test_verified_readback_with_real_cleanup_denial_is_not_complete(owner): ++ n,j,b,c,compiler,*_=owner ++ with j.locked(): ++ start(n,j,b) ++ for i in range(6):n.metadata_call(i) ++ n.verify_metadata() ++ # Actual backend removal denies the task's self-revoke capability. ++ assert c('DELETE','sys/policies/acl/'+n.policy_name)[0] in {200,204} ++ result=run_cleanup(j,b,n) ++ assert result['confirmed'] is False ++ assert j.observe(b)['state']=='verified' ++ with pytest.raises(Refused):j.transition(b,'verified','cleanup-confirmed','0'*64) ++ assert c('POST','auth/token/lookup-accessor',{'accessor':n.task.accessor})[0]==200 ++ ++ ++def test_actual_denied_baseline_cannot_be_caught_then_write_retried(owner): ++ n,j,b,c,compiler,*_=owner ++ with j.locked(): ++ start(n,j,b) ++ # Task still has update but loses read: real 403 is not absent baseline. ++ denied=n.hcl.replace('["read", "create", "update"]','["create", "update"]') ++ assert c('PUT','sys/policies/acl/'+n.policy_name,{'policy':denied})[0] in {200,204} ++ with pytest.raises(Refused):n.metadata_call(0) ++ assert j.observe(b)['state']=='unknown' ++ with pytest.raises(Refused):n.metadata_call(1) ++ assert c('GET',compiler.before_call(n.plan,1).path)[0]==404 ++ assert run_cleanup(j,b,n)['confirmed'] +diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md +index 4ea8015..8b09305 100644 +--- a/workplans/RPF-WP-0035-credential-lane-implementation.md ++++ b/workplans/RPF-WP-0035-credential-lane-implementation.md +@@ -970,6 +970,19 @@ and defaults stay unchanged. T02 still owns login/claim/TTL/use/MASON acceptance + Secrets T06 separately owns dispatcher adoption and steady-state default migration. + + ++2026-10-16 T12 wave 5 source preparation: disposable adapter now binds owner ++canonical before/after metadata snapshots to existing descriptor hashes, rejects ++per-target baseline/readback mismatch, and records readback/verified evidence. ++Mediator reports cleanup-confirmed only after independent fixed cleanup receipts. ++Complete role metadata and policy name/HCL/version/cas are compared; validated ++modification timestamp alone is excluded. This is sequential fixture acceptance, ++not aggregate CAS, concurrent-writer exclusion or native production completion. ++Role conflict may follow a policy write. Restart refuses dispatch and cannot ++recover memory-only token/accessor cleanup handles. T12 remains progress; actual ++fresh CCR/catalog/grant, source/trust/caller/policy admission, isolated owner custody, ++scoped recovery and attended native acceptance remain explicit prerequisites. ++ ++ + 2026-10-11 T11 refinement (same source-review trial): the exact Secrets Engine + v3 manifest `6bb05bac74e4939c3e42db2ae95c5f9fb4616bb20cc285811a315c26669ebce1` + and all named bytes/proof were verified and independently re-reviewed. The three diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/test-evidence.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/test-evidence.json new file mode 100644 index 00000000..2247f586 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-wave5/test-evidence.json @@ -0,0 +1,46 @@ +{ + "scope": "local synthetic identities/approvals and actual loopback OpenBao only; no production admission", + "environment": { + "UV_CACHE_DIR": "/tmp/netkingdom-uv-cache", + "SECRETS_ENGINE_SOURCE_ROOT": "/home/worsch/secrets-engine", + "PYTHONPATH": "/home/worsch/secrets-engine/src:/home/worsch/approval-engine" + }, + "trials": [ + { + "suite": "tests/test_task_execution_native_fixture.py tests/test_task_execution_mediator.py", + "result": "16 passed, 3 failed in 218.18s", + "failures": "fixture assertion expected cleanup-confirmed without mediator transition; two old FixtureAdmission positional constructions. Corrected before focused rerun." + }, + { + "suite": "initial tests/test_task_execution_readback.py", + "result": "11 passed, 1 teardown error in 140.70s", + "failure": "test closed Journal fd twice; changed to closing/reopening actual SQLite connection and retaining original fd for fixture teardown." + }, + { + "suite": "three corrected original native tests + whole strict mediator positive + real cleanup denial after verified readback", + "result": "5 passed in 84.12s" + }, + { + "suite": "tests/test_task_execution_readback.py (before final denial case added)", + "result": "12 passed in 134.86s" + }, + { + "suite": "actual denied baseline cannot catch then retry write + whole strict mediator positive", + "result": "2 passed in 39.82s", + "final_source": "final error handling transitions unknown for native denial as well as snapshot mismatch" + } + ], + "distinct_cases_exercised": 32, + "final_targeted_green_distinct_cases": 17, + "unchanged_green_cases_from_initial_affected_suites": 15, + "checks": [ + "git diff --check passed" + ], + "limits": [ + "No full unchanged compiler/journal/grant suite rerun", + "Reopened SQLite fixture is restart/refusal evidence, not OS crash cleanup recovery", + "Policy and role readbacks are sequential, not atomic final-state proof", + "Native cleanup handles remain memory-only", + "Synthetic positive policy separate; real actual policy remains DENY" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution/manifest.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution/manifest.json new file mode 100644 index 00000000..95623018 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution/manifest.json @@ -0,0 +1,48 @@ +{ + "owner_repository": "/home/worsch/railiance-platform", + "isolated_checkout": "/tmp/netkingdom-platform-20261016", + "base_commit": "3b6e647aa06bed88966a4539f9d3cb7ed6792bd4", + "patch": "platform-cleanup.patch", + "patch_sha256": "be8de4975980e4b88b37ed98d3fdd509d513f85834efc3a5cb744410c4e95392", + "files_sha256": { + "docs/task-execution-cleanup.md": "44cb21e69838c961eca9805ec3d1f3e64d12b46dc9c2e7b02c69610a97a0f33b", + "docs/task-execution-plan-compiler.md": "83f28626f1f6499f52f22c3daafd22c802475e8a77bb1be7ba9901c7ae00bf8b", + "scripts/task_execution_cleanup.py": "c1c0b649e8002b9a6e1599d46f70ad3ad3d743323ddb82367468d5bdb103062c", + "scripts/task_execution_journal.py": "f512d72be880de2a6345d3550c60a0c8e6e3b6d2bc8123f4502a068afabd973b", + "tests/test_task_execution_cleanup.py": "a0d4d6d2c8c22bfed17a109a6f0be02d5f128619866ae5f1ea69f81803133085", + "tests/test_task_execution_journal.py": "707ee5404e567ddf9896d680feae47628b9a3cfc9e3ce0073100e1677af53b5c", + "workplans/RPF-WP-0035-credential-lane-implementation.md": "f1be966f38a96efbedcb7a129586a6f905e1910fe37ef85c61fe29379fa50af6" + }, + "canonical_owner_changed": false, + "canonical_dirty_tracked_observed": [ + "WORK-RECORDS.md" + ], + "canonical_dirty_diff_sha256": "e4b417412197d27214254ac968742f3be2926df21f6cd52cdb92987089e2223a", + "canonical_dirty_diff_handling": "Generated WORK-RECORDS index only; unrelated to executor sources. Inspected but excluded from isolated source patch. No untracked/private source files copied.", + "review_sha256": "dcb5b155eabefd7317eb6fc10a4573f7d59d8d7bf5a0b18d70d712ddbdd26b43", + "tests": { + "command": "SECRETS_ENGINE_SOURCE_ROOT=/home/worsch/secrets-engine PYTHONPATH=/home/worsch/secrets-engine/src python3 -m pytest -q tests/test_task_execution_plan.py tests/test_task_execution_journal.py tests/test_task_execution_cleanup.py", + "cwd": "/tmp/netkingdom-platform-20261016", + "result": "73 passed in 11.68s", + "initial_attempt": "Compiler fixture setup initially failed without SECRETS_ENGINE_SOURCE_ROOT (default /tmp/secrets-engine absent); corrected existing test path configuration and reran.", + "mechanisms": [ + "Actual SQLite FULL synchronous transactions and process file locks", + "Four-process cleanup admission race admits exactly one worker", + "Abrupt os._exit before and after separately fsynced synthetic effect", + "Restart preserves pending outcome and refuses cleanup/business resumption", + "Revoke timeout continues independent cleanup but never confirms full cleanup", + "Existing 45 compiler and 15 journal regressions retained; 13 new cleanup tests" + ], + "native_openbao_proof": false + }, + "unmet_production_gates": [ + "Compiler and full canonical request must bind exact cleanup obligations and authoritative grant", + "Authenticated initiating identity, served claim/PDP/package and independent atomic consume confirmation", + "Owner-held ephemeral token materializer and restricted native adapter", + "Exact native baseline/write/readback and per-call trusted-time/use checks", + "Native cleanup/recovery and audit acceptance; founder-scoped attended trial" + ], + "work_item": "RPF-WP-0035-T12", + "secrets_contract": "SECRETS-WP-0013", + "delivery": "Reviewable patch only; no owner commit/sync/push/deploy" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-cleanup.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-cleanup.patch new file mode 100644 index 00000000..68c5856e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-cleanup.patch @@ -0,0 +1,595 @@ +diff --git a/docs/task-execution-cleanup.md b/docs/task-execution-cleanup.md +new file mode 100644 +index 0000000..c21050b +--- /dev/null ++++ b/docs/task-execution-cleanup.md +@@ -0,0 +1,66 @@ ++# Durable cleanup obligations for the metadata execution pilot ++ ++RPF-WP-0035-T12 source preparation, 2026-10-16. This source is not an installed ++mediator, native execution entitlement, founder approval or owner-native proof. ++ ++Platform's existing admission journal made `unknown` and `failed` terminal and ++blocked all further calls. Its single business-state cleanup transition could not ++express a timed-out write with confirmed token revocation, or a verified write ++with unconfirmed cleanup. A future mediator using that interface could suppress ++cleanup precisely when needed. The journal now retains the original business ++outcome and records cleanup in a separate durable table. ++ ++At fresh reservation the trusted owner supplies at most one commitment each for ++`task-token-revoke`, `identity-token-revoke`, and `grant-policy-delete`. Each is a ++kind and exact native-call digest; no token/accessor, payload, path override or ++shell command is accepted. Obligations and admission are inserted in one SQLite ++transaction with FULL synchronous commits and directory fsync. Existing journal ++files gain the separate table; old admissions gain no obligations or authority. ++Empty cleanup evidence cannot produce a confirmed result or legacy success state. ++ ++Only the same live exclusive invocation that transitioned this binding through ++`consume-confirmed` can record cleanup intent. Neither a caller string nor journal ++observation establishes consume authority: the trusted mediator must independently ++validate the real served approval/PDP/consume before that transition. Cleanup can ++proceed when the business call is pending or state is unknown/failed. Starting it ++permanently closes business dispatch for that admission. An exact obligation moves ++`owed -> pending -> confirmed|unconfirmed`; no retry transition exists. A crash ++leaves pending, and reopening never restores admission. Pending business-call ++uncertainty remains even if every cleanup succeeds. ++ ++`task_execution_cleanup.run_cleanup` is the fixed caller of those journal methods. ++It is intended for the owner mediator's `finally` block inside the original ++`Journal.locked()` context. It dispatches only the three named owner cleanup ++methods, passing their admitted digest for independent native-call comparison. ++Ordinary exceptions/malformed evidence become closed unconfirmed reason evidence ++without logging or hashing raw backend text. Remaining independent obligations ++continue. A process death or BaseException leaves pending intent and requires ++owner reconciliation. A journal write failure prevents the corresponding effect. ++The component has no HTTP client, CLI command execution or native credential opener. ++ ++The owner adapter must retain the exact handles in private memory, implement ++bounded native calls, compare each reconstructed cleanup call against the admitted ++commitment, and return confirmed only after the appropriate native revoke/absence ++verification. A timeout, policy deletion alone, or eventual TTL expiry cannot prove ++issued-token revocation. Cleanup has the original separate obligation rather than ++the expired business deadline; this component does not extend any business grant. ++No automatic postcrash recovery is implemented: owner-private recovery requires ++fresh scoped authority and must never retry a business effect. ++ ++The current Compiler Plan does not yet contain these cleanup commitments. A ++production mediator must bind them into the exact reviewed grant/full canonical ++request/owner plan before reservation. The journal's digest parameter does not ++verify that correspondence. It must also integrate authenticated initiating actor, ++served claim/PDP and independently verified atomic consume, exact source/body and ++baseline/readback validation, ephemeral owner-held token materialization, per-call ++trusted-time/use enforcement, and native audit/recovery. There is no production ++executor in this repository to wire to this driver today; existing production ++readiness remains refused. Approval CAS alone cannot provide exactly-once target ++effects across process/network failure. T12 and SECRETS-WP-0013 remain open. ++ ++Validation uses actual SQLite/process locks and abrupt `os._exit` before and after ++a separately fsynced synthetic fixture effect. Reopened processes cannot resume ++cleanup or business work; a four-process race admits one driver. Other tests cover ++pending business calls, revoke timeout without suppressing later cleanup, changed ++commitments and bindings, malformed confirmations and empty-evidence refusal. ++These are disposable local fixtures, not OpenBao or production acceptance. +diff --git a/docs/task-execution-plan-compiler.md b/docs/task-execution-plan-compiler.md +index 32c440c..9bb49f7 100644 +--- a/docs/task-execution-plan-compiler.md ++++ b/docs/task-execution-plan-compiler.md +@@ -94,7 +94,9 @@ cleanup use; this component records limits, it does not enforce native expiry/us + or provide a hard elapsed timer. Trusted owner runtime must bound parser/import/ + filesystem/compiler work by remaining action budget before any actual call. + Token custody/revoke, temporary object removal, owner-private recovery after unknown +-and truthful native readback remain gates. General lifecycle/apply/exec/default ++and truthful native readback remain gates. The 2026-10-16 source cleanup journal ++and fixed driver are documented in task-execution-cleanup.md; they preserve unknown ++outcomes but are not yet bound into this compiler or a production mediator. General lifecycle/apply/exec/default + migration and SecretsT06/RPF35T02/T12 cannot close from this source component. + + Validation:45 compiler tests and 15 journal tests pass, covering real owner K8s/OIDC body generation, +diff --git a/scripts/task_execution_cleanup.py b/scripts/task_execution_cleanup.py +new file mode 100644 +index 0000000..c865c41 +--- /dev/null ++++ b/scripts/task_execution_cleanup.py +@@ -0,0 +1,72 @@ ++"""Fixed cleanup driver for a trusted owner mediator; no native transport exists here. ++ ++Call from the mediator's finally block while its original Journal.locked context ++is still live. The owner adapter retains credential/handle custody and reconstructs ++and compares the native call with the expected digest before effect. Its three ++methods take no caller-selected path, body, shell command or token. This source ++component neither validates consume nor creates a native execution entitlement. ++""" ++from __future__ import annotations ++ ++from dataclasses import dataclass ++import hashlib ++import re ++from typing import Protocol ++ ++from task_execution_journal import Binding, Cleanup, Journal, Refused ++ ++ ++@dataclass(frozen=True) ++class CleanupResult: ++ confirmed: bool ++ evidence_digest: str ++ ++ def validate(self): ++ if type(self.confirmed) is not bool or type(self.evidence_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}", self.evidence_digest): ++ raise Refused("invalid owner cleanup evidence") ++ ++ ++class OwnerCleanup(Protocol): ++ # Confirm requires actual native revoke/absence evidence; a timeout is never ++ # proof of absence. Implementations must use bounded calls and discard raw ++ # backend failure output. Protected handles never leave the owner adapter. ++ def revoke_task_token(self, expected_call_digest: str) -> CleanupResult: ... ++ def revoke_identity_token(self, expected_call_digest: str) -> CleanupResult: ... ++ def delete_grant_policy(self, expected_call_digest: str) -> CleanupResult: ... ++ ++ ++def run_cleanup(journal: Journal, binding: Binding, owner: OwnerCleanup): ++ """Attempt each fixed obligation once, preserving business/unknown outcomes. ++ ++ Ordinary adapter failures record unconfirmed evidence and do not suppress the ++ remaining independent cleanup. BaseException/process death leaves durable ++ pending intent; neither this driver nor a reopened journal retries it. ++ Journal failures propagate and prohibit dispatch without durable intent. ++ """ ++ obligations = journal.cleanup_status(binding)['obligations'] ++ if not obligations: ++ raise Refused("explicit owner cleanup obligations required") ++ if any(item['outcome'] != 'owed' for item in obligations): ++ raise Refused("cleanup already attempted; owner reconciliation required") ++ for item in obligations: ++ obligation = Cleanup(item['kind'], item['call_digest']) ++ journal.before_cleanup(binding, item['ordinal'], obligation) ++ try: ++ if obligation.kind == 'task-token-revoke': ++ result = owner.revoke_task_token(obligation.call_digest) ++ elif obligation.kind == 'identity-token-revoke': ++ result = owner.revoke_identity_token(obligation.call_digest) ++ elif obligation.kind == 'grant-policy-delete': ++ result = owner.delete_grant_policy(obligation.call_digest) ++ else: ++ raise Refused("unsupported cleanup obligation") ++ if type(result) is not CleanupResult: ++ raise Refused("typed owner cleanup evidence required") ++ result.validate() ++ except Exception: ++ # Closed metadata only. Never hash/log a backend exception, response, ++ # token, accessor or secret-bearing stdout/stderr. ++ result = CleanupResult(False, hashlib.sha256(b'owner-cleanup-unconfirmed/v1').hexdigest()) ++ journal.after_cleanup(binding, item['ordinal'], obligation, ++ confirmed=result.confirmed, evidence_digest=result.evidence_digest) ++ return journal.cleanup_status(binding) +diff --git a/scripts/task_execution_journal.py b/scripts/task_execution_journal.py +index 4bff83c..7c54ac8 100644 +--- a/scripts/task_execution_journal.py ++++ b/scripts/task_execution_journal.py +@@ -57,6 +57,26 @@ _NEXT = { + } + + ++@dataclass(frozen=True) ++class Cleanup: ++ """Owner-derived exact call commitment, never a handle or authority token. ++ ++ The owner mediator must include these commitments in its approved plan before ++ reservation, then compare the actual native cleanup call immediately before ++ dispatch. This journal does not authenticate that plan or native adapter. ++ """ ++ kind: str ++ call_digest: str ++ ++ def validate(self): ++ if type(self.kind) is not str or self.kind not in { ++ "task-token-revoke", "identity-token-revoke", "grant-policy-delete" ++ }: ++ raise Refused("unsupported cleanup obligation") ++ if type(self.call_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}", self.call_digest): ++ raise Refused("invalid cleanup call digest") ++ ++ + class Journal: + def __init__(self, directory: Path): + # Require an existing owner-private directory: deployment creates it. +@@ -82,8 +102,10 @@ class Journal: + self.db.execute("PRAGMA journal_mode=DELETE") + self.db.execute("CREATE TABLE IF NOT EXISTS operations (approval TEXT PRIMARY KEY, request TEXT NOT NULL, operation TEXT UNIQUE NOT NULL, plan TEXT NOT NULL, state TEXT NOT NULL, evidence TEXT)") + self.db.execute("CREATE TABLE IF NOT EXISTS calls (operation TEXT NOT NULL, ordinal INTEGER NOT NULL, call_digest TEXT NOT NULL, outcome TEXT NOT NULL, evidence TEXT, PRIMARY KEY(operation,ordinal))") ++ self.db.execute("CREATE TABLE IF NOT EXISTS cleanup (operation TEXT NOT NULL, ordinal INTEGER NOT NULL, kind TEXT NOT NULL, call_digest TEXT NOT NULL, outcome TEXT NOT NULL, evidence TEXT, PRIMARY KEY(operation,ordinal), UNIQUE(operation,kind))") + self._held = False + self._admission = None ++ self._consumed_admission = None + self._sync_directory() + + @staticmethod +@@ -123,11 +145,20 @@ class Journal: + yield self + finally: + self._admission = None ++ self._consumed_admission = None + self._held = False + fcntl.flock(self._lock_fd, fcntl.LOCK_UN) + +- def reserve(self, binding: Binding): ++ def reserve(self, binding: Binding, cleanup: tuple[Cleanup, ...] = ()): + binding.validate() ++ if type(cleanup) is not tuple or len(cleanup) > 3: ++ raise Refused("closed cleanup plan required") ++ for item in cleanup: ++ if type(item) is not Cleanup: ++ raise Refused("typed cleanup obligation required") ++ item.validate() ++ if len({item.kind for item in cleanup}) != len(cleanup): ++ raise Refused("duplicate cleanup obligation") + if not self._held or self._admission is not None: + raise Refused("reservation requires fresh exclusive invocation") + try: +@@ -135,6 +166,9 @@ class Journal: + self.db.execute("INSERT INTO operations VALUES (?,?,?,?,?,NULL)", + (binding.approval_id, binding.request_digest, + binding.operation_id, binding.plan_digest, "reserved")) ++ self.db.executemany("INSERT INTO cleanup VALUES (?,?,?,?,'owed',NULL)", ++ [(binding.operation_id, i, item.kind, item.call_digest) ++ for i, item in enumerate(cleanup)]) + self.db.execute("COMMIT") + self._sync_directory() + except sqlite3.IntegrityError: +@@ -158,6 +192,13 @@ class Journal: + try: + self.db.execute("BEGIN IMMEDIATE") + pending=self.db.execute("SELECT COUNT(*) FROM calls WHERE operation=? AND outcome='pending'",(binding.operation_id,)).fetchone()[0] ++ started = self.db.execute("SELECT COUNT(*) FROM cleanup WHERE operation=? AND outcome!='owed'", (binding.operation_id,)).fetchone()[0] ++ if started and target not in {'unknown', 'cleanup-confirmed'}: ++ raise Refused("cleanup has closed business execution") ++ if target == 'cleanup-confirmed': ++ total, owed = self.db.execute("SELECT COUNT(*),COALESCE(SUM(outcome!='confirmed'),0) FROM cleanup WHERE operation=?", (binding.operation_id,)).fetchone() ++ if not total or owed: ++ raise Refused("explicit cleanup obligations are unconfirmed") + if pending and target != 'unknown': + raise Refused("unconfirmed external call requires unknown") + changed = self.db.execute( +@@ -168,6 +209,8 @@ class Journal: + raise Refused("journal state changed") + self.db.execute("COMMIT") + self._sync_directory() ++ if target == "consume-confirmed": ++ self._consumed_admission = binding + except BaseException: + if self.db.in_transaction: + self.db.execute("ROLLBACK") +@@ -182,6 +225,7 @@ class Journal: + raise Refused("journal binding mismatch") + # Readback cannot create a live admission, even for an interrupted state. + pending = self.db.execute("SELECT COUNT(*) FROM calls WHERE operation=? AND outcome='pending'", (binding.operation_id,)).fetchone()[0] ++ pending += self.db.execute("SELECT COUNT(*) FROM cleanup WHERE operation=? AND outcome='pending'", (binding.operation_id,)).fetchone()[0] + return {"state": row[3], "evidence_digest": row[4], "dispatch_allowed": False, + "external_outcome_unknown": bool(pending)} + +@@ -201,7 +245,8 @@ class Journal: + self.db.execute("BEGIN IMMEDIATE") + count,pending = self.db.execute("SELECT COUNT(*), COALESCE(SUM(outcome='pending'),0) FROM calls WHERE operation=?",(binding.operation_id,)).fetchone() + state=self.db.execute("SELECT state FROM operations WHERE operation=?",(binding.operation_id,)).fetchone()[0] +- if ordinal != count or pending or state in {'failed','unknown','cleanup-confirmed'}: ++ cleanup_started = self.db.execute("SELECT COUNT(*) FROM cleanup WHERE operation=? AND outcome!='owed'", (binding.operation_id,)).fetchone()[0] ++ if cleanup_started or ordinal != count or pending or state in {'failed','unknown','cleanup-confirmed'}: + raise Refused("call is replayed, uncertain or terminal") + self.db.execute("INSERT INTO calls VALUES (?,?,?,'pending',NULL)",(binding.operation_id,ordinal,call_digest)) + self.db.execute("COMMIT") +@@ -229,6 +274,61 @@ class Journal: + if self.db.in_transaction: self.db.execute("ROLLBACK") + raise + ++ def cleanup_status(self, binding: Binding): ++ # Observation never restores live execution or cleanup authority. ++ if self.observe(binding) is None: ++ raise Refused("operation is not admitted") ++ rows = self.db.execute("SELECT ordinal,kind,call_digest,outcome,evidence FROM cleanup WHERE operation=? ORDER BY ordinal", (binding.operation_id,)).fetchall() ++ return {"confirmed": bool(rows) and all(row[3] == "confirmed" for row in rows), ++ "obligations": [{"ordinal": row[0], "kind": row[1], "call_digest": row[2], ++ "outcome": row[3], "evidence_digest": row[4]} for row in rows]} ++ ++ def _cleanup_admission(self, binding, ordinal, obligation): ++ if not self._held or self._admission != binding or self._consumed_admission != binding: ++ raise Refused("cleanup requires same live consumed admission") ++ if type(ordinal) is not int or not 0 <= ordinal < 3 or type(obligation) is not Cleanup: ++ raise Refused("exact cleanup obligation required") ++ obligation.validate() ++ ++ def before_cleanup(self, binding: Binding, ordinal: int, obligation: Cleanup): ++ """Commit intent for one pre-admitted cleanup call, even after unknown. ++ ++ Never resumes an interrupted invocation. Independent owner recovery after ++ a crash still requires the owner's protected recovery channel/approval. ++ Cleanup has no business deadline: its authority is the original separate ++ explicit obligation, and cannot authorize a read, repair or business write. ++ """ ++ self._cleanup_admission(binding, ordinal, obligation) ++ try: ++ self.db.execute("BEGIN IMMEDIATE") ++ changed = self.db.execute("UPDATE cleanup SET outcome='pending' WHERE operation=? AND ordinal=? AND kind=? AND call_digest=? AND outcome='owed'", ++ (binding.operation_id, ordinal, obligation.kind, obligation.call_digest)).rowcount ++ if changed != 1: ++ raise Refused("cleanup is unadmitted, replayed or uncertain") ++ self.db.execute("COMMIT") ++ self._sync_directory() ++ except BaseException: ++ if self.db.in_transaction: self.db.execute("ROLLBACK") ++ raise ++ ++ def after_cleanup(self, binding: Binding, ordinal: int, obligation: Cleanup, ++ *, confirmed: bool, evidence_digest: str): ++ self._cleanup_admission(binding, ordinal, obligation) ++ if type(confirmed) is not bool or type(evidence_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}", evidence_digest): ++ raise Refused("closed cleanup evidence required") ++ try: ++ self.db.execute("BEGIN IMMEDIATE") ++ changed = self.db.execute("UPDATE cleanup SET outcome=?, evidence=? WHERE operation=? AND ordinal=? AND kind=? AND call_digest=? AND outcome='pending'", ++ ('confirmed' if confirmed else 'unconfirmed', evidence_digest, ++ binding.operation_id, ordinal, obligation.kind, obligation.call_digest)).rowcount ++ if changed != 1: ++ raise Refused("cleanup confirmation mismatch") ++ self.db.execute("COMMIT") ++ self._sync_directory() ++ except BaseException: ++ if self.db.in_transaction: self.db.execute("ROLLBACK") ++ raise ++ + def close(self): + if hasattr(self, "db"): + self.db.close() +diff --git a/tests/test_task_execution_cleanup.py b/tests/test_task_execution_cleanup.py +new file mode 100644 +index 0000000..348d4b8 +--- /dev/null ++++ b/tests/test_task_execution_cleanup.py +@@ -0,0 +1,199 @@ ++"""Disposable local SQLite/owner-adapter fixtures. No OpenBao or credential access.""" ++import multiprocessing ++import os ++from pathlib import Path ++import sys ++import tempfile ++import unittest ++import uuid ++ ++sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts')) ++from task_execution_journal import Binding, Cleanup, Journal, Refused ++from task_execution_cleanup import CleanupResult, run_cleanup ++ ++OBLIGATIONS = (Cleanup('task-token-revoke', '1'*64), ++ Cleanup('identity-token-revoke', '2'*64), ++ Cleanup('grant-policy-delete', '3'*64)) ++ ++ ++class OwnerFixture: ++ def __init__(self, fail=None): self.calls = []; self.fail = fail ++ def call(self, kind, digest): ++ self.calls.append((kind, digest)) ++ if kind == self.fail: raise TimeoutError('SYNTHETIC-PRIVATE-BACKEND-OUTPUT') ++ return CleanupResult(True, 'e'*64) ++ def revoke_task_token(self, digest): return self.call('task-token-revoke', digest) ++ def revoke_identity_token(self, digest): return self.call('identity-token-revoke', digest) ++ def delete_grant_policy(self, digest): return self.call('grant-policy-delete', digest) ++ ++ ++def crash_worker(directory, binding, after_effect): ++ journal = Journal(Path(directory)) ++ with journal.locked(): ++ journal.reserve(binding, OBLIGATIONS) ++ journal.transition(binding, 'reserved', 'consume-confirmed') ++ journal.before_cleanup(binding, 0, OBLIGATIONS[0]) ++ if after_effect: ++ # A real durable synthetic effect, separate from the admission DB. ++ fd = os.open(Path(directory)/'synthetic-effect', os.O_CREAT | os.O_WRONLY | os.O_APPEND, 0o600) ++ os.write(fd, b'one fixture revocation\n'); os.fsync(fd); os.close(fd) ++ os._exit(23) ++ ++ ++def cleanup_racer(directory, binding, gate, results): ++ journal = Journal(Path(directory)) ++ gate.wait() ++ try: ++ with journal.locked(): ++ journal.reserve(binding, OBLIGATIONS) ++ journal.transition(binding, 'reserved', 'consume-confirmed') ++ run_cleanup(journal, binding, OwnerFixture()) ++ results.put('admitted') ++ except Refused: results.put('refused') ++ finally: journal.close() ++ ++ ++class Tests(unittest.TestCase): ++ def setUp(self): ++ self.tmp = tempfile.TemporaryDirectory(); self.addCleanup(self.tmp.cleanup) ++ self.path = Path(self.tmp.name) ++ self.binding = Binding('approval-cleanup', 'sha256:'+'a'*64, str(uuid.uuid4()), 'b'*64) ++ self.journal = Journal(self.path); self.addCleanup(self.journal.close) ++ ++ def consumed(self): ++ self.journal.reserve(self.binding, OBLIGATIONS) ++ self.journal.transition(self.binding, 'reserved', 'consume-confirmed') ++ ++ def test_timeout_business_call_keeps_unknown_while_all_cleanup_runs(self): ++ with self.journal.locked(): ++ self.consumed() ++ self.journal.before_call(self.binding, 0, 'd'*64) ++ self.journal.transition(self.binding, 'consume-confirmed', 'unknown') ++ owner = OwnerFixture() ++ status = run_cleanup(self.journal, self.binding, owner) ++ self.assertTrue(status['confirmed']) ++ self.assertEqual(owner.calls, [(c.kind,c.call_digest) for c in OBLIGATIONS]) ++ self.assertEqual(self.journal.observe(self.binding)['state'], 'unknown') ++ self.assertTrue(self.journal.observe(self.binding)['external_outcome_unknown']) ++ with self.assertRaises(Refused): self.journal.before_call(self.binding, 1, 'f'*64) ++ ++ def test_failed_business_outcome_preserved(self): ++ with self.journal.locked(): ++ self.consumed(); self.journal.transition(self.binding,'consume-confirmed','failed') ++ self.assertTrue(run_cleanup(self.journal,self.binding,OwnerFixture())['confirmed']) ++ self.assertEqual(self.journal.observe(self.binding)['state'], 'failed') ++ ++ def test_one_revoke_failure_never_passes_but_independent_cleanup_continues(self): ++ with self.journal.locked(): ++ self.consumed(); owner = OwnerFixture('task-token-revoke') ++ result = run_cleanup(self.journal,self.binding,owner) ++ self.assertFalse(result['confirmed']); self.assertEqual(len(owner.calls),3) ++ self.assertEqual([r['outcome'] for r in result['obligations']], ['unconfirmed','confirmed','confirmed']) ++ self.assertNotIn('SYNTHETIC-PRIVATE', str(result)) ++ with self.assertRaises(Refused): run_cleanup(self.journal,self.binding,owner) ++ self.assertEqual(len(owner.calls),3) ++ self.assertNotIn(b'SYNTHETIC-PRIVATE', (self.path/'journal.sqlite').read_bytes()) ++ ++ def test_cleanup_closes_business_before_native_attempt(self): ++ with self.journal.locked(): ++ self.consumed(); self.journal.before_cleanup(self.binding,0,OBLIGATIONS[0]) ++ with self.assertRaises(Refused): self.journal.before_call(self.binding,0,'d'*64) ++ with self.assertRaises(Refused): self.journal.transition(self.binding,'consume-confirmed','authenticating') ++ ++ def test_no_preconsume_cleanup_even_when_unknown(self): ++ with self.journal.locked(): ++ self.journal.reserve(self.binding,OBLIGATIONS) ++ self.journal.transition(self.binding,'reserved','unknown') ++ owner = OwnerFixture() ++ with self.assertRaises(Refused): run_cleanup(self.journal,self.binding,owner) ++ self.assertEqual(owner.calls,[]) ++ ++ def test_changed_kind_digest_ordinal_or_binding_never_dispatches(self): ++ with self.journal.locked(): ++ self.consumed() ++ for ordinal,item in [(False,OBLIGATIONS[0]),(3,OBLIGATIONS[0]),(0,Cleanup('task-token-revoke','9'*64)),(0,Cleanup('kv-delete','1'*64)),(0,{'kind':'task-token-revoke'})]: ++ with self.subTest(ordinal=ordinal,item=item): ++ with self.assertRaises(Refused): self.journal.before_cleanup(self.binding,ordinal,item) ++ changed = Binding('another',self.binding.request_digest,self.binding.operation_id,self.binding.plan_digest) ++ with self.assertRaises(Refused): self.journal.before_cleanup(changed,0,OBLIGATIONS[0]) ++ self.assertTrue(all(x['outcome']=='owed' for x in self.journal.cleanup_status(self.binding)['obligations'])) ++ ++ def test_cleanup_plan_must_be_closed_before_reservation(self): ++ for plan in [list(OBLIGATIONS), (OBLIGATIONS[0],OBLIGATIONS[0]), (Cleanup('kv-delete','1'*64),), (Cleanup('task-token-revoke',False),)]: ++ with self.journal.locked(): ++ with self.assertRaises(Refused): self.journal.reserve(self.binding,plan) ++ self.assertIsNone(self.journal.observe(self.binding)) ++ ++ def test_crash_before_and_after_real_synthetic_effect_does_not_resume(self): ++ context = multiprocessing.get_context('fork') ++ for after_effect in (False, True): ++ with tempfile.TemporaryDirectory() as directory: ++ worker = context.Process(target=crash_worker,args=(directory,self.binding,after_effect)) ++ worker.start(); worker.join(10) ++ if worker.is_alive(): worker.terminate(); worker.join(); self.fail('crash fixture exceeded bound') ++ self.assertEqual(worker.exitcode,23) ++ reopened = Journal(Path(directory)) ++ try: ++ self.assertEqual(reopened.cleanup_status(self.binding)['obligations'][0]['outcome'],'pending') ++ self.assertTrue(reopened.observe(self.binding)['external_outcome_unknown']) ++ owner = OwnerFixture() ++ with reopened.locked(): ++ with self.assertRaises(Refused): reopened.reserve(self.binding,OBLIGATIONS) ++ with self.assertRaises(Refused): run_cleanup(reopened,self.binding,owner) ++ with self.assertRaises(Refused): reopened.before_cleanup(self.binding,1,OBLIGATIONS[1]) ++ self.assertEqual(owner.calls,[]) ++ effect = Path(directory)/'synthetic-effect' ++ self.assertEqual(effect.read_bytes() if effect.exists() else b'',b'one fixture revocation\n' if after_effect else b'') ++ self.assertFalse(reopened.cleanup_status(self.binding)['confirmed']) ++ finally: reopened.close() ++ ++ def test_actual_process_race_admits_one_cleanup_driver(self): ++ context = multiprocessing.get_context('fork'); gate = context.Event(); results = context.Queue() ++ workers = [context.Process(target=cleanup_racer,args=(str(self.path),self.binding,gate,results)) for _ in range(4)] ++ for worker in workers: worker.start() ++ gate.set() ++ for worker in workers: ++ worker.join(10) ++ if worker.is_alive(): worker.terminate(); worker.join(); self.fail('race exceeded bound') ++ self.assertEqual(worker.exitcode,0) ++ self.assertEqual(sorted(results.get(timeout=2) for _ in workers),['admitted','refused','refused','refused']) ++ self.assertTrue(self.journal.cleanup_status(self.binding)['confirmed']) ++ ++ def test_empty_or_malformed_native_result_never_proves_cleanup(self): ++ with self.journal.locked(): ++ self.journal.reserve(self.binding) ++ self.journal.transition(self.binding,'reserved','consume-confirmed') ++ with self.assertRaises(Refused): run_cleanup(self.journal,self.binding,OwnerFixture()) ++ self.assertFalse(self.journal.cleanup_status(self.binding)['confirmed']) ++ ++ def test_bad_native_confirmation_still_attempts_remaining_cleanup(self): ++ class BadOwner(OwnerFixture): ++ def revoke_task_token(self, digest): ++ self.calls.append(('task-token-revoke',digest)) ++ return CleanupResult('true','e'*64) ++ with self.journal.locked(): ++ self.consumed(); owner=BadOwner() ++ self.assertFalse(run_cleanup(self.journal,self.binding,owner)['confirmed']) ++ self.assertEqual(len(owner.calls),3) ++ ++ def test_confirmation_exact_types_and_no_duplicate_receipts(self): ++ with self.journal.locked(): ++ self.consumed(); self.journal.before_cleanup(self.binding,0,OBLIGATIONS[0]) ++ for confirmed,digest in [('true','e'*64),(1,'e'*64),(True,None),(True,'E'*64)]: ++ with self.assertRaises(Refused): self.journal.after_cleanup(self.binding,0,OBLIGATIONS[0],confirmed=confirmed,evidence_digest=digest) ++ self.journal.after_cleanup(self.binding,0,OBLIGATIONS[0],confirmed=True,evidence_digest='e'*64) ++ with self.assertRaises(Refused): self.journal.after_cleanup(self.binding,0,OBLIGATIONS[0],confirmed=True,evidence_digest='e'*64) ++ with self.assertRaises(Refused): self.journal.before_cleanup(self.binding,0,OBLIGATIONS[0]) ++ ++ def test_cleanup_state_is_independent_of_legacy_business_transition(self): ++ with self.journal.locked(): ++ self.consumed() ++ states=['consume-confirmed','authenticating','grant-issuing','grant-issued','applying','readback','verified'] ++ for before,after in zip(states,states[1:]): self.journal.transition(self.binding,before,after) ++ with self.assertRaises(Refused): self.journal.transition(self.binding,'verified','cleanup-confirmed','e'*64) ++ run_cleanup(self.journal,self.binding,OwnerFixture()) ++ self.journal.transition(self.binding,'verified','cleanup-confirmed','e'*64) ++ self.assertEqual(self.journal.observe(self.binding)['state'],'cleanup-confirmed') ++ ++ ++if __name__ == '__main__': unittest.main() +diff --git a/tests/test_task_execution_journal.py b/tests/test_task_execution_journal.py +index cb9874d..b1a76fc 100644 +--- a/tests/test_task_execution_journal.py ++++ b/tests/test_task_execution_journal.py +@@ -12,6 +12,7 @@ module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + Binding, Journal, Refused = module.Binding, module.Journal, module.Refused ++Cleanup = module.Cleanup + + + def reserve_worker(directory, binding, gate, results): +@@ -74,8 +75,12 @@ class Tests(unittest.TestCase): + def test_full_path_cleanup_and_terminal_replay(self): + states=['reserved','consume-confirmed','authenticating','grant-issuing','grant-issued','applying','readback','verified','cleanup-confirmed'] + with self.journal.locked(): +- self.journal.reserve(self.binding) ++ cleanup = Cleanup('task-token-revoke','d'*64) ++ self.journal.reserve(self.binding,(cleanup,)) + for before,after in zip(states,states[1:]): ++ if after == 'cleanup-confirmed': ++ self.journal.before_cleanup(self.binding,0,cleanup) ++ self.journal.after_cleanup(self.binding,0,cleanup,confirmed=True,evidence_digest='c'*64) + self.journal.transition(self.binding,before,after,'c'*64) + with self.assertRaises(Refused): self.journal.transition(self.binding,'cleanup-confirmed','applying') + self.assertEqual(self.journal.observe(self.binding),{'state':'cleanup-confirmed','evidence_digest':'c'*64,'dispatch_allowed':False,'external_outcome_unknown':False}) +diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md +index ebd890f..269ad08 100644 +--- a/workplans/RPF-WP-0035-credential-lane-implementation.md ++++ b/workplans/RPF-WP-0035-credential-lane-implementation.md +@@ -1225,3 +1225,17 @@ this change needs its own integration review. T12 retains all existing full + contract, identity/consume, dispatch, custody and native acceptance requirements. + RPF-IN-0012 remains open for the independent contract/wrapped-delivery return; + this code correction does not authorize either operation. ++ ++2026-10-16 T12 source preparation (isolated owner checkout; not owner delivery): ++added durable precommitted cleanup obligations and separate outcome evidence to ++the existing Journal, plus a fixed three-method owner cleanup driver. Same live ++postconsume admission can clean after a business timeout/failed/unknown outcome; ++cleanup closes business dispatch and cannot replay or restore admission after a ++crash. Empty obligations no longer produce cleanup-confirmed. Real SQLite ++multiprocess races and abrupt process death before/after a separately durable ++synthetic effect verify refusal on reopen. Current compiler/journal/cleanup suite: ++73 passed, local source/fixture only. Details: docs/task-execution-cleanup.md. ++No native adapter or production mediator exists; full grant/request/cleanup binding, ++authenticated consume/identity, ephemeral token issuance, deadline/use checks, ++native effect/revoke/absence, audit and protected recovery remain prerequisites. ++T12 remains progress. No CCR38 default/entitlement or live grant was changed. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-owner-review.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-owner-review.json new file mode 100644 index 00000000..44dd0cc4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution/platform-owner-review.json @@ -0,0 +1,22 @@ +{ + "reviewer": "railiance-platform-owner (agent railiance-platform, codex)", + "decision": "approved", + "scope": "Source-only durable cleanup journal and fixed three-method cleanup driver; no native or production admission", + "comment": "INTENT Secure Custody and Durable by Default require cleanup evidence independent from uncertain business effects. Reviewed exact modified source SHA256s below under existing RPF-WP-0035-T12. This owner source review grants no founder approval or execution authority. Full mediated execution remains changes-required pending grant, identity, consume, native adapter, enforcement and proof.", + "owner_work_item": "RPF-WP-0035-T12", + "base_commit": "3b6e647aa06bed88966a4539f9d3cb7ed6792bd4", + "reviewed_files_sha256": { + "docs/task-execution-cleanup.md": "44cb21e69838c961eca9805ec3d1f3e64d12b46dc9c2e7b02c69610a97a0f33b", + "docs/task-execution-plan-compiler.md": "83f28626f1f6499f52f22c3daafd22c802475e8a77bb1be7ba9901c7ae00bf8b", + "scripts/task_execution_cleanup.py": "c1c0b649e8002b9a6e1599d46f70ad3ad3d743323ddb82367468d5bdb103062c", + "scripts/task_execution_journal.py": "f512d72be880de2a6345d3550c60a0c8e6e3b6d2bc8123f4502a068afabd973b", + "tests/test_task_execution_cleanup.py": "a0d4d6d2c8c22bfed17a109a6f0be02d5f128619866ae5f1ea69f81803133085", + "tests/test_task_execution_journal.py": "707ee5404e567ddf9896d680feae47628b9a3cfc9e3ce0073100e1677af53b5c", + "workplans/RPF-WP-0035-credential-lane-implementation.md": "f1be966f38a96efbedcb7a129586a6f905e1910fe37ef85c61fe29379fa50af6" + }, + "native_acceptance": false, + "production_executor_available": false, + "independent_from_implementation": false, + "finding": "Prior business terminal states prevented cleanup after uncertainty; precommitted cleanup now has its own durable lifecycle, with no business replay or admission recovery.", + "boundary": "No standing admin grants or generic executor; no native opener. Cleanup commitments must still be incorporated into independently verified full request/owner plan. Trusted owner adapter remains unimplemented." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-acceptance-next/key40-reconciliation.patch b/docs/changes/netkingdom-target-2026-10-16/recovery-acceptance-next/key40-reconciliation.patch new file mode 100644 index 00000000..de8b0ec6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-acceptance-next/key40-reconciliation.patch @@ -0,0 +1,59 @@ +diff --git a/workplans/KEY-WP-0040-mfa-attended-run-findings.md b/workplans/KEY-WP-0040-mfa-attended-run-findings.md +index 9074902..df7f210 100644 +--- a/workplans/KEY-WP-0040-mfa-attended-run-findings.md ++++ b/workplans/KEY-WP-0040-mfa-attended-run-findings.md +@@ -8,7 +8,7 @@ status: active + owner: claude-code + topic_slug: netkingdom + created: "2026-10-11" +-updated: "2026-10-14" ++updated: "2026-10-16" + flavor: implementation + depends_on: [] + state_hub_workstream_id: "a67f9d60-9b8f-5111-9fe5-9f64e631b1c3" +@@ -90,3 +90,45 @@ T02 is a local source edit only; its source/image publication remains pending. + 2026-10-14: the MFA challenge render test now requires the lost-authenticator + link after the OTP form, with rel="noreferrer" and no request state in the URL. + T02 stays progress until that page is published and an attended journey sees it. ++ ++## Current publication and attended acceptance — 2026-10-16 ++ ++The source-only publication statements above describe the October 12–14 state. ++They are superseded for T02: the October 15 founder-run publication recorded in ++`docs/operations.md` (Current image) and KEY-WP-0041-T02 delivered image ++`sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010` ++from source `8c6c071`. That source includes the OTP help link commit `205a1c0`. ++The founder verified the image-only diff, server dry-run, Recreate rollout, ++Ready pod with zero restarts and discovery HTTP200. No additional image rollout ++is needed to make the existing T02 link available. This reconciliation makes ++no new live observation and does not infer attended acceptance from publication. ++ ++T01 still needs the actual authenticated preview; T02 still needs the actual ++OTP-page observation. Both remain progress. Resume the existing owner procedure ++`user-engine/docs/mfa-attended-run.md`, corrected by its October 11 evidence ++`user-engine/docs/evidence/2026-10-11-mfa-attended-u05-u08.md`: ++ ++1. In the real user-engine-portal OTP challenge, observe **Code rejected or lost ++ your authenticator?**, follow it to `https://users.coulomb.social/security`, ++ and confirm the rejected-code/lost-device guidance is reachable. Preserve no ++ challenge URL, request state, cookie or OTP value in evidence. Record time, ++ client, link text/destination and outcome. This supplies T02 acceptance. ++2. With platform-root's own fresh AAL2 browser session, open the existing ++ `/platform/factor-recovery` form. Preview `tegwick` using the existing support ++ reference `U08-2026-10-11`; inspect any existing reference before changing it. ++ Confirm the current factor metadata and shared-identity warning. The October ++ 11 suffix was …E1D; a changed factor is a reason to reconcile current state, ++ never to force the old selection. Record only time, preview outcome and serial ++ suffix. This preview alone makes no factor change and supplies the remaining ++ T01 preview evidence. ++3. Only in the separately attended recovery window, after the operator verifies ++ the person and exact current scope, continue the existing U08 confirmation, ++ audited disable, reference readback, replacement enrollment and fresh login ++ procedure. Preserve the original reference and safe recovery record id. ++ Neither the published bug fix nor a passing preview closes U08. Stop on ++ stale metadata, replaced factor or failed audit/readback; do not reset or ++ re-enroll merely to make an acceptance check pass. ++ ++No authenticated preview, real-factor mutation or attended page observation was ++performed during this source reconciliation. No passwords, codes, token values, ++provider keys or production Secret objects were requested or read. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/README.md b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/README.md new file mode 100644 index 00000000..664c5272 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/README.md @@ -0,0 +1,29 @@ +# Owner-held KeyCape recovery preparation — candidate R1 + +Owner baseline: `878abc93588e12b6da1bac055cfbb3abb8d7adf1`. Canonical files are unchanged. Review `candidate.patch`, `review.json`, `source-closure.json` and the copied `test-root` sources. R6/R7 originals and all failed/positive trial evidence remain unchanged. This packet has not run an owner-held or Docker fixture attempt. + +The candidate corrects pre-input failures in the existing attended path. It checks the exact admission digest, bounded strict JSON metadata, archive/transfer binding, selected service selector and typed hashes for all 17 invoked local source/config dependencies before requesting custody. Third-party Python dependencies are imported before the prompt. TLS files and ciphertext are checked by file metadata only during preparation; their contents are not opened by preparation. + +The existing custody shell gains an explicit `--sealed-fd --prompt -- COMMAND` mode. A new hidden-prompt helper verifies the expected age recipient, seals a private anonymous Linux memfd and passes only its exact descriptor to the existing child. The existing `private_key_fd` accepts a fully sealed, anonymous, owned, private regular descriptor and returns a non-inheritable duplicate. Simultaneous FILE/FD sources, malformed descriptor text, incomplete seals and named descriptors are refused. Original file mode remains compatible; its original SIGKILL cleanup limitation is not changed or claimed fixed. The launcher selects descriptor mode, so no named key file can survive its parent loss. An admitted child retains its descriptor until it exits; this is not child revocation. Existing session/reaper quiescence bounds that child's later cleanup. + +Private manifest bindings are explicit and versioned. Legacy explicit hashes and `explicit-digest-v1` remain valid. `ciphertext-derived-v1` forbids a supplied manifest hash and requires an independently verified transfer and exact ciphertext hash. After that actual ciphertext is checked, age authentication succeeds and the complete existing archive validation passes, the canonical manifest digest is derived privately for the unchanged payload/native checks. It never enters safe receipts/checkpoints. This binds data through exact ciphertext; it grants no client or consumer authority. + +`attempt-metadata.json` records the known archive: 109738344 bytes, ciphertext `7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30`, and the prior 36-file validation. The admission template intentionally contains null unresolved fields and is refused by the validator. Do not use it to request keys. The original issuer/TLS/trust bindings and selected original service registration still require independently established metadata and owner source review. + +Current ingress source names `kc.coulomb.social`, `sso/kc-tls`, cert-manager `ClusterIssuer/letsencrypt-prod` and TLS termination at Traefik before KeyCape port 8080. The identity backup profile excludes `kc-tls`. Independent recovery of original TLS/trust, or an accepted reissue path under normal issuer/cluster outage, is not established by the archive proof. A missing exact routing-catalog match does not establish absence of an owning subsystem. This preparation neither reads the TLS secret nor substitutes fresh self-signed trust. TLS recovery ownership/provider requirements must be resolved through the existing source-owner process. + +The generated-key tests exercise real age-keygen/decryption, real authenticated ciphertext refusal, complete archive restore, wrong explicit manifest, malformed modes, false transfer bindings, absence of private manifest data in receipts, the actual synthetic hidden-prompt shell, exact parent loss and descriptor release. Docker/issuer results in manifest-builder unit tests are mocked and confer no native proof. The original synthetic native fixture remains available under a new hash-bound source manifest for later reviewed successor trials; it still supplies a generated fixture key using its original fixture override and does not by itself exercise the attended descriptor prompt. + +Validation commands: + +```sh +python3 -m pytest -q --tb=short test-root/tests/test_private_manifest_binding.py test-root/tests/test_sealed_custody.py test-root/tests/test_prepare_isolated_keycape.py test-root/tests/test_isolated_keycape_session.py test-root/tests/test_isolated_keycape_recovery.py test-root/tests/test_isolated_keycape_fixture_plan.py +python3 -m pytest -q --tb=no test-root/tests/test_sops_private.py test-root/tests/test_sops_write.py test-root/tests/test_custody_entrypoints.py test-root/tests/test_service_secret_custody.py +bash -n test-root/tools/bootstrap-recovery/run-isolated-keycape.sh test-root/sso-mfa/bootstrap/sops-custody-unlock.sh +``` + +The existing custody suite passed 94 checks; 61 real-SOPS checks skipped because a trusted `sops` executable is unavailable in this environment. That compatibility coverage remains unverified. Intermediate copied-fixture/source-manifest errors are retained in `review.json`; they were corrected without changing frozen evidence. + +Before any input, review and integrate the exact source, validate the target runtime/dependencies, resolve original TLS/trust and selected registration metadata, finalize the private admission digest and existing owner authority/abort boundaries. The eventual six-argument command remains the canonical `run-isolated-keycape.sh VERIFIED_TRANSFER_RECEIPT PRIVATE_ADMISSION ADMISSION_SHA256 ORIGINAL_TLS_CERT ORIGINAL_TLS_KEY ORIGINAL_TRUST`; all private input stays in the owner's own terminal. Actual recovery execution and selected consumer acceptance remain T18; retained-factor human MFA remains T19. T33 is not ready merely because source preparation has progressed. + +Coordinator public-image readback resolves the pinned issuer executable digest in public-verifier-image-readback.json; the temporary stopped probe was removed and never started. This does not resolve TLS/registration custody or admit native use. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/admission.template.json b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/admission.template.json new file mode 100644 index 00000000..50f5a146 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/admission.template.json @@ -0,0 +1,37 @@ +{ + "manifest_binding_mode": "ciphertext-derived-v1", + "ciphertext_sha256": "7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30", + "issuer": null, + "verifier_sha256": "cd89121393e1465aa76009763602a5b8c75f9dddb5679aea6242623f135b51b4", + "tls_cert_sha256": null, + "trust_sha256": null, + "consumer": { + "client_id": null, + "secret_env": null, + "audience": null, + "scope": null, + "subject": null, + "tenant": null, + "roles": null, + "deny_scope": null + }, + "source_sha256": { + "sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f", + "sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e", + "sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780", + "sso-mfa/k8s/privacyidea/key-custody-remote.py": "sha256:27b2b885c81923d85054e3999c5613e104fe627779b2b50398ed9bed5088f47e", + "tools/bootstrap-recovery/check-privacyidea-database.py": "sha256:90f454c87998d7f5d625e3c6656c3418f8f56275dc1879a9d19ed605431b94a4", + "tools/bootstrap-recovery/identity-backup-profile.json": "sha256:40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1", + "tools/bootstrap-recovery/identity-bundle.py": "sha256:524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6", + "tools/bootstrap-recovery/identity-custody.json": "sha256:f8aee8487ed01d7940b82676bb4a5c8d61f51b6c598521b8b8a73588aaea4d4f", + "tools/bootstrap-recovery/identity-files.py": "sha256:d0063a9eedd3610e3d45bd2d834aae83cff74e4b3504d138b3548f427e89331d", + "tools/bootstrap-recovery/identity-offsite.py": "sha256:e7c31fa9ee081961af873348ee3b4829a645d40c7dc7f1e22c564aa9ed01bc49", + "tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "tools/bootstrap-recovery/isolated-lldap.py": "sha256:35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "tools/bootstrap-recovery/retrieve-identity-offline.py": "sha256:3421dbb3f3280a001d33ea2deb12ab609404a0b4254cd43c21518d77d166f972", + "tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "tools/bootstrap-recovery/verify-identity.py": "sha256:039864d0141b552887752bf17cfdcf1bef4bdc9126f946a9c0533e4fd54ebf4f" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/attempt-metadata.json b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/attempt-metadata.json new file mode 100644 index 00000000..f4db9016 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/attempt-metadata.json @@ -0,0 +1,96 @@ +{ + "status": "incomplete_not_executable_not_approved", + "owner_head": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "source_sha256": { + "sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f", + "sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e", + "sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780", + "sso-mfa/k8s/privacyidea/key-custody-remote.py": "sha256:27b2b885c81923d85054e3999c5613e104fe627779b2b50398ed9bed5088f47e", + "tools/bootstrap-recovery/check-privacyidea-database.py": "sha256:90f454c87998d7f5d625e3c6656c3418f8f56275dc1879a9d19ed605431b94a4", + "tools/bootstrap-recovery/identity-backup-profile.json": "sha256:40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1", + "tools/bootstrap-recovery/identity-bundle.py": "sha256:524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6", + "tools/bootstrap-recovery/identity-custody.json": "sha256:f8aee8487ed01d7940b82676bb4a5c8d61f51b6c598521b8b8a73588aaea4d4f", + "tools/bootstrap-recovery/identity-files.py": "sha256:d0063a9eedd3610e3d45bd2d834aae83cff74e4b3504d138b3548f427e89331d", + "tools/bootstrap-recovery/identity-offsite.py": "sha256:e7c31fa9ee081961af873348ee3b4829a645d40c7dc7f1e22c564aa9ed01bc49", + "tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "tools/bootstrap-recovery/isolated-lldap.py": "sha256:35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "tools/bootstrap-recovery/retrieve-identity-offline.py": "sha256:3421dbb3f3280a001d33ea2deb12ab609404a0b4254cd43c21518d77d166f972", + "tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "tools/bootstrap-recovery/verify-identity.py": "sha256:039864d0141b552887752bf17cfdcf1bef4bdc9126f946a9c0533e4fd54ebf4f" + }, + "archive": { + "artifact": "net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age", + "ciphertext_bytes": 109738344, + "ciphertext_sha256": "7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30", + "etag": "\"28ec1f7014e492de4c40e1ae75502f08\"", + "fetched_path": "/tmp/nk49-identity-fetched-v0x96qlx/net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age" + }, + "manifest": { + "file_count_observed": 36, + "manifest_binding_mode": "ciphertext-derived-v1", + "private_derivation": "After exact independently retrieved ciphertext check, successful authenticated age decryption and full existing archive validation, derive canonical manifest digest only inside owner memory. Never emit it in safe receipts.", + "explicit_compatibility": "Legacy explicit manifest_sha256 remains accepted; explicit-digest-v1 may name that original behavior; unknown modes and conflicting bindings are refused." + }, + "original_identity": { + "issuer": null, + "tls_cert_sha256": null, + "trust_sha256": null, + "verifier_sha256": "cd89121393e1465aa76009763602a5b8c75f9dddb5679aea6242623f135b51b4", + "image": "forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010", + "tls_custody_paths": "Owner-controlled original files; no values or paths inferred from Kubernetes Secrets.", + "public_ingress_source": { + "host": "kc.coulomb.social", + "namespace": "sso", + "secret": "kc-tls", + "issuer_kind": "ClusterIssuer", + "issuer": "letsencrypt-prod", + "termination": "Traefik to KeyCape8080" + }, + "recovery_gap": "Current backup profile excludes ingress TLS Secret kc-tls; independent original TLS/trust custody or an accepted reissue path during normal issuer/cluster outage is not established. No self-signed substitute is admitted.", + "verifier_readback_receipt": "public-verifier-image-readback.json" + }, + "selected_consumer": { + "client_id": null, + "secret_env": null, + "audience": null, + "scope": null, + "subject": null, + "tenant": null, + "roles": null, + "deny_scope": null + }, + "dependency_graph": { + "native_service_registration": [ + "original KeyCape image+archived config/environment+signer+policy", + "shared verified tmpfs", + "loopback trusted TLS transport", + "existing native verify-client" + ], + "restored_provider_configuration": [ + "LLDAP", + "privacyIDEA scoped factor-reader", + "Authelia" + ], + "provider_network_access": "None; network-none shared namespace, no production contact", + "selected_downstream_consumer": "Unselected; no redirect or new grant authorized", + "retained_factor_mfa": "Not exercised by service-client flow" + }, + "execution": { + "endpoint": "unix:///var/run/docker.sock", + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "deadline_seconds": 1800, + "safe_receipt": "fresh /tmp/nk49-isolated-keycape-XXXXXXXX/recovery.json", + "abort": "Stop on any binding, startup, uncertainty, native denial or cleanup failure; preserve safe receipt and exact obligations; no wildcard cleanup or retry as success", + "custody": "Existing shell --sealed-fd --prompt; strict owned sealed anonymous keyFD; no named key persists. Active child retains descriptor until its own exit, bounded by adapter session/reaper." + }, + "pre_input_unresolved": [ + "Original issuer/TLS certificate/trust public bindings require independently established metadata; pinned cached executable digest is verified separately", + "Selected original service registration and consumer owner acceptance remain absent", + "Exact source patch review and owner publication needed before launcher adoption; no source approval asserted", + "Sealed descriptor custody successor must be independently reviewed; original file custody path retains old hard-loss limitations", + "Real SOPS decrypt/write compatibility61 checks are skipped until trusted sops runtime available" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/candidate.patch b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/candidate.patch new file mode 100644 index 00000000..b163ffba --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/candidate.patch @@ -0,0 +1,752 @@ +--- a/sso-mfa/bootstrap/sops-private.py ++++ b/sso-mfa/bootstrap/sops-private.py +@@ -3,6 +3,7 @@ + from __future__ import annotations + + import json ++import fcntl + import os + from pathlib import Path + import re +@@ -99,7 +100,24 @@ + + + def private_key_fd(): ++ inherited = os.environ.get('NETKINGDOM_CUSTODY_AGE_KEY_FD', '') + name = os.environ.get('NETKINGDOM_CUSTODY_AGE_KEY_FILE', '') ++ if inherited: ++ if name or not re.fullmatch(r'[1-9][0-9]{0,8}',inherited) or int(inherited)<3: ++ raise Refused('ambiguous custody descriptor') ++ fd=os.dup(int(inherited)) ++ try: ++ info=os.fstat(fd) ++ required=fcntl.F_SEAL_WRITE|fcntl.F_SEAL_GROW|fcntl.F_SEAL_SHRINK|fcntl.F_SEAL_SEAL ++ if (not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid() or info.st_mode&0o077 ++ or info.st_nlink!=0 or not 0