From a192d24c9a56aedb2b787fe2d8166ca72c230dd9 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 1 Nov 2026 23:14:27 +0100 Subject: [PATCH] docs: preserve reviewed NetKingdom session artifacts --- .../progress-requested-pause.json | 14 + .../progress-wave10.json | 14 + .../progress-wave6.json | 23 + .../protected-execution-recovery/README.md | 33 + .../coordinator-files-r2.json | 28 + .../implementation-r1-to-r2.patch | 394 +++++ .../implementation-r1.patch | 641 ++++++++ .../implementation-r2.patch | 861 +++++++++++ .../manifest-r1.json | 60 + .../manifest-r2.json | 57 + .../owner-source-review-r1.json | 17 + .../owner-source-review-r2.json | 13 + .../docs/task-execution-native-fixture.md | 123 ++ .../scripts/task_execution_journal.py | 390 +++++ .../scripts/task_execution_native_fixture.py | 300 ++++ ...-WP-0035-credential-lane-implementation.md | 1292 ++++++++++++++++ .../r1-refusal-counterexamples.json | 7 + .../validation-r1.json | 57 + .../validation-r2.json | 45 + .../README.md | 21 + .../budget-source-owner-review-r1.json | 12 + .../budget-source-slice-r1.patch | 545 +++++++ .../budget-source-validation-r1.json | 41 + .../task_execution_trusted_servers.py | 174 +++ .../test_task_execution_trusted_whole.py | 294 ++++ .../fullchain-r1/README.md | 31 + .../fullchain-r1/implementation.patch | 440 ++++++ .../fullchain-r1/owner-review.json | 32 + .../docs/task-execution-native-fixture.md | 260 ++++ ...-WP-0035-credential-lane-implementation.md | 1318 +++++++++++++++++ .../fullchain-r1/public-receipt.json | 22 + .../fullchain-r1/validation.json | 37 + .../docs/task-execution-native-fixture.md | 207 +++ .../scripts/task_execution_mediator.py | 184 +++ .../scripts/task_execution_native_fixture.py | 311 ++++ ...-WP-0035-credential-lane-implementation.md | 1309 ++++++++++++++++ .../temporal-contract-r1.json | 22 + .../platform-native-fixture.patch | 838 +++++++++++ .../platform-owner-review.json | 44 + .../test-evidence.txt | 4 + .../manifest-v1.json | 46 + .../platform-grant-projection-v1.patch | 586 ++++++++ .../platform-grant-projection.patch | 602 ++++++++ .../platform-owner-review.json | 37 + .../test-evidence.txt | 9 + 45 files changed, 11795 insertions(+) create mode 100644 docs/changes/netkingdom-target-2026-10-16/progress-requested-pause.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/progress-wave10.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/progress-wave6.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/coordinator-files-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1-to-r2.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r2.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/docs/task-execution-native-fixture.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_journal.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_native_fixture.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/workplans/RPF-WP-0035-credential-lane-implementation.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/r1-refusal-counterexamples.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-owner-review-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-slice-r1.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-validation-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/task_execution_trusted_servers.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/test_task_execution_trusted_whole.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/implementation.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/owner-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/preimages/docs/task-execution-native-fixture.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/preimages/workplans/RPF-WP-0035-credential-lane-implementation.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/public-receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/validation.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/preimages/docs/task-execution-native-fixture.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/preimages/scripts/task_execution_mediator.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/preimages/scripts/task_execution_native_fixture.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/preimages/workplans/RPF-WP-0035-credential-lane-implementation.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/temporal-contract-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave2/platform-native-fixture.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave2/platform-owner-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave2/test-evidence.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave3/manifest-v1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave3/platform-grant-projection-v1.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave3/platform-grant-projection.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave3/platform-owner-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/protected-execution-wave3/test-evidence.txt diff --git a/docs/changes/netkingdom-target-2026-10-16/progress-requested-pause.json b/docs/changes/netkingdom-target-2026-10-16/progress-requested-pause.json new file mode 100644 index 00000000..2ee86b3a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/progress-requested-pause.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "id": "c1524ae9-ae86-440c-8c42-d09fd1f30439", + "topic_id": null, + "task_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "decision_id": null, + "event_type": "note", + "summary": "User-requested pause checkpoint published as 3607d3e08ffdbc473d19e9baf7225843c4075ae0; normal scanner passed for reduced 26-file checkpoint and consistency sync completed 89/89, zero failures/automation errors. Original staged brief restored and independently verified. Browser source 916ba4b and owner receipt cbabb6d published; KeyCape owner review 834b0d published. Independent checker reconstruction passed 39 affected checks and 11 actual-checker scenarios; recovery R5 passed 67 synthetic checks but first real disposable Docker positive trial failed before native verification. Cleanup independently confirmed; no trial or subagent remains running. Six raw evidence files were scanner-rejected; original evidence and complete frozen R5 source packet retained locally, withheld paths and transfer limitation documented in pause-publication-scope.json. All sixteen TSA gates remain open. Rough overall progress estimate 40%, range 30\u201350%, not audited completion. Resume points in docs/netkingdom-target-implementation.md; pause does not complete the workplan.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T20:39:21.428030Z", + "workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/progress-wave10.json b/docs/changes/netkingdom-target-2026-10-16/progress-wave10.json new file mode 100644 index 00000000..55b24b37 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/progress-wave10.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "id": "17e6e0a4-890c-4ef7-b7e7-43fba98569a7", + "topic_id": null, + "task_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "decision_id": null, + "event_type": "note", + "summary": "CUST-WP-0080-T06: published Access dc042022f7c30671cf0a7605cf43ef6d96d04242 (normal Helm zero/one outbox writers) and Platform 84c529f239c81e6d3ca337015700b03e3a862f53 (distinct runtime/storage versus GitOps owner scopes), exact committed hashes and origin ancestry verified. Access sync38/38; Platform exact primary/railiance01 applied, no refused records. Root86 Helm cases plus existing suite,45 native/preparation tests; independent cluster54 overlapping custody tests. R4 scanner refusal retained; typed public OCI references pass unchanged scanner in R5. Cluster own RCLUSTER-WP-0017 published523669fb and bounded formal R5 source role review recorded; ADR007 confirms canonical prefix, C26 historical-majority inference warning is not authority to rename. RolloutR2/custodyP3 being frozen; no native action or gate closure.33 source slices published; all16 TSA gates remain open. Prior rootfa0fc15 sync completed89/89 zero failures.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T18:29:37.495335Z", + "workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/progress-wave6.json b/docs/changes/netkingdom-target-2026-10-16/progress-wave6.json new file mode 100644 index 00000000..88b32fad --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/progress-wave6.json @@ -0,0 +1,23 @@ +{ + "request": { + "summary": "CUST-WP-0080-T06 concrete source progress: published Platform d870596 exact metadata baseline/readback and b9eaa20 authenticated process-loss cleanup recovery; Access919ce25 native preparation and4f6946b real authenticated service HTTP join; Secrets5a2099f exact native CLI preparation; Approval3485415 producer-owned Go fact adapter. Independent32Platform readback tests,53recovery/journal tests,9Accessreceipt+2runtime-twin tests,3B1Go packages,14114Python/Go canonical differential cases and2producer tests passed. Unintegrated trusted successor fullAccessGo suite and176Secretsfocused tests passed. Whole-stack work exposed incompatible15s/30s+reserve timing and retainedregistry normalization/digest mismatch; owner corrections and proof underway, no workaround or nativeacceptance. All16TSA gates remainopen. Normal owner publication/reconciliation succeeded after retainedAccessquerytimeouts; Approval dirtyworkplan/generatedwriteback caveat preserved. No deployment/credential/trust/grant changes.", + "event_type": "note", + "author": "codex", + "workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "task_id": "6aca534c-5c7a-556f-b6b6-406433670c29" + }, + "receipt": { + "workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "id": "7a6a964b-0fa4-4ff1-ba9c-d903b8cb861e", + "topic_id": null, + "task_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "decision_id": null, + "event_type": "note", + "summary": "CUST-WP-0080-T06 concrete source progress: published Platform d870596 exact metadata baseline/readback and b9eaa20 authenticated process-loss cleanup recovery; Access919ce25 native preparation and4f6946b real authenticated service HTTP join; Secrets5a2099f exact native CLI preparation; Approval3485415 producer-owned Go fact adapter. Independent32Platform readback tests,53recovery/journal tests,9Accessreceipt+2runtime-twin tests,3B1Go packages,14114Python/Go canonical differential cases and2producer tests passed. Unintegrated trusted successor fullAccessGo suite and176Secretsfocused tests passed. Whole-stack work exposed incompatible15s/30s+reserve timing and retainedregistry normalization/digest mismatch; owner corrections and proof underway, no workaround or nativeacceptance. All16TSA gates remainopen. Normal owner publication/reconciliation succeeded after retainedAccessquerytimeouts; Approval dirtyworkplan/generatedwriteback caveat preserved. No deployment/credential/trust/grant changes.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T14:45:06.591043Z", + "workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/README.md b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/README.md new file mode 100644 index 00000000..2ff83487 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/README.md @@ -0,0 +1,33 @@ +# Protected cleanup recovery source successor + +`implementation-r1.patch` adds operation-bound private fixture custody and a +separately admitted cleanup-only executor. Actual forked executor death can be +reconciled through fixed accessor revocation/absence and exact grant-policy +retirement. The runner never restores business or consume admission, mints a +session, or retries an effect. Its durable native intent and receipt records +preserve unknown outcomes; a fresh owner permit is required after failure. + +Custody is a separate owner-only directory and persists accessors, not bearer +credentials. This proves process-independent fixture recovery. Same-UID hostile +code isolation, production storage/encryption/retention, exact recovery-role scope +and native actor/audit admission remain unselected/unadmitted. No estate backend, +credential read, new live grant or generic broker is introduced. Lost token-create +response before accessor commit remains explicit unresolved token cleanup debt. + +Exact source hashes and preimages are retained in `manifest-r1.json` and +`preimages/`. Parent owns integration, work-record sync and progress log. +The separate Platform signer-custody proposal remains frozen. + +Validation: 48 regression tests passed; final frozen recovery/journal suite24 +tests passed. Counts overlap. Exact run chronology and retained initial failures +are in `validation-r1.json`. `owner-source-review-r1.json` approves source only. +Canonical apply-check and source/preimage hashes passed. + + +R2 supersedes the R1 source proposal after root refusal review. R1 files remain +unchanged. Use `implementation-r2.patch` (full) or `implementation-r1-to-r2.patch` +(delta over R1). Exact hashes are in `manifest-r2.json`; owner source review and +validation are `owner-source-review-r2.json` / `validation-r2.json`. R2 fixes +arbitrary404 absence, broad rstrip equivalence and unauthenticated/corrupt capsule +acceptance.53successor tests and29final frozen cases passed; counts overlap. +`r1-refusal-counterexamples.json` retains reproduced original failures. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/coordinator-files-r2.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/coordinator-files-r2.json new file mode 100644 index 00000000..2bdf3553 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/coordinator-files-r2.json @@ -0,0 +1,28 @@ +{ + "files": { + "scripts/task_execution_recovery.py": { + "before_sha256": null, + "after_sha256": "910f38919a8b6f57591366a2ef4da40d1ed7dfd7556c845a4b89496eca39ce1c" + }, + "scripts/task_execution_native_fixture.py": { + "before_sha256": "4575d048f79b125b0f87c70b9a76558d8cce6e61cb66b5b0583934d1a62f2ef6", + "after_sha256": "1191bfe2cfd0dd967f51405a1fd453e2f24c929d558554110137c2cc99f71941" + }, + "scripts/task_execution_journal.py": { + "before_sha256": "507b41a790e678dfd16af747bd851f5abc2fed5a8fb70bca05449d9e5ac674e8", + "after_sha256": "604cf285286a8a1be23d9bc0d8a547bd1195b7f80286778e1d4f30c711fd6a7e" + }, + "tests/test_task_execution_recovery.py": { + "before_sha256": null, + "after_sha256": "1322469a116305a3a44bca7191eb52b76614d911b9eb7ce68864c52ce13c36e1" + }, + "docs/task-execution-native-fixture.md": { + "before_sha256": "4501a512bc03a8d912b11a199a1385d75cfa077ba849dfcfe3eca53f31d1c8ea", + "after_sha256": "e299ff92fbd5282ca73066e41d5ef3753fad2e46ded2d8eda9935d20effaae46" + }, + "workplans/RPF-WP-0035-credential-lane-implementation.md": { + "before_sha256": "e90d963f8699360a62907ccc32d2346a33db62945dfb10473217c6dce7b2d383", + "after_sha256": "312a3b4b9228897a1a256736eb0d4995b38274a47c6a52870d106aba16b532fd" + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1-to-r2.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1-to-r2.patch new file mode 100644 index 00000000..c85a49be --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1-to-r2.patch @@ -0,0 +1,394 @@ +diff --git a/docs/task-execution-native-fixture.md b/docs/task-execution-native-fixture.md +index 4fb60fe..f4ff070 100644 +--- a/docs/task-execution-native-fixture.md ++++ b/docs/task-execution-native-fixture.md +@@ -174,3 +174,34 @@ revision drift, namespace policy-name reuse and private-file/binding refusals. + This is source fixture proof only. Native custody, isolation, exact recovery-role + permissions, retention/encryption, clock admission, audit delivery and human scope + approval remain missing. No generic broker, production reader, grant or CLI is added. ++ ++ ++### Recovery R2 exact absence and authenticated capsule refinement ++ ++R1 overclaimed policy absence for arbitrary 404 bodies. R2 requires exactly ++`{"errors":[]}` for both initial GET and post-DELETE GET. Other 404 bodies, ++permission denials, extra data, malformed/duplicate-key/nonfinite JSON and failed ++readback cannot confirm policy cleanup. A successful DELETE status alone cannot ++confirm absence. Recovery native policy comparison admits precisely one optional ++terminal LF, matching the retained OpenBao HCL source/readback convention; spaces, ++tabs, CR, extra terminal LF and any other byte change are refused. This is a ++narrow declared normalization, not full whitespace or semantic equivalence. ++ ++Private fixture capsules now carry HMAC-SHA256 authentication under an exactly ++32-byte integrity key supplied independently by the recovery owner. The key is ++never stored in the capsule or public journal, and is not a bearer credential. ++The fixture owner retains this key independently across executor death; a fresh ++owner custody object must supply the same key. Wrong-key or damaged accessor/HCL ++bytes refuse before admission. This is integrity protection, not encryption or ++same-UID isolation; production key custody, rotation/retention and recovery actor ++selection remain explicitly unadmitted. ++ ++After authentication the reader enforces exact top-level and nested binding/ ++cleanup key sets and types, validated Binding/Cleanup formats and the ordered ++source-derived cleanup_specs operation digests. Duplicate JSON keys, nonfinite ++numbers, malformed phase/revision combinations and type-coercible values refuse. ++The reader bounds bytes, rejects indirect/nonprivate files and detects stat/size ++changes during its single read. A valid older prepared capsule still cannot ++prove a task token absent when durable journal state/calls reached issuance: ++that remains unknown cleanup debt. Authentication alone is not anti-rollback. ++No business or consume admission is restored and no production scope is granted. +diff --git a/scripts/task_execution_recovery.py b/scripts/task_execution_recovery.py +index b4ce2b9..ab8b8cb 100644 +--- a/scripts/task_execution_recovery.py ++++ b/scripts/task_execution_recovery.py +@@ -8,6 +8,7 @@ public request/journal data. No business adapter or resumed admission is built. + from __future__ import annotations + from dataclasses import asdict, dataclass, field + import hashlib ++import hmac + import http.client + import json + import os +@@ -17,17 +18,45 @@ import subprocess + import time + import uuid + +-from task_execution_journal import Binding, Journal, Refused ++from task_execution_journal import Binding, Cleanup, Journal, Refused ++from task_execution_plan import cleanup_specs + + + def _encoded(value): +- return json.dumps(value, sort_keys=True, separators=(',', ':')).encode() ++ return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode() + + + def _digest(value): + return hashlib.sha256(_encoded(value)).hexdigest() + + ++def _json_unique(pairs): ++ result={} ++ for key,value in pairs: ++ if key in result: raise Refused('duplicate private custody field') ++ result[key]=value ++ return result ++ ++ ++def _invalid_constant(value): ++ raise Refused('nonfinite private custody value') ++ ++ ++def _policy_absent(status, response): ++ # Exactly the native OpenBao empty-errors 404. Denial/malformed/error-shaped ++ # bodies never prove absence, including after a successful delete response. ++ return (status==404 and type(response) is dict and set(response)=={'errors'} ++ and type(response['errors']) is list and not response['errors']) ++ ++ ++def _same_policy(actual, expected): ++ # OpenBao may omit one terminal LF from HCL source. Only that equivalence ++ # is admitted: spaces, tabs, CR and additional blank lines remain meaningful. ++ if type(actual) is not str or type(expected) is not str: return False ++ strip_one=lambda value:value[:-1] if value.endswith('\n') else value ++ return strip_one(actual)==strip_one(expected) ++ ++ + class FixtureRecoveryCustody: + """Independent owner-private persistent fixture custody; no public API. + +@@ -36,13 +65,17 @@ class FixtureRecoveryCustody: + enforced owner storage/executor authority; this implementation refuses estate + backends and is never evidence of a production grant. + """ +- def __init__(self, directory: Path): ++ def __init__(self, directory: Path, *, integrity_key: bytes): ++ if type(integrity_key) is not bytes or len(integrity_key)!=32: ++ raise Refused('independent owner capsule integrity key required') ++ self._integrity_key=integrity_key + self.directory = directory + if directory.is_symlink() or directory.absolute() != directory.resolve(): + raise Refused('indirect recovery custody directory') + Journal._private(directory.stat(), directory=True) + + def _path(self, binding): ++ if type(binding) is not Binding: raise Refused('exact recovery binding required') + binding.validate() + return self.directory / (binding.operation_id + '.private.json') + +@@ -52,30 +85,59 @@ class FixtureRecoveryCustody: + finally: os.close(fd) + + def read(self, binding): +- fd = os.open(self._path(binding), os.O_RDONLY | os.O_NOFOLLOW) ++ try: fd = os.open(self._path(binding), os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) ++ except OSError: raise Refused('private custody unavailable') from None + try: +- Journal._private(os.fstat(fd)) ++ before=os.fstat(fd) ++ Journal._private(before) + with os.fdopen(fd, 'rb', closefd=False) as stream: raw = stream.read(65537) ++ after=os.fstat(fd) ++ identity=lambda value:(value.st_dev,value.st_ino,value.st_size,value.st_mtime_ns,value.st_ctime_ns,value.st_mode,value.st_nlink) ++ if identity(before)!=identity(after) or len(raw)!=before.st_size: ++ raise Refused('private custody changed during read') + finally: os.close(fd) + if len(raw) > 65536: raise Refused('oversize private custody record') +- try: value = json.loads(raw) ++ try: capsule = json.loads(raw,object_pairs_hook=_json_unique,parse_constant=_invalid_constant) + except Exception: raise Refused('invalid private custody record') from None +- if type(value) is not dict or value.get('binding') != asdict(binding): +- raise Refused('private custody binding mismatch') ++ if (type(capsule) is not dict or set(capsule)!={'payload','mac'} or type(capsule['mac']) is not str ++ or len(capsule['mac'])!=64 or any(x not in '0123456789abcdef' for x in capsule['mac'])): ++ raise Refused('invalid authenticated private capsule') ++ expected_mac=hmac.new(self._integrity_key,_encoded(capsule['payload']),hashlib.sha256).hexdigest() ++ if not hmac.compare_digest(capsule['mac'],expected_mac): ++ raise Refused('private capsule integrity mismatch') ++ value=capsule['payload'] + expected={'binding','cleanup','port','policy_hcl','identity_accessor','task_accessor','phase','revision'} +- if (set(value)!=expected or type(value['port']) is not int or not 1<=value['port']<=65535 ++ if type(value) is not dict or set(value)!=expected: ++ raise Refused('invalid private custody schema') ++ stored=value['binding'] ++ if (type(stored) is not dict or set(stored)!=set(asdict(binding)) ++ or any(type(v) is not str for v in stored.values())): ++ raise Refused('invalid private custody binding schema') ++ restored=Binding(**stored) ++ restored.validate() ++ if asdict(restored)!=asdict(binding): raise Refused('private custody binding mismatch') ++ if (type(value['port']) is not int or not 1<=value['port']<=65535 + or type(value['policy_hcl']) is not str or not value['policy_hcl'] + or type(value['identity_accessor']) is not str or not value['identity_accessor'] +- or value['phase'] not in {'prepared','mint-intent','issued'} ++ or type(value['phase']) is not str or value['phase'] not in {'prepared','mint-intent','issued'} + or type(value['revision']) is not int or value['revision']!={'prepared':0,'mint-intent':1,'issued':2}[value['phase']] + or (value['phase']=='issued' and (type(value['task_accessor']) is not str or not value['task_accessor'])) + or (value['phase']!='issued' and value['task_accessor'] is not None) + or type(value['cleanup']) is not list or len(value['cleanup'])!=3): + raise Refused('invalid private custody schema') ++ for item in value['cleanup']: ++ if type(item) is not dict or set(item)!={'kind','call_digest'}: ++ raise Refused('invalid private cleanup schema') ++ Cleanup(**item).validate() ++ expected_cleanup=[{'kind':item['kind'],'call_digest':item['call_digest']} ++ for item in cleanup_specs(binding.operation_id)] ++ if value['cleanup']!=expected_cleanup: ++ raise Refused('private cleanup differs from deterministic owner commitment') + return value + + def _write(self, binding, value, initial=False): +- raw = _encoded(value) ++ capsule={'payload':value,'mac':hmac.new(self._integrity_key,_encoded(value),hashlib.sha256).hexdigest()} ++ raw = _encoded(capsule) + if len(raw) > 65536: raise Refused('oversize private custody record') + path = self._path(binding) + if initial: +@@ -159,6 +221,11 @@ class _Recorder: + if all(x['outcome'] == 'confirmed' for x in current): + raise Refused('cleanup already confirmed') + self.current = current ++ # An authenticated older prepared capsule must not erase later issuance ++ # uncertainty. Without an issued accessor, any native call or state at ++ # the grant boundary makes absence unprovable. Recovery is conservative. ++ self.no_mint_proved=(journal.observe(b)['state'] in {'reserved','consume-confirmed','authenticating'} ++ and journal.db.execute('SELECT COUNT(*) FROM calls WHERE operation=?',(b.operation_id,)).fetchone()[0]==0) + db = journal.db + try: db.execute('INSERT INTO recovery_attempts VALUES (?,?,?)', (permit.attempt_id,b.operation_id,json.dumps(current,sort_keys=True))) + except Exception: raise Refused('recovery admission already used') from None +@@ -233,7 +300,7 @@ def recover_fixture_cleanup(journal: Journal, permit: FixtureRecoveryPermit): + headers={'X-Vault-Token':permit.owner_token,'Content-Type':'application/json'}) + response=conn.getresponse();raw=response.read(1048577) + if len(raw)>1048576: raise Refused('oversize recovery response') +- data=json.loads(raw) if raw else {} ++ data=json.loads(raw,object_pairs_hook=_json_unique,parse_constant=_invalid_constant) if raw else {} + if type(data) is not dict: raise Refused('invalid recovery response') + status=response.status + except Exception: raise Refused('recovery transport or response unconfirmed') from None +@@ -245,7 +312,7 @@ def recover_fixture_cleanup(journal: Journal, permit: FixtureRecoveryPermit): + if item['kind'] in {'task-token-revoke','identity-token-revoke'}: + accessor=value['task_accessor' if ordinal==0 else 'identity_accessor'] + if accessor is None: +- confirmed=ordinal==0 and value['phase']=='prepared' ++ confirmed=ordinal==0 and value['phase']=='prepared' and record.no_mint_proved + else: + status,data=call('POST','auth/token/revoke-accessor',{'accessor':accessor}) + # Revoke is idempotent for an already absent accessor; +@@ -256,11 +323,12 @@ def recover_fixture_cleanup(journal: Journal, permit: FixtureRecoveryPermit): + elif item['kind']=='grant-policy-delete': + path='sys/policies/acl/task-exec-'+permit.binding.operation_id + status,data=call('GET',path) +- if status==404:confirmed=True +- elif status==200 and data.get('data',{}).get('policy','').rstrip()==value['policy_hcl'].rstrip(): ++ if _policy_absent(status,data):confirmed=True ++ elif (status==200 and not data.get('errors') and type(data.get('data')) is dict ++ and _same_policy(data['data'].get('policy'),value['policy_hcl'])): + status,data=call('DELETE',path) + if status in {200,204}: +- status,data=call('GET',path);confirmed=status==404 ++ status,data=call('GET',path);confirmed=_policy_absent(status,data) + except Exception: + # No backend errors/response/handle hashes enter public evidence. + confirmed=False +diff --git a/tests/test_task_execution_recovery.py b/tests/test_task_execution_recovery.py +index e29d716..52b8b61 100644 +--- a/tests/test_task_execution_recovery.py ++++ b/tests/test_task_execution_recovery.py +@@ -3,6 +3,7 @@ from dataclasses import replace + import hashlib + import json + import os ++import secrets + from pathlib import Path + import sys + import time +@@ -20,7 +21,7 @@ from task_execution_recovery import FixtureRecoveryCustody, recover_fixture_clea + def protected(owner,tmp_path): + native,journal,binding,call,compiler,descriptor=owner + directory=tmp_path/'separate-owner-custody';directory.mkdir(mode=0o700) +- custody=FixtureRecoveryCustody(directory) ++ custody=FixtureRecoveryCustody(directory,integrity_key=secrets.token_bytes(32)) + native=NativeFixtureAdapter(compiler,native.plan,journal,binding,native.admission,recovery_custody=custody) + return native,journal,binding,call,custody + +@@ -178,7 +179,7 @@ def test_private_custody_permissions_and_identity_binding_fail_closed(owner,tmp_ + path=custody._path(binding);path.chmod(0o644) + with pytest.raises(Refused):custody.read(binding) + path.chmod(0o600) +- data=json.loads(path.read_bytes());data['binding']['operation_id']='other' ++ data=json.loads(path.read_bytes());data['payload']['binding']['operation_id']='other' + path.write_text(json.dumps(data)) + with pytest.raises(Refused):custody.read(binding) + +@@ -205,3 +206,115 @@ def test_lost_recovery_response_stays_unknown_until_new_independent_attempt(owne + # Reconciliation does not erase the historic uncertain invocation. + assert journal.observe(binding)['external_outcome_unknown'] + assert journal.db.execute("SELECT COUNT(*) FROM recovery_calls WHERE outcome='pending'").fetchone()[0]==1 ++ ++ ++@pytest.fixture ++def capsule(tmp_path): ++ from task_execution_journal import Binding,Cleanup ++ from task_execution_plan import cleanup_specs ++ binding=Binding('SYNTHETIC','sha256:'+'a'*64,'12345678-1234-1234-1234-123456789abc','b'*64) ++ directory=tmp_path/'private-capsule';directory.mkdir(mode=0o700) ++ custody=FixtureRecoveryCustody(directory,integrity_key=secrets.token_bytes(32)) ++ cleanup=tuple(Cleanup(x['kind'],x['call_digest']) for x in cleanup_specs(binding.operation_id)) ++ custody.prepare(binding,cleanup,12345,'path "fixture" {}\n','synthetic-private-accessor') ++ return custody,binding ++ ++ ++@pytest.mark.parametrize('mutate',[ ++ lambda v:v.update(extra=True),lambda v:v.pop('phase'),lambda v:v.update(binding=True), ++ lambda v:v['binding'].update(approval_id=True),lambda v:v['binding'].update(extra='x'), ++ lambda v:v['binding'].update(request_digest='0'*64),lambda v:v.update(port=True), ++ lambda v:v.update(port=12345.0),lambda v:v.update(port=0),lambda v:v.update(phase=[]), ++ lambda v:v.update(phase='issued'),lambda v:v.update(revision=False), ++ lambda v:v.update(revision=0.0),lambda v:v.update(identity_accessor=False), ++ lambda v:v.update(task_accessor='unexpected-before-issue'),lambda v:v.update(policy_hcl=[]), ++ lambda v:v.update(cleanup={}),lambda v:v['cleanup'].pop(), ++ lambda v:v['cleanup'].__setitem__(0,False),lambda v:v['cleanup'][0].update(extra=True), ++ lambda v:v['cleanup'][0].update(kind=True),lambda v:v['cleanup'][0].update(call_digest=0), ++ lambda v:v['cleanup'][0].update(call_digest='f'*64),lambda v:v['cleanup'].reverse(), ++]) ++def test_authenticated_but_damaged_capsule_schema_refuses(capsule,mutate): ++ custody,binding=capsule ++ value=custody.read(binding);mutate(value) ++ # Trusted test owner re-signs malformed payload to exercise schema defenses ++ # independently of MAC validation; untrusted corruption cannot do this. ++ custody._write(binding,value) ++ with pytest.raises(Refused):custody.read(binding) ++ ++ ++def test_capsule_corruption_wrong_key_duplicate_and_nonfinite_refuse(capsule): ++ custody,binding=capsule;path=custody._path(binding);original=path.read_bytes() ++ value=json.loads(original);value['payload']['identity_accessor']='different-synthetic-accessor' ++ path.write_text(json.dumps(value)) ++ with pytest.raises(Refused):custody.read(binding) ++ path.write_bytes(original) ++ other=FixtureRecoveryCustody(custody.directory,integrity_key=b'X'*32) ++ with pytest.raises(Refused):other.read(binding) ++ for raw in [b'{',original[:-1]+b',"mac":"duplicate"}',original.replace(b'12345',b'NaN'),b'[]',b'{}',b'{"payload":{},"mac":"'+b'\xc3\xa9'*64+b'"}']: ++ path.write_bytes(raw) ++ with pytest.raises(Refused):custody.read(binding) ++ path.write_bytes(original) ++ assert custody.read(binding)['identity_accessor']=='synthetic-private-accessor' ++ ++ ++def test_exact_absence_and_single_terminal_lf_contract(): ++ from task_execution_recovery import _policy_absent,_same_policy ++ assert _policy_absent(404,{'errors':[]}) ++ for status,body in [(403,{'errors':[]}),(404,{}),(404,{'errors':['denied']}), ++ (404,{'errors':None}),(404,{'errors':[]} | {'data':{}}), ++ (404,{'errors':False}),(404,{'errors':0}),(404,{'errors':{}})]: ++ assert not _policy_absent(status,body) ++ assert _same_policy('fixture','fixture\n') ++ assert _same_policy('fixture\n','fixture\n') ++ for text in ['fixture \n','fixture\t','fixture\r\n','fixture\n\n',False,None]: ++ assert not _same_policy(text,'fixture\n') ++ ++ ++@pytest.mark.parametrize('after_delete',[False,True]) ++def test_recovery_policy_absence_rejects_misleading_native_http_bodies(owner,tmp_path,monkeypatch,after_delete): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ import task_execution_recovery as module ++ malformed=[{}, {'errors':['permission denied']}, {'errors':None}, {'errors':False}, ++ {'errors':0}, {'errors':[],'data':{}}, {'errors':['not really absent']}, ++ b'{"errors":["permission denied"],"errors":[]}',b'{"errors":NaN}',b'not JSON'] ++ path='sys/policies/acl/'+native.policy_name ++ for body in malformed: ++ sent=[];gets=[0] ++ class Response: ++ def __init__(self,status,data):self.status=status;self.data=data ++ def read(self,limit):return self.data if type(self.data) is bytes else json.dumps(self.data).encode() ++ class Connection: ++ def __init__(self,*args,**kwargs):pass ++ def request(self,method,url,**kwargs):sent.append((method,url)) ++ def getresponse(self): ++ method,url=sent[-1] ++ if url=='/v1/'+path: ++ if method=='DELETE':return Response(204,{}) ++ gets[0]+=1 ++ if after_delete and gets[0]==1: ++ return Response(200,{'data':{'policy':native.hcl[:-1]}}) ++ return Response(404,body) ++ return Response(403,{'errors':['synthetic refusal']}) ++ def close(self):pass ++ with monkeypatch.context() as patch: ++ patch.setattr(module.http.client,'HTTPConnection',Connection) ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] and result['obligations'][2]['outcome']=='owed' ++ assert ('DELETE','/v1/'+path) in sent if after_delete else ('DELETE','/v1/'+path) not in sent ++ # The denied/malformed probes above emitted no real native calls. ++ assert call('GET',path)[0]==200 ++ assert recover_fixture_cleanup(journal,permit(native,custody,binding))['confirmed'] ++ ++ ++def test_authenticated_prepared_capsule_rollback_cannot_hide_later_mint(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ old=custody._path(binding).read_bytes() ++ crash(native,journal,binding) ++ current=custody.read(binding) ++ custody._path(binding).write_bytes(old) ++ # Still authentic under the owner key, but the durable journal proves the ++ # operation reached issuance. The stale prepared record cannot prove absence. ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] and result['obligations'][0]['outcome']=='owed' ++ assert call('POST','auth/token/lookup-accessor',{'accessor':current['task_accessor']})[0]==200 ++ assert call('POST','auth/token/revoke-accessor',{'accessor':current['task_accessor']})[0] in {200,204} +diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md +index 1b3a079..f58a6b9 100644 +--- a/workplans/RPF-WP-0035-credential-lane-implementation.md ++++ b/workplans/RPF-WP-0035-credential-lane-implementation.md +@@ -1299,3 +1299,11 @@ without restoring business/consume admission. Separate durable attempt evidence + retains original unknown outcomes. Lost token-create response remains unresolved + when no accessor was committed. Source-only process-loss fixtures are not native + production custody or recovery-role admission; T12 remains progress. ++ ++2026-10-16 T12 recovery R2 source refinement retains R1's overbroad 404 failure: ++policy cleanup now requires exact native empty-errors absence before/after delete; ++HCL uses only documented single optional terminal LF normalization. Private ++capsules are owner-key HMAC authenticated, strict nested schemas/ordered cleanup ++commitments reject damage, and journal history prevents stale prepared capsules ++from hiding issuance. Fixture process-loss source proof remains separate from ++unadmitted production custody, integrity-key storage and recovery-role scope. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1.patch new file mode 100644 index 00000000..da21a3d4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r1.patch @@ -0,0 +1,641 @@ +diff --git a/docs/task-execution-native-fixture.md b/docs/task-execution-native-fixture.md +index 1dd6b91..4fb60fe 100644 +--- a/docs/task-execution-native-fixture.md ++++ b/docs/task-execution-native-fixture.md +@@ -121,3 +121,56 @@ 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. ++ ++ ++## Independent cleanup recovery source successor ++ ++`task_execution_recovery.py` adds separately installed fixture-only owner custody ++and fresh recovery permits. Optional `recovery_custody` on NativeFixtureAdapter ++persists the original binding/cleanup commitments, exact operation policy, backend ++port and identity accessor before any grant effect; it commits mint intent before ++token creation and task accessor immediately after a valid issued response. ++Bearer credentials are never persisted. The custody directory is separate from ++the public journal, owner-only 0700/0600, bounded at 64KiB, no-follow, with atomic ++replacement and fsync. It is real process-independent persistence for synthetic ++fixtures. It does not isolate same-UID hostile execution, encrypt at rest, select ++production storage or establish a production secret policy. Those are explicit ++native custody/deployment gates, not supplied by Python types or file modes. ++ ++The independent owner reads its private custody revision and installs a fresh ++bounded permit, current owner credential and retained live disposable Popen. ++Permit revocation is durably owner-recorded. The recovery runner has no mint, ++consume, policy-apply or business method. It dispatches only fixed exact-accessor ++revoke/readback and exact `task-exec-` policy read/delete/readback. ++A differently recreated policy is never deleted. Backend admission remains the ++same loopback-only owner-spawned dev process; no estate backend can be selected. ++Accessor-based recovery is an independently admitted alternate cleanup projection, ++not a reinterpretation of the original revoke-self call digest. Production needs ++exact owner-reviewed recovery scope; original business grant does not imply it. ++ ++Fresh recovery holds the journal process lock but never sets business admission ++or consumed-admission fields. Separate recovery attempt/call/result tables retain ++original cleanup statuses before reconciliation, write native intent before calls, ++and preserve failed/pending outcomes. Proven cleanup alone updates the exact ++original cleanup obligation to confirmed. Business state and original pending ++call uncertainty remain unchanged. Reusing the original approval, continuing ++metadata calls and reusing a recovery permit refuse. Failure needs another ++independent owner permit; there is no automatic retry. Expired/revoked permits ++or changed private custody revision refuse before calls. ++ ++A token-create response lost before accessor custody remains unresolved token ++cleanup debt: policy deletion/identity revocation can be independently confirmed, ++but cannot imply task-token revocation. Neither expiry guessed from wall clock ++nor policy absence fabricates token absence. A production successor must admit ++an independent exact ambiguous-issuance reconciliation mechanism or prove bounded ++expiry from owner backend evidence. No such new grant is implemented here. ++ ++`test_task_execution_recovery.py` forks a real executor and terminates it without ++finally cleanup after issue, business effect/lost response and revoke/lost response. ++The independent parent, holding the backend and separately installed custody, ++recovers specific obligations and verifies actual token/policy absence. Additional ++cases cover lost-mint response, revoked credential, permit revocation/expiry, ++revision drift, namespace policy-name reuse and private-file/binding refusals. ++This is source fixture proof only. Native custody, isolation, exact recovery-role ++permissions, retention/encryption, clock admission, audit delivery and human scope ++approval remain missing. No generic broker, production reader, grant or CLI is added. +diff --git a/scripts/task_execution_journal.py b/scripts/task_execution_journal.py +index 8f1b9a2..1eaa583 100644 +--- a/scripts/task_execution_journal.py ++++ b/scripts/task_execution_journal.py +@@ -104,6 +104,9 @@ class Journal: + 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.db.execute("CREATE TABLE IF NOT EXISTS cleanup_calls (operation TEXT NOT NULL, obligation INTEGER NOT NULL, step INTEGER NOT NULL, call_digest TEXT NOT NULL, outcome TEXT NOT NULL, evidence TEXT, PRIMARY KEY(operation,obligation,step))") ++ self.db.execute("CREATE TABLE IF NOT EXISTS recovery_attempts (attempt TEXT PRIMARY KEY, operation TEXT NOT NULL, original TEXT NOT NULL)") ++ self.db.execute("CREATE TABLE IF NOT EXISTS recovery_calls (attempt TEXT NOT NULL, obligation INTEGER NOT NULL, step INTEGER NOT NULL, digest TEXT NOT NULL, outcome TEXT NOT NULL, evidence TEXT, PRIMARY KEY(attempt,obligation,step))") ++ self.db.execute("CREATE TABLE IF NOT EXISTS recovery_results (attempt TEXT NOT NULL, obligation INTEGER NOT NULL, confirmed INTEGER NOT NULL, evidence TEXT NOT NULL, PRIMARY KEY(attempt,obligation))") + self._held = False + self._admission = None + self._consumed_admission = None +@@ -228,6 +231,7 @@ class Journal: + 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] + pending += self.db.execute("SELECT COUNT(*) FROM cleanup_calls WHERE operation=? AND outcome='pending'", (binding.operation_id,)).fetchone()[0] ++ pending += self.db.execute("SELECT COUNT(*) FROM recovery_calls AS c JOIN recovery_attempts AS a ON c.attempt=a.attempt WHERE a.operation=? AND c.outcome='pending'", (binding.operation_id,)).fetchone()[0] + return {"state": row[3], "evidence_digest": row[4], "dispatch_allowed": False, + "external_outcome_unknown": bool(pending)} + +diff --git a/scripts/task_execution_native_fixture.py b/scripts/task_execution_native_fixture.py +index caa7da1..31a822b 100644 +--- a/scripts/task_execution_native_fixture.py ++++ b/scripts/task_execution_native_fixture.py +@@ -3,7 +3,8 @@ + This adapter deliberately admits only a disposable loopback fixture. It supplies + real native primitive coverage while production grant schema, identity/consume and + origin/source admission are unfinished. It cannot open an estate backend. Owner +-credentials/handles stay in memory; no subprocess, redirects or ambient proxy. ++bearer credentials stay in memory; optional owner-private fixture custody persists ++accessors for independently admitted cleanup recovery; no redirects or ambient proxy. + Python privacy is not credential isolation; production needs the owner runtime. + """ + from __future__ import annotations +@@ -41,7 +42,7 @@ class FixtureAdmission: + + class NativeFixtureAdapter: + def __init__(self, compiler: Compiler, plan: Plan, journal: Journal, +- binding: Binding, admission: FixtureAdmission): ++ binding: Binding, admission: FixtureAdmission, *, recovery_custody=None): + if type(admission) is not FixtureAdmission or type(admission.descriptor) is not bytes: + raise Refused('independent fixture admission required') + self.compiler,self.plan,self.journal,self.binding=compiler,plan,journal,binding +@@ -100,6 +101,12 @@ class NativeFixtureAdapter: + 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 + self._cleanup_steps={item.kind:0 for item in self.cleanup} ++ self.recovery_custody=recovery_custody ++ if recovery_custody is not None: ++ from task_execution_recovery import FixtureRecoveryCustody ++ if type(recovery_custody) is not FixtureRecoveryCustody or journal.observe(binding) is not None: ++ raise Refused('fresh independent fixture recovery custody required') ++ recovery_custody.prepare(binding,self.cleanup,self.port,self.hcl,admission.identity_session.accessor) + + def _live(self): + if not self.journal._held or self.journal._admission!=self.binding or self.journal._consumed_admission!=self.binding: +@@ -177,6 +184,8 @@ class NativeFixtureAdapter: + 'type':'service','display_name':'task-exec-'+self.binding.operation_id,'no_parent':False} + if body!=self.meta['execution_grant']['openbao']['token_create_body']: + raise Refused('native token body differs from bound owner projection') ++ if self.recovery_custody is not None: ++ self.recovery_custody.checkpoint(self.binding,'mint-intent') + self._mint_attempted=True + status,data=self._business('POST','auth/token/create',root,encoded(body));self._ok(status,data,{200}) + auth=data.get('auth',{}) +@@ -185,6 +194,8 @@ class NativeFixtureAdapter: + # Retain handles before validation so finally cleanup can revoke even a + # malformed overbroad issue response. Never return token-bearing metadata. + self.task=PrivateSession(auth['client_token'],auth['accessor']) ++ if self.recovery_custody is not None: ++ self.recovery_custody.checkpoint(self.binding,'issued',self.task.accessor) + status,data=self._business('POST','auth/token/lookup',root,encoded({'token':self.task.token}));self._ok(status,data,{200}) + info=data.get('data',{}) + if (info.get('policies')!=[self.policy_name] or info.get('identity_policies',[]) or info.get('renewable') is not False +diff --git a/scripts/task_execution_recovery.py b/scripts/task_execution_recovery.py +new file mode 100644 +index 0000000..b4ce2b9 +--- /dev/null ++++ b/scripts/task_execution_recovery.py +@@ -0,0 +1,268 @@ ++"""Owner-only cleanup recovery for the disposable native fixture. ++ ++Private, separately installed custody persists accessors, never bearer tokens. ++This is a fixture implementation, not production custody/isolation admission. ++The recovery owner supplies a fresh capability and credential independently of ++public request/journal data. No business adapter or resumed admission is built. ++""" ++from __future__ import annotations ++from dataclasses import asdict, dataclass, field ++import hashlib ++import http.client ++import json ++import os ++from pathlib import Path ++import secrets ++import subprocess ++import time ++import uuid ++ ++from task_execution_journal import Binding, Journal, Refused ++ ++ ++def _encoded(value): ++ return json.dumps(value, sort_keys=True, separators=(',', ':')).encode() ++ ++ ++def _digest(value): ++ return hashlib.sha256(_encoded(value)).hexdigest() ++ ++ ++class FixtureRecoveryCustody: ++ """Independent owner-private persistent fixture custody; no public API. ++ ++ Mode/ownership and no-follow checks protect against accidental exposure. ++ Same-UID hostile code is NOT isolated. Production must supply independently ++ enforced owner storage/executor authority; this implementation refuses estate ++ backends and is never evidence of a production grant. ++ """ ++ def __init__(self, directory: Path): ++ self.directory = directory ++ if directory.is_symlink() or directory.absolute() != directory.resolve(): ++ raise Refused('indirect recovery custody directory') ++ Journal._private(directory.stat(), directory=True) ++ ++ def _path(self, binding): ++ binding.validate() ++ return self.directory / (binding.operation_id + '.private.json') ++ ++ def _sync(self): ++ fd = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) ++ try: os.fsync(fd) ++ finally: os.close(fd) ++ ++ def read(self, binding): ++ fd = os.open(self._path(binding), os.O_RDONLY | os.O_NOFOLLOW) ++ try: ++ Journal._private(os.fstat(fd)) ++ with os.fdopen(fd, 'rb', closefd=False) as stream: raw = stream.read(65537) ++ finally: os.close(fd) ++ if len(raw) > 65536: raise Refused('oversize private custody record') ++ try: value = json.loads(raw) ++ except Exception: raise Refused('invalid private custody record') from None ++ if type(value) is not dict or value.get('binding') != asdict(binding): ++ raise Refused('private custody binding mismatch') ++ expected={'binding','cleanup','port','policy_hcl','identity_accessor','task_accessor','phase','revision'} ++ if (set(value)!=expected or type(value['port']) is not int or not 1<=value['port']<=65535 ++ or type(value['policy_hcl']) is not str or not value['policy_hcl'] ++ or type(value['identity_accessor']) is not str or not value['identity_accessor'] ++ or value['phase'] not in {'prepared','mint-intent','issued'} ++ or type(value['revision']) is not int or value['revision']!={'prepared':0,'mint-intent':1,'issued':2}[value['phase']] ++ or (value['phase']=='issued' and (type(value['task_accessor']) is not str or not value['task_accessor'])) ++ or (value['phase']!='issued' and value['task_accessor'] is not None) ++ or type(value['cleanup']) is not list or len(value['cleanup'])!=3): ++ raise Refused('invalid private custody schema') ++ return value ++ ++ def _write(self, binding, value, initial=False): ++ raw = _encoded(value) ++ if len(raw) > 65536: raise Refused('oversize private custody record') ++ path = self._path(binding) ++ if initial: ++ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) ++ else: ++ # Existing owner-private record checked before replacement. Unique ++ # temp + fsync + replace + directory fsync never truncates old state. ++ self.read(binding) ++ tmp = self.directory / ('.pending-' + secrets.token_hex(16)) ++ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) ++ try: ++ with os.fdopen(fd, 'wb', closefd=False) as stream: ++ stream.write(raw); stream.flush(); os.fsync(fd) ++ finally: os.close(fd) ++ if not initial: os.replace(tmp, path) ++ self._sync() ++ ++ def prepare(self, binding, cleanup, port, policy_hcl, identity_accessor): ++ value = {'binding': asdict(binding), 'cleanup': [asdict(x) for x in cleanup], ++ 'port': port, 'policy_hcl': policy_hcl, ++ 'identity_accessor': identity_accessor, 'task_accessor': None, ++ 'phase': 'prepared', 'revision': 0} ++ self._write(binding, value, initial=True) ++ ++ def checkpoint(self, binding, phase, task_accessor=None): ++ value = self.read(binding) ++ if (value['phase'], phase) not in {('prepared', 'mint-intent'), ('mint-intent', 'issued')}: ++ raise Refused('invalid protected recovery checkpoint') ++ if phase == 'issued' and (type(task_accessor) is not str or not task_accessor): ++ raise Refused('protected issued accessor required') ++ value.update(phase=phase, task_accessor=task_accessor, revision=value['revision'] + 1) ++ self._write(binding, value) ++ ++ def revoke(self, permit): ++ if type(permit) is not FixtureRecoveryPermit or permit.custody is not self: ++ raise Refused('exact owner recovery permit required') ++ uuid.UUID(permit.attempt_id) ++ path=self.directory / (permit.attempt_id+'.revoked') ++ fd=os.open(path,os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,0o600) ++ try:os.fsync(fd) ++ finally:os.close(fd) ++ self._sync() ++ ++ def admit(self, binding, *, owner_token, server_process, valid_seconds=30): ++ """Trusted recovery-owner action, not deserialization of caller flags. ++ ++ Fresh admission seals the current private custody revision. The owner ++ supplies its credential and retained backend process separately. Neither ++ token nor private capsule digest is emitted to the public journal. ++ """ ++ value = self.read(binding) ++ if type(valid_seconds) is not int or not 1 <= valid_seconds <= 60: ++ raise Refused('bounded recovery admission required') ++ if type(owner_token) is not str or not owner_token: ++ raise Refused('independent recovery owner credential required') ++ return FixtureRecoveryPermit(str(uuid.uuid4()), binding, self, _digest(value), ++ owner_token, server_process, time.monotonic()+valid_seconds) ++ ++ ++@dataclass(frozen=True, repr=False) ++class FixtureRecoveryPermit: ++ attempt_id: str ++ binding: Binding ++ custody: FixtureRecoveryCustody ++ private_revision_digest: str = field(repr=False) ++ owner_token: str = field(repr=False) ++ server_process: subprocess.Popen = field(repr=False) ++ deadline: float ++ ++ ++class _Recorder: ++ def __init__(self, journal, permit, value): ++ self.journal, self.permit = journal, permit ++ b = permit.binding ++ if journal._admission is not None or journal._consumed_admission is not None: ++ raise Refused('recovery cannot share a business admission') ++ if journal.observe(b) is None: raise Refused('original operation absent') ++ current = journal.cleanup_status(b)['obligations'] ++ if [(x['kind'], x['call_digest']) for x in current] != [(x['kind'], x['call_digest']) for x in value['cleanup']]: ++ raise Refused('original cleanup commitment mismatch') ++ if all(x['outcome'] == 'confirmed' for x in current): ++ raise Refused('cleanup already confirmed') ++ self.current = current ++ db = journal.db ++ try: db.execute('INSERT INTO recovery_attempts VALUES (?,?,?)', (permit.attempt_id,b.operation_id,json.dumps(current,sort_keys=True))) ++ except Exception: raise Refused('recovery admission already used') from None ++ journal._sync_directory() ++ ++ def before(self, ordinal, step, method, path): ++ digest = _digest({'kind': self.current[ordinal]['kind'], 'commitment':self.current[ordinal]['call_digest'], 'method':method,'path':path,'step':step}) ++ self.journal.db.execute("INSERT INTO recovery_calls VALUES (?,?,?,?,'pending',NULL)", (self.permit.attempt_id,ordinal,step,digest)) ++ self.journal._sync_directory() ++ return digest ++ ++ def after(self, ordinal, step, status): ++ self.journal.db.execute("UPDATE recovery_calls SET outcome='complete', evidence=? WHERE attempt=? AND obligation=? AND step=? AND outcome='pending'", (_digest({'http_status':status}),self.permit.attempt_id,ordinal,step)) ++ self.journal._sync_directory() ++ ++ def finish(self, ordinal, confirmed): ++ evidence = _digest({'attempt':self.permit.attempt_id,'obligation':ordinal,'confirmed':confirmed}) ++ db = self.journal.db ++ try: ++ db.execute('BEGIN IMMEDIATE') ++ pending = db.execute("SELECT COUNT(*) FROM recovery_calls WHERE attempt=? AND obligation=? AND outcome='pending'",(self.permit.attempt_id,ordinal)).fetchone()[0] ++ if confirmed and pending: raise Refused('pending recovery cannot confirm') ++ db.execute('INSERT INTO recovery_results VALUES (?,?,?,?)',(self.permit.attempt_id,ordinal,int(confirmed),evidence)) ++ if confirmed: ++ item=self.current[ordinal] ++ db.execute("UPDATE cleanup SET outcome='confirmed', evidence=? WHERE operation=? AND ordinal=? AND kind=? AND call_digest=?",(evidence,self.permit.binding.operation_id,ordinal,item['kind'],item['call_digest'])) ++ db.execute('COMMIT');self.journal._sync_directory() ++ except BaseException: ++ if db.in_transaction: db.execute('ROLLBACK') ++ raise ++ ++ ++def recover_fixture_cleanup(journal: Journal, permit: FixtureRecoveryPermit): ++ """Fresh independent owner admission: fixed cleanup only, no business retry. ++ ++ Each attempt preserves original pending/unconfirmed rows in recovery_attempts ++ and native pending call rows unchanged. A fresh owner permit is required for ++ another attempt after failure; never autonomously retry uncertain mutations. ++ """ ++ if type(permit) is not FixtureRecoveryPermit or type(permit.custody) is not FixtureRecoveryCustody: ++ raise Refused('independent typed recovery admission required') ++ if str(uuid.UUID(permit.attempt_id)) != permit.attempt_id: ++ raise Refused('invalid recovery attempt identity') ++ value = permit.custody.read(permit.binding) ++ if _digest(value) != permit.private_revision_digest: ++ raise Refused('private custody revision changed') ++ proc=permit.server_process ++ if (type(proc) is not subprocess.Popen or proc.poll() is not None or type(proc.args) is not list ++ or '-dev' not in proc.args or '-dev-no-store-token' not in proc.args ++ or '-dev-listen-address=127.0.0.1:'+str(value['port']) not in proc.args): ++ raise Refused('same live disposable backend required') ++ def fresh(): ++ if (permit.custody.directory / (permit.attempt_id+'.revoked')).exists(): ++ raise Refused('recovery permit revoked') ++ if time.monotonic() >= permit.deadline: raise Refused('recovery admission expired') ++ if proc.poll() is not None: raise Refused('disposable backend lost') ++ if _digest(permit.custody.read(permit.binding)) != permit.private_revision_digest: ++ raise Refused('private custody changed during recovery') ++ fresh() ++ with journal.locked(): ++ record = _Recorder(journal,permit,value) ++ for ordinal,item in enumerate(record.current): ++ if item['outcome']=='confirmed': continue ++ step=0 ++ def call(method,path,body=None): ++ nonlocal step ++ fresh(); index=step; step+=1 ++ record.before(ordinal,index,method,path) ++ conn=http.client.HTTPConnection('127.0.0.1',value['port'],timeout=2) ++ try: ++ conn.request(method,'/v1/'+path,body=_encoded(body) if body is not None else None, ++ headers={'X-Vault-Token':permit.owner_token,'Content-Type':'application/json'}) ++ response=conn.getresponse();raw=response.read(1048577) ++ if len(raw)>1048576: raise Refused('oversize recovery response') ++ data=json.loads(raw) if raw else {} ++ if type(data) is not dict: raise Refused('invalid recovery response') ++ status=response.status ++ except Exception: raise Refused('recovery transport or response unconfirmed') from None ++ finally:conn.close() ++ record.after(ordinal,index,status) ++ return status,data ++ confirmed=False ++ try: ++ if item['kind'] in {'task-token-revoke','identity-token-revoke'}: ++ accessor=value['task_accessor' if ordinal==0 else 'identity_accessor'] ++ if accessor is None: ++ confirmed=ordinal==0 and value['phase']=='prepared' ++ else: ++ status,data=call('POST','auth/token/revoke-accessor',{'accessor':accessor}) ++ # Revoke is idempotent for an already absent accessor; ++ # actual absence must still be independently read back. ++ if status in {200,204,400}: ++ status,data=call('POST','auth/token/lookup-accessor',{'accessor':accessor}) ++ confirmed=status==400 and data.get('errors') in (['invalid accessor'],['1 error occurred:\n\t* invalid accessor\n\n']) ++ elif item['kind']=='grant-policy-delete': ++ path='sys/policies/acl/task-exec-'+permit.binding.operation_id ++ status,data=call('GET',path) ++ if status==404:confirmed=True ++ elif status==200 and data.get('data',{}).get('policy','').rstrip()==value['policy_hcl'].rstrip(): ++ status,data=call('DELETE',path) ++ if status in {200,204}: ++ status,data=call('GET',path);confirmed=status==404 ++ except Exception: ++ # No backend errors/response/handle hashes enter public evidence. ++ confirmed=False ++ record.finish(ordinal,confirmed) ++ return journal.cleanup_status(permit.binding) +diff --git a/tests/test_task_execution_recovery.py b/tests/test_task_execution_recovery.py +new file mode 100644 +index 0000000..e29d716 +--- /dev/null ++++ b/tests/test_task_execution_recovery.py +@@ -0,0 +1,207 @@ ++"""Actual executor process loss against independent disposable OpenBao.""" ++from dataclasses import replace ++import hashlib ++import json ++import os ++from pathlib import Path ++import sys ++import time ++ ++import pytest ++ ++sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts')) ++from test_task_execution_plan import compiler ++from test_task_execution_native_fixture import bao_dev, owner, admit ++from task_execution_journal import Journal, Refused ++from task_execution_native_fixture import NativeFixtureAdapter ++from task_execution_recovery import FixtureRecoveryCustody, recover_fixture_cleanup ++ ++ ++def protected(owner,tmp_path): ++ native,journal,binding,call,compiler,descriptor=owner ++ directory=tmp_path/'separate-owner-custody';directory.mkdir(mode=0o700) ++ custody=FixtureRecoveryCustody(directory) ++ native=NativeFixtureAdapter(compiler,native.plan,journal,binding,native.admission,recovery_custody=custody) ++ return native,journal,binding,call,custody ++ ++ ++def crash(native,journal,binding,stage='issued'): ++ pid=os.fork() ++ if pid==0: ++ try: ++ # The backend belongs to the independent parent, so this child cannot ++ # waitpid it. Parent retains the actual Popen and checks it on recovery. ++ native.admission.server_process.poll=lambda:None ++ native.journal=Journal(journal.directory) ++ with native.journal.locked(): ++ admit(native,native.journal,binding) ++ if stage=='mint-response-lost': ++ original=native._http ++ def lost(method,path,token,body=b''): ++ result=original(method,path,token,body) ++ if path=='auth/token/create':os._exit(73) ++ return result ++ native._http=lost ++ native.materialize_grant() ++ native.journal.transition(binding,'grant-issued','applying') ++ if stage=='business-response-lost': ++ native.metadata_call(0) ++ original=native._http ++ def lost(method,path,token,body=b''): ++ result=original(method,path,token,body) ++ if method=='PUT':os._exit(73) ++ return result ++ native._http=lost ++ native.metadata_call(1) ++ if stage=='cleanup-response-lost': ++ from task_execution_cleanup import run_cleanup ++ original=native._http ++ def lost(method,path,token,body=b''): ++ result=original(method,path,token,body) ++ if path=='auth/token/revoke-self':os._exit(73) ++ return result ++ native._http=lost ++ run_cleanup(native.journal,binding,native) ++ os._exit(73) ++ except BaseException:os._exit(74) ++ _,status=os.waitpid(pid,0) ++ assert os.waitstatus_to_exitcode(status)==73 ++ # Original admission never appears in the independent parent or reopened DB. ++ assert native.task is None ++ assert journal._admission is None and journal._consumed_admission is None ++ ++ ++def permit(native,custody,binding,**kwargs): ++ return custody.admit(binding,owner_token=kwargs.pop('owner_token',native.admission.owner_token), ++ server_process=native.admission.server_process,**kwargs) ++ ++ ++@pytest.mark.parametrize('stage',['issued','business-response-lost','cleanup-response-lost']) ++def test_process_loss_then_independent_cleanup_never_resumes_business(owner,tmp_path,stage,monkeypatch,request): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ crash(native,journal,binding,stage) ++ journal=Journal(journal.directory) ++ request.addfinalizer(journal.close) ++ native.journal=journal ++ private=custody.read(binding) ++ assert private['phase']=='issued' and private['task_accessor'] ++ target=native.compiler.before_call(native.plan,1).path ++ before=call('GET',target) ++ with journal.locked(): ++ with pytest.raises(Refused):journal.reserve(binding,native.cleanup) ++ with pytest.raises(Refused):native.materialize_grant() ++ with pytest.raises(Refused):native.metadata_call(0) ++ with pytest.raises(Refused):native.revoke_task_token(native.cleanup[0].call_digest) ++ admitted=permit(native,custody,binding) ++ import task_execution_recovery as recovery_module ++ sent=[] ++ original_request=recovery_module.http.client.HTTPConnection.request ++ def tracked(connection,method,url,*args,**kwargs): ++ sent.append((method,url)) # metadata only; never capture body/headers ++ return original_request(connection,method,url,*args,**kwargs) ++ with monkeypatch.context() as patch: ++ patch.setattr(recovery_module.http.client.HTTPConnection,'request',tracked) ++ result=recover_fixture_cleanup(journal,admitted) ++ assert all((method,path) in { ++ ('POST','/v1/auth/token/revoke-accessor'),('POST','/v1/auth/token/lookup-accessor'), ++ ('GET','/v1/sys/policies/acl/'+native.policy_name), ++ ('DELETE','/v1/sys/policies/acl/'+native.policy_name)} for method,path in sent) ++ assert result['confirmed'] ++ for accessor in [private['task_accessor'],private['identity_accessor']]: ++ assert call('POST','auth/token/lookup-accessor',{'accessor':accessor})[0]==400 ++ assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404 ++ after=call('GET',target) ++ assert after[0]==before[0] and after[1].get('data')==before[1].get('data') # request IDs vary ++ assert journal.observe(binding)['dispatch_allowed'] is False ++ if stage!='issued':assert journal.observe(binding)['external_outcome_unknown'] ++ if stage=='cleanup-response-lost': ++ original=json.loads(journal.db.execute('SELECT original FROM recovery_attempts').fetchone()[0]) ++ assert original[0]['outcome']=='pending' ++ assert journal.db.execute("SELECT COUNT(*) FROM cleanup_calls WHERE outcome='pending'").fetchone()[0]==1 ++ with pytest.raises(Refused):recover_fixture_cleanup(journal,admitted) ++ public=(journal.directory/'journal.sqlite').read_bytes() ++ for value in [private['task_accessor'],private['identity_accessor'],native.admission.owner_token]: ++ assert value.encode() not in public ++ assert 'client_token' not in json.dumps(private) ++ assert 'token' not in repr(admitted) and private['task_accessor'] not in repr(admitted) ++ ++ ++def test_process_loss_before_handle_commit_preserves_unknown_mint_debt(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ crash(native,journal,binding,'mint-response-lost') ++ assert custody.read(binding)['phase']=='mint-intent' ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] ++ assert [x['outcome'] for x in result['obligations']]==['owed','confirmed','confirmed'] ++ assert journal.observe(binding)['external_outcome_unknown'] ++ assert journal.db.execute('SELECT confirmed FROM recovery_results ORDER BY obligation').fetchall()==[(0,),(1,),(1,)] ++ ++ ++def test_revoked_expired_wrong_revision_and_public_flag_refuse_before_calls(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ p=permit(native,custody,binding) ++ custody.revoke(p) ++ for bad in [p,replace(permit(native,custody,binding),deadline=time.monotonic()-1), ++ replace(permit(native,custody,binding),private_revision_digest='0'*64),{'verified':True}]: ++ with pytest.raises(Refused):recover_fixture_cleanup(journal,bad) ++ assert call('GET','sys/policies/acl/'+native.policy_name)[0]==200 ++ assert journal.cleanup_status(binding)['obligations'][0]['outcome']=='owed' ++ ++ ++def test_revoked_recovery_credential_cannot_fake_cleanup_success(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ status,data=call('POST','auth/token/create',{'policies':['root'],'ttl':'30s'}) ++ assert status==200 ++ token=data['auth']['client_token'] ++ assert call('POST','auth/token/revoke',{'token':token})[0] in {200,204} ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding,owner_token=token)) ++ assert not result['confirmed'] and all(x['outcome']=='owed' for x in result['obligations']) ++ assert call('GET','sys/policies/acl/'+native.policy_name)[0]==200 ++ # A separately admitted owner attempt can recover after the failed attempt; ++ # neither the failed attempt nor its receipt grants an automatic retry. ++ assert recover_fixture_cleanup(journal,permit(native,custody,binding))['confirmed'] ++ assert journal.db.execute('SELECT COUNT(*) FROM recovery_attempts').fetchone()[0]==2 ++ ++ ++def test_policy_name_reuse_never_deletes_different_policy(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ replacement='path "unrelated" { capabilities = ["read"] }' ++ assert call('PUT','sys/policies/acl/'+native.policy_name,{'policy':replacement})[0] in {200,204} ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] ++ assert [x['outcome'] for x in result['obligations']]==['confirmed','confirmed','owed'] ++ assert call('GET','sys/policies/acl/'+native.policy_name)[1]['data']['policy']==replacement ++ ++ ++def test_private_custody_permissions_and_identity_binding_fail_closed(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ path=custody._path(binding);path.chmod(0o644) ++ with pytest.raises(Refused):custody.read(binding) ++ path.chmod(0o600) ++ data=json.loads(path.read_bytes());data['binding']['operation_id']='other' ++ path.write_text(json.dumps(data)) ++ with pytest.raises(Refused):custody.read(binding) ++ ++ ++def test_lost_recovery_response_stays_unknown_until_new_independent_attempt(owner,tmp_path,monkeypatch): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ import task_execution_recovery as recovery_module ++ original=recovery_module.http.client.HTTPConnection.getresponse ++ once=[True] ++ def lost(connection): ++ response=original(connection) ++ if once[0]: ++ once[0]=False ++ response.close() ++ raise Refused('synthetic lost recovery response after real revoke') ++ return response ++ first=permit(native,custody,binding) ++ with monkeypatch.context() as patch: ++ patch.setattr(recovery_module.http.client.HTTPConnection,'getresponse',lost) ++ assert not recover_fixture_cleanup(journal,first)['confirmed'] ++ assert journal.observe(binding)['external_outcome_unknown'] ++ with pytest.raises(Refused):recover_fixture_cleanup(journal,first) ++ assert recover_fixture_cleanup(journal,permit(native,custody,binding))['confirmed'] ++ # Reconciliation does not erase the historic uncertain invocation. ++ assert journal.observe(binding)['external_outcome_unknown'] ++ assert journal.db.execute("SELECT COUNT(*) FROM recovery_calls WHERE outcome='pending'").fetchone()[0]==1 +diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md +index 8b09305..1b3a079 100644 +--- a/workplans/RPF-WP-0035-credential-lane-implementation.md ++++ b/workplans/RPF-WP-0035-credential-lane-implementation.md +@@ -1290,3 +1290,12 @@ CAS records consumed/unknown with zero native effects. Exact dependency/source + hashes, tests and remaining gates are retained with the source package and in + docs/task-execution-mediator-seam.md. No real grant/custody, policy default, owner + sync or native production acceptance is performed; T12 stays progress. ++ ++2026-10-16 T12 recovery successor source preparation: optional independently ++installed fixture custody durably retains operation-bound private accessors and ++issuance checkpoints. Fresh cleanup-only owner permits can reconcile after actual ++executor process death using fixed revoke/readback and policy delete/readback, ++without restoring business/consume admission. Separate durable attempt evidence ++retains original unknown outcomes. Lost token-create response remains unresolved ++when no accessor was committed. Source-only process-loss fixtures are not native ++production custody or recovery-role admission; T12 remains progress. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r2.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r2.patch new file mode 100644 index 00000000..6627cab2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/implementation-r2.patch @@ -0,0 +1,861 @@ +diff --git a/docs/task-execution-native-fixture.md b/docs/task-execution-native-fixture.md +index 1dd6b91..f4ff070 100644 +--- a/docs/task-execution-native-fixture.md ++++ b/docs/task-execution-native-fixture.md +@@ -121,3 +121,87 @@ 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. ++ ++ ++## Independent cleanup recovery source successor ++ ++`task_execution_recovery.py` adds separately installed fixture-only owner custody ++and fresh recovery permits. Optional `recovery_custody` on NativeFixtureAdapter ++persists the original binding/cleanup commitments, exact operation policy, backend ++port and identity accessor before any grant effect; it commits mint intent before ++token creation and task accessor immediately after a valid issued response. ++Bearer credentials are never persisted. The custody directory is separate from ++the public journal, owner-only 0700/0600, bounded at 64KiB, no-follow, with atomic ++replacement and fsync. It is real process-independent persistence for synthetic ++fixtures. It does not isolate same-UID hostile execution, encrypt at rest, select ++production storage or establish a production secret policy. Those are explicit ++native custody/deployment gates, not supplied by Python types or file modes. ++ ++The independent owner reads its private custody revision and installs a fresh ++bounded permit, current owner credential and retained live disposable Popen. ++Permit revocation is durably owner-recorded. The recovery runner has no mint, ++consume, policy-apply or business method. It dispatches only fixed exact-accessor ++revoke/readback and exact `task-exec-` policy read/delete/readback. ++A differently recreated policy is never deleted. Backend admission remains the ++same loopback-only owner-spawned dev process; no estate backend can be selected. ++Accessor-based recovery is an independently admitted alternate cleanup projection, ++not a reinterpretation of the original revoke-self call digest. Production needs ++exact owner-reviewed recovery scope; original business grant does not imply it. ++ ++Fresh recovery holds the journal process lock but never sets business admission ++or consumed-admission fields. Separate recovery attempt/call/result tables retain ++original cleanup statuses before reconciliation, write native intent before calls, ++and preserve failed/pending outcomes. Proven cleanup alone updates the exact ++original cleanup obligation to confirmed. Business state and original pending ++call uncertainty remain unchanged. Reusing the original approval, continuing ++metadata calls and reusing a recovery permit refuse. Failure needs another ++independent owner permit; there is no automatic retry. Expired/revoked permits ++or changed private custody revision refuse before calls. ++ ++A token-create response lost before accessor custody remains unresolved token ++cleanup debt: policy deletion/identity revocation can be independently confirmed, ++but cannot imply task-token revocation. Neither expiry guessed from wall clock ++nor policy absence fabricates token absence. A production successor must admit ++an independent exact ambiguous-issuance reconciliation mechanism or prove bounded ++expiry from owner backend evidence. No such new grant is implemented here. ++ ++`test_task_execution_recovery.py` forks a real executor and terminates it without ++finally cleanup after issue, business effect/lost response and revoke/lost response. ++The independent parent, holding the backend and separately installed custody, ++recovers specific obligations and verifies actual token/policy absence. Additional ++cases cover lost-mint response, revoked credential, permit revocation/expiry, ++revision drift, namespace policy-name reuse and private-file/binding refusals. ++This is source fixture proof only. Native custody, isolation, exact recovery-role ++permissions, retention/encryption, clock admission, audit delivery and human scope ++approval remain missing. No generic broker, production reader, grant or CLI is added. ++ ++ ++### Recovery R2 exact absence and authenticated capsule refinement ++ ++R1 overclaimed policy absence for arbitrary 404 bodies. R2 requires exactly ++`{"errors":[]}` for both initial GET and post-DELETE GET. Other 404 bodies, ++permission denials, extra data, malformed/duplicate-key/nonfinite JSON and failed ++readback cannot confirm policy cleanup. A successful DELETE status alone cannot ++confirm absence. Recovery native policy comparison admits precisely one optional ++terminal LF, matching the retained OpenBao HCL source/readback convention; spaces, ++tabs, CR, extra terminal LF and any other byte change are refused. This is a ++narrow declared normalization, not full whitespace or semantic equivalence. ++ ++Private fixture capsules now carry HMAC-SHA256 authentication under an exactly ++32-byte integrity key supplied independently by the recovery owner. The key is ++never stored in the capsule or public journal, and is not a bearer credential. ++The fixture owner retains this key independently across executor death; a fresh ++owner custody object must supply the same key. Wrong-key or damaged accessor/HCL ++bytes refuse before admission. This is integrity protection, not encryption or ++same-UID isolation; production key custody, rotation/retention and recovery actor ++selection remain explicitly unadmitted. ++ ++After authentication the reader enforces exact top-level and nested binding/ ++cleanup key sets and types, validated Binding/Cleanup formats and the ordered ++source-derived cleanup_specs operation digests. Duplicate JSON keys, nonfinite ++numbers, malformed phase/revision combinations and type-coercible values refuse. ++The reader bounds bytes, rejects indirect/nonprivate files and detects stat/size ++changes during its single read. A valid older prepared capsule still cannot ++prove a task token absent when durable journal state/calls reached issuance: ++that remains unknown cleanup debt. Authentication alone is not anti-rollback. ++No business or consume admission is restored and no production scope is granted. +diff --git a/scripts/task_execution_journal.py b/scripts/task_execution_journal.py +index 8f1b9a2..1eaa583 100644 +--- a/scripts/task_execution_journal.py ++++ b/scripts/task_execution_journal.py +@@ -104,6 +104,9 @@ class Journal: + 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.db.execute("CREATE TABLE IF NOT EXISTS cleanup_calls (operation TEXT NOT NULL, obligation INTEGER NOT NULL, step INTEGER NOT NULL, call_digest TEXT NOT NULL, outcome TEXT NOT NULL, evidence TEXT, PRIMARY KEY(operation,obligation,step))") ++ self.db.execute("CREATE TABLE IF NOT EXISTS recovery_attempts (attempt TEXT PRIMARY KEY, operation TEXT NOT NULL, original TEXT NOT NULL)") ++ self.db.execute("CREATE TABLE IF NOT EXISTS recovery_calls (attempt TEXT NOT NULL, obligation INTEGER NOT NULL, step INTEGER NOT NULL, digest TEXT NOT NULL, outcome TEXT NOT NULL, evidence TEXT, PRIMARY KEY(attempt,obligation,step))") ++ self.db.execute("CREATE TABLE IF NOT EXISTS recovery_results (attempt TEXT NOT NULL, obligation INTEGER NOT NULL, confirmed INTEGER NOT NULL, evidence TEXT NOT NULL, PRIMARY KEY(attempt,obligation))") + self._held = False + self._admission = None + self._consumed_admission = None +@@ -228,6 +231,7 @@ class Journal: + 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] + pending += self.db.execute("SELECT COUNT(*) FROM cleanup_calls WHERE operation=? AND outcome='pending'", (binding.operation_id,)).fetchone()[0] ++ pending += self.db.execute("SELECT COUNT(*) FROM recovery_calls AS c JOIN recovery_attempts AS a ON c.attempt=a.attempt WHERE a.operation=? AND c.outcome='pending'", (binding.operation_id,)).fetchone()[0] + return {"state": row[3], "evidence_digest": row[4], "dispatch_allowed": False, + "external_outcome_unknown": bool(pending)} + +diff --git a/scripts/task_execution_native_fixture.py b/scripts/task_execution_native_fixture.py +index caa7da1..31a822b 100644 +--- a/scripts/task_execution_native_fixture.py ++++ b/scripts/task_execution_native_fixture.py +@@ -3,7 +3,8 @@ + This adapter deliberately admits only a disposable loopback fixture. It supplies + real native primitive coverage while production grant schema, identity/consume and + origin/source admission are unfinished. It cannot open an estate backend. Owner +-credentials/handles stay in memory; no subprocess, redirects or ambient proxy. ++bearer credentials stay in memory; optional owner-private fixture custody persists ++accessors for independently admitted cleanup recovery; no redirects or ambient proxy. + Python privacy is not credential isolation; production needs the owner runtime. + """ + from __future__ import annotations +@@ -41,7 +42,7 @@ class FixtureAdmission: + + class NativeFixtureAdapter: + def __init__(self, compiler: Compiler, plan: Plan, journal: Journal, +- binding: Binding, admission: FixtureAdmission): ++ binding: Binding, admission: FixtureAdmission, *, recovery_custody=None): + if type(admission) is not FixtureAdmission or type(admission.descriptor) is not bytes: + raise Refused('independent fixture admission required') + self.compiler,self.plan,self.journal,self.binding=compiler,plan,journal,binding +@@ -100,6 +101,12 @@ class NativeFixtureAdapter: + 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 + self._cleanup_steps={item.kind:0 for item in self.cleanup} ++ self.recovery_custody=recovery_custody ++ if recovery_custody is not None: ++ from task_execution_recovery import FixtureRecoveryCustody ++ if type(recovery_custody) is not FixtureRecoveryCustody or journal.observe(binding) is not None: ++ raise Refused('fresh independent fixture recovery custody required') ++ recovery_custody.prepare(binding,self.cleanup,self.port,self.hcl,admission.identity_session.accessor) + + def _live(self): + if not self.journal._held or self.journal._admission!=self.binding or self.journal._consumed_admission!=self.binding: +@@ -177,6 +184,8 @@ class NativeFixtureAdapter: + 'type':'service','display_name':'task-exec-'+self.binding.operation_id,'no_parent':False} + if body!=self.meta['execution_grant']['openbao']['token_create_body']: + raise Refused('native token body differs from bound owner projection') ++ if self.recovery_custody is not None: ++ self.recovery_custody.checkpoint(self.binding,'mint-intent') + self._mint_attempted=True + status,data=self._business('POST','auth/token/create',root,encoded(body));self._ok(status,data,{200}) + auth=data.get('auth',{}) +@@ -185,6 +194,8 @@ class NativeFixtureAdapter: + # Retain handles before validation so finally cleanup can revoke even a + # malformed overbroad issue response. Never return token-bearing metadata. + self.task=PrivateSession(auth['client_token'],auth['accessor']) ++ if self.recovery_custody is not None: ++ self.recovery_custody.checkpoint(self.binding,'issued',self.task.accessor) + status,data=self._business('POST','auth/token/lookup',root,encoded({'token':self.task.token}));self._ok(status,data,{200}) + info=data.get('data',{}) + if (info.get('policies')!=[self.policy_name] or info.get('identity_policies',[]) or info.get('renewable') is not False +diff --git a/scripts/task_execution_recovery.py b/scripts/task_execution_recovery.py +new file mode 100644 +index 0000000..ab8b8cb +--- /dev/null ++++ b/scripts/task_execution_recovery.py +@@ -0,0 +1,336 @@ ++"""Owner-only cleanup recovery for the disposable native fixture. ++ ++Private, separately installed custody persists accessors, never bearer tokens. ++This is a fixture implementation, not production custody/isolation admission. ++The recovery owner supplies a fresh capability and credential independently of ++public request/journal data. No business adapter or resumed admission is built. ++""" ++from __future__ import annotations ++from dataclasses import asdict, dataclass, field ++import hashlib ++import hmac ++import http.client ++import json ++import os ++from pathlib import Path ++import secrets ++import subprocess ++import time ++import uuid ++ ++from task_execution_journal import Binding, Cleanup, Journal, Refused ++from task_execution_plan import cleanup_specs ++ ++ ++def _encoded(value): ++ return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode() ++ ++ ++def _digest(value): ++ return hashlib.sha256(_encoded(value)).hexdigest() ++ ++ ++def _json_unique(pairs): ++ result={} ++ for key,value in pairs: ++ if key in result: raise Refused('duplicate private custody field') ++ result[key]=value ++ return result ++ ++ ++def _invalid_constant(value): ++ raise Refused('nonfinite private custody value') ++ ++ ++def _policy_absent(status, response): ++ # Exactly the native OpenBao empty-errors 404. Denial/malformed/error-shaped ++ # bodies never prove absence, including after a successful delete response. ++ return (status==404 and type(response) is dict and set(response)=={'errors'} ++ and type(response['errors']) is list and not response['errors']) ++ ++ ++def _same_policy(actual, expected): ++ # OpenBao may omit one terminal LF from HCL source. Only that equivalence ++ # is admitted: spaces, tabs, CR and additional blank lines remain meaningful. ++ if type(actual) is not str or type(expected) is not str: return False ++ strip_one=lambda value:value[:-1] if value.endswith('\n') else value ++ return strip_one(actual)==strip_one(expected) ++ ++ ++class FixtureRecoveryCustody: ++ """Independent owner-private persistent fixture custody; no public API. ++ ++ Mode/ownership and no-follow checks protect against accidental exposure. ++ Same-UID hostile code is NOT isolated. Production must supply independently ++ enforced owner storage/executor authority; this implementation refuses estate ++ backends and is never evidence of a production grant. ++ """ ++ def __init__(self, directory: Path, *, integrity_key: bytes): ++ if type(integrity_key) is not bytes or len(integrity_key)!=32: ++ raise Refused('independent owner capsule integrity key required') ++ self._integrity_key=integrity_key ++ self.directory = directory ++ if directory.is_symlink() or directory.absolute() != directory.resolve(): ++ raise Refused('indirect recovery custody directory') ++ Journal._private(directory.stat(), directory=True) ++ ++ def _path(self, binding): ++ if type(binding) is not Binding: raise Refused('exact recovery binding required') ++ binding.validate() ++ return self.directory / (binding.operation_id + '.private.json') ++ ++ def _sync(self): ++ fd = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) ++ try: os.fsync(fd) ++ finally: os.close(fd) ++ ++ def read(self, binding): ++ try: fd = os.open(self._path(binding), os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) ++ except OSError: raise Refused('private custody unavailable') from None ++ try: ++ before=os.fstat(fd) ++ Journal._private(before) ++ with os.fdopen(fd, 'rb', closefd=False) as stream: raw = stream.read(65537) ++ after=os.fstat(fd) ++ identity=lambda value:(value.st_dev,value.st_ino,value.st_size,value.st_mtime_ns,value.st_ctime_ns,value.st_mode,value.st_nlink) ++ if identity(before)!=identity(after) or len(raw)!=before.st_size: ++ raise Refused('private custody changed during read') ++ finally: os.close(fd) ++ if len(raw) > 65536: raise Refused('oversize private custody record') ++ try: capsule = json.loads(raw,object_pairs_hook=_json_unique,parse_constant=_invalid_constant) ++ except Exception: raise Refused('invalid private custody record') from None ++ if (type(capsule) is not dict or set(capsule)!={'payload','mac'} or type(capsule['mac']) is not str ++ or len(capsule['mac'])!=64 or any(x not in '0123456789abcdef' for x in capsule['mac'])): ++ raise Refused('invalid authenticated private capsule') ++ expected_mac=hmac.new(self._integrity_key,_encoded(capsule['payload']),hashlib.sha256).hexdigest() ++ if not hmac.compare_digest(capsule['mac'],expected_mac): ++ raise Refused('private capsule integrity mismatch') ++ value=capsule['payload'] ++ expected={'binding','cleanup','port','policy_hcl','identity_accessor','task_accessor','phase','revision'} ++ if type(value) is not dict or set(value)!=expected: ++ raise Refused('invalid private custody schema') ++ stored=value['binding'] ++ if (type(stored) is not dict or set(stored)!=set(asdict(binding)) ++ or any(type(v) is not str for v in stored.values())): ++ raise Refused('invalid private custody binding schema') ++ restored=Binding(**stored) ++ restored.validate() ++ if asdict(restored)!=asdict(binding): raise Refused('private custody binding mismatch') ++ if (type(value['port']) is not int or not 1<=value['port']<=65535 ++ or type(value['policy_hcl']) is not str or not value['policy_hcl'] ++ or type(value['identity_accessor']) is not str or not value['identity_accessor'] ++ or type(value['phase']) is not str or value['phase'] not in {'prepared','mint-intent','issued'} ++ or type(value['revision']) is not int or value['revision']!={'prepared':0,'mint-intent':1,'issued':2}[value['phase']] ++ or (value['phase']=='issued' and (type(value['task_accessor']) is not str or not value['task_accessor'])) ++ or (value['phase']!='issued' and value['task_accessor'] is not None) ++ or type(value['cleanup']) is not list or len(value['cleanup'])!=3): ++ raise Refused('invalid private custody schema') ++ for item in value['cleanup']: ++ if type(item) is not dict or set(item)!={'kind','call_digest'}: ++ raise Refused('invalid private cleanup schema') ++ Cleanup(**item).validate() ++ expected_cleanup=[{'kind':item['kind'],'call_digest':item['call_digest']} ++ for item in cleanup_specs(binding.operation_id)] ++ if value['cleanup']!=expected_cleanup: ++ raise Refused('private cleanup differs from deterministic owner commitment') ++ return value ++ ++ def _write(self, binding, value, initial=False): ++ capsule={'payload':value,'mac':hmac.new(self._integrity_key,_encoded(value),hashlib.sha256).hexdigest()} ++ raw = _encoded(capsule) ++ if len(raw) > 65536: raise Refused('oversize private custody record') ++ path = self._path(binding) ++ if initial: ++ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) ++ else: ++ # Existing owner-private record checked before replacement. Unique ++ # temp + fsync + replace + directory fsync never truncates old state. ++ self.read(binding) ++ tmp = self.directory / ('.pending-' + secrets.token_hex(16)) ++ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) ++ try: ++ with os.fdopen(fd, 'wb', closefd=False) as stream: ++ stream.write(raw); stream.flush(); os.fsync(fd) ++ finally: os.close(fd) ++ if not initial: os.replace(tmp, path) ++ self._sync() ++ ++ def prepare(self, binding, cleanup, port, policy_hcl, identity_accessor): ++ value = {'binding': asdict(binding), 'cleanup': [asdict(x) for x in cleanup], ++ 'port': port, 'policy_hcl': policy_hcl, ++ 'identity_accessor': identity_accessor, 'task_accessor': None, ++ 'phase': 'prepared', 'revision': 0} ++ self._write(binding, value, initial=True) ++ ++ def checkpoint(self, binding, phase, task_accessor=None): ++ value = self.read(binding) ++ if (value['phase'], phase) not in {('prepared', 'mint-intent'), ('mint-intent', 'issued')}: ++ raise Refused('invalid protected recovery checkpoint') ++ if phase == 'issued' and (type(task_accessor) is not str or not task_accessor): ++ raise Refused('protected issued accessor required') ++ value.update(phase=phase, task_accessor=task_accessor, revision=value['revision'] + 1) ++ self._write(binding, value) ++ ++ def revoke(self, permit): ++ if type(permit) is not FixtureRecoveryPermit or permit.custody is not self: ++ raise Refused('exact owner recovery permit required') ++ uuid.UUID(permit.attempt_id) ++ path=self.directory / (permit.attempt_id+'.revoked') ++ fd=os.open(path,os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,0o600) ++ try:os.fsync(fd) ++ finally:os.close(fd) ++ self._sync() ++ ++ def admit(self, binding, *, owner_token, server_process, valid_seconds=30): ++ """Trusted recovery-owner action, not deserialization of caller flags. ++ ++ Fresh admission seals the current private custody revision. The owner ++ supplies its credential and retained backend process separately. Neither ++ token nor private capsule digest is emitted to the public journal. ++ """ ++ value = self.read(binding) ++ if type(valid_seconds) is not int or not 1 <= valid_seconds <= 60: ++ raise Refused('bounded recovery admission required') ++ if type(owner_token) is not str or not owner_token: ++ raise Refused('independent recovery owner credential required') ++ return FixtureRecoveryPermit(str(uuid.uuid4()), binding, self, _digest(value), ++ owner_token, server_process, time.monotonic()+valid_seconds) ++ ++ ++@dataclass(frozen=True, repr=False) ++class FixtureRecoveryPermit: ++ attempt_id: str ++ binding: Binding ++ custody: FixtureRecoveryCustody ++ private_revision_digest: str = field(repr=False) ++ owner_token: str = field(repr=False) ++ server_process: subprocess.Popen = field(repr=False) ++ deadline: float ++ ++ ++class _Recorder: ++ def __init__(self, journal, permit, value): ++ self.journal, self.permit = journal, permit ++ b = permit.binding ++ if journal._admission is not None or journal._consumed_admission is not None: ++ raise Refused('recovery cannot share a business admission') ++ if journal.observe(b) is None: raise Refused('original operation absent') ++ current = journal.cleanup_status(b)['obligations'] ++ if [(x['kind'], x['call_digest']) for x in current] != [(x['kind'], x['call_digest']) for x in value['cleanup']]: ++ raise Refused('original cleanup commitment mismatch') ++ if all(x['outcome'] == 'confirmed' for x in current): ++ raise Refused('cleanup already confirmed') ++ self.current = current ++ # An authenticated older prepared capsule must not erase later issuance ++ # uncertainty. Without an issued accessor, any native call or state at ++ # the grant boundary makes absence unprovable. Recovery is conservative. ++ self.no_mint_proved=(journal.observe(b)['state'] in {'reserved','consume-confirmed','authenticating'} ++ and journal.db.execute('SELECT COUNT(*) FROM calls WHERE operation=?',(b.operation_id,)).fetchone()[0]==0) ++ db = journal.db ++ try: db.execute('INSERT INTO recovery_attempts VALUES (?,?,?)', (permit.attempt_id,b.operation_id,json.dumps(current,sort_keys=True))) ++ except Exception: raise Refused('recovery admission already used') from None ++ journal._sync_directory() ++ ++ def before(self, ordinal, step, method, path): ++ digest = _digest({'kind': self.current[ordinal]['kind'], 'commitment':self.current[ordinal]['call_digest'], 'method':method,'path':path,'step':step}) ++ self.journal.db.execute("INSERT INTO recovery_calls VALUES (?,?,?,?,'pending',NULL)", (self.permit.attempt_id,ordinal,step,digest)) ++ self.journal._sync_directory() ++ return digest ++ ++ def after(self, ordinal, step, status): ++ self.journal.db.execute("UPDATE recovery_calls SET outcome='complete', evidence=? WHERE attempt=? AND obligation=? AND step=? AND outcome='pending'", (_digest({'http_status':status}),self.permit.attempt_id,ordinal,step)) ++ self.journal._sync_directory() ++ ++ def finish(self, ordinal, confirmed): ++ evidence = _digest({'attempt':self.permit.attempt_id,'obligation':ordinal,'confirmed':confirmed}) ++ db = self.journal.db ++ try: ++ db.execute('BEGIN IMMEDIATE') ++ pending = db.execute("SELECT COUNT(*) FROM recovery_calls WHERE attempt=? AND obligation=? AND outcome='pending'",(self.permit.attempt_id,ordinal)).fetchone()[0] ++ if confirmed and pending: raise Refused('pending recovery cannot confirm') ++ db.execute('INSERT INTO recovery_results VALUES (?,?,?,?)',(self.permit.attempt_id,ordinal,int(confirmed),evidence)) ++ if confirmed: ++ item=self.current[ordinal] ++ db.execute("UPDATE cleanup SET outcome='confirmed', evidence=? WHERE operation=? AND ordinal=? AND kind=? AND call_digest=?",(evidence,self.permit.binding.operation_id,ordinal,item['kind'],item['call_digest'])) ++ db.execute('COMMIT');self.journal._sync_directory() ++ except BaseException: ++ if db.in_transaction: db.execute('ROLLBACK') ++ raise ++ ++ ++def recover_fixture_cleanup(journal: Journal, permit: FixtureRecoveryPermit): ++ """Fresh independent owner admission: fixed cleanup only, no business retry. ++ ++ Each attempt preserves original pending/unconfirmed rows in recovery_attempts ++ and native pending call rows unchanged. A fresh owner permit is required for ++ another attempt after failure; never autonomously retry uncertain mutations. ++ """ ++ if type(permit) is not FixtureRecoveryPermit or type(permit.custody) is not FixtureRecoveryCustody: ++ raise Refused('independent typed recovery admission required') ++ if str(uuid.UUID(permit.attempt_id)) != permit.attempt_id: ++ raise Refused('invalid recovery attempt identity') ++ value = permit.custody.read(permit.binding) ++ if _digest(value) != permit.private_revision_digest: ++ raise Refused('private custody revision changed') ++ proc=permit.server_process ++ if (type(proc) is not subprocess.Popen or proc.poll() is not None or type(proc.args) is not list ++ or '-dev' not in proc.args or '-dev-no-store-token' not in proc.args ++ or '-dev-listen-address=127.0.0.1:'+str(value['port']) not in proc.args): ++ raise Refused('same live disposable backend required') ++ def fresh(): ++ if (permit.custody.directory / (permit.attempt_id+'.revoked')).exists(): ++ raise Refused('recovery permit revoked') ++ if time.monotonic() >= permit.deadline: raise Refused('recovery admission expired') ++ if proc.poll() is not None: raise Refused('disposable backend lost') ++ if _digest(permit.custody.read(permit.binding)) != permit.private_revision_digest: ++ raise Refused('private custody changed during recovery') ++ fresh() ++ with journal.locked(): ++ record = _Recorder(journal,permit,value) ++ for ordinal,item in enumerate(record.current): ++ if item['outcome']=='confirmed': continue ++ step=0 ++ def call(method,path,body=None): ++ nonlocal step ++ fresh(); index=step; step+=1 ++ record.before(ordinal,index,method,path) ++ conn=http.client.HTTPConnection('127.0.0.1',value['port'],timeout=2) ++ try: ++ conn.request(method,'/v1/'+path,body=_encoded(body) if body is not None else None, ++ headers={'X-Vault-Token':permit.owner_token,'Content-Type':'application/json'}) ++ response=conn.getresponse();raw=response.read(1048577) ++ if len(raw)>1048576: raise Refused('oversize recovery response') ++ data=json.loads(raw,object_pairs_hook=_json_unique,parse_constant=_invalid_constant) if raw else {} ++ if type(data) is not dict: raise Refused('invalid recovery response') ++ status=response.status ++ except Exception: raise Refused('recovery transport or response unconfirmed') from None ++ finally:conn.close() ++ record.after(ordinal,index,status) ++ return status,data ++ confirmed=False ++ try: ++ if item['kind'] in {'task-token-revoke','identity-token-revoke'}: ++ accessor=value['task_accessor' if ordinal==0 else 'identity_accessor'] ++ if accessor is None: ++ confirmed=ordinal==0 and value['phase']=='prepared' and record.no_mint_proved ++ else: ++ status,data=call('POST','auth/token/revoke-accessor',{'accessor':accessor}) ++ # Revoke is idempotent for an already absent accessor; ++ # actual absence must still be independently read back. ++ if status in {200,204,400}: ++ status,data=call('POST','auth/token/lookup-accessor',{'accessor':accessor}) ++ confirmed=status==400 and data.get('errors') in (['invalid accessor'],['1 error occurred:\n\t* invalid accessor\n\n']) ++ elif item['kind']=='grant-policy-delete': ++ path='sys/policies/acl/task-exec-'+permit.binding.operation_id ++ status,data=call('GET',path) ++ if _policy_absent(status,data):confirmed=True ++ elif (status==200 and not data.get('errors') and type(data.get('data')) is dict ++ and _same_policy(data['data'].get('policy'),value['policy_hcl'])): ++ status,data=call('DELETE',path) ++ if status in {200,204}: ++ status,data=call('GET',path);confirmed=_policy_absent(status,data) ++ except Exception: ++ # No backend errors/response/handle hashes enter public evidence. ++ confirmed=False ++ record.finish(ordinal,confirmed) ++ return journal.cleanup_status(permit.binding) +diff --git a/tests/test_task_execution_recovery.py b/tests/test_task_execution_recovery.py +new file mode 100644 +index 0000000..52b8b61 +--- /dev/null ++++ b/tests/test_task_execution_recovery.py +@@ -0,0 +1,320 @@ ++"""Actual executor process loss against independent disposable OpenBao.""" ++from dataclasses import replace ++import hashlib ++import json ++import os ++import secrets ++from pathlib import Path ++import sys ++import time ++ ++import pytest ++ ++sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts')) ++from test_task_execution_plan import compiler ++from test_task_execution_native_fixture import bao_dev, owner, admit ++from task_execution_journal import Journal, Refused ++from task_execution_native_fixture import NativeFixtureAdapter ++from task_execution_recovery import FixtureRecoveryCustody, recover_fixture_cleanup ++ ++ ++def protected(owner,tmp_path): ++ native,journal,binding,call,compiler,descriptor=owner ++ directory=tmp_path/'separate-owner-custody';directory.mkdir(mode=0o700) ++ custody=FixtureRecoveryCustody(directory,integrity_key=secrets.token_bytes(32)) ++ native=NativeFixtureAdapter(compiler,native.plan,journal,binding,native.admission,recovery_custody=custody) ++ return native,journal,binding,call,custody ++ ++ ++def crash(native,journal,binding,stage='issued'): ++ pid=os.fork() ++ if pid==0: ++ try: ++ # The backend belongs to the independent parent, so this child cannot ++ # waitpid it. Parent retains the actual Popen and checks it on recovery. ++ native.admission.server_process.poll=lambda:None ++ native.journal=Journal(journal.directory) ++ with native.journal.locked(): ++ admit(native,native.journal,binding) ++ if stage=='mint-response-lost': ++ original=native._http ++ def lost(method,path,token,body=b''): ++ result=original(method,path,token,body) ++ if path=='auth/token/create':os._exit(73) ++ return result ++ native._http=lost ++ native.materialize_grant() ++ native.journal.transition(binding,'grant-issued','applying') ++ if stage=='business-response-lost': ++ native.metadata_call(0) ++ original=native._http ++ def lost(method,path,token,body=b''): ++ result=original(method,path,token,body) ++ if method=='PUT':os._exit(73) ++ return result ++ native._http=lost ++ native.metadata_call(1) ++ if stage=='cleanup-response-lost': ++ from task_execution_cleanup import run_cleanup ++ original=native._http ++ def lost(method,path,token,body=b''): ++ result=original(method,path,token,body) ++ if path=='auth/token/revoke-self':os._exit(73) ++ return result ++ native._http=lost ++ run_cleanup(native.journal,binding,native) ++ os._exit(73) ++ except BaseException:os._exit(74) ++ _,status=os.waitpid(pid,0) ++ assert os.waitstatus_to_exitcode(status)==73 ++ # Original admission never appears in the independent parent or reopened DB. ++ assert native.task is None ++ assert journal._admission is None and journal._consumed_admission is None ++ ++ ++def permit(native,custody,binding,**kwargs): ++ return custody.admit(binding,owner_token=kwargs.pop('owner_token',native.admission.owner_token), ++ server_process=native.admission.server_process,**kwargs) ++ ++ ++@pytest.mark.parametrize('stage',['issued','business-response-lost','cleanup-response-lost']) ++def test_process_loss_then_independent_cleanup_never_resumes_business(owner,tmp_path,stage,monkeypatch,request): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ crash(native,journal,binding,stage) ++ journal=Journal(journal.directory) ++ request.addfinalizer(journal.close) ++ native.journal=journal ++ private=custody.read(binding) ++ assert private['phase']=='issued' and private['task_accessor'] ++ target=native.compiler.before_call(native.plan,1).path ++ before=call('GET',target) ++ with journal.locked(): ++ with pytest.raises(Refused):journal.reserve(binding,native.cleanup) ++ with pytest.raises(Refused):native.materialize_grant() ++ with pytest.raises(Refused):native.metadata_call(0) ++ with pytest.raises(Refused):native.revoke_task_token(native.cleanup[0].call_digest) ++ admitted=permit(native,custody,binding) ++ import task_execution_recovery as recovery_module ++ sent=[] ++ original_request=recovery_module.http.client.HTTPConnection.request ++ def tracked(connection,method,url,*args,**kwargs): ++ sent.append((method,url)) # metadata only; never capture body/headers ++ return original_request(connection,method,url,*args,**kwargs) ++ with monkeypatch.context() as patch: ++ patch.setattr(recovery_module.http.client.HTTPConnection,'request',tracked) ++ result=recover_fixture_cleanup(journal,admitted) ++ assert all((method,path) in { ++ ('POST','/v1/auth/token/revoke-accessor'),('POST','/v1/auth/token/lookup-accessor'), ++ ('GET','/v1/sys/policies/acl/'+native.policy_name), ++ ('DELETE','/v1/sys/policies/acl/'+native.policy_name)} for method,path in sent) ++ assert result['confirmed'] ++ for accessor in [private['task_accessor'],private['identity_accessor']]: ++ assert call('POST','auth/token/lookup-accessor',{'accessor':accessor})[0]==400 ++ assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404 ++ after=call('GET',target) ++ assert after[0]==before[0] and after[1].get('data')==before[1].get('data') # request IDs vary ++ assert journal.observe(binding)['dispatch_allowed'] is False ++ if stage!='issued':assert journal.observe(binding)['external_outcome_unknown'] ++ if stage=='cleanup-response-lost': ++ original=json.loads(journal.db.execute('SELECT original FROM recovery_attempts').fetchone()[0]) ++ assert original[0]['outcome']=='pending' ++ assert journal.db.execute("SELECT COUNT(*) FROM cleanup_calls WHERE outcome='pending'").fetchone()[0]==1 ++ with pytest.raises(Refused):recover_fixture_cleanup(journal,admitted) ++ public=(journal.directory/'journal.sqlite').read_bytes() ++ for value in [private['task_accessor'],private['identity_accessor'],native.admission.owner_token]: ++ assert value.encode() not in public ++ assert 'client_token' not in json.dumps(private) ++ assert 'token' not in repr(admitted) and private['task_accessor'] not in repr(admitted) ++ ++ ++def test_process_loss_before_handle_commit_preserves_unknown_mint_debt(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ crash(native,journal,binding,'mint-response-lost') ++ assert custody.read(binding)['phase']=='mint-intent' ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] ++ assert [x['outcome'] for x in result['obligations']]==['owed','confirmed','confirmed'] ++ assert journal.observe(binding)['external_outcome_unknown'] ++ assert journal.db.execute('SELECT confirmed FROM recovery_results ORDER BY obligation').fetchall()==[(0,),(1,),(1,)] ++ ++ ++def test_revoked_expired_wrong_revision_and_public_flag_refuse_before_calls(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ p=permit(native,custody,binding) ++ custody.revoke(p) ++ for bad in [p,replace(permit(native,custody,binding),deadline=time.monotonic()-1), ++ replace(permit(native,custody,binding),private_revision_digest='0'*64),{'verified':True}]: ++ with pytest.raises(Refused):recover_fixture_cleanup(journal,bad) ++ assert call('GET','sys/policies/acl/'+native.policy_name)[0]==200 ++ assert journal.cleanup_status(binding)['obligations'][0]['outcome']=='owed' ++ ++ ++def test_revoked_recovery_credential_cannot_fake_cleanup_success(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ status,data=call('POST','auth/token/create',{'policies':['root'],'ttl':'30s'}) ++ assert status==200 ++ token=data['auth']['client_token'] ++ assert call('POST','auth/token/revoke',{'token':token})[0] in {200,204} ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding,owner_token=token)) ++ assert not result['confirmed'] and all(x['outcome']=='owed' for x in result['obligations']) ++ assert call('GET','sys/policies/acl/'+native.policy_name)[0]==200 ++ # A separately admitted owner attempt can recover after the failed attempt; ++ # neither the failed attempt nor its receipt grants an automatic retry. ++ assert recover_fixture_cleanup(journal,permit(native,custody,binding))['confirmed'] ++ assert journal.db.execute('SELECT COUNT(*) FROM recovery_attempts').fetchone()[0]==2 ++ ++ ++def test_policy_name_reuse_never_deletes_different_policy(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ replacement='path "unrelated" { capabilities = ["read"] }' ++ assert call('PUT','sys/policies/acl/'+native.policy_name,{'policy':replacement})[0] in {200,204} ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] ++ assert [x['outcome'] for x in result['obligations']]==['confirmed','confirmed','owed'] ++ assert call('GET','sys/policies/acl/'+native.policy_name)[1]['data']['policy']==replacement ++ ++ ++def test_private_custody_permissions_and_identity_binding_fail_closed(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ path=custody._path(binding);path.chmod(0o644) ++ with pytest.raises(Refused):custody.read(binding) ++ path.chmod(0o600) ++ data=json.loads(path.read_bytes());data['payload']['binding']['operation_id']='other' ++ path.write_text(json.dumps(data)) ++ with pytest.raises(Refused):custody.read(binding) ++ ++ ++def test_lost_recovery_response_stays_unknown_until_new_independent_attempt(owner,tmp_path,monkeypatch): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ import task_execution_recovery as recovery_module ++ original=recovery_module.http.client.HTTPConnection.getresponse ++ once=[True] ++ def lost(connection): ++ response=original(connection) ++ if once[0]: ++ once[0]=False ++ response.close() ++ raise Refused('synthetic lost recovery response after real revoke') ++ return response ++ first=permit(native,custody,binding) ++ with monkeypatch.context() as patch: ++ patch.setattr(recovery_module.http.client.HTTPConnection,'getresponse',lost) ++ assert not recover_fixture_cleanup(journal,first)['confirmed'] ++ assert journal.observe(binding)['external_outcome_unknown'] ++ with pytest.raises(Refused):recover_fixture_cleanup(journal,first) ++ assert recover_fixture_cleanup(journal,permit(native,custody,binding))['confirmed'] ++ # Reconciliation does not erase the historic uncertain invocation. ++ assert journal.observe(binding)['external_outcome_unknown'] ++ assert journal.db.execute("SELECT COUNT(*) FROM recovery_calls WHERE outcome='pending'").fetchone()[0]==1 ++ ++ ++@pytest.fixture ++def capsule(tmp_path): ++ from task_execution_journal import Binding,Cleanup ++ from task_execution_plan import cleanup_specs ++ binding=Binding('SYNTHETIC','sha256:'+'a'*64,'12345678-1234-1234-1234-123456789abc','b'*64) ++ directory=tmp_path/'private-capsule';directory.mkdir(mode=0o700) ++ custody=FixtureRecoveryCustody(directory,integrity_key=secrets.token_bytes(32)) ++ cleanup=tuple(Cleanup(x['kind'],x['call_digest']) for x in cleanup_specs(binding.operation_id)) ++ custody.prepare(binding,cleanup,12345,'path "fixture" {}\n','synthetic-private-accessor') ++ return custody,binding ++ ++ ++@pytest.mark.parametrize('mutate',[ ++ lambda v:v.update(extra=True),lambda v:v.pop('phase'),lambda v:v.update(binding=True), ++ lambda v:v['binding'].update(approval_id=True),lambda v:v['binding'].update(extra='x'), ++ lambda v:v['binding'].update(request_digest='0'*64),lambda v:v.update(port=True), ++ lambda v:v.update(port=12345.0),lambda v:v.update(port=0),lambda v:v.update(phase=[]), ++ lambda v:v.update(phase='issued'),lambda v:v.update(revision=False), ++ lambda v:v.update(revision=0.0),lambda v:v.update(identity_accessor=False), ++ lambda v:v.update(task_accessor='unexpected-before-issue'),lambda v:v.update(policy_hcl=[]), ++ lambda v:v.update(cleanup={}),lambda v:v['cleanup'].pop(), ++ lambda v:v['cleanup'].__setitem__(0,False),lambda v:v['cleanup'][0].update(extra=True), ++ lambda v:v['cleanup'][0].update(kind=True),lambda v:v['cleanup'][0].update(call_digest=0), ++ lambda v:v['cleanup'][0].update(call_digest='f'*64),lambda v:v['cleanup'].reverse(), ++]) ++def test_authenticated_but_damaged_capsule_schema_refuses(capsule,mutate): ++ custody,binding=capsule ++ value=custody.read(binding);mutate(value) ++ # Trusted test owner re-signs malformed payload to exercise schema defenses ++ # independently of MAC validation; untrusted corruption cannot do this. ++ custody._write(binding,value) ++ with pytest.raises(Refused):custody.read(binding) ++ ++ ++def test_capsule_corruption_wrong_key_duplicate_and_nonfinite_refuse(capsule): ++ custody,binding=capsule;path=custody._path(binding);original=path.read_bytes() ++ value=json.loads(original);value['payload']['identity_accessor']='different-synthetic-accessor' ++ path.write_text(json.dumps(value)) ++ with pytest.raises(Refused):custody.read(binding) ++ path.write_bytes(original) ++ other=FixtureRecoveryCustody(custody.directory,integrity_key=b'X'*32) ++ with pytest.raises(Refused):other.read(binding) ++ for raw in [b'{',original[:-1]+b',"mac":"duplicate"}',original.replace(b'12345',b'NaN'),b'[]',b'{}',b'{"payload":{},"mac":"'+b'\xc3\xa9'*64+b'"}']: ++ path.write_bytes(raw) ++ with pytest.raises(Refused):custody.read(binding) ++ path.write_bytes(original) ++ assert custody.read(binding)['identity_accessor']=='synthetic-private-accessor' ++ ++ ++def test_exact_absence_and_single_terminal_lf_contract(): ++ from task_execution_recovery import _policy_absent,_same_policy ++ assert _policy_absent(404,{'errors':[]}) ++ for status,body in [(403,{'errors':[]}),(404,{}),(404,{'errors':['denied']}), ++ (404,{'errors':None}),(404,{'errors':[]} | {'data':{}}), ++ (404,{'errors':False}),(404,{'errors':0}),(404,{'errors':{}})]: ++ assert not _policy_absent(status,body) ++ assert _same_policy('fixture','fixture\n') ++ assert _same_policy('fixture\n','fixture\n') ++ for text in ['fixture \n','fixture\t','fixture\r\n','fixture\n\n',False,None]: ++ assert not _same_policy(text,'fixture\n') ++ ++ ++@pytest.mark.parametrize('after_delete',[False,True]) ++def test_recovery_policy_absence_rejects_misleading_native_http_bodies(owner,tmp_path,monkeypatch,after_delete): ++ native,journal,binding,call,custody=protected(owner,tmp_path);crash(native,journal,binding) ++ import task_execution_recovery as module ++ malformed=[{}, {'errors':['permission denied']}, {'errors':None}, {'errors':False}, ++ {'errors':0}, {'errors':[],'data':{}}, {'errors':['not really absent']}, ++ b'{"errors":["permission denied"],"errors":[]}',b'{"errors":NaN}',b'not JSON'] ++ path='sys/policies/acl/'+native.policy_name ++ for body in malformed: ++ sent=[];gets=[0] ++ class Response: ++ def __init__(self,status,data):self.status=status;self.data=data ++ def read(self,limit):return self.data if type(self.data) is bytes else json.dumps(self.data).encode() ++ class Connection: ++ def __init__(self,*args,**kwargs):pass ++ def request(self,method,url,**kwargs):sent.append((method,url)) ++ def getresponse(self): ++ method,url=sent[-1] ++ if url=='/v1/'+path: ++ if method=='DELETE':return Response(204,{}) ++ gets[0]+=1 ++ if after_delete and gets[0]==1: ++ return Response(200,{'data':{'policy':native.hcl[:-1]}}) ++ return Response(404,body) ++ return Response(403,{'errors':['synthetic refusal']}) ++ def close(self):pass ++ with monkeypatch.context() as patch: ++ patch.setattr(module.http.client,'HTTPConnection',Connection) ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] and result['obligations'][2]['outcome']=='owed' ++ assert ('DELETE','/v1/'+path) in sent if after_delete else ('DELETE','/v1/'+path) not in sent ++ # The denied/malformed probes above emitted no real native calls. ++ assert call('GET',path)[0]==200 ++ assert recover_fixture_cleanup(journal,permit(native,custody,binding))['confirmed'] ++ ++ ++def test_authenticated_prepared_capsule_rollback_cannot_hide_later_mint(owner,tmp_path): ++ native,journal,binding,call,custody=protected(owner,tmp_path) ++ old=custody._path(binding).read_bytes() ++ crash(native,journal,binding) ++ current=custody.read(binding) ++ custody._path(binding).write_bytes(old) ++ # Still authentic under the owner key, but the durable journal proves the ++ # operation reached issuance. The stale prepared record cannot prove absence. ++ result=recover_fixture_cleanup(journal,permit(native,custody,binding)) ++ assert not result['confirmed'] and result['obligations'][0]['outcome']=='owed' ++ assert call('POST','auth/token/lookup-accessor',{'accessor':current['task_accessor']})[0]==200 ++ assert call('POST','auth/token/revoke-accessor',{'accessor':current['task_accessor']})[0] in {200,204} +diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md +index 8b09305..f58a6b9 100644 +--- a/workplans/RPF-WP-0035-credential-lane-implementation.md ++++ b/workplans/RPF-WP-0035-credential-lane-implementation.md +@@ -1290,3 +1290,20 @@ CAS records consumed/unknown with zero native effects. Exact dependency/source + hashes, tests and remaining gates are retained with the source package and in + docs/task-execution-mediator-seam.md. No real grant/custody, policy default, owner + sync or native production acceptance is performed; T12 stays progress. ++ ++2026-10-16 T12 recovery successor source preparation: optional independently ++installed fixture custody durably retains operation-bound private accessors and ++issuance checkpoints. Fresh cleanup-only owner permits can reconcile after actual ++executor process death using fixed revoke/readback and policy delete/readback, ++without restoring business/consume admission. Separate durable attempt evidence ++retains original unknown outcomes. Lost token-create response remains unresolved ++when no accessor was committed. Source-only process-loss fixtures are not native ++production custody or recovery-role admission; T12 remains progress. ++ ++2026-10-16 T12 recovery R2 source refinement retains R1's overbroad 404 failure: ++policy cleanup now requires exact native empty-errors absence before/after delete; ++HCL uses only documented single optional terminal LF normalization. Private ++capsules are owner-key HMAC authenticated, strict nested schemas/ordered cleanup ++commitments reject damage, and journal history prevents stale prepared capsules ++from hiding issuance. Fixture process-loss source proof remains separate from ++unadmitted production custody, integrity-key storage and recovery-role scope. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r1.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r1.json new file mode 100644 index 00000000..83d370b0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r1.json @@ -0,0 +1,60 @@ +{ + "schema": "railiance-platform.protected-recovery-source-manifest.v1", + "proposal_id": "RPF35-PROTECTED-RECOVERY-20261016-R1", + "canonical_base_revision": "d870596cc72f74a5762cce2400802e2b351e903e", + "isolated_preimage_revision": "45419a49955b4a05f3f3d468c8216f6e9d0da2ec", + "isolated_source": "/tmp/netkingdom-platform-recovery-20261016", + "patch_sha256": "47812816af915534d6aec9431259f4290fb9b109a4fb47a54ed7dbd3b145c615", + "files": { + "scripts/task_execution_recovery.py": { + "before_sha256": null, + "after_sha256": "419517ea07e602eee975aa5d64c0b150ed5546dfda319beab44246c15bfa7b11" + }, + "scripts/task_execution_native_fixture.py": { + "before_sha256": "4575d048f79b125b0f87c70b9a76558d8cce6e61cb66b5b0583934d1a62f2ef6", + "after_sha256": "1191bfe2cfd0dd967f51405a1fd453e2f24c929d558554110137c2cc99f71941" + }, + "scripts/task_execution_journal.py": { + "before_sha256": "507b41a790e678dfd16af747bd851f5abc2fed5a8fb70bca05449d9e5ac674e8", + "after_sha256": "604cf285286a8a1be23d9bc0d8a547bd1195b7f80286778e1d4f30c711fd6a7e" + }, + "tests/test_task_execution_recovery.py": { + "before_sha256": null, + "after_sha256": "3a1722df7c63fa2a8a9f529cd9611c10a9664ac553dca04c1a940bd43ab75485" + }, + "docs/task-execution-native-fixture.md": { + "before_sha256": "4501a512bc03a8d912b11a199a1385d75cfa077ba849dfcfe3eca53f31d1c8ea", + "after_sha256": "605a9effe69d57abedf734eda66dbd9a7c70c3da07cc3006012de9343c435f91" + }, + "workplans/RPF-WP-0035-credential-lane-implementation.md": { + "before_sha256": "e90d963f8699360a62907ccc32d2346a33db62945dfb10473217c6dce7b2d383", + "after_sha256": "53ec6b02ef9dfb698937607449ac662178e237d7fa33cc402d4796852e2f347a" + } + }, + "canonical_preimages_match": { + "scripts/task_execution_native_fixture.py": true, + "scripts/task_execution_journal.py": true, + "docs/task-execution-native-fixture.md": true, + "workplans/RPF-WP-0035-credential-lane-implementation.md": true + }, + "preimages_directory": "preimages", + "authority": "Source-only independently admitted disposable fixture cleanup. No production custody/grant/credential read.", + "unchanged": [ + "Frozen signer-custody proposal", + "CCR-2026-0048", + "CCR-2026-0049", + "WORK-RECORDS.md" + ], + "validation_pending": false, + "source_input_sha256": { + "/tmp/netkingdom-platform-recovery-20261016/tests/test_task_execution_plan.py": "679bfe0450e7975b8ad6747cd210afce1de33da54acdf64bc9f00b3603772b95", + "/tmp/netkingdom-platform-recovery-20261016/tests/test_task_execution_native_fixture.py": "801dfeb601ac0f2f4ace8847e25333cb191218b86482a22eb6e90780ca9906e8", + "/tmp/netkingdom-platform-recovery-20261016/tests/test_task_execution_journal.py": "b51a4f5b8122221c69ad2486d6e1751298bd4665ac37c94f363dd5a27c5a014c", + "/tmp/netkingdom-platform-recovery-20261016/tests/test_task_execution_readback.py": "3c3ea5b06e8779d396a9d9301cc0e79477f8bb382e15a5e66366dde941f80b33", + "/tmp/netkingdom-platform-recovery-20261016/scripts/task_execution_plan.py": "cd984055ca8a1d51ef563bb7534b30c2fc9e326ed89a459362419b407397b9dc", + "/tmp/netkingdom-platform-recovery-20261016/scripts/task_execution_grant.py": "b76d7dce419c0548f33be3efaec2589e50eec48b9e277c90ab0e4615b9c0f3e7", + "/home/worsch/secrets-engine/src/secrets_engine/execution_contract.py": "75323911c229cb8b49d5116706a329c3f9ec2efdbeab6bfac8dbf5b92729f14a", + "/home/worsch/secrets-engine/src/secrets_engine/task_execution_handoff.py": "b82695b88c201fdcbd8854a0d3c37ee2d2d3feee2c0663d784180fa070ac6677", + "/home/worsch/secrets-engine/src/secrets_engine/catalog.py": "9af3a10cca7f45a201ef0567fb885d086390bc6fd2091e982ef0cd6191619514" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r2.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r2.json new file mode 100644 index 00000000..2b94be62 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/manifest-r2.json @@ -0,0 +1,57 @@ +{ + "schema": "railiance-platform.protected-recovery-source-manifest.v2", + "proposal_id": "RPF35-PROTECTED-RECOVERY-20261016-R2", + "same_trial_as": "RPF35-PROTECTED-RECOVERY-20261016-R1", + "original_base": "d870596cc72f74a5762cce2400802e2b351e903e", + "isolated_source": "/tmp/netkingdom-platform-recovery-r2-20261016", + "r1_isolated_commit": "1f80e617a4119dcea297944dbf328a1a3b2764ed", + "full_patch_sha256": "61f516c876fd612b83300e9913f0e80aad836a50014310797913c7c4505e3114", + "delta_patch_sha256": "6f97bbaa957673a3b28070d1550ffe72e709a573be183062de21e7f7145e8380", + "r1_patch_sha256": "47812816af915534d6aec9431259f4290fb9b109a4fb47a54ed7dbd3b145c615", + "files": { + "scripts/task_execution_recovery.py": { + "original_before_sha256": null, + "r1_before_sha256": "419517ea07e602eee975aa5d64c0b150ed5546dfda319beab44246c15bfa7b11", + "r2_after_sha256": "910f38919a8b6f57591366a2ef4da40d1ed7dfd7556c845a4b89496eca39ce1c" + }, + "scripts/task_execution_native_fixture.py": { + "original_before_sha256": "4575d048f79b125b0f87c70b9a76558d8cce6e61cb66b5b0583934d1a62f2ef6", + "r1_before_sha256": "1191bfe2cfd0dd967f51405a1fd453e2f24c929d558554110137c2cc99f71941", + "r2_after_sha256": "1191bfe2cfd0dd967f51405a1fd453e2f24c929d558554110137c2cc99f71941" + }, + "scripts/task_execution_journal.py": { + "original_before_sha256": "507b41a790e678dfd16af747bd851f5abc2fed5a8fb70bca05449d9e5ac674e8", + "r1_before_sha256": "604cf285286a8a1be23d9bc0d8a547bd1195b7f80286778e1d4f30c711fd6a7e", + "r2_after_sha256": "604cf285286a8a1be23d9bc0d8a547bd1195b7f80286778e1d4f30c711fd6a7e" + }, + "tests/test_task_execution_recovery.py": { + "original_before_sha256": null, + "r1_before_sha256": "3a1722df7c63fa2a8a9f529cd9611c10a9664ac553dca04c1a940bd43ab75485", + "r2_after_sha256": "1322469a116305a3a44bca7191eb52b76614d911b9eb7ce68864c52ce13c36e1" + }, + "docs/task-execution-native-fixture.md": { + "original_before_sha256": "4501a512bc03a8d912b11a199a1385d75cfa077ba849dfcfe3eca53f31d1c8ea", + "r1_before_sha256": "605a9effe69d57abedf734eda66dbd9a7c70c3da07cc3006012de9343c435f91", + "r2_after_sha256": "e299ff92fbd5282ca73066e41d5ef3753fad2e46ded2d8eda9935d20effaae46" + }, + "workplans/RPF-WP-0035-credential-lane-implementation.md": { + "original_before_sha256": "e90d963f8699360a62907ccc32d2346a33db62945dfb10473217c6dce7b2d383", + "r1_before_sha256": "53ec6b02ef9dfb698937607449ac662178e237d7fa33cc402d4796852e2f347a", + "r2_after_sha256": "312a3b4b9228897a1a256736eb0d4995b38274a47c6a52870d106aba16b532fd" + } + }, + "validation_pending": false, + "findings": [ + "R1 accepted arbitrary404 bodies as policy absence, including postdelete readback.", + "R1 broad rstrip comparison did not match claimed exact policy equality.", + "R1 private file shape checks did not authenticate corruption; a damaged still-string accessor could be freshly admitted.", + "R1 nested schema comparison ignored extra cleanup keys and could throw type errors for malformed phase." + ], + "fixes": [ + "Exact native empty-errors404; strict response JSON rejects duplicate and nonfinite values.", + "Explicit single optional terminal LF only, no whitespace folding.", + "Independent owner-key HMAC authenticates private capsule payload before exact nested schema validation; owner key never persisted in capsule/journal/log.", + "Strict Binding/Cleanup formats and ordered deterministic cleanup_specs; journal history prevents authenticated prepared-capsule rollback from hiding issuance." + ], + "authority": "Source-only disposable native fixtures. Production custody/isolation/key persistence/recovery scope remain unadmitted." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r1.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r1.json new file mode 100644 index 00000000..2f7e1775 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r1.json @@ -0,0 +1,17 @@ +{ + "reviewer": "railiance-platform-owner (agent railiance-platform, codex)", + "decision": "approved", + "scope": "Source-only disposable fixture recovery successor, not production custody/grant/native recovery admission.", + "comment": "Platform INTENT principles 3 Durable by Default, 4 Secure Custody and 5 Operable and Observable require real process-loss cleanup recovery with independent owner authority. Reviewed patch sha256:47812816af915534d6aec9431259f4290fb9b109a4fb47a54ed7dbd3b145c615 against canonical base d870596cc72f74a5762cce2400802e2b351e903e and exact preserved preimages, under RPF-WP-0035-T12. Reviewed in isolated owner checkout with owner instructions/INTENT loaded. Recovery never restores business admission, consumes approval, creates credentials or reapplies effects; original uncertainty and failed recovery attempts remain durable. Private accessors stay out of public journal and receipts. Only Platform owner role reviewed.", + "verified_source_evidence": [ + "24 final recovery/journal tests passed, including actual executor process death, reopened journal, native revoked accessors and policy absence, independent recovery permit revocation/expiry, stale custody revision, revoked backend credential, policy replacement protection and lost recovery response unknown preservation.", + "Three separately rerun reopened-journal process-loss cases passed.", + "Canonical git apply --check and isolated diff --check passed." + ], + "retained_limits": [ + "Native production custody/isolation/storage/encryption/retention and exact recovery-role permissions remain unselected and require independent owner/human scope admission.", + "Lost token-create response before accessor commit remains unresolved cleanup debt; no inferred expiry/success.", + "Source fixture recovery is not authorization to use estate backend." + ], + "authority_granted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r2.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r2.json new file mode 100644 index 00000000..0b4fd466 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/owner-source-review-r2.json @@ -0,0 +1,13 @@ +{ + "reviewer": "railiance-platform-owner (agent railiance-platform, codex)", + "decision": "approved", + "scope": "R2 source-only disposable fixture recovery; supersedes R1 source review, not native production admission.", + "comment": "Platform INTENT principles3 Durable by Default,4 Secure Custody and5 Operable and Observable require actual absence evidence, authenticated recoverable handles and explicit uncertainty. Under RPF-WP-0035-T12 reviewed full patch sha256:61f516c876fd612b83300e9913f0e80aad836a50014310797913c7c4505e3114 and R1 delta sha256:6f97bbaa957673a3b28070d1550ffe72e709a573be183062de21e7f7145e8380; exact original and R1 preimages/hashes retained. Root R1 counterexamples were accepted and reproduced offline: damaged valid-string accessor and extra cleanup fields accepted; phase list raised TypeError; rstrip accepted tab changes. R2 authenticates capsules under independently supplied owner key before exact schemas and deterministic cleanup commitments, conservatively detects prepared rollback through journal history, and accepts only actual empty-errors404 absence. HCL admits precisely one optional terminalLF, documented and tested; no broad semantic equality. Owner instructions/INTENT and governing standing authorization loaded in isolated owner checkout; only Platform role reviewed.", + "validation": "53 recovery/journal tests passed; final frozen29targeted cases passed, overlapping counts; exact chronology retained in validation-r2.json. Full/delta patch checks and final hashes pass.", + "remaining_native_gates": [ + "Owner-enforced integrity-key custody and process isolation, durable key availability/rotation, capsule encryption/retention, production actor/role/exact cleanup permission and audit delivery remain unselected/unadmitted.", + "HMAC authenticates payload but is not general anti-rollback; a stale prepared payload cannot establish no issuance at the durable grant/call boundary.", + "Unknown issuance without an authenticated retained accessor remains unconfirmed cleanup debt." + ], + "authority_granted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/docs/task-execution-native-fixture.md b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/docs/task-execution-native-fixture.md new file mode 100644 index 00000000..1dd6b91b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/docs/task-execution-native-fixture.md @@ -0,0 +1,123 @@ +# Executable native metadata/grant/cleanup preparation + +RPF-WP-0035-T12, 2026-10-16, second source package after the cleanup journal. + +The fixed `NativeFixtureAdapter` actually dispatches to disposable loopback OpenBao. +It requires a live owner-spawned OpenBao dev-process handle with the exact loopback +port and dev/no-store-token arguments, and refuses every non-loopback origin. +Production is deliberately not implemented. This is a guard against accidental +misrouting inside the trusted fixture runtime, not a Python security sandbox. +No estate credentials, grants, approvals or policy packages are read or changed. +The tests start in-memory dev servers with synthetic root/session credentials, +synthetic owner admission, grant/state/source digests and a synthetic postconsume +journal transition. This proves native primitives, not authenticated admission. + +The owner Compiler now deterministically includes the operation-specific temporary +policy name and cleanup call commitments in each serialized Plan. Commitments +include identity/task credential slots, exact native revoke/verify or delete/verify +method/path pairs, and the narrow no-issue/no-create skip branches. The full +execution descriptor can be bound into that Plan only by byte equality with an +independently installed owner descriptor, with operation/actor/CCR/catalogue/six +operations correspondence. Compiler revalidation preserves both descriptor and +cleanup content. The descriptor's SHA is preparation provenance; no opaque digest +or local file becomes approval or verified canonical request correspondence. + +The adapter needs the exact compiler/Plan/binding and separately installed owner +fixture inputs. All handles stay in owner memory with hidden representations. It +requires the same live consumed journal admission and exact grant-issuing state. +It refuses an existing per-operation grant policy, creates only the two compiler +metadata endpoints plus revoke-self, verifies HCL, mints one parented service token +with only this policy, no default/identity policies, nonrenewable TTL <=120 seconds +and twelve uses, then verifies native token bounds through the owner. Six ordered +metadata calls use independently recompiled source/method/path/body. A task use is +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. +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 +committed obligation; accessor verification cannot erase an uncertain revoke. +The native adapter compares the exact committed cleanup sequence before request. +Self-revoke success must be followed by owner lookup-accessor's specific invalid +accessor response; generic HTTP400/403, timeout and policy deletion do not prove +revocation. The two explicitly accepted error encodings cover OpenBao's plain and +wrapped single-invalid-accessor forms. Native error text is never logged or hashed. +Cleanup bypasses expired business deadlines under the retained explicit obligation, +never source-driven business repair. Independent cleanup continues after failure; +retirement never overwrites the original unknown/failed business outcome. + +Current actual native tests cover policy/body and K8s-role application/readback, +owner-parented TTL/use policy bounds, native sibling/KV/token/mount/SecretID capability +denial, both token accessor-invalid proofs, policy absence, deadline-expired cleanup, +source drift, no preconsume materialization, changed descriptor/cleanup commitments, +nonloopback refusal, actual write followed by simulated lost response, and actual +policy retirement with failed task revocation. The lost response is fault injection +after a successful real native write; it does not pretend to be an actual network +partition. The failed-revoke fixture independently confirms the still-existing +native token, then performs explicit synthetic owner recovery. No blind retry occurs. + +## Minimal remaining decisions and integration + +1. Define/admit `task-execution-grant` in the owner CCR validator. Bind the exact + cleanup commitments, temporary policy/token bodies and source revisions into its + immutable projection, referenced by the existing descriptor grant SHA. The + descriptor currently has no cleanup field; no engine schema extension is needed + if this owner grant binding is made explicit. The immutable projection/excluded + append-only fields are not implemented in the current owner validator. This is + the concrete owner schema/admission decision, not a need for a new service. +2. Have the existing authoritative engines supply authenticated initiating principal, + exact canonical request + served claim/PDP/package and independent signed CAS + consume verification to the owner mediator before its journal transition. + The native fixture's local transition supplies none of that evidence. +3. Install exact origin/cluster/source/grant/baseline/readback inputs under the + attended owner runtime. The current binder compares independently installed + descriptor bytes, but does not authenticate source-bundle paths, grant digest or + compute immutable CCR projection. The six-operation plan still comes from pinned + actual owner source. Production origin authentication and deadline bounds against + served approval/PDP remain separate. +4. Finish expected-state normalization and conflict handling for native K8s/OIDC + defaults; add per-policy/role approved baseline comparison and exact readback + verification. The fixture checks known fields, not total production equality. + OpenBao metadata PUT has no native atomic CAS; the owner must choose exclusive + admitted target custody or an honest conflict/recovery protocol. +5. Wire the fixed adapter to that mediator with bounded runtime isolation, + owner-private uncertain-issuance recovery, external audit outbox and exact + founder-scoped attended trial. No production caller or protected recovery channel + exists here. The existing production readiness gate remains refused. + +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/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_journal.py b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_journal.py new file mode 100644 index 00000000..8f1b9a24 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_journal.py @@ -0,0 +1,390 @@ +"""Source-only admission primitive; never authorizes or dispatches native work. + +Trusted owner mediation must validate claim/PDP/consume before transitions. +One approval admits one operation. A persisted reservation cannot be resumed. +The process lock is held across the whole owner invocation, including cleanup. +""" +from __future__ import annotations + +import fcntl +import os +import re +import sqlite3 +import stat +import uuid +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path + + +class Refused(Exception): + pass + + +@dataclass(frozen=True) +class Binding: + approval_id: str + request_digest: str + operation_id: str + plan_digest: str + + def validate(self): + if any(type(v) is not str for v in (self.approval_id, self.request_digest, self.operation_id, self.plan_digest)): + raise Refused("binding fields must be strings") + if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}", self.approval_id): + raise Refused("invalid approval identifier") + # Preserve the exact served PDP/Approval value; never silently strip it. + if not re.fullmatch(r"sha256:[0-9a-f]{64}", self.request_digest): + raise Refused("invalid canonical request digest") + if not re.fullmatch(r"[0-9a-f]{64}", self.plan_digest): + raise Refused("invalid owner plan content digest") + try: + if str(uuid.UUID(self.operation_id)) != self.operation_id: + raise ValueError() + except (ValueError, AttributeError): + raise Refused("invalid operation identifier") from None + + +_NEXT = { + "reserved": {"consume-confirmed", "failed", "unknown"}, + "consume-confirmed": {"authenticating", "failed", "unknown"}, + "authenticating": {"grant-issuing", "failed", "unknown"}, + "grant-issuing": {"grant-issued", "failed", "unknown"}, + "grant-issued": {"applying", "failed", "unknown"}, + "applying": {"readback", "failed", "unknown"}, + "readback": {"verified", "failed", "unknown"}, + "verified": {"cleanup-confirmed", "unknown"}, +} + + +@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. + if directory.is_symlink() or directory.resolve() != directory.absolute(): + raise Refused("journal path is indirect") + self.directory = directory + self._private(directory.stat(), directory=True) + self._lock_fd = self._open("execution.lock") + try: + fd = self._open("journal.sqlite") + except BaseException: + os.close(self._lock_fd) + del self._lock_fd + raise + original = os.fstat(fd) + os.close(fd) + self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None) + current = (directory / "journal.sqlite").stat() + if (original.st_dev, original.st_ino) != (current.st_dev, current.st_ino): + self.close() + raise Refused("journal changed during open") + self.db.execute("PRAGMA synchronous=FULL") + 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.db.execute("CREATE TABLE IF NOT EXISTS cleanup_calls (operation TEXT NOT NULL, obligation INTEGER NOT NULL, step INTEGER NOT NULL, call_digest TEXT NOT NULL, outcome TEXT NOT NULL, evidence TEXT, PRIMARY KEY(operation,obligation,step))") + self._held = False + self._admission = None + self._consumed_admission = None + self._sync_directory() + + @staticmethod + def _private(s, directory=False): + kind = stat.S_ISDIR(s.st_mode) if directory else stat.S_ISREG(s.st_mode) + if not kind or s.st_uid != os.getuid() or s.st_mode & 0o077: + raise Refused("journal is not owner private") + if not directory and s.st_nlink != 1: + raise Refused("journal file has multiple links") + + def _open(self, name): + fd = os.open(self.directory / name, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + self._private(os.fstat(fd)) + except BaseException: + os.close(fd) + raise + return fd + + def _sync_directory(self): + fd = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + os.fsync(fd) + finally: + os.close(fd) + + @contextmanager + def locked(self): + if self._held: + raise Refused("nested journal admission") + try: + fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused("owner invocation already active") from None + self._held = True + try: + 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, 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: + self.db.execute("BEGIN IMMEDIATE") + 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: + self.db.execute("ROLLBACK") + raise Refused("approval or operation already admitted") from None + except BaseException: + if self.db.in_transaction: + self.db.execute("ROLLBACK") + raise + self._admission = binding + + def transition(self, binding: Binding, expected: str, target: str, evidence_digest=None): + if not self._held or self._admission != binding: + raise Refused("no live admission") + if target not in _NEXT.get(expected, set()): + raise Refused("invalid state transition") + if target == "cleanup-confirmed" and evidence_digest is None: + raise Refused("cleanup requires evidence") + if evidence_digest is not None and not re.fullmatch(r"[0-9a-f]{64}", evidence_digest): + raise Refused("invalid evidence digest") + 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( + "UPDATE operations SET state=?, evidence=? WHERE approval=? AND request=? AND operation=? AND plan=? AND state=?", + (target, evidence_digest, binding.approval_id, binding.request_digest, + binding.operation_id, binding.plan_digest, expected)).rowcount + if changed != 1: + 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") + raise + + def observe(self, binding: Binding): + binding.validate() + row = self.db.execute("SELECT request,operation,plan,state,evidence FROM operations WHERE approval=?", (binding.approval_id,)).fetchone() + if row is None: + return None + if row[:3] != (binding.request_digest, binding.operation_id, binding.plan_digest): + 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] + pending += self.db.execute("SELECT COUNT(*) FROM cleanup_calls 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)} + + 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. + + Caller must derive ordinal/digest from the independently pinned owner plan. + This primitive doesn't validate method/body or decide whether a call is allowed. + """ + if not self._held or self._admission != binding: + raise Refused("no live admission") + if type(ordinal) is not int or ordinal < 0 or ordinal > 63: + raise Refused("invalid fixed-plan ordinal") + if type(call_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}", call_digest): + raise Refused("invalid call digest") + try: + 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] + 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") + self._sync_directory() + except BaseException: + if self.db.in_transaction: self.db.execute("ROLLBACK") + raise + + def after_call(self, binding: Binding, ordinal: int, call_digest: str, evidence_digest: str): + if not self._held or self._admission != binding: + raise Refused("no live admission") + if type(ordinal) is not int or ordinal < 0 or ordinal > 63: + raise Refused("invalid fixed-plan ordinal") + if type(call_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}", call_digest): + raise Refused("invalid call digest") + if type(evidence_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}", evidence_digest): + raise Refused("invalid evidence digest") + try: + self.db.execute("BEGIN IMMEDIATE") + n=self.db.execute("UPDATE calls SET outcome='complete', evidence=? WHERE operation=? AND ordinal=? AND call_digest=? AND outcome='pending'",(evidence_digest,binding.operation_id,ordinal,call_digest)).rowcount + if n != 1: raise Refused("call confirmation mismatch") + self.db.execute("COMMIT") + self._sync_directory() + except BaseException: + 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") + if confirmed and self.db.execute("SELECT COUNT(*) FROM cleanup_calls WHERE operation=? AND obligation=? AND outcome='pending'", (binding.operation_id,ordinal)).fetchone()[0]: + raise Refused("pending cleanup native call cannot confirm") + 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 before_cleanup_call(self, binding, ordinal, obligation, step, call_digest): + """Journal each native revoke/delete AND its independent verification.""" + self._cleanup_admission(binding,ordinal,obligation) + if type(step) is not int or not 0<=step<2 or type(call_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}",call_digest): + raise Refused("exact bounded cleanup native call required") + try: + self.db.execute("BEGIN IMMEDIATE") + parent=self.db.execute("SELECT kind,call_digest,outcome FROM cleanup WHERE operation=? AND ordinal=?",(binding.operation_id,ordinal)).fetchone() + count,pending=self.db.execute("SELECT COUNT(*),COALESCE(SUM(outcome='pending'),0) FROM cleanup_calls WHERE operation=? AND obligation=?",(binding.operation_id,ordinal)).fetchone() + if parent!=(obligation.kind,obligation.call_digest,'pending') or step!=count or pending: + raise Refused("cleanup call unadmitted, replayed or uncertain") + self.db.execute("INSERT INTO cleanup_calls VALUES (?,?,?,?,'pending',NULL)",(binding.operation_id,ordinal,step,call_digest)) + self.db.execute("COMMIT");self._sync_directory() + except BaseException: + if self.db.in_transaction:self.db.execute("ROLLBACK") + raise + + def after_cleanup_call(self,binding,ordinal,obligation,step,call_digest,evidence_digest): + self._cleanup_admission(binding,ordinal,obligation) + if (type(step) is not int or not 0<=step<2 or type(call_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}",call_digest) + or type(evidence_digest) is not str or not re.fullmatch(r"[0-9a-f]{64}",evidence_digest)): + raise Refused("exact cleanup native evidence required") + try: + self.db.execute("BEGIN IMMEDIATE") + parent=self.db.execute("SELECT kind,call_digest,outcome FROM cleanup WHERE operation=? AND ordinal=?",(binding.operation_id,ordinal)).fetchone() + if parent!=(obligation.kind,obligation.call_digest,'pending'): + raise Refused("cleanup parent is no longer pending") + changed=self.db.execute("UPDATE cleanup_calls SET outcome='complete',evidence=? WHERE operation=? AND obligation=? AND step=? AND call_digest=? AND outcome='pending'", + (evidence_digest,binding.operation_id,ordinal,step,call_digest)).rowcount + if changed!=1:raise Refused("cleanup native 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() + if hasattr(self, "_lock_fd"): + os.close(self._lock_fd) diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_native_fixture.py b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_native_fixture.py new file mode 100644 index 00000000..caa7da17 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/scripts/task_execution_native_fixture.py @@ -0,0 +1,300 @@ +"""Fixed native metadata/grant/cleanup adapter for disposable loopback OpenBao. + +This adapter deliberately admits only a disposable loopback fixture. It supplies +real native primitive coverage while production grant schema, identity/consume and +origin/source admission are unfinished. It cannot open an estate backend. Owner +credentials/handles stay in memory; no subprocess, redirects or ambient proxy. +Python privacy is not credential isolation; production needs the owner runtime. +""" +from __future__ import annotations +from dataclasses import dataclass, field +from datetime import datetime, timezone +import hashlib +import http.client +import json +import subprocess +import time +from pathlib import Path +from urllib.parse import urlsplit + +from task_execution_cleanup import CleanupResult +from task_execution_journal import Binding, Cleanup, Journal, Refused +from task_execution_plan import Compiler, Plan, cleanup_specs, encoded, content_hash + + +@dataclass(frozen=True) +class PrivateSession: + token: str = field(repr=False) + accessor: str = field(repr=False) + + +@dataclass(frozen=True) +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) + + +class NativeFixtureAdapter: + def __init__(self, compiler: Compiler, plan: Plan, journal: Journal, + binding: Binding, admission: FixtureAdmission): + if type(admission) is not FixtureAdmission or type(admission.descriptor) is not bytes: + raise Refused('independent fixture admission required') + self.compiler,self.plan,self.journal,self.binding=compiler,plan,journal,binding + self.admission=admission + self.meta=plan.metadata() + if self.meta.get('execution_grant',{}).get('status')!='approved': + raise Refused('independently source-bound synthetic approved grant required') + descriptor=json.loads(admission.descriptor) + fresh=compiler.bind_execution(compiler.compile(self.meta['request']),descriptor,admission.descriptor) + if fresh.serialized != plan.serialized or binding.plan_digest != plan.content_digest: + raise Refused('owner bound plan mismatch') + if binding.operation_id != descriptor['operation_id'] or binding.request_digest != self.meta['request']['request_digest']: + raise Refused('owner operation mismatch') + origin=urlsplit(descriptor['backend_origin']) + if origin.scheme!='http' or origin.hostname!='127.0.0.1' or not origin.port or origin.username or origin.password or origin.path or origin.query or origin.fragment: + raise Refused('disposable loopback backend only') + self.host,self.port=origin.hostname,origin.port + process=admission.server_process + if (type(process) is not subprocess.Popen or process.poll() is not None + or type(process.args) is not list or '-dev' not in process.args + or '-dev-no-store-token' not in process.args + or '-dev-listen-address=127.0.0.1:'+str(self.port) not in process.args): + raise Refused('live owner-spawned disposable dev process required') + budget=descriptor['backend_budget'] + if set(budget)!={'ttl_seconds','uses'} or type(budget['ttl_seconds']) is not int or not 30<=budget['ttl_seconds']<=120 or type(budget['uses']) is not int or budget['uses']!=12: + raise Refused('fixed native task budget required') + self.ttl=budget['ttl_seconds']; self.uses=budget['uses']; self.used=0 + self.deadline=datetime.fromisoformat(descriptor['operation_deadline'].replace('Z','+00:00')) + if self.deadline.tzinfo is None: raise Refused('aware operation deadline required') + remaining=(self.deadline-datetime.now(timezone.utc)).total_seconds() + self.monotonic_deadline=time.monotonic()+remaining + self.policy_name='task-exec-'+binding.operation_id + calls=[compiler.before_call(plan,i) for i in range(6)] + paths=sorted({call.path for call in calls}) + self.hcl=''.join('path '+json.dumps(path)+' {\n capabilities = ["read", "create", "update"]\n}\n' for path in paths) + 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 + self._cleanup_steps={item.kind:0 for item in self.cleanup} + + def _live(self): + if not self.journal._held or self.journal._admission!=self.binding or self.journal._consumed_admission!=self.binding: + raise Refused('same live consumed owner admission required') + + def _fresh(self): + self._live() + if min((self.deadline-datetime.now(timezone.utc)).total_seconds(),self.monotonic_deadline-time.monotonic())<=5: + raise Refused('business deadline exhausted') + # The constructor independently rebuilt the complete immutable Plan and + # grant. Recheck every pinned byte before each native effect. Metadata + # dispatch additionally reconstructs its exact ordinal/body below; grant + # creation reconstructs and compares its fixed body before issuance. + for path in self.compiler.pins:self.compiler.read(Path(path)) + + def _http(self, method, path, token, body=b''): + # Fixed caller-generated paths only; http.client has no proxy or redirect + # following. Never surface backend body/header or transport exception text. + if self.admission.server_process.poll() is not None: + raise Refused('disposable backend exited; outcome unconfirmed') + conn=http.client.HTTPConnection(self.host,self.port,timeout=2) + try: + conn.request(method,'/v1/'+path,body=body or None, + headers={'X-Vault-Token':token,'Content-Type':'application/json'}) + response=conn.getresponse(); raw=response.read(1024*1024+1) + if len(raw)>1024*1024: raise Refused('oversize native response') + data=json.loads(raw) if raw else {} + if type(data) is not dict: raise Refused('invalid native response') + return response.status,data + except Exception: + raise Refused('native transport or response unconfirmed') from None + finally: conn.close() + + def _business(self, method, path, token, payload=b'', task=False): + self._fresh() + if task: + if self.used>=self.uses-1: raise Refused('task cleanup use reserved') + self.used+=1 + evidence_payload = encoded({'token':'owner-retained-task-token'}) if path=='auth/token/lookup' else payload + digest=content_hash(encoded({'method':method,'path':path,'payload_sha256':'sha256:'+content_hash(evidence_payload),'credential_slot':'task' if task else 'owner'})) + ordinal=self.journal.next_call_ordinal(self.binding) + self.journal.before_call(self.binding,ordinal,digest);self._ordinal=ordinal+1 + try: status,data=self._http(method,path,token,payload) + except Exception: + state=self.journal.observe(self.binding)['state'] + if state!='unknown': self.journal.transition(self.binding,state,'unknown') + raise + # Native responses may contain tokens/accessors; evidence is closed status + # metadata rather than raw bytes or a guessable secret-value fingerprint. + evidence=content_hash(encoded({'call_digest':digest,'http_status':status})) + self.journal.after_call(self.binding,ordinal,digest,evidence) + return status,data + + @staticmethod + def _ok(status,data,allowed): + if status not in allowed or data.get('errors'): + raise Refused('native request refused or unverified') + + def materialize_grant(self): + self._fresh() + if self.journal.observe(self.binding)['state']!='grant-issuing' or self._policy_attempted: + raise Refused('fresh grant issuing state required') + if min((self.deadline-datetime.now(timezone.utc)).total_seconds(),self.monotonic_deadline-time.monotonic())=len(spec['calls']) or spec['calls'][call_index]!={'method':method,'path':path}: + raise Refused('cleanup native method/target differs from commitment') + # Commit shape/slot only: private accessor/token bytes never enter hashes + # or durable evidence. Their exact identity is owner-retained custody. + digest=content_hash(encoded({'obligation_digest':obligation.call_digest,'step':step, + 'method':method,'path':path})) + self.journal.before_cleanup_call(self.binding,ordinal,obligation,step,digest) + self._cleanup_steps[kind]+=1 + status,data=self._http(method,path,token,body) + evidence=content_hash(encoded({'call_digest':digest,'http_status':status})) + self.journal.after_cleanup_call(self.binding,ordinal,obligation,step,digest,evidence) + return status,data + + def _revoke(self, kind, digest, session, issue_uncertain=False): + self._cleanup_admitted(kind,digest) + if session is None: + return CleanupResult(not issue_uncertain,content_hash(encoded({'kind':kind,'issue_uncertain':issue_uncertain}))) + status,data=self._cleanup_http(kind,'POST','auth/token/revoke-self',session.token) + self._ok(status,data,{200,204}) + status,data=self._cleanup_http(kind,'POST','auth/token/lookup-accessor',self.admission.owner_token,encoded({'accessor':session.accessor})) + confirmed=status==400 and data.get('errors') in (['invalid accessor'],['1 error occurred:\n\t* invalid accessor\n\n']) + return CleanupResult(confirmed,content_hash(encoded({'kind':kind,'revocation_confirmed':confirmed}))) + + def revoke_task_token(self, digest): + return self._revoke('task-token-revoke',digest,self.task,self._mint_attempted and self.task is None) + + def revoke_identity_token(self, digest): + return self._revoke('identity-token-revoke',digest,self.admission.identity_session) + + def delete_grant_policy(self, digest): + self._cleanup_admitted('grant-policy-delete',digest) + path='sys/policies/acl/'+self.policy_name + if self._policy_attempted: + status,data=self._cleanup_http('grant-policy-delete','DELETE',path,self.admission.owner_token);self._ok(status,data,{200,204}) + status,data=self._cleanup_http('grant-policy-delete','GET',path,self.admission.owner_token) + confirmed=status==404 and not data.get('data') + return CleanupResult(confirmed,content_hash(encoded({'kind':'grant-policy-delete','absence_confirmed':confirmed}))) diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/workplans/RPF-WP-0035-credential-lane-implementation.md b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/workplans/RPF-WP-0035-credential-lane-implementation.md new file mode 100644 index 00000000..8b093056 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/preimages/workplans/RPF-WP-0035-credential-lane-implementation.md @@ -0,0 +1,1292 @@ +--- +id: RPF-WP-0035 +type: workplan +title: "Implement reviewed credential lanes with separate owner gates" +domain: financials +repo: railiance-platform +status: blocked +flavor: implementation +owner: codex +created: "2026-09-05" +updated: "2026-10-13" +related: + - RPF-WP-0032 + - RPF-WP-0033 + - RPF-WP-0034 +state_hub_workstream_id: "975db491-5412-5e27-8e34-14a2417bb039" +--- + +# Credential lane implementation + +One S3 queue for three designed lanes; each task keeps its own approval, +execution and closure boundary. This replaces the implementation tasks in the +three completed design workplans, not the designs themselves. No approval is +inherited by consolidation. INTENT binding: secure custody, dependable delivery, +stable consumer interfaces. Incident custody remains in RPF-WP-0027/0029. + +## Consolidate completed designs and owner dependencies + +```task +id: RPF-WP-0035-T01 +status: done +priority: medium +state_hub_task_id: "0f3606bb-e95b-5484-8a57-bcdd6e3916fd" +``` + +Completed 2026-09-05. Preserved the three designs under +`docs/credential-lane-designs/`, identified native owner responsibilities and +linked each superseded task below. Owners have not been sent new requests and +no new external acceptance is claimed. STATE-WP-0085-T09 is already done; +the signing dependency belongs to the proposed FLEX-WP-0020-T05 cutover. + +## Accept and provision secrets-engine service JWT login + +```task +id: RPF-WP-0035-T02 +status: wait +decision_id: 62aa95e4-3ccc-4867-8806-5e3db005b483 +depends_on: [KEY-WP-0039-T02, MASON-WP-0009-T05] +blocking_reason: "Agent work, not a human gate: claim refusals (KEY-WP-0039-T02 probe) and capabilities-self deny, then ops-mason MASON-WP-0009-T05." +priority: high +state_hub_task_id: "e0c82ea9-05a5-537e-8fbf-67534263572e" +``` + +2026-10-11: requalified — the remaining waits are other owners' tasks, not a human gate. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. + +Supersedes RPF-WP-0032-T02. Design: +`docs/credential-lane-designs/secrets-engine-service-jwt.md`. + +Platform owns the exact JWT mount/role/policy, effective-policy negative tests +and a metadata-only custody receipt. KeyCape owns issuer/JWKS and service +registration (KEY-WP-0009); secrets-engine owns service authentication and +authority consumption (SECRETS-WP-0008-T06, SECRETS-WP-0007-T04). + +**Unblock:** live registration and protected client custody, reviewed exact source +and attended apply authority. Issuer/JWKS and consumer source claims now agree; +see the 2026-09-27 return below. A service login does +not grant lane mutation authority. Do not build another identity provider or +lifecycle engine here. + +**Done when:** approved exact role succeeds for the intended service, wrong +issuer/audience/subject/claims and unrelated secret access fail, bounded TTL +and revocation are proven, the consumer explicitly opts into the verified +contract, and rollback/cleanup receipts contain no secret material. + +2026-10-09 (T03 source): founder ruled the tenant segment stays `coulomb` +(ops-mason `b72ff77d`). New CCR request type `workload-kv-operator-matrix` +(`scripts/operator_matrix_lane.py`): exact per-entry capabilities, OIDC role on +the existing `netkingdom` mount only, and exact agent denies on all four data +and metadata paths appended to the live `agent-high-risk-boundary` (live content +preserved; partial presence refused). `scripts/fluid_telegram_custody_move.py` +copies the four undocumented `infotech` entries with CAS 0, verifies all, then +metadata-deletes the sources. `scripts/kv_duplicate_retire.py` retires +`activity-core/default/llm-connect-provider-secrets` only if no live non-admin +policy grants it. Founder session (each helper prints nothing; read the receipts): + +```bash +cd ~/railiance-platform +export BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 +W='warden access openbao-platform-admin-login --exec --' +# 1. dry-runs (no value reads, no writes) +$W python3 scripts/fluid_telegram_custody_move.py --dry-run --confirm 'MOVE fluid-telegram hall-of-helix to coulomb' --receipt ~/ft-move-dry.json +$W python3 scripts/kv_duplicate_retire.py --dry-run --confirm 'RETIRE activity-core/default/llm-connect-provider-secrets' --receipt ~/kv-retire-dry.json +# 2. lane: approve, confirm binding, apply +python3 scripts/credential-change.py approve CCR-2026-0039 --reviewer "platform-owner, fluid-telegram-owner, key-cape-owner (founder Bernd Worsch)" --comment "Approve ops-mason plan fluid-telegram-operator-credential-lane (spec 41a0ca27): four-entry matrix at coulomb/fluid-telegram/hall-of-helix, OIDC role on netkingdom, agent denies." \ + && python3 scripts/credential-change.py confirm-binding CCR-2026-0039 --reviewer founder --comment "groups=[fluid-telegram-operators] (LLDAP, member tegwick) and /assurance/level=aal2 match key-cape b2ed890f." \ + && $W python3 scripts/credential-change.py applier-apply CCR-2026-0039 --confirm "DELEGATED APPLY CCR-2026-0039" --actor founder --record-state-hub --quiet +# 3. custody move, then duplicate retirement +$W python3 scripts/fluid_telegram_custody_move.py --confirm 'MOVE fluid-telegram hall-of-helix to coulomb' --receipt ~/ft-move.json +$W python3 scripts/kv_duplicate_retire.py --confirm 'RETIRE activity-core/default/llm-connect-provider-secrets' --receipt ~/kv-retire.json +``` + +2026-10-09 (T03 founder session): CCR-2026-0039 applied at 10:30Z; readback +matched for the 8 boundary denies, policy and role. Custody move `moved`: all +four entries copied at coulomb v1 (operator-session from infotech v4) and the +infotech entries metadata-deleted. Duplicate +`activity-core/default/llm-connect-provider-secrets` retired; its first dry-run +refused on mount-wide `platform/metadata/*` survey grants, after which the check +was narrowed to data-path grants (`64ae594`). Receipts: +`docs/evidence/2026-10-09-fluid-telegram-t03-founder-session.json`. + +2026-10-03: the historical D1 routing was withdrawn because provision/delivery already completed. Owner routing now uses decision `ab85ae8a-12a0-4945-ae64-1d3d86d5b26f`, specifically independent archive readback and coordinated interim audit bearer revocation acceptance. `needs_human` and the wait remain; resolving a decision requires owner receipt re-check, not automatic completion. + +2026-10-03 (CUST-WP-0075): gate check. SECRETS-WP-0007-T04 is done (2026-09-27), so the gate named in the design is open. It is not the last one: the design says to confirm the exact live client registration and protected client-side custody before writing, and positive-login proof needs a real KeyCape service JWT. KeyCape's contract (`openbao-service-auth-contract.md`) and KEY-WP-0009 claim source registration only; the live registration of `secrets-engine-openbao` and its client-credential custody are "a separate approved custody action" that no workplan task owns yet (SECRETS-WP-0008-T06 waits on this task). Re-qualified with `blocked_on: message-from:key-cape`; key-cape was asked for the owning task id, which will replace it as `depends_on`. The design stays proposed and no child command was written. Not approvable in D1 yet. + +2026-10-04: Founder authorized the prepared coordination request. Sent +`b4fc62ea-f92c-4410-919a-9f3cce2d406b` to key-cape on preparation thread +`f84637fa-2355-4971-a2dc-7f1fd01710a8`, referencing platform source +`6b91d510395215d178d60ee8884e7fc00930a0cf`. Requested actual registration and +protected-custody owning tasks/status/revisions and non-secret receipts. Source +registration is not live custody; no value fetch or live registration was +requested. T02 remains waiting on the owner return and separate execution gates. + +2026-10-04 blocked-task review: KeyCape returns 58826056/eb20a78c now identify +KEY-WP-0037-T02/T03/T04, replacing the unowned-registration wait with real +dependencies. Prepared proposed CCR-2026-0033 and exact-path issuer policy/auth, +sso-only ESO and evidence checklist from the returned candidate coordinates. +No issuer/consumer value, registration or native issuance is supplied by them. +secrets-engine must return independent source acceptance of consumer host/path, +protected delivery and rotation/revocation ownership; awaiting that source phase +does not depend on its full live service-login task. KeyCape's exact-primary sync +remains pending its scoped historical identity repair. Requirements and acceptance +boundaries: `docs/provisioning-windows/2026-10-04-1000/dependency-requirements.md`. +This workplan was already blocked and remains blocked on its open owner/live gates. + +2026-10-06: secrets-engine returned consumer custody source acceptance +(message 680136e2, SECRETS-WP-0012, commit a3897c2). CCR-2026-0033 finalized as +**proposed**: KV path corrected to `platform/workloads/keycape/secrets-engine-openbao` +(applier scope), consumer copy coordinates with D1-D4 pending founder choice, +rotation/revocation ownership, live order and rollback. Attended custody child +`scripts/keycape_secrets_engine_openbao_custody.py` with fake-only tests +(`tests/test_keycape_secrets_engine_openbao_custody.py`) refuses without an exact +founder decision id recorded in the approved CCR. Nothing executed, generated or +applied; nothing added to active GitOps. The founder approval it needs is T09. + +2026-10-09: KEY-WP-0037 finished 2026-10-07 (registration live, issuance +verified 10/10, key-cape e0b5fae); CCR-2026-0033/0034 applied. Source for this +lane is now complete as **CCR-2026-0038** (proposed, +`credential-change-requests/CCR-2026-0038-secrets-engine-service-jwt-login.yaml`). +The delegated applier gained a narrow `service-jwt-login` request type +(`scripts/service_jwt_lane.py`): auth mount fixed to `keycape-services`, policy +must equal the generated login-self policy, role/config keys allow-listed, +TTLs at most 15m, uses 1..16, `token_no_default_policy`, refusal if the mount +exists with another type, and a value-free readback compare after the writes. +Policy `secrets-engine-login-self` now also carries explicit `deny` on +`platform/{data,metadata}/workloads/keycape/secrets-engine-openbao` so +ops-mason invariant (ii) (no capability on the client-secret path) holds even +if identity groups were ever attached. Tests: `tests/test_service_jwt_lane.py` +(8) plus the updated contract test; full suite unchanged apart from 5 +pre-existing failures unrelated to this lane. + +Founder apply (one line; approval, binding confirmation and silent apply): + +```bash +cd /home/worsch/railiance-platform && python3 scripts/credential-change.py approve CCR-2026-0038 --reviewer "platform-owner, key-cape-owner, secrets-engine-owner (founder Bernd Worsch)" --comment "Approve service-JWT login lane: mount keycape-services, role secrets-engine-openbao, policy secrets-engine-login-self (self endpoints + deny on the client-secret path)." && python3 scripts/credential-change.py confirm-binding CCR-2026-0038 --reviewer founder --comment "Bound claims match KeyCape registration: aud secrets-engine-openbao, sub service:secrets-engine, principal_type service, tenant tenant:platform, roles secrets-engine, scope openbao:login; issuer https://kc.coulomb.social RS256." && warden access openbao-platform-admin-login --exec -- python3 /home/worsch/railiance-platform/scripts/credential-change.py applier-apply CCR-2026-0038 --confirm "DELEGATED APPLY CCR-2026-0038" --actor founder --record-state-hub --quiet +``` + +Done after that: login proof with a real KeyCape service JWT +(SECRETS-WP-0012-T03 probe-openbao; policies exactly +[secrets-engine-login-self], capabilities-self deny on the guarded paths, +revoke-self), negative claim checks, then publish the consumer contract file +and ops-mason MASON-WP-0009-T05 rechecks invariant (ii). + +## Implement the platform operator-write CCR contract and Fluid lane + +```task +id: RPF-WP-0035-T03 +status: wait +decision_id: 62aa95e4-3ccc-4867-8806-5e3db005b483 +blocking_reason: "Founder session done 2026-10-09; waits on ops-mason verification (MASON-WP-0005-T05) and the FT-WP-0002 owner salt CAS probe." +priority: high +depends_on: [MASON-WP-0005-T05, FT-WP-0002] +state_hub_task_id: "f47e5bc5-6f3c-54bb-aa35-260ad03892ba" +``` + +2026-10-11: requalified — founder part done; remaining waits are owner tasks. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. + +Supersedes RPF-WP-0033-T02. Design: +`docs/credential-lane-designs/fluid-telegram-operator-kv.md`. + +Platform owns the per-path capability schema/validator, exact OpenBao policy +and accepted custody coordinates. MASON-WP-0005 owns construction coordination +and engine integration; KeyCape/NetKingdom own OIDC/MFA and group membership; +FT-WP-0002 owns client CAS, prefix correction, output containment and Telegram +application acceptance. Retain the existing read-only CCR semantics. + +**Unblock:** accept tenant/path and per-entry field/capability matrix; confirm +actual group/assurance and callbacks, reviewed construction contract and writer +authority. Contract review can proceed without a live credential; the final +schema cannot be treated as accepted solely because a draft exists. + +**Done when:** validation rejects broad/unsupported grants, the consumer proves +CAS=0 first-write behavior and no value output, approved identities can perform +only the exact matrix, negative/expiry/revocation checks pass, custody is seeded +through the separate writer, and the verified route has a safe handoff receipt. +The unattended adapter remains a separate demand and gets no operator session. + +2026-10-03: the historical D1 routing was withdrawn because provision/delivery already completed. Owner routing now uses decision `ab85ae8a-12a0-4945-ae64-1d3d86d5b26f`, specifically independent archive readback and coordinated interim audit bearer revocation acceptance. `needs_human` and the wait remain; resolving a decision requires owner receipt re-check, not automatic completion. + +## Accept the needed signing lane and deliver it to the owning runtime + +```task +id: RPF-WP-0035-T04 +status: done +priority: medium +state_hub_task_id: "35a85846-61d5-54ce-8b18-ede45733d53c" +``` + +Supersedes RPF-WP-0034-T02. Design: +`docs/credential-lane-designs/state-hub-preflight-signing.md`. + +Platform owns signing-key custody, exact read policy/role and scoped delivery +acceptance. State Hub owns API chart/env wiring, all-replica rotation fencing +and health. FLEX-WP-0020-T05 owns the rename preflight/cutover dependency; +STATE-WP-0085-T09's adoption-plan delivery is already complete. + +**Unblock:** State Hub/repo-manager and the consuming migration owner confirm +that this transitional State Hub lane is still needed during retirement; +record the target runtime, namespace/SA/auth audience and an executable +rotation fence, plus approved writer and deployment window. Do not broaden the +lane into a general repository-rename authority or provision for a stale demand. + +**Done when:** protected one-time generation, API-only ESO delivery, negative +access checks and a non-mutating signed preflight pass; every API replica uses +the accepted version; rotation/invalidation and recovery are evidenced. No +repository rename is part of S3 lane acceptance. If demand is withdrawn, record +the owning decision and cancel this task explicitly rather than provision it. + +2026-09-05 continuation: verified the live primary cluster identity and healthy +single API replica; the signing ExternalSecret is still absent. Added a writer +guard against wrong-cluster kubeconfigs before any OpenBao access. The default +workstation kubeconfig's local port-forward listener was unavailable. Activation +still needs the contained attended OIDC/MFA login and the acceptance evidence +above; source preparation is not live completion. See +`history/2026-09-05-preflight-signing-activation-readiness.md`. + + +Completed 2026-09-05: user-led attended activation generated version 1 and +rotated to version 2 with every API replica stopped. Exact access and negative +identity checks passed; ESO delivery and API-only exposure passed; the recovered +single API replica accepts new signed preflight and rejects its predecessor by +signature, with healthy primary identity and no preflight blockers. State Hub +chart commit `49e3182`, Helm revision 59. No repository rename executed. +Evidence: `docs/evidence/RPF-WP-0035-T04-signing-activation-2026-09-05.json`; +closure: `history/2026-09-05-preflight-signing-activation-complete.md`. + +## Admit KeyCape approval-engine client custody and delivery + +```task +id: RPF-WP-0035-T05 +status: done +needs_human: false +intervention_note: "" +priority: high +state_hub_task_id: "e15d62c9-e5da-5721-a135-87c050f7851c" +``` + +Answers KEY-WP-0013-T02 (State Hub message +`278a3ebe-b529-49f6-bd1a-e3ebcf318260`). Admission: +`docs/credential-lane-designs/keycape-approval-clients.md`; requests +`CCR-2026-0017` (secrets-engine-approval) and `CCR-2026-0018` +(approval-engine-operator). + +Platform owns the two custody paths, exact-path read policies, Kubernetes auth +roles and the KeyCape-side ESO delivery. KeyCape owns the client registrations, +issuance, claim set and disablement; approval-engine owns approval semantics and +audit of emitted actions. + +2026-09-08 admission response: both proposed KV paths confirmed unchanged; field +`client_secret` corrected to `CLIENT_SECRET` for the uppercase KV convention and +CCR validator. Kubernetes delivery references confirmed against the live `sso` +namespace, which already resolves `KEYCAPE_RAPP_QONTO_CLIENT_SECRET` through the +same `secretKeyRef{key: client-secret}` shape on image `main-153258b`. Attended +authority named as the governed `openbao-platform-admin-login` lane +(`founder_required`); KeyCape's refusal to treat its generic `warden plan` +database match as authorization was correct. Rollout agreed as one attended +session with the KeyCape reading build pinned but undeployed, ordered after the +Authelia issuer precondition in message `c8b1ad10-dae8-48fb-a0ea-7e2a101c54bf`. +Policies, ClusterSecretStores and ExternalSecrets are written and validated, +none applied. + +**Unblock:** owner approval of both CCRs, a KeyCape image that reads both +environment names, the settled Authelia `iss` pin, and a founder-attended window +date. Verifier-side custody only — no client-side read lane is admitted, and no +scope beyond the two declared sets. + +**Done when:** both CCRs are approved, the attended window seeds both KV paths +with CAS=0, policies/roles/stores/ExternalSecrets apply and sync, the KeyCape +build resolves both environment names, KeyCape evidences live JWKS signature and +exact claim bindings with excess scopes denied (operator `consume`, human +`consume`), platform evidences cross-path/wrong-identity/out-of-namespace denial, +and every receipt is metadata-only. If either registration is abandoned, KeyCape +disables it before the KV version is destroyed. + +### 2026-09-08 actual upstream issuer returned + +The admitted KeyCape one-shot probe verified the actual signed upstream issuer +as **`https://auth.coulomb.social`** at 21:44:44 UTC. Signature, audience, +validity window and nonce checks passed and the pinned Job exited 0. All +five temporary resources and the Pod were removed; normal KeyCape Deployment +and config Secret metadata are unchanged. See +`docs/evidence/2026-09-08-keycape-upstream-issuer-proof.json`. + +T05's unknown-issuer input is resolved. The configuration owner still ensures +`authelia.issuer` equals that exact HTTPS value before the custody window. +Both CCRs remain proposed and await the named reviews; this probe grants no +custody mutation or client-side read. Keep the current authority preflight and +this signed-token proof as separate receipts. Live ESO/client/approval and +separate audit/client-side custody acceptance remain open. + + +2026-09-09 configuration and review-queue return: NetKingdom pinned the verified +HTTPS issuer, using an atomic UID/resourceVersion test and independent readback. +Secret revision `58713343`; unrelated configuration and signing key unchanged; +no Deployment rollout. Receipt: `docs/evidence/2026-09-09-keycape-upstream-issuer-pin.json`. + +The two existing CCRs remain proposed. They now link to concrete pending review +decisions, with platform-operator and key-cape-owner named explicitly. See +`docs/credential-lane-designs/keycape-approval-clients-review.md`. T05 owns this +human dependency; HFACT consumes it without a duplicate approval request. +Custody/ESO activation, client verification and the separate client-side/audit +lanes remain open. Current CLI apply plans refuse the proposed requests. + + +2026-09-09 admission: the user explicitly approved both CCRs in the named +platform-operator and key-cape-owner roles. Both source requests are approved +and both linked Hub decisions are resolved. Receipt: +`docs/evidence/2026-09-09-keycape-approval-admission.json`. The completed review +intervention is cleared; T05 is progress while the contained first-provision +procedure is exercised and the admitted attended rollout is carried through. +Verifier-side scope, separate client-side/audit lanes and live acceptance remain +as defined in the reviewed requests. + +2026-09-09 verifier-side return: both CCRs are verified after recorded user approval, +CAS=0 version-1 custody, native read/auth denials and revocation, Valid stores, +SecretSynced delivery, compatible KeyCape rollout and live positive/negative +service checks. Existing human OpenBao login passed again on the new image. +Receipt: `docs/evidence/2026-09-09-keycape-verifier-admission.json`. Failed attempts restored the compatible +configuration/image and detached delivery; final resume reused version 1. + +T05 is done against its verifier-only acceptance criteria. Its former +"remains progress for client-side reads" statement expanded the task past its +reviewed scope; that residual now has the live owner task RPF-WP-0035-T06 below. +Neither Warden fetch selector is resolvable through these verifier-only CCRs. +Do not re-request the completed two named reviews or reseed these paths. +Rotation is a distinct, version-guarded operation. + +## Dispose of the ungoverned whynot-design npm duplicate + +```task +id: RPF-WP-0035-T07 +status: wait +decision_id: 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa +blocking_reason: "Hold kept by D3 until SECRETS-WP-0006-T06 moves publishing off the legacy whynot-design npm path; then rotate and prove a fresh publish." +priority: high +depends_on: [SECRETS-WP-0006-T06] +state_hub_task_id: "0dd7c9a5-65a0-53fc-b6c3-5ad96ee56f7b" +``` + +2026-10-11: requalified after founder decision D3 (trust root, build-mode default) resolved — see the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. + +Found by the attended session on 2026-09-10 while settling the npm field +question. `secret/coulomb/whynot-design/npm/publish` exists at version 1, +created 2026-07-03T15:00:44Z and never updated — five days after the governed +lane at `platform/workloads/coulomb/whynot-design/npm-publish` was verified +(CCR-2026-0001, 2026-06-28). It sits outside that lane's exact-path policy and +outside any CCR here. Receipt: +`docs/evidence/2026-09-10-npm-lane-field-resolution.json`. + +Only metadata was read. Its field names were not enumerated, its value was not +read, and nothing was deleted — a location holding real credential material is +disposed of deliberately, not tidied away in the session that found it. + +The likely explanation, unconfirmed: secrets-engine reports the lane's field +under a lowercase name that is absent from the governed path, and their catalog +declares this legacy location. If their proven pilot publish read from here, +then a working production lane has been running off an ungoverned duplicate, +and the governed lane's acceptance evidence describes a path the consumer does +not use. That is worth establishing before anything is removed. + +**Operator decision, 2026-09-15:** destroy the legacy path after confirming the +governed lane is the live consumer. Do not wrap `secret/coulomb/whynot-design/npm/publish` +in a CCR. Value remains unread. Platform will ask secrets-engine which path +publish actually reads before any attended destroy. + +**Unblock:** admit the governed exact-path AppRole policy and coordinated catalog +path/field migration under SECRETS-WP-0006-T06; prove native delivery from the +governed lane before the attended legacy destroy. secrets-engine confirmed the +legacy source path on 2026-09-21 (message `355424d4-17ab-435e-9e1d-6921775cb4a2`). +No comparison of values, fingerprints, lengths or shapes is needed or authorized. + +**Done when:** the legacy path's provenance and consumer are established, the +governed lane is confirmed as the one in use or the consumer is moved to it as a +reviewed lane change, the duplicate is destroyed under the September 15 operator decision and the +disposition is recorded. Do not bring the legacy duplicate under a CCR. If the value proves to be live and +ungoverned, treat it as an exposure with the same custody rules as RPF-WP-0027: +never record the value, fingerprint, length or shape. + +2026-10-03: human gate routed to hub decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa (D3: Trust root and secret custody: unseal/recovery holders, break-glass, and the deferred rotation of exposed material) by CUST-WP-0075; resolving that decision raises C-40 here. + +## Admit the separate approval client-side readers + +```task +id: RPF-WP-0035-T06 +status: done +decision_id: 62aa95e4-3ccc-4867-8806-5e3db005b483 +priority: high +intervention_note: "" +state_hub_task_id: "68bff751-e48b-548b-8fb4-dfb3b16210c2" +``` + +2026-10-11: requalified after founder decision D1 (attended window standing rule) resolved — see the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. + +**Current return, 2026-09-10:** CCR-2026-0020 is cancelled on the requesting +Approval Engine owner's explicit withdrawal, source +`849c75bb094613ff6ac1a1d4cda56a745520c5a1:docs/keycape-service-registrations.md`. +No presenter exists and no reader is wanted. The validator now accepts terminal +cancellation of an incomplete request only with owner/source/reason evidence, +retained declared omissions and a disabled, non-resolvable front door. Apply +remains refused. No live identity, policy, role, custody or verifier was changed. +Human approval follows existing INFD-WP-0001-T07/T08; future service requesters +need a separate narrow registration, not this withdrawn bundle. + +**Operator binding confirmed, 2026-09-14:** The user explicitly selected +`net-kingdom-admins` and authorized applying CCR-2026-0019. Its OIDC role is +`secrets-engine-approval-client-workload-kv-read`, aligned with the platform +applier naming rule; policy and exact KV scope are unchanged. The metadata dry +run passes with no warnings. Attended apply and exact role/policy readback passed; the enclosing Warden +command exited successfully after its self-revocation/cleanup. Receipt: +`docs/evidence/2026-09-14-ccr0019-operator-binding.json`. Scoped delivery proof +remains pending; the front door stays non-resolvable. Completed CCR-2026-0017/0018 +remain closed and T06 stays `wait`. + +Consumer procedure returned in source, 2026-09-10: +`secrets-engine@98d72ceb1dbcff2d2a80fb8c3058d76777a0ac93:docs/approval-service-auth.md` +specifies the requested private 0700 runtime directory, 0600 file outside Git, +path-only use, claim/consume lifetime and attended cleanup including hard-stop +residual inspection. CCR-2026-0019 retains this source-review comment without +changing admission status. Exact group, required-owner review, attended apply and +native positive/negative/cleanup evidence remain. No role or credential changed. + +Separate retained owner decision: KeyCape's 2026-09-10 return +`21427688-725f-4dea-aab4-7c78fd4328d2` identifies the already-live, unpresented +CCR-2026-0018 registration. Approval Engine, KeyCape and Platform must explicitly +decide retention/expiry or coordinated disablement. Reader cancellation neither +disables that live registration nor accepts indefinite retention. Keep this +disposition in T06; it is separate from the wanted CCR-2026-0019 factory reader. +No unilateral issuer/config/Secret change is authorized by this closeout. + +**Operator decision, 2026-09-15:** coordinated disablement with KeyCape and +Approval Engine. Platform will not disable the live registration or verifier +delivery unilaterally. T06 stays `wait` until both owners confirm the joint +disablement sequence. + +Residual handoff from RPF-WP-0035-T05; consumes the completed verifier custody +without extending CCR-2026-0017/0018. Owner: railiance-platform with the named +secrets-engine and approval-engine operator consumers. + +Prepare separate exact-reader admissions for existing version-1 paths: +`platform/workloads/secrets-engine/approval-client` and +`platform/workloads/approval-engine/operator-client`, field `CLIENT_SECRET`. +Name each actual actor/placement, bounded authentication/reader policy, protected +consumer delivery, cleanup/revocation and rollback, then obtain the required +owner reviews for those concrete requests. No reseeding, rotation, shared reader, +reuse of the sso verifier Secret, or implicit operator consume grant. + +2026-09-09 preparation return: both exact-reader admissions are prepared as +`CCR-2026-0019` (secrets-engine) and `CCR-2026-0020` (approval-engine-operator), +with their exact-path read policies written and committed. Design: +`docs/credential-lane-designs/keycape-approval-client-side-readers.md`. Both +reuse existing version-1 custody; no reseed, rotation, shared reader or verifier +Secret reuse. Both are `in_flight` and unapproved; nothing is applied. + +Read-only survey findings that decided the two shapes. Namespace +`secrets-engine` holds only ServiceAccount `secrets-engine` with no workload, +and `flex-auth/flex-auth-secrets-engine` is that consumer's PDP, not the +service; secrets-engine consumes the client secret through an operator-run CLI +reading `SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE` (SECRETS-WP-0009-T03, +revision `9eb07fd`). So reader 1 is an attended operator-workstation OIDC lane, +not an ESO lane. approval-engine has no namespace, workload or Service; its +manifest is unapplied, and no owner source names who presents the operator +client — approval-engine verifies these tokens and never holds them. + +**Unblock:** reader 1 needs the exact NetKingdom/KeyCape group claim for the +authorized operator, plus secrets-engine confirming the file mode and removal +step; `openbao.auth` is declared missing rather than filled with a placeholder +that would later read as a confirmed binding. Reader 2 needs its owner to name +the presenting actor and placement, or to record that no client-side reader is +wanted so the request is cancelled. Naming a reader for the widest approval +scope without that decision is the failure this task exists to avoid. + +**Done when:** each reader has a named actor and placement, a completed and +reviewed auth binding, admitted delivery, and its owner approvals; positive and +negative checks pass, including cross-path, wrong-identity and parent-list +denial and operator `consume` denial; or the request is explicitly cancelled by +its owner. Rotation stays a distinct version-guarded operation shared with +CCR-2026-0017/0018. + + +Consumer input supplied 2026-09-09: secrets-engine now implements per-request +`secrets-engine-approval` exchange from a protected temporary client-secret file, +with exact approval-engine / tenant:platform / read-or-consume scope, no token +file and no fallback. The pinned KeyCape image and actual Approval Engine source +passed its disposable component exercise; only the PDP was a sequencing double. +See `secrets-engine/docs/approval-service-auth.md` and the metadata receipt +`secrets-engine/docs/evidence/2026-09-09-approval-identity-exercise.json`. + +Routing check: `warden route show keycape-secrets-engine-approval-client` is +unknown; generic `openbao-api-key` routes ownership only and is not a delegable +lane. Do not advertise a working fetch command before this task supplies its +own accepted native delivery return. Client-side admission is not ready for a +new human decision until exact requests and contained execution/rollback checks +are reviewable; the completed verifier reviews must not recur in that queue. + +**Done when:** each retained consumer admission is approved and implemented, or +its requesting owner explicitly cancels it (CCR-2026-0020 now satisfies that branch); +actual consumer authentication/delivery succeeds; sibling/listing/wrong-reader +and scope refusals plus reader expiry/revocation/cleanup are evidenced; version-1 +custody and verifier availability remain intact. Return the source and metadata +receipts to HFACT-WP-0001-T03, SECRETS-WP-0009-T03 and APPROVAL-WP-0002-T01/T05. +Audit receiver/sender custody remains AUDIT-WP-0009-T09 and must not be folded +into this client-identity grant. + +2026-10-03: the historical D1 routing was withdrawn because provision/delivery already completed. Owner routing now uses decision `ab85ae8a-12a0-4945-ae64-1d3d86d5b26f`, specifically independent archive readback and coordinated interim audit bearer revocation acceptance. `needs_human` and the wait remain; resolving a decision requires owner receipt re-check, not automatic completion. + +## Admit the two factory audit-sender custody lanes + +```task +id: RPF-WP-0035-T08 +status: wait +decision_id: "ab85ae8a-12a0-4945-ae64-1d3d86d5b26f" +blocking_reason: "Approved conditionally (ab85ae8a, standing change rule): executes once independent archive readback and the interim bearer revocation receipt exist; owner work with audit-core." +priority: high +assignee: railiance-platform +intervention_note: null +blocked_on: message-from:audit-core +state_hub_task_id: "0ef52c26-2669-5cd1-8149-b1f6fcdda6cb" +``` + +2026-10-11: requalified — ab85ae8a resolved under the standing build-mode change rule. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. + +2026-10-03 correction (CUST-WP-0075, informed-decision sitting draft `55fb078`): the D1 routing was wrong. First provision and delivery already ran on 2026-09-11, with seed and delivery receipts, and the review packet says not to repeat them. The task is not a provisioning-window act. It was taken off decision D1, and its blocking_reason now names what actually remains. railiance-platform should re-qualify it further if the receiver rollout has since happened. + +2026-10-03 (CUST-WP-0075): re-qualified. The receiver rollout already happened: Audit Core c82e0442de0f carries evidence_kind and native producer accepted/duplicate and scope/read-refusal checks passed 2026-09-11 10:42 UTC (`docs/credential-lane-designs/factory-audit-senders-review.md`, `factory-native-acceptance.md`). Remaining: independent archive readback and audit bearer-revocation acceptance. Still not a provisioning-window act. + +Consume AUDIT-WP-0009-T09/T11's exact declarations for approval-engine and +informed-decision: tenant:platform, write-only, load-bearing, redact. Source +preparation supplies two approved CCRs, independent exact-path policies/ESO +projections, and the attended `factory_audit_custody.py` first-provision helper. +Its CAS and request-provenance checks preserve unrelated senders and recover +interrupted writes without generating replacements. The user approved all named review roles on 2026-09-11. Native +provision/delivery/receiver-ingestion acceptance remains open. + +The 2026-09-11 native read-only check found an additional deployment dependency: +receiver image c2fe39a is 1/1 Ready but does not support evidence_kind. The +synthetic native probe now refuses before any credential read. Current receiver +source passes. AUDIT T09/T11 retain compatible release/source-manifest rollout; +Approval Engine's missing namespace remains its owner prerequisite. + +[Review packet](../docs/credential-lane-designs/factory-audit-senders-review.md). +[Preparation evidence](../docs/evidence/2026-09-11-factory-audit-custody.json). +Do not fold these senders into CCR-2026-0019 or the cancelled CCR-2026-0020. + +2026-10-03: the historical D1 routing was withdrawn because provision/delivery already completed. Owner routing now uses decision `ab85ae8a-12a0-4945-ae64-1d3d86d5b26f`, specifically independent archive readback and coordinated interim audit bearer revocation acceptance. `needs_human` and the wait remain; resolving a decision requires owner receipt re-check, not automatic completion. + +## Dependency review — 2026-09-06 + +### 2026-09-09 tenant question resolved — no change needed + +The `tenant:coulomb` / `tenant:platform` discrepancy raised against T02 is not a +discrepancy. KeyCape confirms decision `5ed3fb35-eca9-413a-82b9-95171ba85bf6` +binds the *approval chain* to the landlord zone and was applied to exactly two +registrations, `secrets-engine-approval` and `approval-engine-operator`. +`codex-railiance-platform` is not in that chain and correctly stays +`tenant:coulomb`, as do `secrets-engine-openbao` and the human directory default. +KEY-WP-0013-T04 records this and their +`TestServiceRegistrationTenantsAreExactPerDecision` pins every reviewed client's +tenant against the real fixture, so a reintroduced alias fails their build. + +`docs/credential-lane-designs/secrets-engine-service-jwt.md` already says +`tenant:coulomb` and is therefore correct as written. Bind the exact role to +`tenant:coulomb`. Do not "correct" it to `tenant:platform` later; that would +break the issued claim. + +SECRETS-WP-0008-T02 now records the local PIP claim/validation join implemented +and tested. Its remaining gate is the unreachable approval-engine claim endpoint +and access-engine Check (SECRETS-WP-0007-T04). Do not carry forward the old local +stub as a blocker. T02 still needs the accepted service issuer/JWKS, claims, +audience and consumer binding. T03 still needs confirmed operator group/tenant +and consumer semantics; T04 is already complete. No new owner acceptance inferred. + + +### 2026-09-08 attended capability preflight + +HFACT-WP-0001's continued execution verified the existing first-provision +front door without provisioning anything. `warden plan` selected +`openbao-platform-admin-login` / founder_required / one oidc_login. The native +`scripts/openbao-attended-exec.py` supplied the existing WSL browser launcher; +a direct attempt had no launcher and failed before handoff. Its first revocation +result was unconfirmed and is preserved in the receipt rather than silently +reclassified. The two subsequent contained sessions completed self-revocation +and helper cleanup. + +`scripts/keycape-approval-custody-preflight.py --receipt ` is +a silent, read-only command for that envelope. The installed Bao CLI supports +one path per `token capabilities` call; the final run used six single-path calls. +Create/update authority is present on both exact policy paths, both Kubernetes +auth-role paths and both KV data paths from CCR-2026-0017/0018. This is authority +metadata, not a secret-data read or policy/role/KV mutation. Receipt: +`docs/evidence/2026-09-08-keycape-approval-custody-preflight.json`. + +T05 remains wait: both CCRs are still proposed, the actual upstream ID-token +issuer precondition remains open, and no verifier-side credential is provisioned. +Client-side retrieval and audit-sender custody are still separate owner returns. +The capability receipt is not a review approval or service readiness proof. + +### Sender approval recorded — 2026-09-11 + +The user replied "good, go on" to the explicit CCR-2026-0021/0022 question naming +platform-operator, audit-core-owner and both producer owners. Both exact requests +are approved; their existing State Hub decisions are resolved. This clears the +review request only. Receiver rollout, namespace readiness, attended custody and +positive/negative native evidence remain the execution gates. + +### Native receiver and attended execution return — 2026-09-11 + +Audit Core c82e0442de0f is deployed and passes the native synthetic contract. +Its existing 32-event chain/head is unchanged. The owner Approval Engine +namespace now exists and all four ESO objects pass strict server dry-run. +The two exact ingress rules are applied; sender custody/ESO application is not. + +Warden selected founder_required/oidc_login and attempted the reviewed silent +seed command through the contained wrapper. Login failed before command +handoff; no custody receipt was created. Warden could not confirm remote +session revocation. Await operator browser/MFA feedback and login/session +resolution. Approvals remain valid; T08 waits for the identity act, not review. + +The receiver check now selects component=receiver, so retained attestation-job +pods cannot falsely block a stable receiver. Eighteen custody tests pass. +[Native return](../docs/evidence/2026-09-11-factory-receiver-live.json). + +### Attended custody retry succeeded — 2026-09-11 + +The user requested a retry and completed the attended identity act. Both CCRs +are applied: independent version-1 custody and registry CAS 7→8 succeeded, +preserving other senders/fields. Warden exited 0 with self-revocation confirmed +for this session. This does not retrospectively attest the earlier failed +login's remote session state. The old login wait is cleared; T08 is progress +for ESO delivery, exact native reader/namespace refusals, receiver reload and +producer acceptance. No token is reseeded or rotated on continuation. +[Seed receipt](../docs/evidence/2026-09-11-factory-audit-sender-seed.json). + +### Delivery prepared; fresh login did not hand off — 2026-09-11 + +The delivery helper and custody receipt are committed at `44314ca` and exactly +projected on primary/railiance01. Seven delivery tests and 18 custody tests +(including 11 actual disposable OpenBao cases) pass. The fresh attended +delivery invocation exited 5 before command handoff; no native delivery +receipt exists, no ESO application or receiver restart ran. The receiver +remains 1/1 Ready on c82e0442de0f. Warden could not confirm revocation for this +failed login; that is separate from the successful seed session. T08 now waits +for this delivery identity act. Neither CCR review nor first provision reopens. + +[Delivery return](../docs/evidence/2026-09-11-factory-audit-delivery-login.json). +Use the reviewed delivery command with a new receipt after login feedback. +Actual producer ingestion/duplicate/refusal and bearer revocation acceptance +remain open under AUDIT-WP-0009-T09/T11. + +### Native delivery accepted — 2026-09-11 08:27 UTC + +The user returned and requested another login attempt. It succeeded and the +reviewed delivery command completed in 28 seconds. Both native ESO readers +passed exact-path access and explicit sibling/registry/metadata/listing denials, +wrong-SA/namespace refusal and the coding-agent deny check. Each 900-second +reader session was revoked and lookup refusal verified. Both intended +ExternalSecrets are Ready; exact producer value and ESO ownership match. The +disallowed namespace produced an explicit store restriction and no Secret; +the uniquely owned namespace is now absent. + +Registry version 8 matches the delivered receiver JSON. Audit Core reloaded +and remains 1/1 Ready on c82e0442de0f; /readyz reports operational/durable with +tamper_evidence=false, as bounded by AUDIT-WP-0009-T12. Warden exited 0 and +confirmed self-revocation for this successful session. Earlier failed login +receipts remain historical and are not retroactively attested. + +The login wait is cleared. T08 remains progress and CCR21/22 remain applied +for native producer ingestion/duplicate/scope/read-refusal and bearer +revocation acceptance under AUDIT T09/T11. No credentials were reseeded or +rotated. No producer was deployed and no factory attempt or paid call ran. +[Native delivery receipt](../docs/evidence/2026-09-11-factory-audit-delivery-live.json). + +### Native producer return — 2026-09-11 10:42 UTC + +Both actual source adapters used their own ESO-delivered credential inside a +bounded native Job. Each synthetic event received 202, retained its durable +outbox after an injected lost receipt, and received duplicate 200 after a fresh +process reopened the outbox. Each source's reconciliation reported exactly one +record for its unique probe class. Wrong source/tenant, seven archive read +routes, sibling reconciliation and invalid-bearer requests were refused. + +The first Informed Decision probe exposed a private-store startup defect: +fsGroup volumes add inherited setgid to a newly created 0700 directory. +Informed Decision `1a12223` corrects creation without repairing unsafe existing +data. The regression failed before the fix; the available suite now passes +335 tests with 39 optional checks skipped. Native retry verifies the exact +corrected helper. Six probe tests and eleven rebuilt-container checks pass, +including actual setgid-volume startup, restart/restore and cleanup. + +All temporary probe Jobs, Pods, ConfigMaps and NetworkPolicies are verified +absent. Receiver c82e0442 remains operational/durable; tamper_evidence=false. +Producer verification requires no operator registry read or new OpenBao login. +Independent readback is a separate attended command consuming the successful +receipt; it must not rerun the producer jobs. Both attended verification-login +attempts this session ended before handoff (exit 5; remote revocation +unconfirmed), so independent archive readback remains pending. + +This proves synthetic outbox delivery/recovery with native sender credentials, +not domain-transaction atomicity, deployed producer services or human binding. +An invalid bearer is not a revoked formerly valid bearer. AUDIT T09/T11 and +RPF T08 remain progress for independent readback and audit bearer-revocation +acceptance; CCR21/22 remain applied. Service/policy/human admission and native +runtime/spend gates remain separate. Factory attempts and paid calls remain 0. + +[Native producer receipt](../docs/evidence/2026-09-11-factory-native-producers.json). + + +## Independent audit readback — 2026-09-11 13:11 UTC + +The existing attended readback command completed with exit 0 while the user +was present. Independent read-only identity `operator` found both retained +`factory-audit-20260911104152-94aced` producer events with exact source and +`tenant:platform`; receiver integrity is intact across 36 events. The wrapper +confirmed this session's self-revocation and private-helper cleanup. Prior +failed sessions' unconfirmed revocation remains historical unresolved evidence. + +This supersedes the earlier readback-login wait. No producer jobs were rerun, +no credentials were reseeded and no event was written by the reader. The +inherited zero registry-read count belongs to the producer phase; the attended +reader did consume registry version 8. CCR21/22 stay applied and owner tasks +stay progress pending formerly-valid audit-bearer revocation acceptance. +Service/domain-transaction/human and factory runtime/spend admission remain +separate; factory attempts and paid model calls remain zero. + +Receipt: [native readback](../docs/evidence/2026-09-11-factory-native-readback.json). + + +### T06 native reader acceptance — 2026-09-14 + +The scoped Warden login lane `secrets-engine-approval-client-login` now routes +to the applied OIDC reader and the platform's silent preflight. The installed +Warden package still carries an older catalog; use the explicit source catalog +`WARDEN_ROUTING_CATALOG=/home/worsch/ops-warden/registry/routing/catalog.yaml` +until its next normal installation refresh. This lane authenticates the reader; +it is not a raw secret fetch or retained-file delivery interface. + +Live effective-policy and capability checks passed: only exact data/metadata +read, no sibling secret, parent listing, write or control-plane authority. +A second contained session read existing version 1 into an operator-owned 0600 +file in a private 0700 runtime tmpfs directory. The native consumer exchanged +separate read and consume scopes; Approval Engine verified the read token before +returning 404 for a fresh nonexistent approval, and refused the consume-only +token's read request with 403. No approval was created, bound or consumed. +Both Warden sessions exited 0 after self-revocation/helper cleanup; temporary +credential file and directory were removed. Four refusal/path/redirect tests +pass. Receipts are in platform `docs/evidence/2026-09-14-ccr0019-{reader-preflight,delivery-check}.json`. + +Remaining: an actual nonmember login refusal, real approval claim/consume, +separate narrow requester admission and deployed Informed Decision review with +an explicitly admitted human mandate. The reader group alone grants no review +mandate. T03 and CCR delivery activation remain open; no OpenRouter key was read. + + +### T03 scoped review deployment — 2026-09-14 + +The operator explicitly admitted `net-kingdom-admins` as the human review group +for only the T03 apply, verify and read-only key-check records, separately from +its credential-reader membership. The review service is live and ready at +https://decisions.coulomb.social with verified KeyCape groups, fresh MFA and a +dedicated caller-bound Flex Auth policy. Its mandate does not grant consumption. + +The new `secrets-engine-requester` client has subject `secrets-engine`, tenant +`tenant:platform`, role `secrets-engine-requester`, and only `approval:create`. +CCR-2026-0024 and CCR-2026-0025 provide distinct verifier and attended reader +custody. Native signature, subject, scope and TTL checks passed; excess scopes +and a wrong secret were refused. Existing consumer identity is unchanged. +Three real requested approvals were created with human control, required count +one, and zero entries. Platform evidence is +`docs/evidence/2026-09-14-t03-native-approval-requests.json`. + +Review image: sha256:8f55bcecf37a8d65f96e073510b1ffb4636c0a91d75e1ee7d582ad4bce8b953a. +Policy image: sha256:c9f028b49dfcede930a9cc48757ec8371ecc71d20b1bfee2733e55298dffcc7c. +Review tests: 339 passed, 39 optional integration tests skipped; 11 container +checks and HIGH/CRITICAL image scan passed. Policy checks: 57 local and 6 native +caller checks passed, using synthetic subjects, not human binding evidence. +Approval Engine CPU request was reduced from 25m to 10m after observing 1m use; +review requests 20m and its PDP 5m. Limits are unchanged. Native services ready. + +Remaining T03 gate: the operator's exact signed-in account is needed to address +three prepared immutable memos, followed by real acknowledgements and acceptance +in Informed Decision. No human entry or consume has been generated by an agent. +Then execute claim -> validated PDP Check -> CAS consume separately for apply, +verify and exec using scoped attended authority, and capture native denial, +revocation, workload health and key-check evidence. No OpenRouter credential has +been read and no inference or spend was performed. T03 remains waiting; this +entry supersedes earlier statements that requester or group admission is missing. + +### 2026-09-27 T02/T07 source follow-up + +T02 now has the exact proposed RS256 configuration, bounded JWT role and +self-only policy in `openbao/`. Operator correction: platform infrastructure +belongs to `tenant:platform`; KeyCape registration and consumer preflight are +corrected together. Existing live registrations must be migrated and verified. +Public discovery confirms issuer/JWKS/token endpoint. Registration/custody, +attended provisioning and live rejection/cleanup evidence remain required; +T02 stays wait. No service credential or backend entitlement was issued. + +T07 consumed the confirmed legacy consumer return. The stale value-comparison +ask is removed. WARDEN-WP-0037-T03's no-rotation hold stays in force until the +governed native migration is evidenced. Legacy destruction follows migration; +neither source reconciliation nor the historical pilot is that evidence. + +## Loose-end review — 2026-09-27 + +T08 now explicitly waits for formerly-valid audit-bearer revocation acceptance from AUDIT-WP-0009-T09/T11. Native delivery, producer checks and independent readback already passed; do not reopen login, seed or review gates. T02/T03/T06/T07 retain their latest exact service registration, signed-in human acceptance, owner delivery/disposition and governed-consumer migration dependencies. No credential value was read or changed. + +## Founder approval of the CCR-2026-0033 custody child + +```task +id: RPF-WP-0035-T09 +status: done +needs_human: false +priority: high +decision_id: 2ab717e1-7881-491e-aed4-28946550c289 +depends_on: [] +state_hub_task_id: "7c753be6-42f1-58a6-a4a4-a7ed686f461f" +``` + +Packets: `credential-change-requests/CCR-2026-0033.yaml` (custody child, KV +metadata, issuer ESO reader, live_order, rollback) and `CCR-2026-0034.yaml` +(attended operator OIDC reader, in-memory, 5m/4 uses). Revised 2026-10-06 per +founder direction; the consumer file is removed. Exact founder commands: +`docs/provisioning-windows/2026-10-04-1000/keycape/founder-runbook.md`. Live +order: custody child → platform applies policies/roles/ESO (Secret +sso/keycape-secrets-engine-openbao-client exists) → NetKingdom env patch +(NK-WP-0046) → KeyCape registration merge (KEY-WP-0037-T03) → KeyCape restart +immediately → live issuance check (KEY-WP-0037-T04) → OpenBao JWT role/login +proof (RPF-WP-0035-T02). Each live step after the custody run needs its own go. + + +2026-10-11 evidence reconciliation: T09's approval obligation is complete. +Authoritative founder decision 2ab717e1-7881-491e-aed4-28946550c289 is resolved, +decided by Bernd Worsch at2026-10-06T13:07:18.627943Z, and explicitly covers +A1 design, A2 CCR33/CCR34 plus owner reviews/bound claims, and A3 one custody run. +The approval comments in both CCRs record those exact grants; both source records +are applied. MASON-WP-0009-T02/T03 additionally record the October8 approval/owner-delivery +reconciliation and reference KeyCape +`docs/evidence/2026-10-07-secrets-engine-openbao-issuance.json`. Closing this approval task clears +its obsolete human gate. It grants no new run, credential access, rotation or +service execution: RPF35T02/T12, MASON9T05 and SECRETS8T06 retain their actual +native/adoption obligations. Historical per-step exclusions remain above. + +## Retire the approval-engine-operator client delivery and custody (CCR-2026-0018) + +```task +id: RPF-WP-0035-T10 +status: wait +priority: medium +blocked_on: message-from:approval-engine +blocking_reason: "Blue lane under the standing change rule (removing access, with reviewed order, rollback and receipts): executes once approval-engine confirms there is no presenter. No founder window needed." +state_hub_task_id: "af2d4dba-f83a-5819-9a8b-671bae799364" +``` + +2026-10-11: access removal is blue lane (canon build-mode-change-authorization_v0.1); no founder window. + +key-cape proposed a joint disablement on 2026-10-10 (message `7f2128fb`). +Precondition: approval-engine reconfirms there is no presenter. The order is the +reverse of provisioning, so KeyCape never starts with a client whose secret is +missing. key-cape removes the registration and restarts (1), checks that +`invalid_client` is returned (2), and net-kingdom removes +`KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET` from Deployment `sso/keycape` +(3). Platform steps, amended from the proposal: + +4. **Delivery.** In one commit, remove ExternalSecret + `sso/keycape-approval-engine-operator-client` from + `argocd/platform-addons/openbao-secretstore/keycape-approval-clients.externalsecrets.yaml` + and ClusterSecretStore `openbao-keycape-approval-engine-operator` from + `openbao-keycape-approval-clients.clustersecretstore.yaml`. Keep the + secrets-engine-approval objects. Then bump `targetRevision` of + `argocd/railiance01/applications/openbao-secretstore.application.yaml`. + The app is pinned and has no automated sync, so nothing changes live until + an attended `argocd app sync openbao-secretstore --prune` runs after step 3. + Verify, value-free, that the ExternalSecret, the store and Secret + `sso/keycape-approval-engine-operator-client` are gone. The target is + `creationPolicy: Owner` and `deletionPolicy: Retain`, so the Secret should + follow its owner; delete it explicitly if it does not. Then delete OpenBao + role `auth/kubernetes/role/external-secrets-keycape-approval-engine-operator` + and policy `workload-kv-read-keycape-approval-engine-operator`, and remove + their source files. Also remove policy + `workload-kv-read-approval-engine-operator-client` (CCR-2026-0020, cancelled) + if it exists live. +5. **Custody.** Run the metadata delete of + `platform/workloads/approval-engine/operator-client`, the destruction + foreseen in `docs/credential-lane-designs/keycape-approval-clients.md`. A + disabled client's secret has no recovery value; a narrowed successor gets a + fresh secret through the mint-secret lane at `platform/workloads/keycape/`. + Only after step 2's `invalid_client` receipt. Keep the agent boundary denies + for that path until the delete is verified, then drop them. + +2026-10-10: key-cape agreed the amended steps 4 and 5 (message `59304e33`, +KEY-WP-0013 c528daa). Waits on approval-engine confirming there is no presenter, +and on the founder's window. + +The manifest edits wait for the scheduled window, so a stray `targetRevision` +bump cannot remove KeyCape's secret before step 3. + + +## Complete independent service-JWT source preparation and execution design + +```task +id: RPF-WP-0035-T11 +status: done +priority: high +state_hub_task_id: "7952c8cd-c066-5b72-a025-78d24341c1ab" +``` + +2026-10-11: Platform-scoped agent loaded this checkout's AGENTS.md and INTENT, +RPF35-T02, actual CCR38/login contract/typed applier and Secrets Engine's native +claim/PDP/CAS/provider/apply flow. Frozen Secrets v2 manifest and all named staged +bytes/proof were hash-verified. Independent tests reproduced three source +counterexamples while retaining the frozen files: close exception can coexist +with native_verified=true; successful result needs no source/session fingerprints; +HTTP200 mixed errors+data envelope is accepted. Original 37 tests plus the three +counterexample tests pass (40 cases, synthetic/local only). Proposed owner design +`docs/credential-lane-designs/secrets-engine-task-execution-entitlement.md` binds +an explicit metadata-only execute-ccr action, immutable typed owner bodies, +CAS/journal replay control, 120s/12-use owner-held task token, independent payload +validation, native ACL endpoint bounds, revoke/readback and recovery. This completed +preparation record is established before the formal source review. It grants no +live authority and does not close T02 or represent acceptance by another owner. +Formal review artifact follows under this work item; all implementation/live +obligations are retained in T12 and existing T02 rather than a finished residual. + +## Implement and prove the separately approved task execution entitlement + +```task +id: RPF-WP-0035-T12 +status: progress +priority: high +depends_on: [RPF-WP-0035-T11] +state_hub_task_id: "914bf383-c13f-5247-9af0-91d7e3a4784a" +``` + +Review the proposed task-execution-grant schema and exact metadata-only execute-ccr +backend with Secrets Engine/access-engine/approval-engine; implement trusted service +subject and served consume verification, durable one-shot reservation, bounded +owner-held token issuer/middleware and exact source/payload enforcement. Obtain +separate exact founder approval/attendance before provisioning or live actions. +Prove disposable intended result and identity/action/target/body/time/replay/crash/ +cleanup negatives, and return owner-native acceptance. Existing service login policy +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 +v2 counterexamples are corrected: close exceptions cannot pass through stale true +flags, complete typed source/session/attempted/revoke evidence is required, and +mixed HTTP200 errors/data fails closed. 49 maker/current-provider tests plus three +independent regressions passed (52, synthetic/local only). Platform source review +`docs/reviews/2026-10-11-secrets-service-jwt-v3-platform-review.json` approves only +the corrected boundary verifier and truthful packet. Prior changes-requested v2 +review is retained; this is not a second native success or changed authority. +No native or founder go is granted. T02, T12 and Secrets T06 remain open; the +metadata-only execute-ccr pilot is explicitly insufficient for generic lifecycle +adoption. All consume-verification/journal/action/body/backend implementation and +native acceptance gates remain real prerequisites, not architecture promises. + + +2026-10-11 T11 readiness refinement: independently reviewed frozen consumer +manifest `709b9dddd1c8f00a995b1ff14264323fbe080ed95df244270ce9607528878fa7`. +Platform source review `docs/reviews/2026-10-11-secrets-execution-readiness-v2-platform-review.json` +approves the local fail-closed guard and proposed consumer contract direction only. +All ten current protected backend handlers pass provider args through readiness +before authorization/consume; session revoke checks before handle reads. Configured +auto/explicit service-jwt therefore cannot burn a protected-action approval or +open the insufficient login-self backend. Explicit env/bootstrap and self-only +probes retain their existing contracts; local compromise/reactivate and disabled +destroy remain separate. Existing ambient-token route readiness reads are unchanged, +not claimed governed by this guard. 58 isolated tests passed: original36 plus20 +provider/action cases, handler enumeration and actual session-revoke ordering. +Prior v2 changes-requested and v3 boundary source reviews remain unchanged, same +T11 trial. No runtime grant, native acceptance, new Access/Approval semantics, +backend entitlement or full Lifecycle adoption is approved. T02/T12/SecretsT06 and +WP13 implementation/native gates remain open; the metadata pilot substitutes for +none of the ordinary generic lifecycle outcomes. + + +## Complete independent KEY39 v4 lifecycle source review preparation + +```task +id: RPF-WP-0035-T13 +status: done +priority: high +state_hub_task_id: "8f6966fa-6e9a-58b2-8b40-c47340ec19d1" +``` + +2026-10-11: Platform agent loaded its owner instructions/INTENT and RPF35-T02, +actual applied CCR38, exact native role/policy source and frozen KEY39 lifecycle +v4 manifest `d079f85c47d5c2635e1ee633ea4d1346b1737ce2be9ca273f22a38e620f418d8`. +All staged entry hashes were verified without changing the reviewed inputs. +Source role omits expiration_leeway: its observed actual default must be confirmed +natively, with -1→0 and 0→150 normalization preserved as a bounded model. Source +claims and login-only policy are unchanged. The supplied 41 lifecycle tests pass; +five independent cases also pass, including a new exceptional-response-close +elapsed-bound counterexample and four current-leeway normalization checks. +A 10ms HTTP request can take over70ms because exceptional finally response.close +runs outside the hard elapsed timer. Unknown native login is correctly recorded, +but source total elapsed enforcement is incomplete. This completed owner +preparation record exists before the formal exact source review entry. No +credential was requested/read, native action run, owner checkout edited or native +session grant inherited. T02 retains native role/policy/leeway, negative claims, +cleanup acceptance and MASON gates; KEY39 producer handoff and founder attendance +remain prerequisites. Source correction/re-review is required, not a native success. + + +2026-10-11 T13 correction preparation (same trial): exact KEY39 v5 manifest +`d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285` +and all entry hashes verified. The v4 exceptional-close finding is addressed: +one 20ms abort is clipped to original remaining time and the nonresettable +aggregate cleanup budget; zero calls after aggregate expiry; failed abort stays +unconfirmed/unknown with signal state restored. 46 supplied lifecycle checks and +five independent regressions pass; the unchanged runner's earlier17 tests remain +valid against identical source SHA. Existing T13 preparation record is retained +before the formal v5 exact source review. V4 changes-requested review and earlier +failed history remain retained, with no native success, inherited grant or scope +promotion. Actual private producer admission, fresh installed role/leeway/policy +observation, authenticated founder window and native refusal/cleanup remain gates. + + +2026-10-11 T11 journal preparation refinement (source-only): independently +inspected frozen root-maker v2 manifest +`341cac1de945d2fbe8f516119909bd44fc37e85dccfb38b451981d4de2d402e7`, +verified all input hashes, and ran14 supplied tests plus5 independent checks. +The actual four-process race admits once; a separately forked process exiting +immediately after pending intent leaves durable unknown/nonresumable state. +Private SQLite modes, lock admission and binding/replay protections remain guarded. +Three independent counterexamples show after_call accepts False,0.0 or text'0' +as persisted integer ordinal0, contrary to before_call's exact integer validation. +Completed preparation under this existing done ownerT11 record precedes formal +review; require identical ordinal/call-digest validation before confirmation so +malformed calls cannot clear pending outcome. RPF35-T12 retains caller/PDP/consume +validation, exact body compiler/adapter and owner-private cleanup/recovery after +unknown; this unintegrated primitive proves none of those or full contract/native +acceptance. Inputs/prior reviews are preserved; no privileged/native action occurred. + + +2026-10-11 T11 journal v3 correction preparation (same trial): frozen manifest +`905ff6666d05df6a19d165a535579d917d7de31a43e066c2afc34a57e8cbdc9d` +verified against every file. Strict after_call integer0..63 and64hex string digest +validation now precedes SQLite confirmation; False,0.0 or text'0' cannot clear +pending external outcome.15 supplied tests plus5 independent regressions pass, +including realfork abrupt exit after durable intent and privatefile checks. +This existing completed ownerT11 refinement precedes the formal corrected-source +review. Prior v2 refusal is retained; no extra success/authority trial is counted. +Acceptance is only unintegrated journal source. Actual caller/PDP/consume validation, +independently compiled body/ordinal plan, restricted adapter, owner-private unknown +cleanup recovery and native/whole-contract adoption remain live RPF35-T12 gates. + + +2026-10-11 T13 node-route source preparation refinement (same trial): exact +successor manifest `23a972abc5175e121e594726d2b7f7a28c62109253f58f12623eaf2e5564a035` +verified against all entries. Platform Service openbao-ui-gateway8080 and its +nginx /v1 proxy agree with the proposed bounded founder-held node port-forward; +workstation tunnel/DNS assumptions are not reused as node evidence.54 supplied +lifecycle/route checks plus6 independent public-health/abort checks pass, offline. +Initial execute/recovery health pin and repeated pre-create check fail before +native/Secret effects; public-preflight reads neither stdin nor Kubernetes. +Completed ownT13 preparation precedes this fresh exact source review, not reuse +of v5 acknowledgment. Main execute currently reads private stdin before execute's +health gate; the packet's pre-token-input phrase is not accepted. Private producer +handoff must preserve actual ordering or move the health gate before credential +input when implemented. Public cluster digest detects drift, not authenticated +estate identity. No route was opened or observed; no source packet authenticates +a founder approval. Native role/plugin version and raw/effective expiration, +clock-skew and not-before leeways require actual owner observation/semantic proof. +Issuer readiness_state/Argo ownership is not established by the inspected source; +unmapped means production-tier. If adopted/production-approved, temporary direct +CAS requires an explicit reviewed one-use maintenance exception and close-down +entry, plus reconciliation, before native go. No helper/source acknowledgment +waives that gate. Private token producer/callback/client/custody admission and +actual native negative/revoke/restoration/cleanup remain prerequisites. + + +2026-10-11 T13 v7 node-route preparation refinement (same trial): exact frozen +manifest `90aabec336008d7d51e37c04b5ff96ba8ed6630821cca1a4c333d803367247f7` +and all entries verified.58 supplied lifecycle/route checks plus9 independent +checks pass offline. Actual main now verifies source/public health before private +stdin; execute preserves the original wall/monotonic window rather than renewing +it. Actual child-process partial-pipe counterexample shows unbounded stdin.read +remains blocked beyond a synthetic20ms original window (still alive after100ms +without EOF). No credential/native/provider access is involved. Before private +producer admission, require byte-capped, deadline-bounded JSON input and EOF within +the original window, with fail-closed fixed receipt and no protected effects. +This completed ownT13 preparation precedes formal v7 source changes-requested +review. Retain v6 qualified health-gate agreement and all prior reviews; neither +v7 health ordering nor any source acknowledgment establishes custody completion, +actual route/plugin/leeway/readiness/Argo identity or founder/runtime authority. +Native and private-producer gates remain open; no owner checkout write occurred. + + +2026-10-11 T13 v8 bounded-input correction preparation (same trial): exact +frozen manifest `316fd6005cc4cb7fe14ea69a5fd408aed13e6122afef60afd13d97952b27cf4a` +and all entry hashes verified.65 supplied tests plus12 fresh independent checks +pass offline, including a standard buffered stdin pipe with complete JSON/noEOF +interrupted within original20ms window and restored SIGALRM state; exact64KiB +JSON/EOF accepted and extra byte refused, UTF8 token bytecap enforced, original +wall deadline clips longer monotonic budget. Original preinput window is retained. +Completed ownT13 preparation precedes fresh exact source review; v7 refusal and +v6 qualified agreement/prior reviews retained. This accepts child bounded-input +source only. No parent-ready handshake exists: producer must not eagerly pipe +private inputs, and producer/session/window readiness protocol remains an +unimplemented admission gate. No custody/native completion, sourcepacket approval +authentication, route/plugin/leeway/readiness/Argo observation or inherited grant. +Native prerequisites and producer handoff remain open; no owner checkout changed. + + +2026-10-11 T11 journal v4 canonical-format preparation refinement (same trial): +exact frozen manifest `22c6d225d393b994ab7b44a43904ced1b6d7f74e0e2bebf71ffe99f87191ef4c` +and entry hashes verified. Actual access-engine pkg/api/canonical.go RequestDigest/ +CanonicalDigest, Approval binding.py and Secrets authorization/approval_consume +all use sha256:<64 lowercase hex> request digests. V4 now preserves this exact +request value without prefix stripping; plan/call/evidence content hashes remain +explicit bare64hex.15 supplied checks plus15 fresh independent checks pass, +including malformed prefix/bare/uppercase/trailing whitespace refusal before +reservation, exact persistent value/reopen/mismatch replay, private-file mode, +real child crash after pending intent, ill-typed confirmations and pending-unknown +refusal. Completed ownT11 preparation precedes formal v4 source review; prior v2 +refusal/v3 approval and T13 v7 refusal/v8 source acknowledgment retained. +Acceptance is unintegrated primitive source only; no data migration/native use +of v3 journal has occurred. RPF35-T12 caller/PDP/consume validation, independent +owner plan compiler/restricted adapter, private runtime custody, bounded execution +and owner-private unknown cleanup recovery remain open. No checkout/native writes. + + +2026-10-11 T13 paired ready-child/launcher preparation refinement (same trial): +exact NetV9 manifest `84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14` +and KeyproducerV2 `55a8e794af983ca2202e7a0054d0415a120d2c95018fb03fdb5f28313a109d58` +verified against all entries.74 Net checks plus31 Key checks and10 independent +checks pass offline including a truthful-unknown counterexample: adapter uncertain +private delivery with confirmed=false is copied into launcher receipt, but +consumer_outcome_unknown incorrectly remains false after local SSH teardown. +Complete bytes may have reached remote child before adapter timeout/EOF; local +terminate does not establish remote no-effects. Require sticky propagation of +uncertain delivery into consumer outcome before early return, retain unknown +unless independently established native outcome/recovery. Bad markers/source +pins/injection reject before private reads; extra marker authority/lines reject; +prohibited child output never forwards. Child-ready synchronization is not +authentication/approval. Proposed founder directSSH profile is incompatible with +warden quiet-output lane and not admitted here. Completed ownT13 preparation +precedes formal paired source review; preserve v7 refusal/v8 acknowledgment and +journal histories. No transport grant, coding-agent/private custody admission, +live SSH, credential/native action or owner checkout write. All native role/plugin/ +leeway/route/issuer readiness/Argo/maintenanceexception gates remain prerequisites. + + +2026-10-11 T13 paired producerV4 correction preparation (same trial): exact +producer manifest `d6f641dc1e4aae7d36658d4199d89af14d2fd7a4bd4e526856eb340234f2d33e` +and unchanged NetV9 `84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14` +entry hashes verified.74 Net tests,40 producer checks,9 actual local fakeSSH/ +realwrapper/child synthetic roundtrip regressions and12 fresh Platform checks +pass (135 total). V2 deliveryunknown now remains sticky consumerunknown with +completion=false after earlyreturn/localteardown. V3 Net FIFO acquisition finding +and originalbudgetreset are corrected: actualCLI starts original5s timer before +regular/nofollow/bytebounded contract acquisition, each later preparation phase +clips remaining time. ActualCLI FIFO refuses promptly; expired original budget +never validates/spawns/reads private files. Completed ownT13 preparation precedes +formal corrected pair source agreement. Preserve V2 Platform refusal, Key old +exactV2 acknowledgment and NetV3 refusal; no old acknowledgment repinned. +Agreement is only reviewed source synchronization/launcher behavior, not admission. +Founder authenticated SSH host/profile/channel/runtime pins, exclusive sameUID +file custody and explicit tmpfs file allowance/cleanup, verified human and +coding-agent producer/client/source, native role/plugin/leeway/route/issuer +readiness/Argo/maintenanceexception, refusal/revoke/restoration/readback and +unknown owner recovery remain prerequisites. Warden quietlane cannot carry ready +controlstdout; no transport/custody/native grant. No owner checkout/liveSSH/native. + + +2026-10-11 RPF35-T12 source implementation progress: durable journal source v4 +was independently accepted/committed; owner-derived typed plan compiler component +v2 is prepared at immutable manifest +`41cda74ab32384c789ea8a760475671aadad4e5121335c8f60567ff53d66665b` +with40 offline tests. Completed own bounded source-preparation ADHOCT02 anchor +was persisted at7adbcb4 before the qualified Platform contract/readiness receipt. +T12 progresses for actual source implementation underway, not native acceptance +or full descriptor/authority readiness. The compiler derives local CCR/HCL/role +bodies, matches actual independently pinned Secrets catalogue and revalidates +source/body before fixed calls; no body/target chosen by service is dispatched. +Independent technical integration review and exact full ExecutionContract/owner +grant/baseline/estate/deadline matching, initiating human/User/Tenant PIP/transport +representation/service identity, served PDP/claim/consume correspondence, journal +call dispatch/restricted adapter, native token TTL/uses/custody/revoke/cleanup and +owner-private unknown recovery remain unimplemented integration/native gates. +Actual CCR42 has no eligible approved pilot/catalogue; no source metadata is +invented. T02 and original negative/capability/native evidence remain unchanged; +T11/T13 history retained, no defaultflip/nativego or SecretsT06 closure follows. + + +2026-10-13 T12 source correction: the compiler hashed the catalogue sources but +used cached Python modules with matching paths, which did not establish executing +those exact bytes. A failing regression reproduced the substitution. Catalogue, +package and dependencies now execute from the verified snapshot in temporary +private modules; ambient modules remain untouched and cleanup runs on refusal. +45 compiler plus 15 journal tests pass. The v2 source review remains historical; +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. + +2026-10-16 T12 second source package: Compiler Plans now bind deterministic cleanup +commitments and can bind the complete independently installed engine execution +descriptor. A fixed disposable-loopback native adapter actually materializes exact +per-operation metadata ACL/task token, executes six source-derived metadata calls, +and verifies self-revocation/accessor invalidation and policy retirement. Each +cleanup request has its own durable intent. Native fault tests cover lost response +after real write and policy deletion while task revoke fails. All initiating +identity/approval/consume/grant/state/source admission in this fixture is synthetic; +no production authority or complete native owner acceptance is claimed. Exact +remaining owner schema/admission and mediator work: docs/task-execution-native-fixture.md. +T12 stays progress; no live grant, credential/default change or owner sync performed. + +2026-10-16 T12 third source package: implemented proposed task-execution-grant +immutable projection and exact compiler/native-fixture integration through the +existing descriptor grant ID/SHA fields. Only typed review.comments/evidence.events +are excluded; separate append-only checks reject removal/rewrite/reordering and +unknown guessed evidence exclusions refuse. Owner-pinned grant bytes and validator +source must match independently reconstructed descriptor/source/cleanup/ACL/token +bodies before a native fixture can materialize. Proposed source records gain no +execution authority; no real grant, policy or legacy applier registration added. +Details and remaining production gates: docs/task-execution-grant-projection.md. +T12 remains progress; no owner sync or production effect performed by this package. + +2026-10-16 T12 fourth source package: connected the Secrets-owned strict task +handoff to actual Approval ExecutionConfirmationClient independent GET before +consume-confirmed/native fixture work. Platform accepts no supplied authorization +boolean/object/callback and preserves its exact owner descriptor/grant/Plan join. +Durable CAS/GET intents precede native ordinals through the repaired journal +prefix API. Actual source-server fixture uses signed Access/enforced synthetic +TokenReview, real Approval WSGI/SQLite with distinct synthetic human/service +identities, actual strict Secrets signature/CAS validation and disposable native +OpenBao. Untouched actual task policy remains signed DENY; separately named +synthetic fixture policy is never production admission. Read-scope denial after +CAS records consumed/unknown with zero native effects. Exact dependency/source +hashes, tests and remaining gates are retained with the source package and in +docs/task-execution-mediator-seam.md. No real grant/custody, policy default, owner +sync or native production acceptance is performed; T12 stays progress. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/r1-refusal-counterexamples.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/r1-refusal-counterexamples.json new file mode 100644 index 00000000..7fcfa6e1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/r1-refusal-counterexamples.json @@ -0,0 +1,7 @@ +{ + "damaged_still_string_accessor_accepted_by_R1": true, + "extra_cleanup_key_accepted_by_R1": true, + "unhashable_phase_result": "TypeError", + "broad_rstrip_accepts_tab_change": true, + "scope": "Synthetic offline R1 reader counterexamples only; no backend or credentials." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r1.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r1.json new file mode 100644 index 00000000..a041471f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r1.json @@ -0,0 +1,57 @@ +{ + "schema": "railiance-platform.protected-recovery-validation.v1", + "patch_sha256": "47812816af915534d6aec9431259f4290fb9b109a4fb47a54ed7dbd3b145c615", + "scope": "Only synthetic disposable loopback OpenBao, local owner-private fixture custody and local source checks; no estate backend, real credentials, production grant, live rollout or publication.", + "runs": [ + { + "name": "initial setup", + "result": "8 errors", + "reason": "Missing SECRETS_ENGINE_SOURCE_ROOT for isolated checkout; no native fixture effects reached. Fixed runner environment, no implementation relaxation." + }, + { + "name": "initial process-loss run", + "result": "7 passed, 1 assertion failure", + "reason": "OpenBao per-request UUID differs between reads. Compared stable metadata data/status instead; source recovery behavior unchanged." + }, + { + "name": "broader recovery/native/journal/readback regression", + "result": "48 passed", + "seconds": 393.9, + "command": "SECRETS_ENGINE_SOURCE_ROOT=/home/worsch/secrets-engine PYTHONPATH=/home/worsch/secrets-engine/src:/home/worsch/approval-engine pytest -q tests/test_task_execution_recovery.py tests/test_task_execution_native_fixture.py tests/test_task_execution_journal.py tests/test_task_execution_readback.py", + "limit": "Started before the final recovery-pending observation addition and ninth recovery test; final frozen journal/module/test delta is covered by the final24 run below." + }, + { + "name": "reopened journal process-loss subset", + "result": "3 passed, 5 deselected", + "seconds": 54.42, + "command": "same source environment; pytest -q tests/test_task_execution_recovery.py -k independent_cleanup_never_resumes_business" + }, + { + "name": "final frozen recovery/journal suite", + "result": "24 passed", + "seconds": 159.31, + "command": "SECRETS_ENGINE_SOURCE_ROOT=/home/worsch/secrets-engine PYTHONPATH=/home/worsch/secrets-engine/src:/home/worsch/approval-engine pytest -q tests/test_task_execution_recovery.py tests/test_task_execution_journal.py", + "covers": [ + "actual executor os.fork/os._exit at issued, business write/lost response, revoke/lost response", + "reopened public Journal plus separate independently retained private custody", + "native actual accessor invalidation and exact grant-policy absence", + "no mint/consume/business calls in recovery transport trace", + "original approval and recovery permit replay refusals", + "lost mint response without accessor remains unresolved debt", + "revoked/expired/stale/private-drift and public-flag refusals", + "revoked backend credential cannot fake cleanup success; fresh independent owner attempt recovers", + "same-name different policy never deleted", + "private file permissions and binding refusal", + "lost recovery response remains unknown across subsequent independently confirmed cleanup" + ] + } + ], + "checks": { + "canonical_git_apply_check": "passed", + "isolated_git_diff_check": "passed", + "source_py_compile": "passed", + "frozen_patch_and_source_after_hashes": "passed" + }, + "repeat_counts": "Runs overlap; counts are not additive unique tests.", + "authority_granted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r2.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r2.json new file mode 100644 index 00000000..554ece08 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-recovery/validation-r2.json @@ -0,0 +1,45 @@ +{ + "schema": "railiance-platform.protected-recovery-validation.v2", + "same_trial_as": "validation-r1.json", + "full_patch_sha256": "61f516c876fd612b83300e9913f0e80aad836a50014310797913c7c4505e3114", + "delta_patch_sha256": "6f97bbaa957673a3b28070d1550ffe72e709a573be183062de21e7f7145e8380", + "preserved_counterexamples": "r1-refusal-counterexamples.json", + "runs": [ + { + "name": "fast nested schema and normalization cases", + "result": "26 passed, 11 deselected", + "seconds": 0.54 + }, + { + "name": "recovery and journal successor regression", + "result": "53 passed", + "seconds": 168.51, + "command": "SECRETS_ENGINE_SOURCE_ROOT=/home/worsch/secrets-engine PYTHONPATH=/home/worsch/secrets-engine/src:/home/worsch/approval-engine pytest -q tests/test_task_execution_recovery.py tests/test_task_execution_journal.py", + "limit": "Started before final strict native duplicate/nonfinite JSON decode refinement and expanded malformed-body byte fixtures; final frozen29cases below verify that delta including real native cleanup." + }, + { + "name": "final frozen capsule/normalization/native HTTP absence/replay cases", + "result": "29 passed, 9 deselected", + "seconds": 58.81, + "command": "same source environment; pytest -q tests/test_task_execution_recovery.py -k \"capsule or terminal or absence\"", + "includes": [ + "MAC damage and wrong owner key refusal", + "24 authenticated malformed nested schema and typed-value cases", + "duplicate/nonfinite JSON, malformed MAC and invalid framing refusal", + "single optional terminal LF comparison, all other whitespace refused", + "both initial GET and post-DELETE GET misleading404 bodies, duplicate keys, nonfinite response and malformed bytes refuse", + "real owner-issued native cleanup succeeds after failed synthetic HTTP attempts", + "authenticated prepared capsule rollback cannot hide actual token issuance" + ] + } + ], + "checks": { + "full_patch_canonical_apply": "passed", + "delta_patch_frozen_R1_apply": "passed", + "diff_check": "passed", + "py_compile": "passed", + "all_frozen_after_hashes": "passed" + }, + "counts": "Runs overlap; do not add counts as unique tests.", + "authority": "Synthetic disposable loopback fixtures only. No production custody/key selection, credential retrieval, live scope grant or canonical Platform mutation." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/README.md b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/README.md new file mode 100644 index 00000000..7c7ff3d6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/README.md @@ -0,0 +1,21 @@ +# Native budget and exact source slice + +`budget-source-slice-r1.patch` is independently reviewable against canonical +Platform `b9eaa20` (frozen recovery R2 integrated). Its five files tighten absolute +native/cleanup budgets, actual credential expiry and actual-v2 descriptor source +bytes.17final frozen tests pass, including a real disposable native positive. +Exact preimages/hashes, source-only owner review and run history are retained. + +This slice does **not** establish actual v2 full-stack ALLOW/native acceptance. +`retained-registry-failure-r1.json` preserves the concrete500source correspondence +failure beforeCAS/native; `temporal-contract-r1.json` preserves the original +15s/30s+5s timing contradiction and agreed bounded temporal source successor. +No registry/policy/digest workaround, fixture backdating or TTL extension is used. + +`followup-draft/` retains the unfinished actual-provider fixture and tests as +source preparation, excluded from the integration patch. Access/Secrets owners +must supply authoritative normalized preapproval and immediateCAS precision joins; +then the actual builder→source→strictprepare→consume/GET→sixnativecalls/readback/ +cleanup positive and refusal cases must be completed under CUST-WP-0080/T12. +The legacy ec215 proof, signer proposal and recovery R2 remain preserved. +No canonical Platform mutation, real credential, live grant or publication. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-owner-review-r1.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-owner-review-r1.json new file mode 100644 index 00000000..2463eb2f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-owner-review-r1.json @@ -0,0 +1,12 @@ +{ + "reviewer": "railiance-platform-owner (agent railiance-platform, codex)", + "decision": "approved", + "scope": "Independent budget/source/credential-expiry source slice only. No actual-v2 whole-stack/native admission approval.", + "comment": "Platform INTENT principles3 Durable by Default,4 Secure Custody and5 Operable and Observable require bounded effects and verifiable owner source. Under RPF-WP-0035-T12 reviewed patch sha256:7c17f607dacc9ed4f24fd8032ddac5f4fc9ae2f24555cf87a0b73c871ec8eb19 against exact canonical b9eaa20 preimages, in isolated owner checkout with owner AGENTS/INTENT loaded. Absolute native method/transport/cleanup bounds and effective actual credential expiry narrow existing authority; source bundle checks bind installed descriptor paths/hash bytes before authorization/effects. No global deadline/TTL is extended, no credential renewed/reminted, no generic service or broker is introduced. Native policy absence/JSON use reviewed R2 strict helpers. Only Platform role reviewed.", + "validation": "17 final frozen tests passed including real local native backend and absolute header/body/close/compile failures; failed actual provider correspondence remains explicitly open.", + "native_gates": [ + "Dedicated production owner process/time/runtime enforcement, trusted clock, recovery/custody and exact role/actor admission remain unadmitted.", + "Actual retained registry + source-normalized preapproval + precise immediateCAS full-stack proof is still required; unfinished fixture draft is excluded from this patch." + ], + "authority_granted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-slice-r1.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-slice-r1.patch new file mode 100644 index 00000000..31838b1f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-slice-r1.patch @@ -0,0 +1,545 @@ +diff --git a/docs/task-execution-native-fixture.md b/docs/task-execution-native-fixture.md +index f4ff070..c4292db 100644 +--- a/docs/task-execution-native-fixture.md ++++ b/docs/task-execution-native-fixture.md +@@ -205,3 +205,56 @@ changes during its single read. A valid older prepared capsule still cannot + prove a task token absent when durable journal state/calls reached issuance: + that remains unknown cleanup debt. Authentication alone is not anti-rollback. + No business or consume admission is restored and no production scope is granted. ++ ++ ++## Absolute budgets, effective credential expiry and exact source bytes ++ ++This source successor tightens the disposable native owner adapter; it does not ++establish actual v2 whole-stack ALLOW/native acceptance. The retained-registry ++Approval correspondence failure is preserved in the coordinator artifact. The ++new real provider fixture is retained separately as unfinished follow-up, while ++the existing ec215-based fixture evidence remains its original scoped proof. ++ ++Every native business/grant method now runs inside the earlier wall/monotonic ++operation deadline minus the five-second reserve. After exact owner token lookup, ++the adapter requires aware expire_time and retains the earlier actual expiry and ++conservative mint-start + declared TTL bound, in wall and monotonic coordinates. ++Every subsequent business call uses the minimum of global and credential bounds. ++The descriptor and signed global deadline are never extended or rewritten. ++Missing/expired credential evidence refuses issuance acceptance. Cleanup continues ++under its original separate obligation after either business bound expires. ++ ++The fixture owner main-thread execution profile uses absolute SIGALRM budgets, ++not socket inactivity timeouts alone. Each native transport and cleanup native ++call has a two-second execution deadline, including intent/source checking and ++send/header/body/close work. Larger compiler reconstruction remains within the ++earlier operation/credential window. A 50ms alarm backstop keeps blocked teardown ++interruptible after timeout; it cannot authorize another business effect. Nested ++timers retain the shorter outer deadline rather than refreshing it. Unsupported ++threads, invalid budgets and conflicting ambient timers refuse. This fixed Unix ++fixture profile does not select a production process supervisor or provide a ++shared arbitrary-thread execution service. Native rollout still needs exact owner ++runtime/admission, trusted clock and independent recovery/custody enforcement. ++ ++The actual v2 package/version selects additional mediator checks over the exact ++independently installed descriptor source_bundle before authorization and each ++freshness gate, including pre-native grant entry. It reads bounded regular, ++files without group/world write, non-indirect paths, compares exact source hashes and stat stability, ++and applies an absolute read budget. The existing compiler's own pin set does not ++implicitly authenticate other source_bundle roles; these explicit checks fill ++that gap. No requester-selected path, approval flag or source boolean can enable ++this path. Legacy synthetic fixture proof remains separately scoped and is not ++used to satisfy v2 acceptance. ++ ++Native policy/grant absence now requires exact empty-errors404 and policy HCL ++comparison admits only the already reviewed single optional terminal LF, reusing ++frozen recovery R2 helpers. Native response JSON rejects duplicate/nonfinite data. ++ ++The independent slice tests cover actual header/body drips, stalled close after ++timeout, slow compiler and shorter actual credential expiry before any HTTP ++effect, nested/conflicting/foreign-thread budgets, private source hash drift, ++unsafe modes/indirection/FIFO/oversize refusal, and bounded cleanup after expiry. ++Actual native fixed grant/metadata/readback/cleanup remains a separate positive ++compatibility check. There is no successful actual normalized provider-chain ++claim in this slice; source-managed pre-approval normalization and immediate CAS ++precision must pass the follow-up full-stack proof first. +diff --git a/scripts/task_execution_mediator.py b/scripts/task_execution_mediator.py +index 6614974..ec9d235 100644 +--- a/scripts/task_execution_mediator.py ++++ b/scripts/task_execution_mediator.py +@@ -10,13 +10,15 @@ from dataclasses import dataclass + from datetime import datetime,timezone + import importlib + import json ++import os ++import stat + 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 ++from task_execution_native_fixture import FixtureAdmission,NativeFixtureAdapter,_absolute_budget + + + @dataclass(frozen=True) +@@ -43,7 +45,36 @@ def _strict_dependency(): + raise Refused('strict Secrets task execution handoff dependency unavailable') from None + + ++def _trusted_v2(cfg): ++ return (getattr(cfg,'authorization_policy_package',None)=='secrets-engine.task-execution.service-pilot' ++ and getattr(cfg,'authorization_policy_version',None)=='v2') ++ ++ ++def _read_source_bundle(descriptor): ++ # Paths/hash expectations come from independently installed owner_descriptor, ++ # never a requester-selected file lookup. No bytes or raw OS errors escape. ++ for item in descriptor['source_bundle'].values(): ++ path=Path(item['path']);fd=None ++ try: ++ if not path.is_absolute() or path.resolve()!=path:raise ValueError() ++ fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) ++ before=os.fstat(fd) ++ if not stat.S_ISREG(before.st_mode) or before.st_nlink!=1 or before.st_mode&0o022 or not 00 and previous is not _budget_expired): ++ raise Refused('conflicting owner execution timer') ++ started=time.monotonic() ++ try: ++ signal.signal(signal.SIGALRM,_budget_expired) ++ signal.setitimer(signal.ITIMER_REAL,min(seconds,old) if old>0 else seconds) ++ yield ++ finally: ++ signal.setitimer(signal.ITIMER_REAL,0) ++ elapsed=time.monotonic()-started ++ signal.signal(signal.SIGALRM,previous) ++ if old>0: ++ if old<=elapsed:raise Refused('outer native authority budget exhausted') ++ signal.setitimer(signal.ITIMER_REAL,old-elapsed) ++ ++ ++def _business_window(method): ++ @wraps(method) ++ def bounded(self,*args,**kwargs): ++ remaining=self._remaining_business()-5 ++ with _absolute_budget(remaining):return method(self,*args,**kwargs) ++ return bounded ++ ++ ++def _call_window(method): ++ @wraps(method) ++ def bounded(self,*args,**kwargs): ++ with _absolute_budget(2):return method(self,*args,**kwargs) ++ return bounded ++ ++ + @dataclass(frozen=True) + class PrivateSession: + token: str = field(repr=False) +@@ -101,6 +159,7 @@ class NativeFixtureAdapter: + 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 + self._cleanup_steps={item.kind:0 for item in self.cleanup} ++ self._task_wall_deadline=None;self._task_monotonic_deadline=None + self.recovery_custody=recovery_custody + if recovery_custody is not None: + from task_execution_recovery import FixtureRecoveryCustody +@@ -112,9 +171,17 @@ class NativeFixtureAdapter: + if not self.journal._held or self.journal._admission!=self.binding or self.journal._consumed_admission!=self.binding: + raise Refused('same live consumed owner admission required') + ++ def _remaining_business(self): ++ bounds=[(self.deadline-datetime.now(timezone.utc)).total_seconds(),self.monotonic_deadline-time.monotonic()] ++ task_wall=getattr(self,'_task_wall_deadline',None) ++ task_mono=getattr(self,'_task_monotonic_deadline',None) ++ if task_wall is not None:bounds.append((task_wall-datetime.now(timezone.utc)).total_seconds()) ++ if task_mono is not None:bounds.append(task_mono-time.monotonic()) ++ return min(bounds) ++ + def _fresh(self): + self._live() +- if min((self.deadline-datetime.now(timezone.utc)).total_seconds(),self.monotonic_deadline-time.monotonic())<=5: ++ if self._remaining_business()<=5: + raise Refused('business deadline exhausted') + # The constructor independently rebuilt the complete immutable Plan and + # grant. Recheck every pinned byte before each native effect. Metadata +@@ -122,6 +189,7 @@ class NativeFixtureAdapter: + # creation reconstructs and compares its fixed body before issuance. + for path in self.compiler.pins:self.compiler.read(Path(path)) + ++ @_call_window + def _http(self, method, path, token, body=b''): + # Fixed caller-generated paths only; http.client has no proxy or redirect + # following. Never surface backend body/header or transport exception text. +@@ -133,13 +201,15 @@ class NativeFixtureAdapter: + headers={'X-Vault-Token':token,'Content-Type':'application/json'}) + response=conn.getresponse(); raw=response.read(1024*1024+1) + if len(raw)>1024*1024: raise Refused('oversize native response') +- data=json.loads(raw) if raw else {} ++ data=json.loads(raw,object_pairs_hook=_json_unique,parse_constant=_invalid_constant) if raw else {} + if type(data) is not dict: raise Refused('invalid native response') + return response.status,data + except Exception: + raise Refused('native transport or response unconfirmed') from None + finally: conn.close() + ++ @_business_window ++ @_call_window + def _business(self, method, path, token, payload=b'', task=False): + self._fresh() + if task: +@@ -165,6 +235,7 @@ class NativeFixtureAdapter: + if status not in allowed or data.get('errors'): + raise Refused('native request refused or unverified') + ++ @_business_window + def materialize_grant(self): + self._fresh() + if self.journal.observe(self.binding)['state']!='grant-issuing' or self._policy_attempted: +@@ -173,12 +244,12 @@ class NativeFixtureAdapter: + raise Refused('insufficient lifetime for exact fixed task TTL') + root=self.admission.owner_token; path='sys/policies/acl/'+self.policy_name + status,data=self._business('GET',path,root) +- if status!=404 or data.get('data'): ++ if not _policy_absent(status,data): + raise Refused('per-operation grant policy must be absent') + self._policy_attempted=True + status,data=self._business('PUT',path,root,encoded({'policy':self.hcl}));self._ok(status,data,{200,204}) + status,data=self._business('GET',path,root);self._ok(status,data,{200}) +- if data.get('data',{}).get('policy','').rstrip()!=self.hcl.rstrip(): raise Refused('grant policy readback mismatch') ++ if not _same_policy(data.get('data',{}).get('policy'),self.hcl): raise Refused('grant policy readback mismatch') + body={'policies':[self.policy_name],'no_default_policy':True,'renewable':False, + 'ttl':str(self.ttl)+'s','explicit_max_ttl':str(self.ttl)+'s','num_uses':self.uses, + 'type':'service','display_name':'task-exec-'+self.binding.operation_id,'no_parent':False} +@@ -187,6 +258,7 @@ class NativeFixtureAdapter: + if self.recovery_custody is not None: + self.recovery_custody.checkpoint(self.binding,'mint-intent') + self._mint_attempted=True ++ minted_wall=datetime.now(timezone.utc);minted_mono=time.monotonic() + status,data=self._business('POST','auth/token/create',root,encoded(body));self._ok(status,data,{200}) + auth=data.get('auth',{}) + if type(auth.get('client_token')) is not str or not auth['client_token'] or type(auth.get('accessor')) is not str or not auth['accessor']: +@@ -202,9 +274,19 @@ class NativeFixtureAdapter: + or info.get('num_uses')!=self.uses or not 00 ++ finally: ++ signal.setitimer(signal.ITIMER_REAL,0) ++ signal.signal(signal.SIGALRM,original_handler) ++ ++ ++def test_actual_credential_bound_interrupts_before_later_global_deadline(): ++ native=object.__new__(NativeFixtureAdapter) ++ native.deadline=datetime.now(timezone.utc)+timedelta(minutes=10) ++ native.monotonic_deadline=time.monotonic()+600 ++ native._task_wall_deadline=datetime.now(timezone.utc)+timedelta(seconds=5.05) ++ native._task_monotonic_deadline=time.monotonic()+5.05 ++ native.binding='synthetic';native.plan=None;native.task=object();native._next_metadata=0 ++ native.journal=SimpleNamespace(_held=True,_admission='synthetic',_consumed_admission='synthetic',observe=lambda binding:{'state':'applying'}) ++ native.compiler=SimpleNamespace(pins=[],before_call=lambda *args:time.sleep(.5)) ++ sent=[];native._business=lambda *args,**kwargs:sent.append(True) ++ started=time.monotonic() ++ with pytest.raises(Refused):native.metadata_call(0) ++ assert time.monotonic()-started<.3 and sent==[] ++ ++ ++ ++@pytest.mark.parametrize('case',['valid','drift','writable','symlink','fifo','oversize']) ++def test_exact_owner_source_bundle_is_bounded_and_fails_closed(tmp_path,case): ++ import hashlib ++ import os ++ from task_execution_mediator import _source_bundle ++ source=tmp_path/'owner-source.py';source.write_bytes(b'# independently installed source\n');source.chmod(0o600) ++ roles=['engine_dispatcher','platform_mediator','typed_ccr','policy_auth'] ++ item={'path':str(source),'sha256':'sha256:'+hashlib.sha256(source.read_bytes()).hexdigest()} ++ descriptor={'source_bundle':{role:dict(item) for role in roles}} ++ if case=='valid':_source_bundle(descriptor);return ++ if case=='drift':source.write_bytes(b'# drift\n') ++ elif case=='writable':source.chmod(0o666) ++ elif case=='symlink': ++ target=tmp_path/'target.py';source.rename(target);source.symlink_to(target) ++ elif case=='fifo':source.unlink();os.mkfifo(source,0o600) ++ elif case=='oversize':source.write_bytes(b'X'*1048577) ++ started=time.monotonic() ++ with pytest.raises(Refused):_source_bundle(descriptor) ++ assert time.monotonic()-started<.5 +diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md +index f58a6b9..92a3f3f 100644 +--- a/workplans/RPF-WP-0035-credential-lane-implementation.md ++++ b/workplans/RPF-WP-0035-credential-lane-implementation.md +@@ -1307,3 +1307,12 @@ capsules are owner-key HMAC authenticated, strict nested schemas/ordered cleanup + commitments reject damage, and journal history prevents stale prepared capsules + from hiding issuance. Fixture process-loss source proof remains separate from + unadmitted production custody, integrity-key storage and recovery-role scope. ++ ++2026-10-16 T12 independently reviewable budget/source slice: bound native HTTP ++header/body/close, compiler work and cleanup through fixed owner main-thread ++absolute budgets, with actual task expiry narrowing global authority. Actual v2 ++mediator now verifies each independently installed source_bundle byte hash before ++authorization/effects. No deadline/TTL extension, remint or generic service added. ++Retained registry actual-provider proof still refuses Approval correspondence ++before CAS/native; normalized pre-approval + precise immediate CAS whole-stack ++positive remains live owner follow-up under CUST-WP-0080. T12 stays progress. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-validation-r1.json b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-validation-r1.json new file mode 100644 index 00000000..818c90fe --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/budget-source-validation-r1.json @@ -0,0 +1,41 @@ +{ + "schema": "railiance-platform.native-budget-source-validation.v1", + "patch_sha256": "7c17f607dacc9ed4f24fd8032ddac5f4fc9ae2f24555cf87a0b73c871ec8eb19", + "final_frozen_run": { + "result": "17 passed", + "seconds": 32.33, + "command": "SECRETS_ENGINE_SOURCE_ROOT=/tmp/netkingdom-secrets-trusted-successor PYTHONPATH=/tmp/netkingdom-secrets-trusted-successor/src:/home/worsch/approval-engine pytest -q tests/test_task_execution_native_budget.py tests/test_task_execution_native_fixture.py::test_native_fixed_grant_metadata_denials_and_confirmed_cleanup", + "covers": [ + "true header/body drip and blocked teardown", + "slow compiler interrupted before effect", + "earlier actual credential bound interrupts before later global deadline", + "nested budget cannot refresh, invalid input/foreign thread/ambient timer refusal", + "separate bounded cleanup after business and credential expiry leaves failure unknown but continues independent cleanup", + "bounded exact source bundle valid bytes and drift/modes/symlink/FIFO/oversize refusal", + "actual fixed native grant, six metadata calls/readback and confirmed cleanup compatibility" + ] + }, + "earlier_checks": [ + { + "result": "9 passed", + "scope": "absolute budget tests including actual credential window and blocked close", + "seconds": 6.96 + }, + { + "result": "6 passed", + "scope": "native fixed grant, source drift, lost-response and recovery process-loss compatibility", + "seconds": 71.47 + }, + { + "result": "6 passed", + "scope": "actual-provider refusal cases and frozen source retained registry refusal", + "seconds": 94.58 + }, + { + "result": "2 failed actual-provider positive attempts", + "scope": "retained registry source correspondence failed beforeCAS/native; closed classification preserved in retained-registry-failure-r1.json" + } + ], + "overlap": "Counts overlap, not unique totals. Earlier tests are retained intermediate evidence; final17 checks the frozen slice.", + "whole_positive": "Not established; normalized preapproval receipt and precise immediateCAS source successors require actual full-stack follow-up." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/task_execution_trusted_servers.py b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/task_execution_trusted_servers.py new file mode 100644 index 00000000..e9517b47 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/task_execution_trusted_servers.py @@ -0,0 +1,174 @@ +"""Actual v2 provider fixture; no generic policy, fabricated claim or live identity.""" +from contextlib import contextmanager +from dataclasses import replace +from datetime import datetime,timedelta,timezone +import base64 +import hashlib +import json +import os +from pathlib import Path +import socket +import ssl +import subprocess +import threading +import time +from types import SimpleNamespace +from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer +from wsgiref.simple_server import make_server + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization,hashes +from cryptography.hazmat.primitives.asymmetric import ed25519,rsa +from cryptography.x509.oid import NameOID + +from task_execution_source_servers import QuietWSGI,private,free_port,http +from task_execution_journal import Binding +from task_execution_native_fixture import FixtureAdmission,NativeFixtureAdapter +import task_execution_plan as plan_source +import task_execution_grant as grant + +ACCESS=Path(os.environ.get('TRUSTED_ACCESS_SOURCE_ROOT','/tmp/access-trusted-b2-b3-r2')) +SECRETS=Path(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT','/tmp/netkingdom-secrets-trusted-successor')) +BINARY=Path(os.environ.get('TRUSTED_ACCESS_BINARY','/tmp/netkingdom-access-trusted-r2')) +VERIFIER=Path(os.environ.get('TRUSTED_PDP_VERIFIER','/tmp/secrets-trusted-successor-verifier')) +POLICY_DIGEST=os.environ.get('TRUSTED_POLICY_DIGEST','sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d') +PRINCIPAL='system:serviceaccount:secrets-engine:secrets-engine' + + +def certificate(directory): + key=rsa.generate_private_key(public_exponent=65537,key_size=2048) + name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'localhost')]) + now=datetime.now(timezone.utc) + cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()) + .serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)) + .not_valid_after(now+timedelta(hours=1)) + .add_extension(x509.SubjectAlternativeName([x509.DNSName('localhost')]),critical=False) + .sign(key,hashes.SHA256())) + ca=directory/'trusted-ca.pem';ca.write_bytes(cert.public_bytes(serialization.Encoding.PEM));ca.chmod(0o600) + private_key=directory/'trusted-tls.key' + private_key.write_bytes(key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()));private_key.chmod(0o600) + context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER);context.load_cert_chain(ca,private_key) + return ca,context + + +def serve(server,*,tls=None): + if tls is not None:server.socket=tls.wrap_socket(server.socket,server_side=True) + thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() + return thread + + +@contextmanager +def trusted_providers(directory): + from approval_engine.api import App + from approval_engine.auth import Identity,StaticTokenAuthenticator + from approval_engine.store import Engine + ca,tls=certificate(directory) + state=SimpleNamespace(principal=PRINCIPAL,caller_valid=True) + expiry=int(time.time())+600 + encoded=base64.urlsafe_b64encode(json.dumps({'exp':expiry}).encode()).decode().rstrip('=') + caller='synthetic.'+encoded+'.synthetic';reviewer='synthetic-trusted-reviewer';calls=[] + class Review(BaseHTTPRequestHandler): + def log_message(self,*args):pass + def do_POST(self): + raw=self.rfile.read(int(self.headers.get('Content-Length','0')));body=json.loads(raw) + valid=(state.caller_valid and self.path=='/apis/authentication.k8s.io/v1/tokenreviews' + and self.headers.get('Authorization')=='Bearer '+reviewer + and body.get('spec',{}).get('token')==caller + and 'flex-auth' in body.get('spec',{}).get('audiences',[])) + calls.append(('review',valid)) + response={'status':{'authenticated':valid,'audiences':['flex-auth'],'user':{'username':state.principal}}} + payload=json.dumps(response).encode();self.send_response(200);self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(payload)));self.end_headers();self.wfile.write(payload) + review=ThreadingHTTPServer(('127.0.0.1',0),Review);review_thread=serve(review,tls=tls) + def identity(subject,kind,scopes): + return Identity(subject,'synthetic-whole-source',('approval-engine',),kind,'tenant:platform', + frozenset(),frozenset(scopes),{'level':'aal2','methods':['synthetic']}, + 'fixture:whole-source',tenant_source='directory' if kind=='human' else 'registration') + service='synthetic-whole-service';reader='synthetic-whole-reader';human='synthetic-whole-human' + authenticator=StaticTokenAuthenticator({service:identity('secrets-engine','service',['approval:create','approval:read','approval:consume']), + reader:identity('access-source-reader','service',['approval:read']), + human:identity('whole-human','human',['approval:approve','approval:revoke'])}) + # Explicit synthetic owner observation contract, not an API default change. + engine=Engine(str(directory/'whole-approval.sqlite'),freshness_ttl=120) + app=App(engine,authenticator) + def logged(channel): + def handle(environ,start_response): + calls.append((channel,environ['REQUEST_METHOD'],environ['PATH_INFO'])) + return app(environ,start_response) + return handle + source=make_server('127.0.0.1',0,logged('source'),handler_class=QuietWSGI) + consumer=make_server('127.0.0.1',0,logged('consumer'),handler_class=QuietWSGI) + source_thread=serve(source,tls=tls);consumer_thread=serve(consumer) + reader_file=private(directory/'approval-reader.token',reader) + review_file=private(directory/'reviewer.token',reviewer) + cfg={'source_id':'synthetic-whole-actual-approval','source_revision':'approval-3485415-actual-fixture', + 'origin':'https://localhost:'+str(source.server_port),'store_tenant':'tenant:platform', + 'ca_file':str(ca),'token_file':str(reader_file)} + config=directory/'approval-source.json';private(config,json.dumps(cfg)) + seed_key=ed25519.Ed25519PrivateKey.generate() + b64=lambda value:base64.urlsafe_b64encode(value).decode().rstrip('=') + seed=private(directory/'whole-signing.seed',b64(seed_key.private_bytes(serialization.Encoding.Raw,serialization.PrivateFormat.Raw,serialization.NoEncryption()))) + public=b64(seed_key.public_key().public_bytes(serialization.Encoding.Raw,serialization.PublicFormat.Raw)) + registry=directory/'whole-registry.json';registry.write_bytes((ACCESS/'examples/secrets-engine/registry_snapshot.json').read_bytes()) + policy=ACCESS/'examples/secrets-engine-service-task-execution-v2/policy_package.md' + record=directory/'whole-access-record.jsonl';port=free_port();origin='http://127.0.0.1:'+str(port) + args=[str(BINARY),'serve','--addr','127.0.0.1:'+str(port),'--registry',str(registry),'--policy',str(policy), + '--log',str(record),'--envelope-signing-key-file',str(seed),'--envelope-signing-kid','synthetic-whole-key', + '--caller-auth-mode','enforce','--caller-audience','flex-auth', + '--caller-kubernetes-url','https://localhost:'+str(review.server_port), + '--caller-reviewer-token-file',str(review_file),'--caller-ca-file',str(ca), + '--caller-binding','secrets-engine='+PRINCIPAL,'--approval-source-file',str(config)] + process=subprocess.Popen(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env={}) + try: + for _ in range(100): + if process.poll() is not None:raise RuntimeError('actual trusted Access source exited') + try: + with socket.create_connection(('127.0.0.1',port),timeout=.1):break + except OSError:time.sleep(.025) + else:raise RuntimeError('actual trusted Access source startup unconfirmed') + trust={'schema':'secrets-engine.pdp-trust.v1','evaluator':'flex-auth/local','mode':'standalone', + 'policy_package':'secrets-engine.task-execution.service-pilot','policy_version':'v2', + 'policy_package_digest':POLICY_DIGEST,'keys':{'synthetic-whole-key':public}} + trustfile=private(directory/'whole-pdp-trust.json',json.dumps(trust)) + approval_token=private(directory/'whole-consumer-approval.token',service) + pdp_token=private(directory/'whole-consumer-pdp.token',caller) + from task_execution_mediator import ConfirmationConfig + cfg=SimpleNamespace(approval_url='http://127.0.0.1:'+str(consumer.server_port),approval_token_file=approval_token, + approval_client_secret_file=None,pdp_url=origin,pdp_token_file=pdp_token,pdp_trust_file=trustfile,pdp_verifier=VERIFIER, + authorization_subject_id='secrets-engine',authorization_subject_type='service', + authorization_policy_package=trust['policy_package'],authorization_policy_version='v2',clock_trust_file=None) + yield SimpleNamespace(cfg=cfg,engine=engine,calls=calls,state=state,record=record,process=process, + confirmation=ConfirmationConfig(cfg.approval_url,approval_token),human_token=human, + source_config=config,reader_file=reader_file) + finally: + process.terminate();process.wait(timeout=5) + for server in [source,consumer,review]:server.shutdown();server.server_close() + for thread in [source_thread,consumer_thread,review_thread]:thread.join(5) + engine.close() + + +def install_exact_owner_descriptor(owner,*,window_seconds=60): + """All server startup precedes this exact proposal time; no remint/reissue.""" + native,journal,binding,call,compiler,descriptor=owner + from secrets_engine.execution_contract import ExecutionContract + descriptor=json.loads(json.dumps(descriptor)) + descriptor['backend_budget']={'ttl_seconds':30,'uses':12} + descriptor['operation_deadline']=(datetime.now(timezone.utc)+timedelta(seconds=window_seconds)).replace(microsecond=0).isoformat() + ccr=compiler._derive()[0] + sources={'engine_dispatcher':SECRETS/'src/secrets_engine/task_execution_handoff.py', + 'platform_mediator':Path(__file__).resolve().parents[1]/'scripts/task_execution_mediator.py', + 'typed_ccr':compiler.validator,'policy_auth':compiler.config.platform_repo/ccr['openbao']['policy_file']} + descriptor['source_bundle']={role:{'path':str(path),'sha256':plan_source.tagged(path.read_bytes())} for role,path in sources.items()} + # Exact source descriptor participates in the existing reviewed pure grant + # projection. Synthetic fixture approval is not a production CCR admission. + record=json.loads(json.dumps(__import__('yaml').safe_load(compiler.grant_record.read_text()))) + record['execution'],record['openbao']=compiler.grant_effects(compiler.compile(native.meta['request']),descriptor) + compiler.grant_record.write_text(__import__('yaml').safe_dump(record,sort_keys=False)) + pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=plan_source.tagged(compiler.grant_record.read_bytes()) + compiler=plan_source.Compiler(replace(compiler.config,source_pins=tuple(pins.items()))) + descriptor['grant_immutable_sha256']=grant.immutable_digest(record) + descriptor=ExecutionContract.parse(descriptor).as_dict();raw=plan_source.encoded(descriptor) + plan=compiler.bind_execution(compiler.compile(native.meta['request']),descriptor,raw) + binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest) + admission=replace(native.admission,descriptor=raw) + native=NativeFixtureAdapter(compiler,plan,journal,binding,admission) + return native,journal,binding,call,compiler,descriptor diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/test_task_execution_trusted_whole.py b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/test_task_execution_trusted_whole.py new file mode 100644 index 00000000..419d2905 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/followup-draft/test_task_execution_trusted_whole.py @@ -0,0 +1,294 @@ +"""Exact actual source successor path; legacy fixture evidence remains separate.""" +from contextlib import contextmanager +from dataclasses import replace +from datetime import datetime,timedelta,timezone +import json +from pathlib import Path +import signal +import sys +import threading +import time +from types import SimpleNamespace +import uuid + +import pytest +sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts')) +from test_task_execution_native_fixture import owner,bao_dev,compiler +from task_execution_trusted_servers import trusted_providers,install_exact_owner_descriptor,http,private +import task_execution_trusted_servers as providers_source +from task_execution_mediator import run_native_fixture_task +from task_execution_native_fixture import NativeFixtureAdapter,_absolute_budget +from task_execution_journal import Refused +from secrets_engine.approval_claim import binding_from_check_request +from secrets_engine.approval_consume import _expected_request +from secrets_engine.authorization import approval_binding_digest +from secrets_engine.execution_contract import ExecutionContract + + +@contextmanager +def whole(owner,directory,*,window_seconds=60): + # OpenBao, Approval HTTPS, TokenReview TLS and actual Access CLI are all up + # before proposing an immutable descriptor/deadline or issuing approval. + with trusted_providers(directory) as provider: + native,journal,binding,call,compiler,descriptor=install_exact_owner_descriptor(owner,window_seconds=window_seconds) + snapshot={path:compiler.read(Path(path)) for path in compiler.pins} + entry=compiler._catalogue_entry(snapshot) + approval_id=str(uuid.uuid4()) + entry=replace(entry,approval={**entry.approval,'authorization_id':approval_id}) + request=_expected_request(provider.cfg,entry,'execute-ccr',execution_contract=ExecutionContract.parse(descriptor)) + now=datetime.now(timezone.utc) + body={'id':approval_id,'binding':binding_from_check_request(request),'pdp_digest':approval_binding_digest(request), + 'pdp_path':True,'human_control':True,'required_count':1, + 'validity':{'not_before':(now-timedelta(minutes=1)).isoformat(),'expires_at':(now+timedelta(minutes=3)).isoformat()}} + assert http(provider.cfg.approval_url,'POST','/v1/approvals',body,'synthetic-whole-service')[0]==201 + assert http(provider.cfg.approval_url,'POST','/v1/approvals/'+approval_id+'/entries',{},provider.human_token)[0]==200 + yield SimpleNamespace(native=native,journal=journal,call=call,compiler=compiler,descriptor=descriptor, + entry=entry,provider=provider,cfg=provider.cfg,approval_id=approval_id,request=request) + + +def invoke(fixture): + return run_native_fixture_task(compiler=fixture.compiler,journal=fixture.journal, + plan_request=fixture.native.meta['request'],owner_descriptor=fixture.native.admission.descriptor, + secrets_cfg=fixture.cfg,entry=fixture.entry,confirmation=fixture.provider.confirmation, + native_admission=fixture.native.admission) + + +def untouched(fixture): + assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)==(404,{'errors':[]}) + for ordinal in [1,4]: + path=fixture.compiler.before_call(fixture.native.plan,ordinal).path + assert fixture.call('GET',path)==(404,{'errors':[]}) + + +def test_actual_successor_allow_strict_cas_confirm_six_calls_readback_cleanup(owner,tmp_path,monkeypatch): + with whole(owner,tmp_path) as fixture: + trace=[];private_task=[];issued_native=[] + original=NativeFixtureAdapter._http + def observed(native,method,path,token,body=b''): + result=original(native,method,path,token,body) + trace.append((method,path,result[0])) + if method=='POST' and path=='auth/token/create' and result[0]==200: + private_task.append((result[1]['auth']['accessor'],result[1]['auth']['client_token'])) + issued_native.append(native) + return result + monkeypatch.setattr(NativeFixtureAdapter,'_http',observed) + try:result=invoke(fixture) + except Exception as error: + from urllib.error import HTTPError + cause=error.__cause__ + if isinstance(cause,HTTPError): + raw=cause.read(4097) + known={b'trusted Approval source refused\n':'approval-source-correspondence', + b'service task execution input refused\n':'service-input-correspondence'} + diagnostic={'status':cause.code,'constant_reason':known.get(raw,'other-closed-classification'), + 'native_operations':fixture.journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]} + (tmp_path/'closed-source-refusal.json').write_text(json.dumps(diagnostic)) + print(json.dumps(diagnostic)) + raise + assert result['native_apply']=='verified' and result['native_calls_completed']==6 + assert result['cleanup_confirmed'] and result['business_state']=='cleanup-confirmed' + assert fixture.provider.engine.get(fixture.approval_id).status=='consumed' + records=[json.loads(line) for line in fixture.provider.record.read_bytes().splitlines()] + assert len(records)==1 and records[0]['effect']=='allow' and records[0]['signature']['mode']=='signed' + assert records[0]['id']==result['decision_id'] + assert records[0]['expires_at']==fixture.descriptor['operation_deadline'] + assert records[0]['provenance']['policy_package']=='secrets-engine.task-execution.service-pilot' + assert records[0]['provenance']['policy_version']=='v2' + assert records[0]['provenance']['approval_observation']['source_id']=='synthetic-whole-actual-approval' + assert records[0]['binding']['context']['execution_contract']==fixture.descriptor + native_calls=fixture.journal.db.execute('SELECT ordinal,outcome FROM calls ORDER BY ordinal').fetchall() + assert native_calls[:2]==[(0,'complete'),(1,'complete')] and len(native_calls)==13 + assert all(value=='complete' for _,value in native_calls) + assert fixture.provider.calls.count(('consumer','POST','/v1/approvals/'+fixture.approval_id+'/consume'))==1 + assert fixture.provider.calls.count(('consumer','GET','/v1/approvals/'+fixture.approval_id))==1 + assert any(item==('source','GET','/v1/approvals/'+fixture.approval_id+'/claim') for item in fixture.provider.calls) + expected=fixture.compiler.proposal()['operations'] + assert [(method,path) for method,path,_ in trace if path in {item['path'] for item in expected}]==[(x['method'],x['path']) for x in expected] + assert len(private_task)==1 + assert issued_native[0]._task_wall_deadline0 + finally: + signal.setitimer(signal.ITIMER_REAL,0) + signal.signal(signal.SIGALRM,original_handler) + + +def test_actual_credential_bound_interrupts_before_later_global_deadline(): + native=object.__new__(NativeFixtureAdapter) + native.deadline=datetime.now(timezone.utc)+timedelta(minutes=10) + native.monotonic_deadline=time.monotonic()+600 + native._task_wall_deadline=datetime.now(timezone.utc)+timedelta(seconds=5.05) + native._task_monotonic_deadline=time.monotonic()+5.05 + native.binding='synthetic';native.plan=None;native.task=object();native._next_metadata=0 + native.journal=SimpleNamespace(_held=True,_admission='synthetic',_consumed_admission='synthetic',observe=lambda binding:{'state':'applying'}) + native.compiler=SimpleNamespace(pins=[],before_call=lambda *args:time.sleep(.5)) + sent=[];native._business=lambda *args,**kwargs:sent.append(True) + started=time.monotonic() + with pytest.raises(Refused):native.metadata_call(0) + assert time.monotonic()-started<.3 and sent==[] diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/README.md b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/README.md new file mode 100644 index 00000000..4e835e7e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/README.md @@ -0,0 +1,31 @@ +# Actual v2 whole-chain source proof R1 + +Apply implementation.patch after the frozen budget/source slice SHA7c17f607. +The exact isolated snapshot base, changed-file preimages and hashes, and actual +Access R3 / Secrets R4 / Approval precision dependencies are in manifest.json. +No canonical Platform source was changed by this owner. + +The owner fixture starts OpenBao and actual providers before the immutable +proposal. The real Secrets builder requests signed claim-free DENY normalization +facts from retained Access registry source, then actual Approval App issues and +records synthetic human approval. The original builder request remains unchanged. +Real Access v2 supplies signed ALLOW; actual strict Secrets CAS and independently +verified GET precede six owner-native calls and exact readback/cleanup. Immediate +CAS uses Approval precision source; no sleep or backdating occurs in that path. +Startup readiness polling and the negative registry-restart readiness polling are +not authorization clock waivers. + +The test uses exact 60-second whole-second descriptor deadline, backend TTL30, +synthetic observation TTL120, existing Access120 cap and five-second cleanup +reserve. Credential expiry further narrows business time; all inherited absolute +HTTP/source/cleanup bounds remain active. The public receipt contains only closed +status/counter/boolean evidence, never bearer material, signing seed or accessors. + +Eight tests passed, then the final positive rerun passed after adding public +receipt output and exact precision provenance label. The earlier assertion-only +KeyError is preserved in validation history. Existing retained-registry failure +evidence is preserved in the parent directory. + +This is executable source integration proof with synthetic identities and keys, +not production native admission, Kubernetes/OIDC/MFA proof, live signing custody, +CCR admission or a standing grant. Owner review is source-only. diff --git a/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/implementation.patch new file mode 100644 index 00000000..5f093765 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/protected-execution-trusted-successor/fullchain-r1/implementation.patch @@ -0,0 +1,440 @@ +diff --git a/tests/task_execution_trusted_servers.py b/tests/task_execution_trusted_servers.py +new file mode 100644 +index 0000000..a9dc9f6 +--- /dev/null ++++ b/tests/task_execution_trusted_servers.py +@@ -0,0 +1,189 @@ ++"""Actual v2 provider fixture; no generic policy, fabricated claim or live identity.""" ++from contextlib import contextmanager ++from dataclasses import replace ++from datetime import datetime,timedelta,timezone ++import base64 ++import hashlib ++import json ++import os ++from pathlib import Path ++import socket ++import ssl ++import subprocess ++import threading ++import time ++from types import SimpleNamespace ++from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer ++from wsgiref.simple_server import make_server ++ ++from cryptography import x509 ++from cryptography.hazmat.primitives import serialization,hashes ++from cryptography.hazmat.primitives.asymmetric import ed25519,rsa ++from cryptography.x509.oid import NameOID ++ ++from task_execution_source_servers import QuietWSGI,private,free_port,http ++from task_execution_journal import Binding ++from task_execution_native_fixture import FixtureAdmission,NativeFixtureAdapter ++import task_execution_plan as plan_source ++import task_execution_grant as grant ++ ++ACCESS=Path(os.environ.get('TRUSTED_ACCESS_SOURCE_ROOT','/tmp/access-trusted-b2-b3-r3')) ++SECRETS=Path(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT','/tmp/netkingdom-secrets-trusted-successor')) ++BINARY=Path(os.environ.get('TRUSTED_ACCESS_BINARY','/tmp/netkingdom-access-trusted-r3')) ++VERIFIER=Path(os.environ.get('TRUSTED_PDP_VERIFIER','/tmp/secrets-trusted-successor-verifier')) ++POLICY_DIGEST=os.environ.get('TRUSTED_POLICY_DIGEST','sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d') ++PRINCIPAL='system:serviceaccount:secrets-engine:secrets-engine' ++ ++ ++def certificate(directory): ++ key=rsa.generate_private_key(public_exponent=65537,key_size=2048) ++ name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'localhost')]) ++ now=datetime.now(timezone.utc) ++ cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()) ++ .serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)) ++ .not_valid_after(now+timedelta(hours=1)) ++ .add_extension(x509.SubjectAlternativeName([x509.DNSName('localhost')]),critical=False) ++ .sign(key,hashes.SHA256())) ++ ca=directory/'trusted-ca.pem';ca.write_bytes(cert.public_bytes(serialization.Encoding.PEM));ca.chmod(0o600) ++ private_key=directory/'trusted-tls.key' ++ private_key.write_bytes(key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()));private_key.chmod(0o600) ++ context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER);context.load_cert_chain(ca,private_key) ++ return ca,context ++ ++ ++def serve(server,*,tls=None): ++ if tls is not None:server.socket=tls.wrap_socket(server.socket,server_side=True) ++ thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() ++ return thread ++ ++ ++@contextmanager ++def trusted_providers(directory): ++ from approval_engine.api import App ++ from approval_engine.auth import Identity,StaticTokenAuthenticator ++ from approval_engine.store import Engine ++ ca,tls=certificate(directory) ++ state=SimpleNamespace(principal=PRINCIPAL,caller_valid=True) ++ expiry=int(time.time())+600 ++ encoded=base64.urlsafe_b64encode(json.dumps({'exp':expiry}).encode()).decode().rstrip('=') ++ caller='synthetic.'+encoded+'.synthetic';reviewer='synthetic-trusted-reviewer';calls=[] ++ class Review(BaseHTTPRequestHandler): ++ def log_message(self,*args):pass ++ def do_POST(self): ++ raw=self.rfile.read(int(self.headers.get('Content-Length','0')));body=json.loads(raw) ++ valid=(state.caller_valid and self.path=='/apis/authentication.k8s.io/v1/tokenreviews' ++ and self.headers.get('Authorization')=='Bearer '+reviewer ++ and body.get('spec',{}).get('token')==caller ++ and 'flex-auth' in body.get('spec',{}).get('audiences',[])) ++ calls.append(('review',valid)) ++ response={'status':{'authenticated':valid,'audiences':['flex-auth'],'user':{'username':state.principal}}} ++ payload=json.dumps(response).encode();self.send_response(200);self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(payload)));self.end_headers();self.wfile.write(payload) ++ review=ThreadingHTTPServer(('127.0.0.1',0),Review);review_thread=serve(review,tls=tls) ++ def identity(subject,kind,scopes): ++ return Identity(subject,'synthetic-whole-source',('approval-engine',),kind,'tenant:platform', ++ frozenset(),frozenset(scopes),{'level':'aal2','methods':['synthetic']}, ++ 'fixture:whole-source',tenant_source='directory' if kind=='human' else 'registration') ++ service='synthetic-whole-service';reader='synthetic-whole-reader';human='synthetic-whole-human' ++ authenticator=StaticTokenAuthenticator({service:identity('secrets-engine','service',['approval:create','approval:read','approval:consume']), ++ reader:identity('access-source-reader','service',['approval:read']), ++ human:identity('whole-human','human',['approval:approve','approval:revoke'])}) ++ # Explicit synthetic owner observation contract, not an API default change. ++ engine=Engine(str(directory/'whole-approval.sqlite'),freshness_ttl=120) ++ app=App(engine,authenticator) ++ def logged(channel): ++ def handle(environ,start_response): ++ calls.append((channel,environ['REQUEST_METHOD'],environ['PATH_INFO'])) ++ return app(environ,start_response) ++ return handle ++ source=make_server('127.0.0.1',0,logged('source'),handler_class=QuietWSGI) ++ consumer=make_server('127.0.0.1',0,logged('consumer'),handler_class=QuietWSGI) ++ source_thread=serve(source,tls=tls);consumer_thread=serve(consumer) ++ reader_file=private(directory/'approval-reader.token',reader) ++ review_file=private(directory/'reviewer.token',reviewer) ++ cfg={'source_id':'synthetic-whole-actual-approval','source_revision':'approval-3485415-precision-74eb0ff6-fixture', ++ 'origin':'https://localhost:'+str(source.server_port),'store_tenant':'tenant:platform', ++ 'ca_file':str(ca),'token_file':str(reader_file)} ++ config=directory/'approval-source.json';private(config,json.dumps(cfg)) ++ seed_key=ed25519.Ed25519PrivateKey.generate() ++ b64=lambda value:base64.urlsafe_b64encode(value).decode().rstrip('=') ++ seed=private(directory/'whole-signing.seed',b64(seed_key.private_bytes(serialization.Encoding.Raw,serialization.PrivateFormat.Raw,serialization.NoEncryption()))) ++ public=b64(seed_key.public_key().public_bytes(serialization.Encoding.Raw,serialization.PublicFormat.Raw)) ++ registry=directory/'whole-registry.json';registry.write_bytes((ACCESS/'examples/secrets-engine/registry_snapshot.json').read_bytes()) ++ policy=ACCESS/'examples/secrets-engine-service-task-execution-v2/policy_package.md' ++ record=directory/'whole-access-record.jsonl';port=free_port();origin='http://127.0.0.1:'+str(port) ++ args=[str(BINARY),'serve','--addr','127.0.0.1:'+str(port),'--registry',str(registry),'--policy',str(policy), ++ '--log',str(record),'--envelope-signing-key-file',str(seed),'--envelope-signing-kid','synthetic-whole-key', ++ '--caller-auth-mode','enforce','--caller-audience','flex-auth', ++ '--caller-kubernetes-url','https://localhost:'+str(review.server_port), ++ '--caller-reviewer-token-file',str(review_file),'--caller-ca-file',str(ca), ++ '--caller-binding','secrets-engine='+PRINCIPAL,'--approval-source-file',str(config)] ++ process=subprocess.Popen(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env={}) ++ try: ++ for _ in range(100): ++ if process.poll() is not None:raise RuntimeError('actual trusted Access source exited') ++ try: ++ with socket.create_connection(('127.0.0.1',port),timeout=.1):break ++ except OSError:time.sleep(.025) ++ else:raise RuntimeError('actual trusted Access source startup unconfirmed') ++ trust={'schema':'secrets-engine.pdp-trust.v1','evaluator':'flex-auth/local','mode':'standalone', ++ 'policy_package':'secrets-engine.task-execution.service-pilot','policy_version':'v2', ++ 'policy_package_digest':POLICY_DIGEST,'keys':{'synthetic-whole-key':public}} ++ trustfile=private(directory/'whole-pdp-trust.json',json.dumps(trust)) ++ approval_token=private(directory/'whole-consumer-approval.token',service) ++ pdp_token=private(directory/'whole-consumer-pdp.token',caller) ++ from task_execution_mediator import ConfirmationConfig ++ cfg=SimpleNamespace(approval_url='http://127.0.0.1:'+str(consumer.server_port),approval_token_file=approval_token, ++ approval_client_secret_file=None,pdp_url=origin,pdp_token_file=pdp_token,pdp_trust_file=trustfile,pdp_verifier=VERIFIER, ++ authorization_subject_id='secrets-engine',authorization_subject_type='service', ++ authorization_policy_package=trust['policy_package'],authorization_policy_version='v2',clock_trust_file=None) ++ def restart_registry(): ++ nonlocal process ++ process.terminate();process.wait(timeout=5) ++ changed=json.loads(registry.read_bytes()) ++ for subject in changed['subjects']: ++ if subject['id']=='secrets-engine':subject['metadata']['description']+=' synthetic revision drift' ++ registry.write_text(json.dumps(changed)) ++ process=subprocess.Popen(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env={}) ++ for _ in range(100): ++ if process.poll() is not None:raise RuntimeError('changed-registry source startup refused') ++ try: ++ with socket.create_connection(('127.0.0.1',port),timeout=.1):break ++ except OSError:time.sleep(.025) ++ else:raise RuntimeError('changed-registry source startup unconfirmed') ++ yield SimpleNamespace(cfg=cfg,engine=engine,calls=calls,state=state,record=record,process=process, ++ registry=registry,restart_registry=restart_registry, ++ confirmation=ConfirmationConfig(cfg.approval_url,approval_token),human_token=human, ++ source_config=config,reader_file=reader_file) ++ finally: ++ process.terminate();process.wait(timeout=5) ++ for server in [source,consumer,review]:server.shutdown();server.server_close() ++ for thread in [source_thread,consumer_thread,review_thread]:thread.join(5) ++ engine.close() ++ ++ ++def install_exact_owner_descriptor(owner,*,window_seconds=60): ++ """All server startup precedes this exact proposal time; no remint/reissue.""" ++ native,journal,binding,call,compiler,descriptor=owner ++ from secrets_engine.execution_contract import ExecutionContract ++ descriptor=json.loads(json.dumps(descriptor)) ++ descriptor['backend_budget']={'ttl_seconds':30,'uses':12} ++ descriptor['operation_deadline']=(datetime.now(timezone.utc)+timedelta(seconds=window_seconds)).replace(microsecond=0).isoformat().replace('+00:00','Z') ++ ccr=compiler._derive()[0] ++ sources={'engine_dispatcher':SECRETS/'src/secrets_engine/task_execution_handoff.py', ++ 'platform_mediator':Path(__file__).resolve().parents[1]/'scripts/task_execution_mediator.py', ++ 'typed_ccr':compiler.validator,'policy_auth':compiler.config.platform_repo/ccr['openbao']['policy_file']} ++ descriptor['source_bundle']={role:{'path':str(path),'sha256':plan_source.tagged(path.read_bytes())} for role,path in sources.items()} ++ # Exact source descriptor participates in the existing reviewed pure grant ++ # projection. Synthetic fixture approval is not a production CCR admission. ++ record=json.loads(json.dumps(__import__('yaml').safe_load(compiler.grant_record.read_text()))) ++ record['execution'],record['openbao']=compiler.grant_effects(compiler.compile(native.meta['request']),descriptor) ++ compiler.grant_record.write_text(__import__('yaml').safe_dump(record,sort_keys=False)) ++ pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=plan_source.tagged(compiler.grant_record.read_bytes()) ++ compiler=plan_source.Compiler(replace(compiler.config,source_pins=tuple(pins.items()))) ++ descriptor['grant_immutable_sha256']=grant.immutable_digest(record) ++ descriptor=ExecutionContract.parse(descriptor).as_dict();raw=plan_source.encoded(descriptor) ++ plan=compiler.bind_execution(compiler.compile(native.meta['request']),descriptor,raw) ++ binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest) ++ admission=replace(native.admission,descriptor=raw) ++ native=NativeFixtureAdapter(compiler,plan,journal,binding,admission) ++ return native,journal,binding,call,compiler,descriptor +diff --git a/tests/test_task_execution_trusted_whole.py b/tests/test_task_execution_trusted_whole.py +new file mode 100644 +index 0000000..232fce4 +--- /dev/null ++++ b/tests/test_task_execution_trusted_whole.py +@@ -0,0 +1,195 @@ ++"""Exact actual source successor path; legacy fixture evidence remains separate.""" ++from contextlib import contextmanager ++from dataclasses import replace ++from datetime import datetime,timedelta,timezone ++import json ++from pathlib import Path ++import signal ++import sys ++import threading ++import time ++from types import SimpleNamespace ++import uuid ++ ++import pytest ++sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts')) ++from test_task_execution_native_fixture import owner,bao_dev,compiler ++from task_execution_trusted_servers import trusted_providers,install_exact_owner_descriptor,http,private ++import task_execution_trusted_servers as providers_source ++from task_execution_mediator import run_native_fixture_task ++from task_execution_native_fixture import NativeFixtureAdapter,_absolute_budget ++from task_execution_journal import Refused ++from secrets_engine.approval_claim import binding_from_check_request ++from secrets_engine.approval_consume import _expected_request ++from secrets_engine.authorization import approval_binding_digest ++from secrets_engine.execution_contract import ExecutionContract ++ ++ ++@contextmanager ++def whole(owner,directory,*,window_seconds=60): ++ # OpenBao, Approval HTTPS, TokenReview TLS and actual Access CLI are all up ++ # before proposing an immutable descriptor/deadline or issuing approval. ++ with trusted_providers(directory) as provider: ++ native,journal,binding,call,compiler,descriptor=install_exact_owner_descriptor(owner,window_seconds=window_seconds) ++ snapshot={path:compiler.read(Path(path)) for path in compiler.pins} ++ entry=compiler._catalogue_entry(snapshot) ++ approval_id=str(uuid.uuid4()) ++ entry=replace(entry,approval={**entry.approval,'authorization_id':approval_id}) ++ request=_expected_request(provider.cfg,entry,'execute-ccr',execution_contract=ExecutionContract.parse(descriptor)) ++ from secrets_engine.task_execution_handoff import prepare_task_approval ++ preparation=prepare_task_approval(provider.cfg,entry,execution_contract=ExecutionContract.parse(descriptor)) ++ normalization=preparation.as_owner_handoff() ++ assert normalization['original_request']==request ++ assert 'attributes' not in request['subject'] ++ assert normalization['approval_binding_digest']!=normalization['submitted_request_digest'] ++ now=datetime.now(timezone.utc) ++ body={'id':approval_id,'binding':normalization['native_binding'],'pdp_digest':normalization['approval_binding_digest'], ++ 'pdp_path':True,'human_control':True,'required_count':1, ++ 'validity':{'not_before':(now-timedelta(minutes=1)).isoformat(),'expires_at':(now+timedelta(minutes=3)).isoformat()}} ++ assert http(provider.cfg.approval_url,'POST','/v1/approvals',body,'synthetic-whole-service')[0]==201 ++ assert http(provider.cfg.approval_url,'POST','/v1/approvals/'+approval_id+'/entries',{},provider.human_token)[0]==200 ++ yield SimpleNamespace(native=native,journal=journal,call=call,compiler=compiler,descriptor=descriptor, ++ entry=entry,provider=provider,cfg=provider.cfg,approval_id=approval_id,request=request,normalization=normalization,preparation=preparation) ++ ++ ++def invoke(fixture): ++ return run_native_fixture_task(compiler=fixture.compiler,journal=fixture.journal, ++ plan_request=fixture.native.meta['request'],owner_descriptor=fixture.native.admission.descriptor, ++ secrets_cfg=fixture.cfg,entry=fixture.entry,confirmation=fixture.provider.confirmation, ++ native_admission=fixture.native.admission) ++ ++ ++def untouched(fixture): ++ assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)==(404,{'errors':[]}) ++ for ordinal in [1,4]: ++ path=fixture.compiler.before_call(fixture.native.plan,ordinal).path ++ assert fixture.call('GET',path)==(404,{'errors':[]}) ++ ++ ++def test_actual_successor_allow_strict_cas_confirm_six_calls_readback_cleanup(owner,tmp_path,monkeypatch): ++ with whole(owner,tmp_path) as fixture: ++ from secrets_engine.task_execution_handoff import consume_task_execution ++ with pytest.raises(Exception):consume_task_execution(fixture.cfg,fixture.preparation) ++ assert not any(item==('consumer','POST','/v1/approvals/'+fixture.approval_id+'/consume') for item in fixture.provider.calls) ++ trace=[];private_task=[];issued_native=[] ++ original=NativeFixtureAdapter._http ++ def observed(native,method,path,token,body=b''): ++ result=original(native,method,path,token,body) ++ trace.append((method,path,result[0])) ++ if method=='POST' and path=='auth/token/create' and result[0]==200: ++ private_task.append((result[1]['auth']['accessor'],result[1]['auth']['client_token'])) ++ issued_native.append(native) ++ return result ++ monkeypatch.setattr(NativeFixtureAdapter,'_http',observed) ++ try:result=invoke(fixture) ++ except Exception as error: ++ from urllib.error import HTTPError ++ cause=error.__cause__ ++ if isinstance(cause,HTTPError): ++ raw=cause.read(4097) ++ known={b'trusted Approval source refused\n':'approval-source-correspondence', ++ b'service task execution input refused\n':'service-input-correspondence'} ++ diagnostic={'status':cause.code,'constant_reason':known.get(raw,'other-closed-classification'), ++ 'native_operations':fixture.journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]} ++ (tmp_path/'closed-source-refusal.json').write_text(json.dumps(diagnostic)) ++ print(json.dumps(diagnostic)) ++ raise ++ assert result['native_apply']=='verified' and result['native_calls_completed']==6 ++ assert result['cleanup_confirmed'] and result['business_state']=='cleanup-confirmed' ++ assert fixture.provider.engine.get(fixture.approval_id).status=='consumed' ++ records=[json.loads(line) for line in fixture.provider.record.read_bytes().splitlines()] ++ assert len(records)==2 and [x['effect'] for x in records]==['deny','allow'] ++ assert all(x['signature']['mode']=='signed' for x in records) ++ normalization_record,record=records ++ assert normalization_record['id']==fixture.normalization['decision_id'] ++ assert record['id']==result['decision_id'] ++ assert record['lifetime']['expires_at']==fixture.descriptor['operation_deadline'] ++ assert record['provenance']['policy_package']=='secrets-engine.task-execution.service-pilot' ++ assert record['provenance']['policy_version']=='v2' ++ assert record['provenance']['approval_observation']['source_id']=='synthetic-whole-actual-approval' ++ assert record['binding']['context']['execution_contract']==fixture.descriptor ++ native_calls=fixture.journal.db.execute('SELECT ordinal,outcome FROM calls ORDER BY ordinal').fetchall() ++ assert native_calls[:2]==[(0,'complete'),(1,'complete')] and len(native_calls)==13 ++ assert all(value=='complete' for _,value in native_calls) ++ assert fixture.provider.calls.count(('consumer','POST','/v1/approvals/'+fixture.approval_id+'/consume'))==1 ++ assert fixture.provider.calls.count(('consumer','GET','/v1/approvals/'+fixture.approval_id))==1 ++ assert any(item==('source','GET','/v1/approvals/'+fixture.approval_id+'/claim') for item in fixture.provider.calls) ++ expected=fixture.compiler.proposal()['operations'] ++ assert [(method,path) for method,path,_ in trace if path in {item['path'] for item in expected}]==[(x['method'],x['path']) for x in expected] ++ assert len(private_task)==1 ++ assert issued_native[0]._task_wall_deadline