diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-checks.json b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-checks.json new file mode 100644 index 00000000..7f215f20 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-checks.json @@ -0,0 +1,53 @@ +{ + "observed_at": "2026-10-17T06:49:35.014746+00:00", + "bytecode_suppressed": true, + "pytest_cache_disabled": true, + "ruff_cache_disabled": true, + "checks": [ + { + "name": "canonical-focused", + "argv": [ + "/home/worsch/ops-warden/.venv/bin/python", + "-B", + "-m", + "pytest", + "-q", + "-p", + "no:cacheprovider", + "tests/test_case_run.py", + "tests/test_case_original_context_consistency.py" + ], + "exit_code": 0, + "stdout": "...................................................... [100%]\n54 passed in 1.92s\n", + "stderr": "" + }, + { + "name": "canonical-ruff", + "argv": [ + "/home/worsch/ops-warden/.venv/bin/ruff", + "check", + "--no-cache", + "src/warden/case_run.py", + "tests/test_case_original_context_consistency.py" + ], + "exit_code": 0, + "stdout": "All checks passed!\n", + "stderr": "" + }, + { + "name": "canonical-format", + "argv": [ + "/home/worsch/ops-warden/.venv/bin/ruff", + "format", + "--check", + "--no-cache", + "src/warden/case_run.py", + "tests/test_case_original_context_consistency.py" + ], + "exit_code": 0, + "stdout": "2 files already formatted\n", + "stderr": "" + } + ], + "native_calls": false +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-focused.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-focused.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-focused.stdout.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-focused.stdout.txt new file mode 100644 index 00000000..2fb66ef1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-focused.stdout.txt @@ -0,0 +1,2 @@ +...................................................... [100%] +54 passed in 1.92s diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-format.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-format.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-format.stdout.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-format.stdout.txt new file mode 100644 index 00000000..983e5b7d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-format.stdout.txt @@ -0,0 +1 @@ +2 files already formatted diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-ruff.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-ruff.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-ruff.stdout.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-ruff.stdout.txt new file mode 100644 index 00000000..1f5f344d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/canonical-ruff.stdout.txt @@ -0,0 +1 @@ +All checks passed! diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/failed-canonical-collection.json b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/failed-canonical-collection.json new file mode 100644 index 00000000..c0cb26f5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/failed-canonical-collection.json @@ -0,0 +1,26 @@ +{ + "observed_at": "2026-10-17T06:49:09.624471+00:00", + "bytecode_suppressed": true, + "pytest_cache_disabled": true, + "ruff_cache_disabled": true, + "checks": [ + { + "name": "canonical-focused", + "argv": [ + "/home/worsch/ops-warden/.venv/bin/python", + "-B", + "-m", + "pytest", + "-q", + "-p", + "no:cacheprovider", + "tests/test_case_run.py", + "tests/test_case_original_context_consistency.py" + ], + "exit_code": 2, + "stdout": "\n==================================== ERRORS ====================================\n_______ ERROR collecting tests/test_case_original_context_consistency.py _______\nImportError while importing test module '/home/worsch/ops-warden/tests/test_case_original_context_consistency.py'.\nHint: make sure your test modules/packages have valid Python names.\nTraceback:\n/usr/lib/python3.12/importlib/__init__.py:90: in import_module\n return _bootstrap._gcd_import(name[level:], package, level)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\ntests/test_case_original_context_consistency.py:14: in \n from test_case_run import rig as rig\nE ModuleNotFoundError: No module named 'test_case_run'\n=========================== short test summary info ============================\nERROR tests/test_case_original_context_consistency.py\n!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!\n1 error in 0.23s\n", + "stderr": "" + } + ], + "native_calls": false +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/manifest.json b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/manifest.json new file mode 100644 index 00000000..29e51b4f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/manifest.json @@ -0,0 +1,92 @@ +{ + "files": [ + { + "path": "after.json", + "sha256": "sha256:f25824546658f48a4faebf7d50ff79ff9d28945441fef93a1574667937a38672" + }, + { + "path": "before.json", + "sha256": "sha256:0e7bf27b2a864cd167b6a535b50073036b21e83ad5cddba21d81d140e2d2111f" + }, + { + "path": "canonical-checks.json", + "sha256": "sha256:d1919a93949fa7392547ad66fe4719dfe3d02f3d70ba83ee544d4307085f722e" + }, + { + "path": "canonical-focused.stderr.txt", + "sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "canonical-focused.stdout.txt", + "sha256": "sha256:7ec6e18dc428aca8fd9aa197b47f5f9e8fdb4c3131262723cf4f58cad45c684e" + }, + { + "path": "canonical-format.stderr.txt", + "sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "canonical-format.stdout.txt", + "sha256": "sha256:3bc53bf3e981a98a34a852e175bf9b77af841edea74fca595d9aedcbaf9a4938" + }, + { + "path": "canonical-ruff.stderr.txt", + "sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "canonical-ruff.stdout.txt", + "sha256": "sha256:82b3e6a6c090a57601d22943bd23fca9218d1031dbe5a7b754092f9a156b4f18" + }, + { + "path": "failed-canonical-collection.json", + "sha256": "sha256:914c95e62018ad5243a780d0522152c23109cb37ec202c611e6e1238a5dd63d1" + }, + { + "path": "new-test-import-predecessor.py", + "sha256": "sha256:8483bb5915701e8bd0749e85097e11076ffd539084ca68e9ec1c565babc157d1" + }, + { + "path": "owner-followthrough.patch", + "sha256": "sha256:d729c6078a5bc398edd22a12f5589f50f7544e857cd7096157240478af193271" + }, + { + "path": "postimages/docs/security-case-execution-kernel-v1.md", + "sha256": "sha256:e5570f542ec3ef4e6c11332cfe48fea16bf90cde5b9a5d926f5a068b7cafdbc7" + }, + { + "path": "postimages/src/warden/case_run.py", + "sha256": "sha256:77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768" + }, + { + "path": "postimages/tests/test_case_original_context_consistency.py", + "sha256": "sha256:10c5187cbc49222017319249ccb408816497ec6e006859d23003f3b2b375445b" + }, + { + "path": "postimages/workplans/WARDEN-WP-0044-security-case-follow-through.md", + "sha256": "sha256:721a90b2ed8c896257c34a1451eb642cb8824c8263e37c483fb61b36adc54088" + }, + { + "path": "preimages/docs/security-case-execution-kernel-v1.md", + "sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb" + }, + { + "path": "preimages/src/warden/case_run.py", + "sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671" + }, + { + "path": "preimages/workplans/WARDEN-WP-0044-security-case-follow-through.md", + "sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1" + }, + { + "path": "retained-evidence-integrity.json", + "sha256": "sha256:18183540e961551b3e652a08b177658e3bdbd305ff13191537b94db56c2603ea" + }, + { + "path": "unrelated-WORK-RECORDS.index", + "sha256": "sha256:c61591efad763a3dcb86751f5bf46f03fec8a162bfe06c4272032fea15d2988f" + }, + { + "path": "unrelated-WORK-RECORDS.worktree", + "sha256": "sha256:b6af9b1cb21e3a64b2ef8a5e732bc35c39cdeaef6af7a18f24824e8ec9fb5a76" + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/new-test-import-predecessor.py b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/new-test-import-predecessor.py new file mode 100644 index 00000000..574fcc4a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/new-test-import-predecessor.py @@ -0,0 +1,198 @@ +"""Original-intent consistency checks with generated metadata only. + +Reuse the existing synthetic owner rig. These checks do not provide a native +holder or independently authenticate original creation from local SQL/JSON. +""" + +from dataclasses import asdict, replace +import json +from pathlib import Path +import sqlite3 +import subprocess +import sys +import pytest +from test_case_run import rig as rig +from warden.case_receipt_state import Outcome, Refused +from warden.case_run import OriginalIntent + + +def interrupted(rig): + executor, args, calls, sources, kwargs = rig + old = executor.adapters[args[0].adapter] + seen = [] + + def lost(inv): + calls.append(inv) + raise TimeoutError("generated lost response") + + def original(ctx): + seen.append(ctx) + return old.reconcile(ctx.attempt) + + executor.adapters[old.reference] = replace( + old, invoke=lost, reconcile=None, reconcile_original=original + ) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + return executor, args, calls, old, seen + + +def alter_payload(executor, **changes): + row = executor.journal.db.execute( + "SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1" + ).fetchone() + obj = json.loads(row[1]) + obj.update(changes) + executor.journal.db.execute( + "UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0]) + ) + + +def test_original_actor_mismatch_is_refused(rig): + executor, args, calls, old, _ = interrupted(rig) + alter_payload(executor, actor="agent") + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + + +def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig): + executor, args, calls, old, _ = interrupted(rig) + actual = calls[0].attempt_id + alter_payload(executor, attempt_id="attempt-unrelated") + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + assert ( + executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0] + == actual + ) + + +def test_orphan_intent_without_original_running_receipt_is_refused(rig): + executor, args, calls, old, _ = interrupted(rig) + executor.journal.db.execute( + "DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)" + ) + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.UNCERTAIN + ) + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + + +def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig): + _, args, _, _, _ = rig + inv = replace( + args[1], + target={"unexpected": "synthetic-owner-payload"}, + operation="run", + attempt_id="attempt-fixture", + ) + record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64) + with pytest.raises(Refused): + OriginalIntent.decode(record.payload()) + # This is a direct serialization API gap, not an observed normal-admit leak. + + # Owner trust is the existing synthetic fixture; this proves call shape only. + + +@pytest.mark.parametrize( + "field,value", + [ + ("principal", "another-person"), + ("tenant", "another-tenant"), + ("realm", "another-realm"), + ], +) +def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch( + rig, field, value +): + executor, args, _, old, _ = interrupted(rig) + alter_payload(executor, **{field: value}) + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + + +def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig): + executor, args, calls, _, _ = rig + executor.journal.db.execute( + "CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END" + ) + with pytest.raises(sqlite3.IntegrityError): + executor.run(*args) + assert not calls + assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == ( + 0, + ) + assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( + 0, + ) + + +def test_existing_missing_original_refuses_before_callback(rig): + executor, args, _, _, seen = interrupted(rig) + executor.journal.db.execute("DELETE FROM intents") + with pytest.raises(Refused): + executor.reconcile(*args) + assert not seen + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.UNCERTAIN + ) + + +def test_existing_current_parent_revocation_remains_mandatory(rig): + executor, args, _, _, _ = interrupted(rig) + old = executor.evidence_verifier + + def refuse_session(purpose, source, evidence): + return ( + False + if purpose.startswith("session-revocation:") + else old(purpose, source, evidence) + ) + + executor.evidence_verifier = refuse_session + with pytest.raises(Refused): + executor.reconcile(*args) + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.UNCERTAIN + ) + + +def test_existing_intent_survives_actual_distinct_interpreter(rig): + executor, args, calls, old, _ = interrupted(rig) + code = """import json,sys +from pathlib import Path +from warden.case_run import Journal,Invocation +from warden.case_receipt_state import Pins +obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1])) +with j.locked(): + i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2]) + print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation})) +""" + result = subprocess.run( + [sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source], + input=json.dumps(asdict(args[1])), + text=True, + capture_output=True, + timeout=15, + env={ + "PATH": "/usr/bin:/bin", + "PYTHONPATH": str(Path(__file__).parents[1] / "src"), + "PYTHONDONTWRITEBYTECODE": "1", + }, + check=True, + ) + assert json.loads(result.stdout) == { + "attempt": calls[0].attempt_id, + "operation": "run", + } diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/owner-followthrough.patch b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/owner-followthrough.patch new file mode 100644 index 00000000..3a463fd5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/owner-followthrough.patch @@ -0,0 +1,515 @@ +--- a/src/warden/case_run.py ++++ b/src/warden/case_run.py +@@ -180,16 +180,76 @@ + """Durable record of the original mutating dispatch, written before dispatch. + + Binds the original Invocation (attempt, step, owner, target, actor, principal, +- lane), all pins, the admitted adapter source and the tenant/realm provenance +- the caller was admitted with. It is evidence of what Warden dispatched, not +- owner proof of creation: owners still authenticate created-only provenance. ++ lane), all pins and the admitted adapter source. Current admit leaves tenant ++ and realm empty; their reserved fields are not admitted provenance. This is ++ local dispatch correspondence, not owner proof of creation. Owners still ++ authenticate original created-only provenance independently. + """ + + binding: str + invocation: Invocation + source: str + ++ def validate(self) -> None: ++ """Fixed bounded metadata only; tenant/realm are not authority claims. ++ ++ Current admit leaves tenant/realm empty. Nonempty future coordinates need ++ separate owner provenance; serializability alone must never supply it. ++ """ ++ if ( ++ type(self.invocation) is not Invocation ++ or type(self.invocation.pins) is not Pins ++ ): ++ raise Refused("exact original invocation required") ++ i = self.invocation ++ fields = (i.step, i.owner, i.target, i.principal, i.lane, i.attempt_id) ++ if ( ++ not all( ++ type(value) is str and IDENTIFIER.fullmatch(value) for value in fields ++ ) ++ or type(i.actor) is not str ++ or i.actor not in ("human", "agent") ++ or type(i.operation) is not str ++ or i.operation not in ("run", "rollback") ++ or not all( ++ type(value) is str and (not value or IDENTIFIER.fullmatch(value)) ++ for value in (i.tenant, i.realm) ++ ) ++ or type(self.source) is not str ++ or not DIGEST.fullmatch(self.source) ++ or type(self.binding) is not str ++ or self.binding != f"{i.step}:{i.target}" ++ ): ++ raise Refused("invalid original intent metadata") ++ if set(Invocation.__dataclass_fields__) != { ++ "step", ++ "owner", ++ "target", ++ "actor", ++ "principal", ++ "lane", ++ "pins", ++ "attempt_id", ++ "operation", ++ "tenant", ++ "realm", ++ }: ++ raise Refused("original invocation field contract changed") ++ if not all( ++ type(v) is str and DIGEST.fullmatch(v) ++ for v in ( ++ i.pins.case, ++ i.pins.case_type, ++ i.pins.adapter, ++ i.pins.catalog, ++ i.pins.acceptance, ++ i.pins.run_sheet, ++ ) ++ ): ++ raise Refused("invalid original intent pins") ++ + def payload(self) -> str: ++ self.validate() + return json.dumps( + {"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source} + | asdict(self.invocation), +@@ -199,13 +259,44 @@ + + @classmethod + def decode(cls, raw: str) -> OriginalIntent: +- obj = json.loads(raw) +- fields = {f for f in Invocation.__dataclass_fields__} +- if set(obj) != fields | {"schema", "binding", "source"} or obj["schema"] != INTENT_SCHEMA: +- raise Refused("unknown original intent schema") +- inv = {k: obj[k] for k in fields} +- inv["pins"] = Pins(**inv["pins"]) +- return cls(obj["binding"], Invocation(**inv), obj["source"]) ++ def unique(pairs): ++ result = {} ++ for key, value in pairs: ++ if key in result: ++ raise Refused("duplicate original intent field") ++ result[key] = value ++ return result ++ ++ try: ++ if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: ++ raise Refused("original intent input exceeds bound") ++ obj = json.loads(raw, object_pairs_hook=unique) ++ fields = { ++ "step", ++ "owner", ++ "target", ++ "actor", ++ "principal", ++ "lane", ++ "pins", ++ "attempt_id", ++ "operation", ++ "tenant", ++ "realm", ++ } ++ if ( ++ type(obj) is not dict ++ or set(obj) != fields | {"schema", "binding", "source"} ++ or obj["schema"] != INTENT_SCHEMA ++ ): ++ raise Refused("unknown original intent schema") ++ inv = {k: obj[k] for k in fields} ++ inv["pins"] = Pins(**inv["pins"]) ++ result = cls(obj["binding"], Invocation(**inv), obj["source"]) ++ result.validate() ++ return result ++ except (TypeError, ValueError, KeyError): ++ raise Refused("invalid original intent record") from None + + + @dataclass(frozen=True, slots=True) +@@ -256,28 +347,76 @@ + "CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + +- def original_intent(self, binding: str, current: Invocation, source: str) -> OriginalIntent: ++ def original_intent( ++ self, binding: str, current: Invocation, source: str ++ ) -> OriginalIntent: + """Latest original run intent for binding, refused unless it matches exactly. + + Missing or mismatched records refuse; the step stays uncertain. + """ + row = self.db.execute( +- "SELECT payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1", ++ "SELECT attempt_id,payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + raise Refused("original intent record missing; step stays uncertain") +- intent = OriginalIntent.decode(row[0]) ++ intent = OriginalIntent.decode(row[1]) + o = intent.invocation ++ if row[0] != o.attempt_id: ++ raise Refused("original intent attempt identity differs") + if intent.binding != binding or o.operation != "run": + raise Refused("original intent binding mismatch") + if o.pins != current.pins: + raise Refused("original intent pins mismatch") + if intent.source != source: + raise Refused("original intent source mismatch") +- for name in ("step", "owner", "target", "principal", "lane", "tenant", "realm"): ++ for name in ( ++ "step", ++ "owner", ++ "target", ++ "actor", ++ "principal", ++ "lane", ++ "tenant", ++ "realm", ++ ): + if getattr(o, name) != getattr(current, name): + raise Refused(f"original intent {name} mismatch") ++ # Match the persisted original running receipt. An orphan context or a ++ # payload from another attempt cannot become the current lookup key. ++ receipts = self.db.execute( ++ "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT ?", ++ (binding, MAX_HISTORY + 1), ++ ).fetchall() ++ if len(receipts) > MAX_HISTORY: ++ raise Refused("original intent receipt history exceeds bound") ++ matched = False ++ try: ++ for (raw,) in receipts: ++ if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: ++ raise Refused("original receipt input exceeds bound") ++ payload = json.loads(raw) ++ if ( ++ payload.get("attempt_id") != o.attempt_id ++ or payload.get("operation") != "run" ++ ): ++ continue ++ last = payload.get("receipts", [])[-1] ++ if last.get("outcome") != "running": ++ continue ++ if ( ++ payload.get("journal_schema") != "warden.case.step-journal.v1" ++ or payload.get("step_id") != o.step ++ or payload.get("owner") != o.owner ++ or payload.get("pins") != asdict(o.pins) ++ ): ++ raise Refused("original running receipt binding differs") ++ matched = True ++ break ++ except (TypeError, ValueError, KeyError, IndexError, AttributeError): ++ raise Refused("invalid original running receipt") from None ++ if not matched: ++ raise Refused("original running receipt missing; no backfill") + return intent + + @contextmanager +@@ -355,6 +494,23 @@ + invocation: Invocation | None = None, + intent: OriginalIntent | None = None, + ): ++ if intent is not None: ++ if type(intent) is not OriginalIntent: ++ raise Refused("exact original intent required") ++ intent.validate() ++ expected = ( ++ Outcome.RUNNING ++ if intent.invocation.operation == "run" ++ else Outcome.ROLLBACK_RUNNING ++ ) ++ if ( ++ intent.binding != binding ++ or intent.invocation != invocation ++ or intent.invocation.pins != state.pins ++ or not state.last ++ or state.last.outcome is not expected ++ ): ++ raise Refused("original intent and running receipt differ") + self.db.execute("BEGIN IMMEDIATE") + try: + if intent is not None: +--- /dev/null ++++ b/tests/test_case_original_context_consistency.py +@@ -0,0 +1,198 @@ ++"""Original-intent consistency checks with generated metadata only. ++ ++Reuse the existing synthetic owner rig. These checks do not provide a native ++holder or independently authenticate original creation from local SQL/JSON. ++""" ++ ++from dataclasses import asdict, replace ++import json ++from pathlib import Path ++import sqlite3 ++import subprocess ++import sys ++import pytest ++from tests.test_case_run import rig as rig ++from warden.case_receipt_state import Outcome, Refused ++from warden.case_run import OriginalIntent ++ ++ ++def interrupted(rig): ++ executor, args, calls, sources, kwargs = rig ++ old = executor.adapters[args[0].adapter] ++ seen = [] ++ ++ def lost(inv): ++ calls.append(inv) ++ raise TimeoutError("generated lost response") ++ ++ def original(ctx): ++ seen.append(ctx) ++ return old.reconcile(ctx.attempt) ++ ++ executor.adapters[old.reference] = replace( ++ old, invoke=lost, reconcile=None, reconcile_original=original ++ ) ++ assert executor.run(*args).last.outcome is Outcome.UNCERTAIN ++ return executor, args, calls, old, seen ++ ++ ++def alter_payload(executor, **changes): ++ row = executor.journal.db.execute( ++ "SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1" ++ ).fetchone() ++ obj = json.loads(row[1]) ++ obj.update(changes) ++ executor.journal.db.execute( ++ "UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0]) ++ ) ++ ++ ++def test_original_actor_mismatch_is_refused(rig): ++ executor, args, calls, old, _ = interrupted(rig) ++ alter_payload(executor, actor="agent") ++ with pytest.raises(Refused): ++ executor.journal.original_intent( ++ "mint-secret:fixture-client", args[1], old.source ++ ) ++ ++ ++def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig): ++ executor, args, calls, old, _ = interrupted(rig) ++ actual = calls[0].attempt_id ++ alter_payload(executor, attempt_id="attempt-unrelated") ++ with pytest.raises(Refused): ++ executor.journal.original_intent( ++ "mint-secret:fixture-client", args[1], old.source ++ ) ++ assert ( ++ executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0] ++ == actual ++ ) ++ ++ ++def test_orphan_intent_without_original_running_receipt_is_refused(rig): ++ executor, args, calls, old, _ = interrupted(rig) ++ executor.journal.db.execute( ++ "DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)" ++ ) ++ assert ( ++ executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome ++ is Outcome.UNCERTAIN ++ ) ++ with pytest.raises(Refused): ++ executor.journal.original_intent( ++ "mint-secret:fixture-client", args[1], old.source ++ ) ++ ++ ++def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig): ++ _, args, _, _, _ = rig ++ inv = replace( ++ args[1], ++ target={"unexpected": "synthetic-owner-payload"}, ++ operation="run", ++ attempt_id="attempt-fixture", ++ ) ++ record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64) ++ with pytest.raises(Refused): ++ OriginalIntent.decode(record.payload()) ++ # This is a direct serialization API gap, not an observed normal-admit leak. ++ ++ # Owner trust is the existing synthetic fixture; this proves call shape only. ++ ++ ++@pytest.mark.parametrize( ++ "field,value", ++ [ ++ ("principal", "another-person"), ++ ("tenant", "another-tenant"), ++ ("realm", "another-realm"), ++ ], ++) ++def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch( ++ rig, field, value ++): ++ executor, args, _, old, _ = interrupted(rig) ++ alter_payload(executor, **{field: value}) ++ with pytest.raises(Refused): ++ executor.journal.original_intent( ++ "mint-secret:fixture-client", args[1], old.source ++ ) ++ ++ ++def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig): ++ executor, args, calls, _, _ = rig ++ executor.journal.db.execute( ++ "CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END" ++ ) ++ with pytest.raises(sqlite3.IntegrityError): ++ executor.run(*args) ++ assert not calls ++ assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == ( ++ 0, ++ ) ++ assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( ++ 0, ++ ) ++ ++ ++def test_existing_missing_original_refuses_before_callback(rig): ++ executor, args, _, _, seen = interrupted(rig) ++ executor.journal.db.execute("DELETE FROM intents") ++ with pytest.raises(Refused): ++ executor.reconcile(*args) ++ assert not seen ++ assert ( ++ executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome ++ is Outcome.UNCERTAIN ++ ) ++ ++ ++def test_existing_current_parent_revocation_remains_mandatory(rig): ++ executor, args, _, _, _ = interrupted(rig) ++ old = executor.evidence_verifier ++ ++ def refuse_session(purpose, source, evidence): ++ return ( ++ False ++ if purpose.startswith("session-revocation:") ++ else old(purpose, source, evidence) ++ ) ++ ++ executor.evidence_verifier = refuse_session ++ with pytest.raises(Refused): ++ executor.reconcile(*args) ++ assert ( ++ executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome ++ is Outcome.UNCERTAIN ++ ) ++ ++ ++def test_existing_intent_survives_actual_distinct_interpreter(rig): ++ executor, args, calls, old, _ = interrupted(rig) ++ code = """import json,sys ++from pathlib import Path ++from warden.case_run import Journal,Invocation ++from warden.case_receipt_state import Pins ++obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1])) ++with j.locked(): ++ i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2]) ++ print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation})) ++""" ++ result = subprocess.run( ++ [sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source], ++ input=json.dumps(asdict(args[1])), ++ text=True, ++ capture_output=True, ++ timeout=15, ++ env={ ++ "PATH": "/usr/bin:/bin", ++ "PYTHONPATH": str(Path(__file__).parents[1] / "src"), ++ "PYTHONDONTWRITEBYTECODE": "1", ++ }, ++ check=True, ++ ) ++ assert json.loads(result.stdout) == { ++ "attempt": calls[0].attempt_id, ++ "operation": "run", ++ } +--- a/docs/security-case-execution-kernel-v1.md ++++ b/docs/security-case-execution-kernel-v1.md +@@ -125,3 +125,30 @@ + by `check-evidence`, outside this admission path; Warden nevertheless pins and + copies it with the complete reviewed producer set. Missing or modified bytes in + any of the four modules refuse admission. The test covers every dependency. ++ ++ ++## Original-intent consistency boundary — 2026-10-17 ++ ++`OriginalIntent` and the optional additive ++`reconcile_original(ReconcileContext(original, attempt))` callback retain the ++original run separately from a fresh reconciliation attempt. Existing one-argument ++reconcile adapters remain source-compatible. Intent and pre-dispatch receipt are ++committed in one SQLite transaction; lookup now requires their exact attempt, ++actor, step/owner/target, source and pin correspondence. Missing legacy context, ++an orphan receipt, changed context or an invalid bounded field contract refuses; ++there is no backfill or new runtime holder. ++ ++Original intent serialization accepts only the fixed reviewed Invocation/Pins ++field contract and bounded operational identifiers. Duplicate, oversized and ++malformed original records refuse. Current Executor.admit leaves tenant and realm ++empty: those reserved fields are not admitted tenant/realm provenance, and future ++nonempty coordinates need an authoritative owner contract. Raw owner payloads, ++credentials, parameters and caller output are not part of this context contract. ++ ++Local dispatch correspondence is not independent owner proof. A real owner must ++still authenticate the exact original created-only result and original revocation, ++current native revision and the fresh attempt's session/grant evidence before ++recovery can count. No actual original holder/verifier or native adapter is added. ++NetKingdom's current registration adapter still has no reconciliation callback; ++source tests do not authorize its post-restart undo, replay or a changed caller/ ++session/run-sheet migration. WARDEN-WP-0044-T03 remains a native acceptance gate. +--- a/workplans/WARDEN-WP-0044-security-case-follow-through.md ++++ b/workplans/WARDEN-WP-0044-security-case-follow-through.md +@@ -168,13 +168,36 @@ + tenant, realm — plus admitted adapter source) in the same SQLite transaction as + the pre-dispatch journal row. `Journal.original_intent(binding, current, source)` + returns it after restart and refuses on a missing record or any +-pins/source/step/owner/target/principal/lane/tenant/realm mismatch, before any ++pins/source/step/owner/target/actor/principal/lane/tenant/realm mismatch, before any + journal write, so the step stays uncertain. An adapter's + `reconcile_original(ReconcileContext(original, attempt))` receives the original + intent separately from the new reconcile attempt. This is Warden's record of what +-it dispatched, not owner created-only provenance; tenant/realm are as supplied at +-admission. Owner descriptors/adapters, contained session integration, Forgejo ++it dispatched, not owner created-only provenance. Current Executor.admit leaves ++tenant/realm empty; the reserved coordinates establish no admitted provenance. ++Owner descriptors/adapters, contained session integration, Forgejo + materialization and the native run remain open. ++ ++2026-10-17 source consistency follow-through: retained the additive intents table ++and reconcile_original callback and fixed four reproduced local defects: original ++actor mismatch, payload/SQL attempt-ID mismatch, orphan intent without its ++matching original running receipt, and arbitrary nonmetadata values accepted by ++the original-intent serializer. Exact bounded field validation, duplicate-free ++bounded decode and save-time intent/invocation/running-state equality now preserve ++the original receipt linkage before owner lookup. No new context store, native ++verifier, trust root, adapter activation or authority migration was introduced. ++ ++The actual canonical focused kernel and new consistency suite passes 54 checks; ++owner-installed Ruff check/format passes. The existing generated-data/synthetic ++owner fixture and a distinct interpreter prove source correspondence/refusal, ++not independent native custody or creation. Earlier comparison probes and failed ++preparation are retained. Current caller and fresh session/grant revocation checks ++remain required; original owner payload/revocation, exact native readback and ++created-only provenance still need independent authenticated owner admission. ++No JSON/SQL context, unknown tenant/realm, changed run-sheet or legacy missing ++intent supplies recovery, replay or undo authority. T03 remains progress and all ++native acceptance gates/statuses/dependencies/UUIDs remain unchanged. Evidence: ++Custodian original-context-compare-r2 and original-context-owner-followthrough-r3 ++under docs/changes/netkingdom-target-2026-10-17/; publication/projection is separate. + + Agreed with railiance-platform (RPF-WP-0054-T01, 2026-10-09): + - `mint-secret` runs `credential.py generate --grant keycape/client-secret-generate diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/docs/security-case-execution-kernel-v1.md b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/docs/security-case-execution-kernel-v1.md new file mode 100644 index 00000000..13bec321 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/docs/security-case-execution-kernel-v1.md @@ -0,0 +1,154 @@ +# Closed case execution kernel v1 — 2026-10-11 + +Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal +at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +was accepted through memo `infd-20261010-security-case-execution`, version 1, +approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition +disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original +approved proposal remains byte-identical. That decision accepts source design; +it grants no execution session, credential or deployment authority. + +`src/warden/case_run.py` advances the pure reducer into an executable injected +owner harness with private durable receipts. Descriptor version +`warden.case.execution.v1` is a closed local implementation seam, not an accepted +NetKingdom type schema. It contains fixed owner/adapter/source digest, actor, +lane, target parameter, effect, reviewed created-object identifiers declared +retry safety and explicit required revocation scopes (parent-session and broker-grant). +A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused. +An acknowledged type must additionally pin the descriptor bytes under its +`execution_descriptors` map and independent owner evidence must authenticate +that exact machine-seam revision. Existing descriptive types lack this map and +remain plan-only. Their acknowledged labels do not authorize this new seam. + +There are no installed owner adapters, native provenance verifier or dispatch CLI +in this change. Callables are injected only through a fixed adapter registry, +with immutable owner reference/source pins. A real integration must supply +independent owner/native verification or explicit attended manual evidence; +a bare JSON receipt identifier is insufficient. Fixture verifiers in tests +are deliberately fake and attest no native case. Caller sessions are independently +verified for exact principal/actor/lane at admission and again before dispatch; +input actor labels alone are never sufficient. Agent mint is refused, including +when WARDEN_AGENT_ID appears after initial human admission. + +Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference +and run-sheet identity. Actual ops-mason materialization evidence is required +against the case source, separately from its status label. Case parameters supply +an exact metadata target. Source/session changes cannot inherit journal success. +Owner results have a closed metadata shape, result evidence fingerprint and +separate independently authenticated grant/parent-session revocation evidence +fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and +created objects outside the reviewed scope become uncertain, never success. +Python and OS stdout/stderr, including inherited child-process output, are +discarded without buffering or retention under a serialized fd containment +context. Independent provenance verification uses the same containment and +sanitizes exceptions. All source inputs are capped at 256 KiB before parsing. +Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other +owner interruptions are rethrown with a fixed message while retaining running +intent for crash recovery. + +The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks +and insecure modes, uses SQLite FULL synchronization and commits running intent +before calling the adapter. A separate process lock prevents concurrent mutation. +A crash between dispatch and final receipt leaves running; the next lock holder +persists uncertain before refusing replay. Both intent and outcome remain in +history. Each persisted attempt has a unique identity bound to all exact pins, +operation (run/reconcile/rollback), sequence and nonce. Adapter results and both +revocation proof purposes carry that identity. Prior-attempt evidence cannot +verify a safe retry, even if a caller rewrites result labels. Descriptor-created +objects are capped at 32 and histories at 128 transitions, with dispatch stopped +before archival is required. A throw or lost response is not failed-before-change. A safe retry needs +verified failed-before-change, unchanged pins, descriptor retry-safe and explicit +retry. Success and no-change cannot replay. + +Reconciliation is an explicit fixed owner callback, never the mutation callback. +It may verify an already-present native CAS result as no-change/success, or prove +failed-before-change; it never tells Warden to regenerate a credential. Uncertain +or failed-after-change requires this owner-native inspection before any retry. +Owner proof and revocation remain mandatory. Rollback visits only objects in a +verified creation receipt, in reverse order; no-change has no undo and shared +objects are excluded. Unknown/lost undo blocks another rollback. + +The kernel operates individual admitted steps. Full ordered run-sheet orchestration, +verification inventory, contained login integration, native owner adapters and +CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is +implemented. Current Forgejo source is not materialized and has no acknowledged +machine descriptors. Its first native run remains an acceptance requirement, +with real materialization, separately authenticated broker-grant and parent-session +revocation receipts, session/owner receipts and consumer enforcement. This +implementation neither promotes a type nor changes any case status. + +Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open +an independent SQLite connection inside dispatch to prove running was committed, +crash after mutation and reopen a fresh journal to prove no duplicate invocation, +lose a response then reconcile without mint, require explicit retry, check exact +pins/session expiry, withhold malicious output and unconfirmed revocation, and +prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and +checks pass. No live token or target action occurred. + +## Browser input projection — 2026-10-16 source preparation + +An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser +cases before admission. It calls the real NetKingdom type/case validator over +verified producer bytes in a disposable Python process (`-I`, empty environment, +timeout, no inherited output). The validator, composer, capability contract and lazy evidence +modules have exact source pins; missing or changed code refuses. There is no +automatic discovery or download. A producer upgrade needs reviewed pin changes. + +Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest +of each complete parameter definition list prevents reclassifying secrets as +metadata, loosening constraints, or adding fields under a recognized version. +The complete case passes producer validation, including required fields, +constraints and credential-shape checks. Explicit fixed/default overrides, +unknown fields and secret references refuse. Security-sensitive browser metadata +can therefore be present without being passed to an owner adapter: the only +projection is its descriptor's exact public/operational target identifier. +No raw parameter values are added to invocations or persisted receipts. + +`require_implemented=False` validates the acknowledged type's source, not its +execution authority. Existing descriptor/type/source/catalog/caller provenance +gates stay mandatory. Neither historical browser type contains the required +exact machine-descriptor map, and neither is promoted by this change. There are +still no installed native owner adapters, authenticated provenance verifier, +contained session/ordered run orchestration or live CLI dispatch. A journal digest +is a fingerprint and cannot authenticate native evidence. Completion still needs +owner-admitted descriptors and actual structural, mint/delivery, consumer and +revocation/rollback evidence. Local fixture success is not native acceptance. + +Tests use the actual producer checkout (override sibling location with +`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected +test-only owner adapter/evidence verifier. Their descriptor extensions and case +materialized labels exist only in memory. Canonical case/type bytes stay intact. + +Producer integration refinement: the accepted NetKingdom browser-case patch +`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05` +is now the tested producer revision. Its evidence module is imported lazily only +by `check-evidence`, outside this admission path; Warden nevertheless pins and +copies it with the complete reviewed producer set. Missing or modified bytes in +any of the four modules refuse admission. The test covers every dependency. + + +## Original-intent consistency boundary — 2026-10-17 + +`OriginalIntent` and the optional additive +`reconcile_original(ReconcileContext(original, attempt))` callback retain the +original run separately from a fresh reconciliation attempt. Existing one-argument +reconcile adapters remain source-compatible. Intent and pre-dispatch receipt are +committed in one SQLite transaction; lookup now requires their exact attempt, +actor, step/owner/target, source and pin correspondence. Missing legacy context, +an orphan receipt, changed context or an invalid bounded field contract refuses; +there is no backfill or new runtime holder. + +Original intent serialization accepts only the fixed reviewed Invocation/Pins +field contract and bounded operational identifiers. Duplicate, oversized and +malformed original records refuse. Current Executor.admit leaves tenant and realm +empty: those reserved fields are not admitted tenant/realm provenance, and future +nonempty coordinates need an authoritative owner contract. Raw owner payloads, +credentials, parameters and caller output are not part of this context contract. + +Local dispatch correspondence is not independent owner proof. A real owner must +still authenticate the exact original created-only result and original revocation, +current native revision and the fresh attempt's session/grant evidence before +recovery can count. No actual original holder/verifier or native adapter is added. +NetKingdom's current registration adapter still has no reconciliation callback; +source tests do not authorize its post-restart undo, replay or a changed caller/ +session/run-sheet migration. WARDEN-WP-0044-T03 remains a native acceptance gate. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/src/warden/case_run.py b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/src/warden/case_run.py new file mode 100644 index 00000000..552c33da --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/src/warden/case_run.py @@ -0,0 +1,982 @@ +"""Closed local execution kernel v1; no installed adapters or live CLI dispatch. + +An owner must acknowledge each descriptor and provide an independent provenance +verifier. Descriptive case types remain plan-only. Nothing executes shell prose. +""" + +from __future__ import annotations + +from contextlib import contextmanager, redirect_stderr, redirect_stdout +from dataclasses import asdict, dataclass, replace +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import sqlite3 +import threading +import uuid +from typing import Callable + +import yaml +from .case_inputs import BrowserInputs +from .case_receipt_state import ( + Outcome, + Pins, + Receipt, + Refused, + StepState, + begin, + finish, + recover, + begin_rollback, + finish_rollback, +) + +IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z") +MAX_SOURCE_BYTES = 262144 +MAX_HISTORY = 128 +OUTPUT_LOCK = threading.RLock() + + +def bounded_source(value: bytes): + if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES: + raise Refused("source exceeds bounded input contract") + + +@contextmanager +def contained_output(): + """Discard Python and inherited OS stdout/stderr; serialize process fd changes.""" + with OUTPUT_LOCK: + saved = [] + null = os.open(os.devnull, os.O_WRONLY) + try: + for target in (1, 2): + saved.append((target, os.dup(target))) + os.dup2(null, target) + with redirect_stdout(_Discard()), redirect_stderr(_Discard()): + try: + yield + except Exception: + raise Refused("owner callback failed") from None + except BaseException: + raise OwnerInterrupted() from None + finally: + for target, original in saved: + os.dup2(original, target) + os.close(original) + os.close(null) + + +DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") + + +def digest(value: bytes) -> str: + return "sha256:" + hashlib.sha256(value).hexdigest() + + +@dataclass(frozen=True, slots=True) +class Descriptor: + version: str + step: str + owner: str + adapter: str + adapter_source: str + actor: str + lane: str + target_parameter: str + effect: str + allowed_created: tuple[str, ...] + retry_safe: bool + revocation_scopes: tuple[str, ...] + + @classmethod + def decode(cls, source: bytes) -> Descriptor: + bounded_source(source) + try: + obj = json.loads(source) + if set(obj) != set(cls.__dataclass_fields__): + raise ValueError + if not isinstance(obj["allowed_created"], list): + raise ValueError + obj["allowed_created"] = tuple(obj["allowed_created"]) + if not isinstance(obj["revocation_scopes"], list): + raise ValueError + obj["revocation_scopes"] = tuple(obj["revocation_scopes"]) + result = cls(**obj) + identifiers = ( + result.step, + result.owner, + result.adapter, + result.lane, + result.target_parameter, + *result.allowed_created, + ) + if result.version != "warden.case.execution.v1" or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers + ): + raise ValueError + if not DIGEST.fullmatch(result.adapter_source) or result.actor not in ( + "human", + "agent", + ): + raise ValueError + if ( + result.effect not in ("read", "change", "mint") + or type(result.retry_safe) is not bool + ): + raise ValueError + scopes = set(result.revocation_scopes) + if ( + len(scopes) != len(result.revocation_scopes) + or not scopes <= {"parent-session", "broker-grant"} + or "parent-session" not in scopes + ): + raise ValueError + if result.effect == "mint" and "broker-grant" not in scopes: + raise ValueError + if len(result.allowed_created) > 32: + raise ValueError + if len(set(result.allowed_created)) != len(result.allowed_created): + raise ValueError + return result + except (ValueError, TypeError, KeyError): + raise Refused("invalid closed execution descriptor") from None + + +@dataclass(frozen=True, slots=True) +class OwnerResult: + outcome: Outcome + evidence: str + created: tuple[str, ...] = () + revoked: bool = False + session_revocation_evidence: str | None = None + grant_revocation_evidence: str | None = None + attempt_id: str = "" + operation: str = "" + + +@dataclass(frozen=True, slots=True) +class Invocation: + step: str + owner: str + target: str + actor: str + principal: str + lane: str + pins: Pins + attempt_id: str = "" + operation: str = "admission" + tenant: str = "" + realm: str = "" + + +INTENT_SCHEMA = "warden.case.original-intent.v1" + + +@dataclass(frozen=True, slots=True) +class OriginalIntent: + """Durable record of the original mutating dispatch, written before dispatch. + + Binds the original Invocation (attempt, step, owner, target, actor, principal, + lane), all pins and the admitted adapter source. Current admit leaves tenant + and realm empty; their reserved fields are not admitted provenance. This is + local dispatch correspondence, not owner proof of creation. Owners still + authenticate original created-only provenance independently. + """ + + binding: str + invocation: Invocation + source: str + + def validate(self) -> None: + """Fixed bounded metadata only; tenant/realm are not authority claims. + + Current admit leaves tenant/realm empty. Nonempty future coordinates need + separate owner provenance; serializability alone must never supply it. + """ + if ( + type(self.invocation) is not Invocation + or type(self.invocation.pins) is not Pins + ): + raise Refused("exact original invocation required") + i = self.invocation + fields = (i.step, i.owner, i.target, i.principal, i.lane, i.attempt_id) + if ( + not all( + type(value) is str and IDENTIFIER.fullmatch(value) for value in fields + ) + or type(i.actor) is not str + or i.actor not in ("human", "agent") + or type(i.operation) is not str + or i.operation not in ("run", "rollback") + or not all( + type(value) is str and (not value or IDENTIFIER.fullmatch(value)) + for value in (i.tenant, i.realm) + ) + or type(self.source) is not str + or not DIGEST.fullmatch(self.source) + or type(self.binding) is not str + or self.binding != f"{i.step}:{i.target}" + ): + raise Refused("invalid original intent metadata") + if set(Invocation.__dataclass_fields__) != { + "step", + "owner", + "target", + "actor", + "principal", + "lane", + "pins", + "attempt_id", + "operation", + "tenant", + "realm", + }: + raise Refused("original invocation field contract changed") + if not all( + type(v) is str and DIGEST.fullmatch(v) + for v in ( + i.pins.case, + i.pins.case_type, + i.pins.adapter, + i.pins.catalog, + i.pins.acceptance, + i.pins.run_sheet, + ) + ): + raise Refused("invalid original intent pins") + + def payload(self) -> str: + self.validate() + return json.dumps( + {"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source} + | asdict(self.invocation), + separators=(",", ":"), + sort_keys=True, + ) + + @classmethod + def decode(cls, raw: str) -> OriginalIntent: + def unique(pairs): + result = {} + for key, value in pairs: + if key in result: + raise Refused("duplicate original intent field") + result[key] = value + return result + + try: + if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: + raise Refused("original intent input exceeds bound") + obj = json.loads(raw, object_pairs_hook=unique) + fields = { + "step", + "owner", + "target", + "actor", + "principal", + "lane", + "pins", + "attempt_id", + "operation", + "tenant", + "realm", + } + if ( + type(obj) is not dict + or set(obj) != fields | {"schema", "binding", "source"} + or obj["schema"] != INTENT_SCHEMA + ): + raise Refused("unknown original intent schema") + inv = {k: obj[k] for k in fields} + inv["pins"] = Pins(**inv["pins"]) + result = cls(obj["binding"], Invocation(**inv), obj["source"]) + result.validate() + return result + except (TypeError, ValueError, KeyError): + raise Refused("invalid original intent record") from None + + +@dataclass(frozen=True, slots=True) +class ReconcileContext: + """Original intent and the new reconciliation attempt, kept separate.""" + + original: OriginalIntent + attempt: Invocation + + +@dataclass(frozen=True, slots=True) +class Adapter: + reference: str + source: str + invoke: Callable[[Invocation], OwnerResult] + verify: Callable[[Invocation, OwnerResult], bool] + rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None + reconcile: Callable[[Invocation], OwnerResult] | None = None + reconcile_original: Callable[[ReconcileContext], OwnerResult] | None = None + + +class Journal: + """Private SQLite journal plus process lock; transaction commits before dispatch.""" + + def __init__(self, directory: Path): + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + if ( + directory.is_symlink() + or directory.stat().st_uid != os.getuid() + or directory.stat().st_mode & 0o077 + ): + raise Refused("journal directory is not private") + self.directory = directory + for name in ("journal.sqlite", "execution.lock"): + path = directory / name + fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077: + raise Refused("journal file is not private") + finally: + os.close(fd) + self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None) + self.db.execute("PRAGMA synchronous=FULL") + self.db.execute( + "CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + self.db.execute( + "CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + + def original_intent( + self, binding: str, current: Invocation, source: str + ) -> OriginalIntent: + """Latest original run intent for binding, refused unless it matches exactly. + + Missing or mismatched records refuse; the step stays uncertain. + """ + row = self.db.execute( + "SELECT attempt_id,payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + raise Refused("original intent record missing; step stays uncertain") + intent = OriginalIntent.decode(row[1]) + o = intent.invocation + if row[0] != o.attempt_id: + raise Refused("original intent attempt identity differs") + if intent.binding != binding or o.operation != "run": + raise Refused("original intent binding mismatch") + if o.pins != current.pins: + raise Refused("original intent pins mismatch") + if intent.source != source: + raise Refused("original intent source mismatch") + for name in ( + "step", + "owner", + "target", + "actor", + "principal", + "lane", + "tenant", + "realm", + ): + if getattr(o, name) != getattr(current, name): + raise Refused(f"original intent {name} mismatch") + # Match the persisted original running receipt. An orphan context or a + # payload from another attempt cannot become the current lookup key. + receipts = self.db.execute( + "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT ?", + (binding, MAX_HISTORY + 1), + ).fetchall() + if len(receipts) > MAX_HISTORY: + raise Refused("original intent receipt history exceeds bound") + matched = False + try: + for (raw,) in receipts: + if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: + raise Refused("original receipt input exceeds bound") + payload = json.loads(raw) + if ( + payload.get("attempt_id") != o.attempt_id + or payload.get("operation") != "run" + ): + continue + last = payload.get("receipts", [])[-1] + if last.get("outcome") != "running": + continue + if ( + payload.get("journal_schema") != "warden.case.step-journal.v1" + or payload.get("step_id") != o.step + or payload.get("owner") != o.owner + or payload.get("pins") != asdict(o.pins) + ): + raise Refused("original running receipt binding differs") + matched = True + break + except (TypeError, ValueError, KeyError, IndexError, AttributeError): + raise Refused("invalid original running receipt") from None + if not matched: + raise Refused("original running receipt missing; no backfill") + return intent + + @contextmanager + def locked(self): + with (self.directory / "execution.lock").open("rb") as fh: + try: + fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused("case execution already active") from None + try: + yield + finally: + fcntl.flock(fh, fcntl.LOCK_UN) + + def load(self, binding: str, initial: StepState) -> StepState: + row = self.db.execute( + "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + return initial + obj = json.loads(row[0]) + expected = { + "step_id", + "owner", + "pins", + "allowed_created", + "retry_safe", + "receipts", + "journal_schema", + "evidence_digest", + "session_revoked", + "session_revocation_digest", + "grant_revocation_digest", + "attempt_id", + "operation", + } + if ( + set(obj) != expected + or obj["journal_schema"] != "warden.case.step-journal.v1" + ): + raise Refused("unknown journal receipt schema") + state = StepState( + obj["step_id"], + obj["owner"], + Pins(**obj["pins"]), + tuple(obj["allowed_created"]), + obj["retry_safe"], + tuple( + Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"])) + for r in obj["receipts"] + ), + ) + if ( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ) != ( + initial.step_id, + initial.owner, + initial.pins, + initial.allowed_created, + initial.retry_safe, + ): + raise Refused("journal pins changed") + return state + + def save( + self, + binding: str, + state: StepState, + evidence: OwnerResult | None = None, + invocation: Invocation | None = None, + intent: OriginalIntent | None = None, + ): + if intent is not None: + if type(intent) is not OriginalIntent: + raise Refused("exact original intent required") + intent.validate() + expected = ( + Outcome.RUNNING + if intent.invocation.operation == "run" + else Outcome.ROLLBACK_RUNNING + ) + if ( + intent.binding != binding + or intent.invocation != invocation + or intent.invocation.pins != state.pins + or not state.last + or state.last.outcome is not expected + ): + raise Refused("original intent and running receipt differ") + self.db.execute("BEGIN IMMEDIATE") + try: + if intent is not None: + self.db.execute( + "INSERT INTO intents(attempt_id,binding,payload) VALUES (?,?,?)", + (intent.invocation.attempt_id, binding, intent.payload()), + ) + self.db.execute( + "INSERT INTO receipts(binding,payload) VALUES (?,?)", + ( + binding, + json.dumps( + asdict(state) + | { + "journal_schema": "warden.case.step-journal.v1", + "attempt_id": invocation.attempt_id if invocation else None, + "operation": invocation.operation if invocation else None, + "evidence_digest": digest(evidence.evidence.encode()) + if evidence + else None, + "session_revoked": evidence.revoked if evidence else None, + "session_revocation_digest": digest( + evidence.session_revocation_evidence.encode() + ) + if evidence and evidence.session_revocation_evidence + else None, + "grant_revocation_digest": digest( + evidence.grant_revocation_evidence.encode() + ) + if evidence and evidence.grant_revocation_evidence + else None, + }, + separators=(",", ":"), + ), + ), + ) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK") + raise + + +class Executor: + """Injected fixed owner adapters and provenance verifier; none ship by default. + + evidence verifier must authenticate owner custody/provenance or explicitly + attest attended manual evidence; a bare JSON reference is never sufficient. + It receives purpose, exact source digest, and named evidence reference. + """ + + def __init__( + self, + journal: Journal, + adapters: dict[str, Adapter], + evidence_verifier: Callable[[str, str, str], bool], + *, + browser_inputs: BrowserInputs | None = None, + ): + self.journal, self.adapters, self.evidence_verifier = ( + journal, + dict(adapters), + evidence_verifier, + ) + self._admitted = {} + self.browser_inputs = browser_inputs + + def admit( + self, + case_source: bytes, + type_source: bytes, + descriptor_source: bytes, + catalog_source: bytes, + *, + principal: str, + actor: str, + lane: str, + acceptance_evidence: str, + materialization_evidence: str, + descriptor_evidence: str, + session_evidence: str, + ) -> tuple[Descriptor, Invocation, StepState]: + for source in (case_source, type_source, descriptor_source, catalog_source): + bounded_source(source) + descriptor = Descriptor.decode(descriptor_source) + try: + case = yaml.safe_load(case_source)["spec"] + case_type = yaml.safe_load(type_source)["spec"] + if not isinstance(case, dict) or not isinstance(case_type, dict): + raise ValueError + except (yaml.YAMLError, KeyError, TypeError, ValueError): + raise Refused("invalid case or type source shape") from None + if case.get("status") != "materialized" or not case.get("acceptance"): + raise Refused("case is not accepted and structurally materialized") + if case_type.get("status") not in ("acknowledged", "implemented"): + raise Refused("type is not acknowledged") + if case.get("type") != f"{case_type['id']}@{case_type['version']}": + raise Refused("case type pin mismatch") + if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest( + descriptor_source + ): + raise Refused( + "type has no exact acknowledged machine descriptor; plan only" + ) + for purpose, source, evidence in ( + ("case-acceptance", digest(case_source), acceptance_evidence), + ( + "ops-mason-materialization", + digest(case_source), + materialization_evidence, + ), + ( + "owner-descriptor-acceptance:" + descriptor.owner, + digest(descriptor_source), + descriptor_evidence, + ), + ("type-machine-seam-acceptance", digest(type_source), descriptor_evidence), + ("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence), + ( + f"caller-session:{actor}:{principal}:{lane}", + digest(case_source), + session_evidence, + ), + ): + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, source, evidence) is not True + ): + raise Refused("independent owner evidence not verified") + if ( + not IDENTIFIER.fullmatch(principal) + or actor != descriptor.actor + or lane != descriptor.lane + ): + raise Refused("principal actor or lane mismatch") + if descriptor.effect == "mint" and ( + actor != "human" or os.getenv("WARDEN_AGENT_ID") + ): + raise Refused("agent mint is forbidden") + parameters = case.get("parameters", {}) + declared = { + item["name"]: item + for item in case_type.get("parameters", []) + if isinstance(item, dict) and isinstance(item.get("name"), str) + } + if ( + not isinstance(parameters, dict) + or descriptor.target_parameter not in declared + ): + raise Refused("target parameter is not declared by acknowledged type") + if case_type.get("id") == "oidc-browser-relying-party": + if type(self.browser_inputs) is not BrowserInputs: + raise Refused("browser metadata validator is not configured") + target = self.browser_inputs.target( + yaml.safe_load(case_source), + yaml.safe_load(type_source), + descriptor.target_parameter, + ) + else: + for name in parameters: + definition = declared.get(name) + if ( + definition is None + or definition.get("sensitivity") not in ("public", "operational") + or definition.get("case_settable") is False + ): + raise Refused("unknown, fixed or secret-sensitivity case parameter") + target = parameters.get(descriptor.target_parameter) + if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): + raise Refused("target must be exact case metadata") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("unknown adapter or owner source pin") + sheet = json.dumps( + [ + descriptor.step, + descriptor.owner, + target, + actor, + principal, + lane, + acceptance_evidence, + materialization_evidence, + descriptor_evidence, + session_evidence, + ], + separators=(",", ":"), + ).encode() + pins = Pins( + digest(case_source), + digest(type_source), + digest(descriptor_source), + digest(catalog_source), + digest(acceptance_evidence.encode()), + digest(sheet), + ) + invocation = Invocation( + descriptor.step, descriptor.owner, target, actor, principal, lane, pins + ) + initial = StepState( + descriptor.step, + descriptor.owner, + pins, + descriptor.allowed_created, + descriptor.retry_safe, + ) + self._admitted[invocation] = (descriptor, initial, session_evidence) + return descriptor, invocation, initial + + def _result( + self, adapter: Adapter, invocation: Invocation, result: OwnerResult + ) -> OwnerResult: + if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome): + raise Refused("unknown owner result shape") + if type(result.created) is not tuple or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created + ): + raise Refused("owner created-object result invalid") + if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch( + result.evidence + ): + raise Refused("owner evidence identifier invalid") + if adapter.verify(invocation, result) is not True or result.revoked is not True: + raise Refused("owner result or session revocation unverified") + if (result.attempt_id, result.operation) != ( + invocation.attempt_id, + invocation.operation, + ): + raise Refused("owner receipt belongs to another attempt") + descriptor = self._admitted[ + replace(invocation, attempt_id="", operation="admission") + ][0] + proof_fields = { + "parent-session": ( + f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.session_revocation_evidence, + ), + "broker-grant": ( + f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.grant_revocation_evidence, + ), + } + proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes] + for purpose, evidence in proofs: + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, invocation.pins.case, evidence) + is not True + ): + raise Refused("independent grant or parent-session revocation absent") + return result + + def _verify_evidence(self, purpose, source, evidence): + try: + with contained_output(): + return self.evidence_verifier(purpose, source, evidence) is True + except Exception: + raise Refused("independent evidence verification failed") from None + + def _admission( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ): + if self._admitted.get(invocation, ())[:2] != (descriptor, initial): + raise Refused("execution requires verified admission on this executor") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("adapter changed after admission") + session = self._admitted[invocation][2] + if ( + self._verify_evidence( + f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}", + invocation.pins.case, + session, + ) + is not True + ): + raise Refused("caller session no longer verified") + if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"): + raise Refused("agent mint is forbidden") + return adapter + + def _attempt(self, invocation: Invocation, state: StepState, operation: str): + identity = json.dumps( + [asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex], + separators=(",", ":"), + ).encode() + return replace( + invocation, + attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(), + operation=operation, + ) + + def run( + self, + descriptor: Descriptor, + invocation: Invocation, + initial: StepState, + *, + retry=False, + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + recovered = recover(state) + if recovered != state: + self.journal.save(binding, recovered) + state = recovered + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin(state, invocation.pins, retry=retry) + dispatched = self._attempt(invocation, running, "run") + self.journal.save( + binding, + running, + invocation=dispatched, + intent=OriginalIntent(binding, dispatched, adapter.source), + ) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, dispatched, adapter.invoke(dispatched) + ) + completed = finish( + running, invocation.pins, result.outcome, created=result.created + ) + evidence = result + except Exception: + completed = finish(running, invocation.pins, Outcome.UNCERTAIN) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def rollback( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + if recover(state) != state: + self.journal.save(binding, recover(state)) + raise Refused("lost rollback result requires owner reconciliation") + if adapter.rollback is None: + raise Refused("owner has no reviewed undo adapter") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin_rollback(state, invocation.pins) + dispatched = self._attempt(invocation, running, "rollback") + self.journal.save( + binding, + running, + invocation=dispatched, + intent=OriginalIntent(binding, dispatched, adapter.source), + ) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.rollback(dispatched, running.last.created), + ) + verified = result.outcome is Outcome.ROLLED_BACK and not result.created + evidence = result if verified else None + except Exception: + verified = False + completed = finish_rollback(running, invocation.pins, verified=verified) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def reconcile( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + """Explicit owner-native state inspection; never redispatches the mutation.""" + adapter = self._admission(descriptor, invocation, initial) + if adapter.reconcile is None and adapter.reconcile_original is None: + raise Refused("owner reconciliation adapter absent") + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = recover(self.journal.load(binding, initial)) + if not state.last or state.last.outcome not in ( + Outcome.UNCERTAIN, + Outcome.FAILED_AFTER, + ): + raise Refused("no uncertain mutation to reconcile") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + state = replace( + state, + receipts=( + *state.receipts, + Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN), + ), + ) + original = ( + self.journal.original_intent(binding, invocation, adapter.source) + if adapter.reconcile_original is not None + else None + ) + dispatched = self._attempt(invocation, state, "reconcile") + self.journal.save(binding, state, invocation=dispatched) + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.reconcile_original(ReconcileContext(original, dispatched)) + if original is not None + else adapter.reconcile(dispatched), + ) + if result.outcome not in ( + Outcome.NO_CHANGE, + Outcome.SUCCESS, + Outcome.FAILED_BEFORE, + ): + raise Refused("owner did not reconcile target state") + # Validate created-object scope using the same closed result reducer. + finish( + begin( + StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ), + state.pins, + ), + state.pins, + result.outcome, + created=result.created, + ) + reconciled = StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ( + *state.receipts, + Receipt(len(state.receipts) + 1, result.outcome, result.created), + ), + ) + self.journal.save(binding, reconciled, result, dispatched) + return reconciled + + +class OwnerInterrupted(KeyboardInterrupt): + """Sanitized interruption preserves persisted running/crash recovery semantics.""" + + def __init__(self): + super().__init__("owner execution interrupted") + + +class _Discard: + """Never retain or forward adapter output, including unbounded malicious text.""" + + def write(self, text): + return len(text) + + def flush(self): + pass diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/tests/test_case_original_context_consistency.py b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/tests/test_case_original_context_consistency.py new file mode 100644 index 00000000..ddd429dd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/tests/test_case_original_context_consistency.py @@ -0,0 +1,198 @@ +"""Original-intent consistency checks with generated metadata only. + +Reuse the existing synthetic owner rig. These checks do not provide a native +holder or independently authenticate original creation from local SQL/JSON. +""" + +from dataclasses import asdict, replace +import json +from pathlib import Path +import sqlite3 +import subprocess +import sys +import pytest +from tests.test_case_run import rig as rig +from warden.case_receipt_state import Outcome, Refused +from warden.case_run import OriginalIntent + + +def interrupted(rig): + executor, args, calls, sources, kwargs = rig + old = executor.adapters[args[0].adapter] + seen = [] + + def lost(inv): + calls.append(inv) + raise TimeoutError("generated lost response") + + def original(ctx): + seen.append(ctx) + return old.reconcile(ctx.attempt) + + executor.adapters[old.reference] = replace( + old, invoke=lost, reconcile=None, reconcile_original=original + ) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + return executor, args, calls, old, seen + + +def alter_payload(executor, **changes): + row = executor.journal.db.execute( + "SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1" + ).fetchone() + obj = json.loads(row[1]) + obj.update(changes) + executor.journal.db.execute( + "UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0]) + ) + + +def test_original_actor_mismatch_is_refused(rig): + executor, args, calls, old, _ = interrupted(rig) + alter_payload(executor, actor="agent") + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + + +def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig): + executor, args, calls, old, _ = interrupted(rig) + actual = calls[0].attempt_id + alter_payload(executor, attempt_id="attempt-unrelated") + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + assert ( + executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0] + == actual + ) + + +def test_orphan_intent_without_original_running_receipt_is_refused(rig): + executor, args, calls, old, _ = interrupted(rig) + executor.journal.db.execute( + "DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)" + ) + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.UNCERTAIN + ) + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + + +def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig): + _, args, _, _, _ = rig + inv = replace( + args[1], + target={"unexpected": "synthetic-owner-payload"}, + operation="run", + attempt_id="attempt-fixture", + ) + record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64) + with pytest.raises(Refused): + OriginalIntent.decode(record.payload()) + # This is a direct serialization API gap, not an observed normal-admit leak. + + # Owner trust is the existing synthetic fixture; this proves call shape only. + + +@pytest.mark.parametrize( + "field,value", + [ + ("principal", "another-person"), + ("tenant", "another-tenant"), + ("realm", "another-realm"), + ], +) +def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch( + rig, field, value +): + executor, args, _, old, _ = interrupted(rig) + alter_payload(executor, **{field: value}) + with pytest.raises(Refused): + executor.journal.original_intent( + "mint-secret:fixture-client", args[1], old.source + ) + + +def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig): + executor, args, calls, _, _ = rig + executor.journal.db.execute( + "CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END" + ) + with pytest.raises(sqlite3.IntegrityError): + executor.run(*args) + assert not calls + assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == ( + 0, + ) + assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( + 0, + ) + + +def test_existing_missing_original_refuses_before_callback(rig): + executor, args, _, _, seen = interrupted(rig) + executor.journal.db.execute("DELETE FROM intents") + with pytest.raises(Refused): + executor.reconcile(*args) + assert not seen + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.UNCERTAIN + ) + + +def test_existing_current_parent_revocation_remains_mandatory(rig): + executor, args, _, _, _ = interrupted(rig) + old = executor.evidence_verifier + + def refuse_session(purpose, source, evidence): + return ( + False + if purpose.startswith("session-revocation:") + else old(purpose, source, evidence) + ) + + executor.evidence_verifier = refuse_session + with pytest.raises(Refused): + executor.reconcile(*args) + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.UNCERTAIN + ) + + +def test_existing_intent_survives_actual_distinct_interpreter(rig): + executor, args, calls, old, _ = interrupted(rig) + code = """import json,sys +from pathlib import Path +from warden.case_run import Journal,Invocation +from warden.case_receipt_state import Pins +obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1])) +with j.locked(): + i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2]) + print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation})) +""" + result = subprocess.run( + [sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source], + input=json.dumps(asdict(args[1])), + text=True, + capture_output=True, + timeout=15, + env={ + "PATH": "/usr/bin:/bin", + "PYTHONPATH": str(Path(__file__).parents[1] / "src"), + "PYTHONDONTWRITEBYTECODE": "1", + }, + check=True, + ) + assert json.loads(result.stdout) == { + "attempt": calls[0].attempt_id, + "operation": "run", + } diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/workplans/WARDEN-WP-0044-security-case-follow-through.md b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/workplans/WARDEN-WP-0044-security-case-follow-through.md new file mode 100644 index 00000000..f6413241 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/postimages/workplans/WARDEN-WP-0044-security-case-follow-through.md @@ -0,0 +1,312 @@ +--- +id: WARDEN-WP-0044 +type: workplan +title: "Follow through on KnownSecurityCases along their type's route" +domain: infotech +repo: ops-warden +status: active +flavor: implementation +owner: claude-code +topic_slug: netkingdom +created: "2026-10-08" +updated: "2026-10-11" +depends_on: [] +related_repos: + - net-kingdom + - ops-mason + - key-cape + - railiance-platform +state_hub_workstream_id: "f7e81013-6868-5d99-8528-ba75a442eca9" +--- + +# WARDEN-WP-0044 — Follow through on KnownSecurityCases along their type's route + +## Why + +NetKingdom is introducing SecurityCaseTypes (net-kingdom `NK-WP-0048`, canon +`security-case-types_v0.1`, proposed). Once a case of an implemented type is +accepted and ops-mason has materialized its structure (`MASON-WP-0010`), +ops-warden carries out the remaining steps. These are the owners' declared entry +points, run on the authentication path the type defines, followed by the type's +verification. + +Today ops-warden routes needs and, for `exec_capable` lanes, proxies fetches as +the caller. It does not yet know about cases, and each new integration ends with +a founder running a hand-written script (for example the proposed KEY-WP-0038-T02 +rollout). This plan gives ops-warden the follow-through role, without widening +what it may do: each step still runs under its owner's entry point and the +caller's identity. + +## Tasks + +```task +id: WARDEN-WP-0044-T01 +status: done +depends_on: [NK-WP-0048-T03] +priority: high +state_hub_task_id: "ef0920fc-418e-52cb-acff-9a5c0c728bf5" +``` + +### T01 — Define the type's follow-through section + +Propose the `follow_through` part of the SecurityCaseType schema together with +net-kingdom. For each step it names the owner entry point, `warden_executes`, +the auth method and lane, whether an attended human login is unavoidable (with +the type's justification), and the verification command. Write it for the first +type, `oidc-browser-relying-party`, covering key-cape client registration, +custody minting and LLDAP group membership. + +2026-10-08: NK-WP-0048-T03/T04 landed with a drafted section. Review is in +`history/2026-10-08-oidc-rp-follow-through-review.md`. It requires three +changes before T02: a per-step `lane`, a narrow `warden_executes`, and +`value_handling`. It also lists four refinements. This task is done when +net-kingdom acknowledges the review (NK-WP-0048-T05). + +2026-10-09: R4 was withdrawn in favour of ops-mason's per-case +`{consuming_service}-keycape-client`, with the entry body templated in the type +(R4′, see the addendum in the review). + +2026-10-09: the revised 1.0 type and canon now carry R1–R3, R4′, R5–R7. +`warden case check-type ` (`src/warden/case_type.py`, tests in +`tests/test_case_type.py`) checks a type's follow-through mechanically: the +live type is schema-conformant, with one attended session +(`key-cape-oidc-login`). It is not yet plannable, because two lanes are not +in the catalog: `identity-provisioner-groups` (net-kingdom) and +`keycape-client-secret-generate` (railiance-platform). These gate T02, not +T01. T01 still closes on net-kingdom's acknowledgement (NK-WP-0048-T05). + +2026-10-09: ops-warden cannot add `identity-provisioner-groups` as the type +declares it. The provisioner accepts only user-engine's runtime token and +admits only user-engine pods, so no caller-identity path exists. R8 (review +addendum) asks net-kingdom either to move `ensure-groups` into the existing +attended session (recommended) or to give the provisioner an operator +credential. `keycape-client-secret-generate` needs a generate-in-custody entry +point from railiance-platform, and the broker does not have one yet. Both were +sent to their owners. + +2026-10-09: done. net-kingdom took R8 option 1, so `ensure-groups` runs attended +in the `register-client` session and `identity-provisioner-groups` is dropped +(net-kingdom `0e86f95`). The founder acknowledged the type with memo r4 (hub +decision `e418263e`, NK-WP-0048-T05 done). + +```task +id: WARDEN-WP-0044-T02 +status: done +depends_on: [WARDEN-WP-0044-T01] +priority: high +state_hub_task_id: "5f68c991-f490-58f8-a121-92f74073d272" +``` + +### T02 — Plan a case run + +`warden case plan ` checks that the case is accepted and materialized, +resolves each step to its catalog entry and entry point, and prints the run: +which steps warden executes, which run as the attended caller, what the +verification is. No secret values; no execution. + +Tests: refuse an unaccepted or unmaterialized case; refuse a step whose catalog +entry is missing or not reviewed; show attended steps explicitly. + +2026-10-09: done. `warden case plan [--type] [--json]` +(`src/warden/case_plan.py`, 14 tests in `tests/test_case_plan.py`). It refuses: +- a case that is not `materialized`, or has no acceptance record +- a type that is mismatched or not acknowledged +- any `case check-type` finding, including a missing or unreviewed lane +- undeclared, type-fixed or secret-sensitivity parameters +- a placeholder the case does not set, or an unsupported `when` + +It fills type defaults (`issuer`), evaluates ` == ` conditions, +renders owner, entry point, auth and ready_check, groups attended steps per +lane, and lists the applicable verification. + +The live Forgejo case is correctly refused: it is `accepted`, not +materialized. A materialized copy, with a stand-in lane for +`keycape-client-secret-generate`, renders six steps: two attended steps plus +`register-client` in one `key-cape-oidc-login` session, one warden step +(`mint-secret`), and two owner steps. Seven checks follow. + +Observation for net-kingdom (not blocking): `bind-env` runs before +`register-client`, in type order. Its entry carries a literal +`KEYCAPE__CLIENT_SECRET`, which is not a `{param}` placeholder, so it +reaches the plan unrendered. + +2026-10-09 follow-up: +- **Format specs.** `_render` supports net-kingdom's `{param:env}` spec (upper + case, `-` as `_`, the same as `keycape-client.py env_name`) and refuses any + other spec or `!conversion`. +- **Case reference.** The plan prints `case-ref: @sha256:` over the + case file's exact bytes. This is the reference proposed to railiance-platform + for `credential.py generate --case-ref` (RPF-WP-0054-T01); the broker + re-derives it from the file. +- 18 tests now. + +```task +id: WARDEN-WP-0044-T03 +status: progress +depends_on: [WARDEN-WP-0044-T02, MASON-WP-0010-T03] +blocking_reason: "Local durable execution kernel and injected failure harness implemented. Native acceptance still requires authenticated owner machine descriptors/adapters, actual Forgejo materialization receipt, contained session integration, ordered verification and first native run." +priority: high +state_hub_task_id: "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89" +``` + +### T03 — Run and verify a case + +`warden case run ` executes the plan. Owner entry points run through +the existing proxy as the caller; steps that need an attended login use the +existing contained login envelope (silent command, session revoked afterwards), +grouped into as few attended sessions as possible. Then it runs the type's +verification and writes a receipt without values. `warden case rollback` +reverses exactly this case's steps. + +Acceptance: the Forgejo reference case (NK-WP-0048-T06) reaches `live` through +this path, with no case-specific script. + +2026-10-17: original-intent lookup/context interface for NK-WP-0048-T15, source +and synthetic tests only. `src/warden/case_run.py`: `Executor.run` writes an +`OriginalIntent` (schema `warden.case.original-intent.v1`: the dispatched run +Invocation — attempt, step, owner, target, actor, principal, lane, all six pins, +tenant, realm — plus admitted adapter source) in the same SQLite transaction as +the pre-dispatch journal row. `Journal.original_intent(binding, current, source)` +returns it after restart and refuses on a missing record or any +pins/source/step/owner/target/actor/principal/lane/tenant/realm mismatch, before any +journal write, so the step stays uncertain. An adapter's +`reconcile_original(ReconcileContext(original, attempt))` receives the original +intent separately from the new reconcile attempt. This is Warden's record of what +it dispatched, not owner created-only provenance. Current Executor.admit leaves +tenant/realm empty; the reserved coordinates establish no admitted provenance. +Owner descriptors/adapters, contained session integration, Forgejo +materialization and the native run remain open. + +2026-10-17 source consistency follow-through: retained the additive intents table +and reconcile_original callback and fixed four reproduced local defects: original +actor mismatch, payload/SQL attempt-ID mismatch, orphan intent without its +matching original running receipt, and arbitrary nonmetadata values accepted by +the original-intent serializer. Exact bounded field validation, duplicate-free +bounded decode and save-time intent/invocation/running-state equality now preserve +the original receipt linkage before owner lookup. No new context store, native +verifier, trust root, adapter activation or authority migration was introduced. + +The actual canonical focused kernel and new consistency suite passes 54 checks; +owner-installed Ruff check/format passes. The existing generated-data/synthetic +owner fixture and a distinct interpreter prove source correspondence/refusal, +not independent native custody or creation. Earlier comparison probes and failed +preparation are retained. Current caller and fresh session/grant revocation checks +remain required; original owner payload/revocation, exact native readback and +created-only provenance still need independent authenticated owner admission. +No JSON/SQL context, unknown tenant/realm, changed run-sheet or legacy missing +intent supplies recovery, replay or undo authority. T03 remains progress and all +native acceptance gates/statuses/dependencies/UUIDs remain unchanged. Evidence: +Custodian original-context-compare-r2 and original-context-owner-followthrough-r3 +under docs/changes/netkingdom-target-2026-10-17/; publication/projection is separate. + +Agreed with railiance-platform (RPF-WP-0054-T01, 2026-10-09): +- `mint-secret` runs `credential.py generate --grant keycape/client-secret-generate + --client-id --case-ref --case-file ` as the caller's + child process. The broker re-derives the case-ref and checks status and + client_id. +- warden refuses that step when `WARDEN_AGENT_ID` is set. + +2026-10-10: railiance-platform implemented the command (RPF-WP-0054-T03). It is +catalogued as draft `keycape-client-secret-generate`, with `exec_owner` +railiance-platform and `exec_capable: false`. `warden case check-type` now +reports the lane as LANE-UNREVIEWED rather than LANE-MISSING. Before it can be +activated: +- the founder binds the issuer policy to the operator OIDC role +- key-cape confirms the secret format +- the RPF-WP-0054-T05 probe passes + +2026-10-10 (later): +- **Broker lane.** The issuer role is bound (CCR-2026-0043) and the T05 probe + passed (railiance-platform `21b8af3`). Only key-cape's format confirmation is + left. The playbook now includes the issuer-role login, a 15m token with + unlimited uses that `warden case run` must group with the attended session. +- **Env spec.** net-kingdom adopted `{client_id:env}` as a 1.0 patch + (`9a9b8e3`). The plan renders `KEYCAPE_FORGEJO_CLIENT_SECRET`. +- **Forgejo case.** Rendered and built by ops-mason. It is not yet + `materialized`: MASON-WP-0010-T03 waits on the railiance-platform CCRs and + their founder apply. + +2026-10-10: `keycape-client-secret-generate` activated on railiance-platform's +go-ahead (message 20973a9c) after key-cape confirmed the format (4e0213b3). +`warden case check-type` on the live type now reports `plannable=True`. The +only thing T03 still waits on is the Forgejo case reaching `materialized` +(MASON-WP-0010-T03). + +2026-10-10: net-kingdom moved the type to 1.1 (`e2174ce`) and re-pinned the +Forgejo case. 1.1 adds the verification `consumer-enforces-groups` and requires +the hand-over to enforce `access_groups`. `check-type` on 1.1 reports +`plannable=True`, and a materialized copy renders six steps and the new check. +The case-ref digest covers the whole case file, including `status`, so it +changes when the case becomes `materialized`. `warden case run` therefore takes +it from the file at run time, and the broker re-derives it from the same file. + +```task +id: WARDEN-WP-0044-T04 +status: wait +depends_on: [WARDEN-WP-0044-T03] +blocking_reason: "Catalog integration follows a proven run." +priority: medium +state_hub_task_id: "4a0fa4a7-5ca9-518a-ab92-a79a5e2cf93e" +``` + +### T04 — Catalog entries from types + +Let a type contribute reviewed catalog entries once, so that every case of that +type routes without a new catalog edit. Cases add parameter values, not routes. +`warden route find` should be able to answer "which SecurityCaseType covers this +need?" before anyone builds by hand. + + +2026-10-10 consolidation source prototype (not an accepted owner adapter API): +`src/warden/case_receipt_state.py` provides a pure immutable receipt reducer, +with exact case/type/adapter/catalog/acceptance/run-sheet pins, running intent +before dispatch, uncertain recovery after a lost result, no success replay, +explicit retry-safe failed-before-change retry and created-only reverse undo. +It executes nothing and accepts no command output. The caller must durably +persist running before any future side effect; storage/authenticated owner +receipts/live adapters are not implemented. Focused offline tests cover changed +pins, lost receipts, retry/refusal, uncertain rollback and shared-object +preservation. Source proposal: `docs/security-case-execution-proposal.md`. + +T03 remains open: typed owner execution/receipt contracts require review, the +Forgejo structural materialization needs owner receipts, and CLI dispatch, +contained sessions, native verification and provenance remain unimplemented. +There is a circularity to resolve with net-kingdom/ops-mason: materialization +cannot require mint-secret if mint-secret is itself a follow-through step whose +planning requires materialized. No case status is changed by this prototype. + + +2026-10-11: the exact execution-design memo version1 has confirmed native human +acceptance (approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca). The approved proposal +bytes remain unchanged. `src/warden/case_run.py` now implements a closed v1 local +execution seam, independent admission/evidence callbacks, private transactional +SQLite receipt storage, process locking, running-before-dispatch, crash recovery +to uncertainty, explicit safe retry, separately required broker-grant and parent-session revocation +proofs, native owner reconciliation and created-only +reverse rollback. Thirty-seven kernel tests and fourteen reducer tests pass; +ruff checks pass. See `docs/security-case-execution-kernel-v1.md` for exact scope +and receipt provenance. No installed native adapter or live CLI execution is +claimed. Current prose types stay plan-only; acknowledged descriptor/type machine +seams and authenticated owner/caller receipts are still required. T03 progresses +for implementation but cannot finish before ordered native Forgejo acceptance. + +2026-10-16 source preparation (isolated checkout; no native acceptance): +T03's browser parameter mismatch now has a producer-validator-backed projection +in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2 +parameter definitions and all four reviewed producer modules are pinned; full case +validation admits reviewed security-sensitive metadata while unknown/fixed/secret +inputs and type/producer drift refuse. Only the reviewed descriptor target +reaches the owner callback; no parameter journal payload is added. Actual +Executor/producer tests cover positive versions and refusal/containment paths. +Acknowledged machine descriptors, authenticated owner evidence, real Forgejo +materialization and contained native sessions remain mandatory and unimplemented; +T03 stays progress, T04 stays wait. No type or case status changed. + +2026-10-16 producer source refresh: independently reviewed NetKingdom +`015694ee414c802dc2ef1fa0120dfed610086e69` optional descriptor-map repair. +Only the validator/evidence source pins change; exact parameter and transitive +source identity remain enforced. Actual canonical producer, Executor and reducer +suite passes 87 checks, including 14 new malformed-map integration cases; all +four missing/changed modules still refuse. Review: +`docs/reviews/2026-10-16-browser-producer-pin-refresh.md`. No adapter attestation, +native acceptance, credential use or privileged dispatch. T03 remains progress. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/docs/security-case-execution-kernel-v1.md b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/docs/security-case-execution-kernel-v1.md new file mode 100644 index 00000000..35289d77 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/docs/security-case-execution-kernel-v1.md @@ -0,0 +1,127 @@ +# Closed case execution kernel v1 — 2026-10-11 + +Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal +at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +was accepted through memo `infd-20261010-security-case-execution`, version 1, +approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition +disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original +approved proposal remains byte-identical. That decision accepts source design; +it grants no execution session, credential or deployment authority. + +`src/warden/case_run.py` advances the pure reducer into an executable injected +owner harness with private durable receipts. Descriptor version +`warden.case.execution.v1` is a closed local implementation seam, not an accepted +NetKingdom type schema. It contains fixed owner/adapter/source digest, actor, +lane, target parameter, effect, reviewed created-object identifiers declared +retry safety and explicit required revocation scopes (parent-session and broker-grant). +A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused. +An acknowledged type must additionally pin the descriptor bytes under its +`execution_descriptors` map and independent owner evidence must authenticate +that exact machine-seam revision. Existing descriptive types lack this map and +remain plan-only. Their acknowledged labels do not authorize this new seam. + +There are no installed owner adapters, native provenance verifier or dispatch CLI +in this change. Callables are injected only through a fixed adapter registry, +with immutable owner reference/source pins. A real integration must supply +independent owner/native verification or explicit attended manual evidence; +a bare JSON receipt identifier is insufficient. Fixture verifiers in tests +are deliberately fake and attest no native case. Caller sessions are independently +verified for exact principal/actor/lane at admission and again before dispatch; +input actor labels alone are never sufficient. Agent mint is refused, including +when WARDEN_AGENT_ID appears after initial human admission. + +Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference +and run-sheet identity. Actual ops-mason materialization evidence is required +against the case source, separately from its status label. Case parameters supply +an exact metadata target. Source/session changes cannot inherit journal success. +Owner results have a closed metadata shape, result evidence fingerprint and +separate independently authenticated grant/parent-session revocation evidence +fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and +created objects outside the reviewed scope become uncertain, never success. +Python and OS stdout/stderr, including inherited child-process output, are +discarded without buffering or retention under a serialized fd containment +context. Independent provenance verification uses the same containment and +sanitizes exceptions. All source inputs are capped at 256 KiB before parsing. +Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other +owner interruptions are rethrown with a fixed message while retaining running +intent for crash recovery. + +The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks +and insecure modes, uses SQLite FULL synchronization and commits running intent +before calling the adapter. A separate process lock prevents concurrent mutation. +A crash between dispatch and final receipt leaves running; the next lock holder +persists uncertain before refusing replay. Both intent and outcome remain in +history. Each persisted attempt has a unique identity bound to all exact pins, +operation (run/reconcile/rollback), sequence and nonce. Adapter results and both +revocation proof purposes carry that identity. Prior-attempt evidence cannot +verify a safe retry, even if a caller rewrites result labels. Descriptor-created +objects are capped at 32 and histories at 128 transitions, with dispatch stopped +before archival is required. A throw or lost response is not failed-before-change. A safe retry needs +verified failed-before-change, unchanged pins, descriptor retry-safe and explicit +retry. Success and no-change cannot replay. + +Reconciliation is an explicit fixed owner callback, never the mutation callback. +It may verify an already-present native CAS result as no-change/success, or prove +failed-before-change; it never tells Warden to regenerate a credential. Uncertain +or failed-after-change requires this owner-native inspection before any retry. +Owner proof and revocation remain mandatory. Rollback visits only objects in a +verified creation receipt, in reverse order; no-change has no undo and shared +objects are excluded. Unknown/lost undo blocks another rollback. + +The kernel operates individual admitted steps. Full ordered run-sheet orchestration, +verification inventory, contained login integration, native owner adapters and +CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is +implemented. Current Forgejo source is not materialized and has no acknowledged +machine descriptors. Its first native run remains an acceptance requirement, +with real materialization, separately authenticated broker-grant and parent-session +revocation receipts, session/owner receipts and consumer enforcement. This +implementation neither promotes a type nor changes any case status. + +Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open +an independent SQLite connection inside dispatch to prove running was committed, +crash after mutation and reopen a fresh journal to prove no duplicate invocation, +lose a response then reconcile without mint, require explicit retry, check exact +pins/session expiry, withhold malicious output and unconfirmed revocation, and +prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and +checks pass. No live token or target action occurred. + +## Browser input projection — 2026-10-16 source preparation + +An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser +cases before admission. It calls the real NetKingdom type/case validator over +verified producer bytes in a disposable Python process (`-I`, empty environment, +timeout, no inherited output). The validator, composer, capability contract and lazy evidence +modules have exact source pins; missing or changed code refuses. There is no +automatic discovery or download. A producer upgrade needs reviewed pin changes. + +Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest +of each complete parameter definition list prevents reclassifying secrets as +metadata, loosening constraints, or adding fields under a recognized version. +The complete case passes producer validation, including required fields, +constraints and credential-shape checks. Explicit fixed/default overrides, +unknown fields and secret references refuse. Security-sensitive browser metadata +can therefore be present without being passed to an owner adapter: the only +projection is its descriptor's exact public/operational target identifier. +No raw parameter values are added to invocations or persisted receipts. + +`require_implemented=False` validates the acknowledged type's source, not its +execution authority. Existing descriptor/type/source/catalog/caller provenance +gates stay mandatory. Neither historical browser type contains the required +exact machine-descriptor map, and neither is promoted by this change. There are +still no installed native owner adapters, authenticated provenance verifier, +contained session/ordered run orchestration or live CLI dispatch. A journal digest +is a fingerprint and cannot authenticate native evidence. Completion still needs +owner-admitted descriptors and actual structural, mint/delivery, consumer and +revocation/rollback evidence. Local fixture success is not native acceptance. + +Tests use the actual producer checkout (override sibling location with +`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected +test-only owner adapter/evidence verifier. Their descriptor extensions and case +materialized labels exist only in memory. Canonical case/type bytes stay intact. + +Producer integration refinement: the accepted NetKingdom browser-case patch +`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05` +is now the tested producer revision. Its evidence module is imported lazily only +by `check-evidence`, outside this admission path; Warden nevertheless pins and +copies it with the complete reviewed producer set. Missing or modified bytes in +any of the four modules refuse admission. The test covers every dependency. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/src/warden/case_run.py b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/src/warden/case_run.py new file mode 100644 index 00000000..331ce11a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/src/warden/case_run.py @@ -0,0 +1,826 @@ +"""Closed local execution kernel v1; no installed adapters or live CLI dispatch. + +An owner must acknowledge each descriptor and provide an independent provenance +verifier. Descriptive case types remain plan-only. Nothing executes shell prose. +""" + +from __future__ import annotations + +from contextlib import contextmanager, redirect_stderr, redirect_stdout +from dataclasses import asdict, dataclass, replace +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import sqlite3 +import threading +import uuid +from typing import Callable + +import yaml +from .case_inputs import BrowserInputs +from .case_receipt_state import ( + Outcome, + Pins, + Receipt, + Refused, + StepState, + begin, + finish, + recover, + begin_rollback, + finish_rollback, +) + +IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z") +MAX_SOURCE_BYTES = 262144 +MAX_HISTORY = 128 +OUTPUT_LOCK = threading.RLock() + + +def bounded_source(value: bytes): + if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES: + raise Refused("source exceeds bounded input contract") + + +@contextmanager +def contained_output(): + """Discard Python and inherited OS stdout/stderr; serialize process fd changes.""" + with OUTPUT_LOCK: + saved = [] + null = os.open(os.devnull, os.O_WRONLY) + try: + for target in (1, 2): + saved.append((target, os.dup(target))) + os.dup2(null, target) + with redirect_stdout(_Discard()), redirect_stderr(_Discard()): + try: + yield + except Exception: + raise Refused("owner callback failed") from None + except BaseException: + raise OwnerInterrupted() from None + finally: + for target, original in saved: + os.dup2(original, target) + os.close(original) + os.close(null) + + +DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") + + +def digest(value: bytes) -> str: + return "sha256:" + hashlib.sha256(value).hexdigest() + + +@dataclass(frozen=True, slots=True) +class Descriptor: + version: str + step: str + owner: str + adapter: str + adapter_source: str + actor: str + lane: str + target_parameter: str + effect: str + allowed_created: tuple[str, ...] + retry_safe: bool + revocation_scopes: tuple[str, ...] + + @classmethod + def decode(cls, source: bytes) -> Descriptor: + bounded_source(source) + try: + obj = json.loads(source) + if set(obj) != set(cls.__dataclass_fields__): + raise ValueError + if not isinstance(obj["allowed_created"], list): + raise ValueError + obj["allowed_created"] = tuple(obj["allowed_created"]) + if not isinstance(obj["revocation_scopes"], list): + raise ValueError + obj["revocation_scopes"] = tuple(obj["revocation_scopes"]) + result = cls(**obj) + identifiers = ( + result.step, + result.owner, + result.adapter, + result.lane, + result.target_parameter, + *result.allowed_created, + ) + if result.version != "warden.case.execution.v1" or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers + ): + raise ValueError + if not DIGEST.fullmatch(result.adapter_source) or result.actor not in ( + "human", + "agent", + ): + raise ValueError + if ( + result.effect not in ("read", "change", "mint") + or type(result.retry_safe) is not bool + ): + raise ValueError + scopes = set(result.revocation_scopes) + if ( + len(scopes) != len(result.revocation_scopes) + or not scopes <= {"parent-session", "broker-grant"} + or "parent-session" not in scopes + ): + raise ValueError + if result.effect == "mint" and "broker-grant" not in scopes: + raise ValueError + if len(result.allowed_created) > 32: + raise ValueError + if len(set(result.allowed_created)) != len(result.allowed_created): + raise ValueError + return result + except (ValueError, TypeError, KeyError): + raise Refused("invalid closed execution descriptor") from None + + +@dataclass(frozen=True, slots=True) +class OwnerResult: + outcome: Outcome + evidence: str + created: tuple[str, ...] = () + revoked: bool = False + session_revocation_evidence: str | None = None + grant_revocation_evidence: str | None = None + attempt_id: str = "" + operation: str = "" + + +@dataclass(frozen=True, slots=True) +class Invocation: + step: str + owner: str + target: str + actor: str + principal: str + lane: str + pins: Pins + attempt_id: str = "" + operation: str = "admission" + tenant: str = "" + realm: str = "" + + +INTENT_SCHEMA = "warden.case.original-intent.v1" + + +@dataclass(frozen=True, slots=True) +class OriginalIntent: + """Durable record of the original mutating dispatch, written before dispatch. + + Binds the original Invocation (attempt, step, owner, target, actor, principal, + lane), all pins, the admitted adapter source and the tenant/realm provenance + the caller was admitted with. It is evidence of what Warden dispatched, not + owner proof of creation: owners still authenticate created-only provenance. + """ + + binding: str + invocation: Invocation + source: str + + def payload(self) -> str: + return json.dumps( + {"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source} + | asdict(self.invocation), + separators=(",", ":"), + sort_keys=True, + ) + + @classmethod + def decode(cls, raw: str) -> OriginalIntent: + obj = json.loads(raw) + fields = {f for f in Invocation.__dataclass_fields__} + if set(obj) != fields | {"schema", "binding", "source"} or obj["schema"] != INTENT_SCHEMA: + raise Refused("unknown original intent schema") + inv = {k: obj[k] for k in fields} + inv["pins"] = Pins(**inv["pins"]) + return cls(obj["binding"], Invocation(**inv), obj["source"]) + + +@dataclass(frozen=True, slots=True) +class ReconcileContext: + """Original intent and the new reconciliation attempt, kept separate.""" + + original: OriginalIntent + attempt: Invocation + + +@dataclass(frozen=True, slots=True) +class Adapter: + reference: str + source: str + invoke: Callable[[Invocation], OwnerResult] + verify: Callable[[Invocation, OwnerResult], bool] + rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None + reconcile: Callable[[Invocation], OwnerResult] | None = None + reconcile_original: Callable[[ReconcileContext], OwnerResult] | None = None + + +class Journal: + """Private SQLite journal plus process lock; transaction commits before dispatch.""" + + def __init__(self, directory: Path): + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + if ( + directory.is_symlink() + or directory.stat().st_uid != os.getuid() + or directory.stat().st_mode & 0o077 + ): + raise Refused("journal directory is not private") + self.directory = directory + for name in ("journal.sqlite", "execution.lock"): + path = directory / name + fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077: + raise Refused("journal file is not private") + finally: + os.close(fd) + self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None) + self.db.execute("PRAGMA synchronous=FULL") + self.db.execute( + "CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + self.db.execute( + "CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + + def original_intent(self, binding: str, current: Invocation, source: str) -> OriginalIntent: + """Latest original run intent for binding, refused unless it matches exactly. + + Missing or mismatched records refuse; the step stays uncertain. + """ + row = self.db.execute( + "SELECT payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + raise Refused("original intent record missing; step stays uncertain") + intent = OriginalIntent.decode(row[0]) + o = intent.invocation + if intent.binding != binding or o.operation != "run": + raise Refused("original intent binding mismatch") + if o.pins != current.pins: + raise Refused("original intent pins mismatch") + if intent.source != source: + raise Refused("original intent source mismatch") + for name in ("step", "owner", "target", "principal", "lane", "tenant", "realm"): + if getattr(o, name) != getattr(current, name): + raise Refused(f"original intent {name} mismatch") + return intent + + @contextmanager + def locked(self): + with (self.directory / "execution.lock").open("rb") as fh: + try: + fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused("case execution already active") from None + try: + yield + finally: + fcntl.flock(fh, fcntl.LOCK_UN) + + def load(self, binding: str, initial: StepState) -> StepState: + row = self.db.execute( + "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + return initial + obj = json.loads(row[0]) + expected = { + "step_id", + "owner", + "pins", + "allowed_created", + "retry_safe", + "receipts", + "journal_schema", + "evidence_digest", + "session_revoked", + "session_revocation_digest", + "grant_revocation_digest", + "attempt_id", + "operation", + } + if ( + set(obj) != expected + or obj["journal_schema"] != "warden.case.step-journal.v1" + ): + raise Refused("unknown journal receipt schema") + state = StepState( + obj["step_id"], + obj["owner"], + Pins(**obj["pins"]), + tuple(obj["allowed_created"]), + obj["retry_safe"], + tuple( + Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"])) + for r in obj["receipts"] + ), + ) + if ( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ) != ( + initial.step_id, + initial.owner, + initial.pins, + initial.allowed_created, + initial.retry_safe, + ): + raise Refused("journal pins changed") + return state + + def save( + self, + binding: str, + state: StepState, + evidence: OwnerResult | None = None, + invocation: Invocation | None = None, + intent: OriginalIntent | None = None, + ): + self.db.execute("BEGIN IMMEDIATE") + try: + if intent is not None: + self.db.execute( + "INSERT INTO intents(attempt_id,binding,payload) VALUES (?,?,?)", + (intent.invocation.attempt_id, binding, intent.payload()), + ) + self.db.execute( + "INSERT INTO receipts(binding,payload) VALUES (?,?)", + ( + binding, + json.dumps( + asdict(state) + | { + "journal_schema": "warden.case.step-journal.v1", + "attempt_id": invocation.attempt_id if invocation else None, + "operation": invocation.operation if invocation else None, + "evidence_digest": digest(evidence.evidence.encode()) + if evidence + else None, + "session_revoked": evidence.revoked if evidence else None, + "session_revocation_digest": digest( + evidence.session_revocation_evidence.encode() + ) + if evidence and evidence.session_revocation_evidence + else None, + "grant_revocation_digest": digest( + evidence.grant_revocation_evidence.encode() + ) + if evidence and evidence.grant_revocation_evidence + else None, + }, + separators=(",", ":"), + ), + ), + ) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK") + raise + + +class Executor: + """Injected fixed owner adapters and provenance verifier; none ship by default. + + evidence verifier must authenticate owner custody/provenance or explicitly + attest attended manual evidence; a bare JSON reference is never sufficient. + It receives purpose, exact source digest, and named evidence reference. + """ + + def __init__( + self, + journal: Journal, + adapters: dict[str, Adapter], + evidence_verifier: Callable[[str, str, str], bool], + *, + browser_inputs: BrowserInputs | None = None, + ): + self.journal, self.adapters, self.evidence_verifier = ( + journal, + dict(adapters), + evidence_verifier, + ) + self._admitted = {} + self.browser_inputs = browser_inputs + + def admit( + self, + case_source: bytes, + type_source: bytes, + descriptor_source: bytes, + catalog_source: bytes, + *, + principal: str, + actor: str, + lane: str, + acceptance_evidence: str, + materialization_evidence: str, + descriptor_evidence: str, + session_evidence: str, + ) -> tuple[Descriptor, Invocation, StepState]: + for source in (case_source, type_source, descriptor_source, catalog_source): + bounded_source(source) + descriptor = Descriptor.decode(descriptor_source) + try: + case = yaml.safe_load(case_source)["spec"] + case_type = yaml.safe_load(type_source)["spec"] + if not isinstance(case, dict) or not isinstance(case_type, dict): + raise ValueError + except (yaml.YAMLError, KeyError, TypeError, ValueError): + raise Refused("invalid case or type source shape") from None + if case.get("status") != "materialized" or not case.get("acceptance"): + raise Refused("case is not accepted and structurally materialized") + if case_type.get("status") not in ("acknowledged", "implemented"): + raise Refused("type is not acknowledged") + if case.get("type") != f"{case_type['id']}@{case_type['version']}": + raise Refused("case type pin mismatch") + if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest( + descriptor_source + ): + raise Refused( + "type has no exact acknowledged machine descriptor; plan only" + ) + for purpose, source, evidence in ( + ("case-acceptance", digest(case_source), acceptance_evidence), + ( + "ops-mason-materialization", + digest(case_source), + materialization_evidence, + ), + ( + "owner-descriptor-acceptance:" + descriptor.owner, + digest(descriptor_source), + descriptor_evidence, + ), + ("type-machine-seam-acceptance", digest(type_source), descriptor_evidence), + ("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence), + ( + f"caller-session:{actor}:{principal}:{lane}", + digest(case_source), + session_evidence, + ), + ): + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, source, evidence) is not True + ): + raise Refused("independent owner evidence not verified") + if ( + not IDENTIFIER.fullmatch(principal) + or actor != descriptor.actor + or lane != descriptor.lane + ): + raise Refused("principal actor or lane mismatch") + if descriptor.effect == "mint" and ( + actor != "human" or os.getenv("WARDEN_AGENT_ID") + ): + raise Refused("agent mint is forbidden") + parameters = case.get("parameters", {}) + declared = { + item["name"]: item + for item in case_type.get("parameters", []) + if isinstance(item, dict) and isinstance(item.get("name"), str) + } + if ( + not isinstance(parameters, dict) + or descriptor.target_parameter not in declared + ): + raise Refused("target parameter is not declared by acknowledged type") + if case_type.get("id") == "oidc-browser-relying-party": + if type(self.browser_inputs) is not BrowserInputs: + raise Refused("browser metadata validator is not configured") + target = self.browser_inputs.target( + yaml.safe_load(case_source), + yaml.safe_load(type_source), + descriptor.target_parameter, + ) + else: + for name in parameters: + definition = declared.get(name) + if ( + definition is None + or definition.get("sensitivity") not in ("public", "operational") + or definition.get("case_settable") is False + ): + raise Refused("unknown, fixed or secret-sensitivity case parameter") + target = parameters.get(descriptor.target_parameter) + if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): + raise Refused("target must be exact case metadata") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("unknown adapter or owner source pin") + sheet = json.dumps( + [ + descriptor.step, + descriptor.owner, + target, + actor, + principal, + lane, + acceptance_evidence, + materialization_evidence, + descriptor_evidence, + session_evidence, + ], + separators=(",", ":"), + ).encode() + pins = Pins( + digest(case_source), + digest(type_source), + digest(descriptor_source), + digest(catalog_source), + digest(acceptance_evidence.encode()), + digest(sheet), + ) + invocation = Invocation( + descriptor.step, descriptor.owner, target, actor, principal, lane, pins + ) + initial = StepState( + descriptor.step, + descriptor.owner, + pins, + descriptor.allowed_created, + descriptor.retry_safe, + ) + self._admitted[invocation] = (descriptor, initial, session_evidence) + return descriptor, invocation, initial + + def _result( + self, adapter: Adapter, invocation: Invocation, result: OwnerResult + ) -> OwnerResult: + if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome): + raise Refused("unknown owner result shape") + if type(result.created) is not tuple or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created + ): + raise Refused("owner created-object result invalid") + if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch( + result.evidence + ): + raise Refused("owner evidence identifier invalid") + if adapter.verify(invocation, result) is not True or result.revoked is not True: + raise Refused("owner result or session revocation unverified") + if (result.attempt_id, result.operation) != ( + invocation.attempt_id, + invocation.operation, + ): + raise Refused("owner receipt belongs to another attempt") + descriptor = self._admitted[ + replace(invocation, attempt_id="", operation="admission") + ][0] + proof_fields = { + "parent-session": ( + f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.session_revocation_evidence, + ), + "broker-grant": ( + f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.grant_revocation_evidence, + ), + } + proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes] + for purpose, evidence in proofs: + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, invocation.pins.case, evidence) + is not True + ): + raise Refused("independent grant or parent-session revocation absent") + return result + + def _verify_evidence(self, purpose, source, evidence): + try: + with contained_output(): + return self.evidence_verifier(purpose, source, evidence) is True + except Exception: + raise Refused("independent evidence verification failed") from None + + def _admission( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ): + if self._admitted.get(invocation, ())[:2] != (descriptor, initial): + raise Refused("execution requires verified admission on this executor") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("adapter changed after admission") + session = self._admitted[invocation][2] + if ( + self._verify_evidence( + f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}", + invocation.pins.case, + session, + ) + is not True + ): + raise Refused("caller session no longer verified") + if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"): + raise Refused("agent mint is forbidden") + return adapter + + def _attempt(self, invocation: Invocation, state: StepState, operation: str): + identity = json.dumps( + [asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex], + separators=(",", ":"), + ).encode() + return replace( + invocation, + attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(), + operation=operation, + ) + + def run( + self, + descriptor: Descriptor, + invocation: Invocation, + initial: StepState, + *, + retry=False, + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + recovered = recover(state) + if recovered != state: + self.journal.save(binding, recovered) + state = recovered + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin(state, invocation.pins, retry=retry) + dispatched = self._attempt(invocation, running, "run") + self.journal.save( + binding, + running, + invocation=dispatched, + intent=OriginalIntent(binding, dispatched, adapter.source), + ) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, dispatched, adapter.invoke(dispatched) + ) + completed = finish( + running, invocation.pins, result.outcome, created=result.created + ) + evidence = result + except Exception: + completed = finish(running, invocation.pins, Outcome.UNCERTAIN) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def rollback( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + if recover(state) != state: + self.journal.save(binding, recover(state)) + raise Refused("lost rollback result requires owner reconciliation") + if adapter.rollback is None: + raise Refused("owner has no reviewed undo adapter") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin_rollback(state, invocation.pins) + dispatched = self._attempt(invocation, running, "rollback") + self.journal.save( + binding, + running, + invocation=dispatched, + intent=OriginalIntent(binding, dispatched, adapter.source), + ) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.rollback(dispatched, running.last.created), + ) + verified = result.outcome is Outcome.ROLLED_BACK and not result.created + evidence = result if verified else None + except Exception: + verified = False + completed = finish_rollback(running, invocation.pins, verified=verified) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def reconcile( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + """Explicit owner-native state inspection; never redispatches the mutation.""" + adapter = self._admission(descriptor, invocation, initial) + if adapter.reconcile is None and adapter.reconcile_original is None: + raise Refused("owner reconciliation adapter absent") + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = recover(self.journal.load(binding, initial)) + if not state.last or state.last.outcome not in ( + Outcome.UNCERTAIN, + Outcome.FAILED_AFTER, + ): + raise Refused("no uncertain mutation to reconcile") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + state = replace( + state, + receipts=( + *state.receipts, + Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN), + ), + ) + original = ( + self.journal.original_intent(binding, invocation, adapter.source) + if adapter.reconcile_original is not None + else None + ) + dispatched = self._attempt(invocation, state, "reconcile") + self.journal.save(binding, state, invocation=dispatched) + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.reconcile_original(ReconcileContext(original, dispatched)) + if original is not None + else adapter.reconcile(dispatched), + ) + if result.outcome not in ( + Outcome.NO_CHANGE, + Outcome.SUCCESS, + Outcome.FAILED_BEFORE, + ): + raise Refused("owner did not reconcile target state") + # Validate created-object scope using the same closed result reducer. + finish( + begin( + StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ), + state.pins, + ), + state.pins, + result.outcome, + created=result.created, + ) + reconciled = StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ( + *state.receipts, + Receipt(len(state.receipts) + 1, result.outcome, result.created), + ), + ) + self.journal.save(binding, reconciled, result, dispatched) + return reconciled + + +class OwnerInterrupted(KeyboardInterrupt): + """Sanitized interruption preserves persisted running/crash recovery semantics.""" + + def __init__(self): + super().__init__("owner execution interrupted") + + +class _Discard: + """Never retain or forward adapter output, including unbounded malicious text.""" + + def write(self, text): + return len(text) + + def flush(self): + pass diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/workplans/WARDEN-WP-0044-security-case-follow-through.md b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/workplans/WARDEN-WP-0044-security-case-follow-through.md new file mode 100644 index 00000000..40f1b5b5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/preimages/workplans/WARDEN-WP-0044-security-case-follow-through.md @@ -0,0 +1,289 @@ +--- +id: WARDEN-WP-0044 +type: workplan +title: "Follow through on KnownSecurityCases along their type's route" +domain: infotech +repo: ops-warden +status: active +flavor: implementation +owner: claude-code +topic_slug: netkingdom +created: "2026-10-08" +updated: "2026-10-11" +depends_on: [] +related_repos: + - net-kingdom + - ops-mason + - key-cape + - railiance-platform +state_hub_workstream_id: "f7e81013-6868-5d99-8528-ba75a442eca9" +--- + +# WARDEN-WP-0044 — Follow through on KnownSecurityCases along their type's route + +## Why + +NetKingdom is introducing SecurityCaseTypes (net-kingdom `NK-WP-0048`, canon +`security-case-types_v0.1`, proposed). Once a case of an implemented type is +accepted and ops-mason has materialized its structure (`MASON-WP-0010`), +ops-warden carries out the remaining steps. These are the owners' declared entry +points, run on the authentication path the type defines, followed by the type's +verification. + +Today ops-warden routes needs and, for `exec_capable` lanes, proxies fetches as +the caller. It does not yet know about cases, and each new integration ends with +a founder running a hand-written script (for example the proposed KEY-WP-0038-T02 +rollout). This plan gives ops-warden the follow-through role, without widening +what it may do: each step still runs under its owner's entry point and the +caller's identity. + +## Tasks + +```task +id: WARDEN-WP-0044-T01 +status: done +depends_on: [NK-WP-0048-T03] +priority: high +state_hub_task_id: "ef0920fc-418e-52cb-acff-9a5c0c728bf5" +``` + +### T01 — Define the type's follow-through section + +Propose the `follow_through` part of the SecurityCaseType schema together with +net-kingdom. For each step it names the owner entry point, `warden_executes`, +the auth method and lane, whether an attended human login is unavoidable (with +the type's justification), and the verification command. Write it for the first +type, `oidc-browser-relying-party`, covering key-cape client registration, +custody minting and LLDAP group membership. + +2026-10-08: NK-WP-0048-T03/T04 landed with a drafted section. Review is in +`history/2026-10-08-oidc-rp-follow-through-review.md`. It requires three +changes before T02: a per-step `lane`, a narrow `warden_executes`, and +`value_handling`. It also lists four refinements. This task is done when +net-kingdom acknowledges the review (NK-WP-0048-T05). + +2026-10-09: R4 was withdrawn in favour of ops-mason's per-case +`{consuming_service}-keycape-client`, with the entry body templated in the type +(R4′, see the addendum in the review). + +2026-10-09: the revised 1.0 type and canon now carry R1–R3, R4′, R5–R7. +`warden case check-type ` (`src/warden/case_type.py`, tests in +`tests/test_case_type.py`) checks a type's follow-through mechanically: the +live type is schema-conformant, with one attended session +(`key-cape-oidc-login`). It is not yet plannable, because two lanes are not +in the catalog: `identity-provisioner-groups` (net-kingdom) and +`keycape-client-secret-generate` (railiance-platform). These gate T02, not +T01. T01 still closes on net-kingdom's acknowledgement (NK-WP-0048-T05). + +2026-10-09: ops-warden cannot add `identity-provisioner-groups` as the type +declares it. The provisioner accepts only user-engine's runtime token and +admits only user-engine pods, so no caller-identity path exists. R8 (review +addendum) asks net-kingdom either to move `ensure-groups` into the existing +attended session (recommended) or to give the provisioner an operator +credential. `keycape-client-secret-generate` needs a generate-in-custody entry +point from railiance-platform, and the broker does not have one yet. Both were +sent to their owners. + +2026-10-09: done. net-kingdom took R8 option 1, so `ensure-groups` runs attended +in the `register-client` session and `identity-provisioner-groups` is dropped +(net-kingdom `0e86f95`). The founder acknowledged the type with memo r4 (hub +decision `e418263e`, NK-WP-0048-T05 done). + +```task +id: WARDEN-WP-0044-T02 +status: done +depends_on: [WARDEN-WP-0044-T01] +priority: high +state_hub_task_id: "5f68c991-f490-58f8-a121-92f74073d272" +``` + +### T02 — Plan a case run + +`warden case plan ` checks that the case is accepted and materialized, +resolves each step to its catalog entry and entry point, and prints the run: +which steps warden executes, which run as the attended caller, what the +verification is. No secret values; no execution. + +Tests: refuse an unaccepted or unmaterialized case; refuse a step whose catalog +entry is missing or not reviewed; show attended steps explicitly. + +2026-10-09: done. `warden case plan [--type] [--json]` +(`src/warden/case_plan.py`, 14 tests in `tests/test_case_plan.py`). It refuses: +- a case that is not `materialized`, or has no acceptance record +- a type that is mismatched or not acknowledged +- any `case check-type` finding, including a missing or unreviewed lane +- undeclared, type-fixed or secret-sensitivity parameters +- a placeholder the case does not set, or an unsupported `when` + +It fills type defaults (`issuer`), evaluates ` == ` conditions, +renders owner, entry point, auth and ready_check, groups attended steps per +lane, and lists the applicable verification. + +The live Forgejo case is correctly refused: it is `accepted`, not +materialized. A materialized copy, with a stand-in lane for +`keycape-client-secret-generate`, renders six steps: two attended steps plus +`register-client` in one `key-cape-oidc-login` session, one warden step +(`mint-secret`), and two owner steps. Seven checks follow. + +Observation for net-kingdom (not blocking): `bind-env` runs before +`register-client`, in type order. Its entry carries a literal +`KEYCAPE__CLIENT_SECRET`, which is not a `{param}` placeholder, so it +reaches the plan unrendered. + +2026-10-09 follow-up: +- **Format specs.** `_render` supports net-kingdom's `{param:env}` spec (upper + case, `-` as `_`, the same as `keycape-client.py env_name`) and refuses any + other spec or `!conversion`. +- **Case reference.** The plan prints `case-ref: @sha256:` over the + case file's exact bytes. This is the reference proposed to railiance-platform + for `credential.py generate --case-ref` (RPF-WP-0054-T01); the broker + re-derives it from the file. +- 18 tests now. + +```task +id: WARDEN-WP-0044-T03 +status: progress +depends_on: [WARDEN-WP-0044-T02, MASON-WP-0010-T03] +blocking_reason: "Local durable execution kernel and injected failure harness implemented. Native acceptance still requires authenticated owner machine descriptors/adapters, actual Forgejo materialization receipt, contained session integration, ordered verification and first native run." +priority: high +state_hub_task_id: "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89" +``` + +### T03 — Run and verify a case + +`warden case run ` executes the plan. Owner entry points run through +the existing proxy as the caller; steps that need an attended login use the +existing contained login envelope (silent command, session revoked afterwards), +grouped into as few attended sessions as possible. Then it runs the type's +verification and writes a receipt without values. `warden case rollback` +reverses exactly this case's steps. + +Acceptance: the Forgejo reference case (NK-WP-0048-T06) reaches `live` through +this path, with no case-specific script. + +2026-10-17: original-intent lookup/context interface for NK-WP-0048-T15, source +and synthetic tests only. `src/warden/case_run.py`: `Executor.run` writes an +`OriginalIntent` (schema `warden.case.original-intent.v1`: the dispatched run +Invocation — attempt, step, owner, target, actor, principal, lane, all six pins, +tenant, realm — plus admitted adapter source) in the same SQLite transaction as +the pre-dispatch journal row. `Journal.original_intent(binding, current, source)` +returns it after restart and refuses on a missing record or any +pins/source/step/owner/target/principal/lane/tenant/realm mismatch, before any +journal write, so the step stays uncertain. An adapter's +`reconcile_original(ReconcileContext(original, attempt))` receives the original +intent separately from the new reconcile attempt. This is Warden's record of what +it dispatched, not owner created-only provenance; tenant/realm are as supplied at +admission. Owner descriptors/adapters, contained session integration, Forgejo +materialization and the native run remain open. + +Agreed with railiance-platform (RPF-WP-0054-T01, 2026-10-09): +- `mint-secret` runs `credential.py generate --grant keycape/client-secret-generate + --client-id --case-ref --case-file ` as the caller's + child process. The broker re-derives the case-ref and checks status and + client_id. +- warden refuses that step when `WARDEN_AGENT_ID` is set. + +2026-10-10: railiance-platform implemented the command (RPF-WP-0054-T03). It is +catalogued as draft `keycape-client-secret-generate`, with `exec_owner` +railiance-platform and `exec_capable: false`. `warden case check-type` now +reports the lane as LANE-UNREVIEWED rather than LANE-MISSING. Before it can be +activated: +- the founder binds the issuer policy to the operator OIDC role +- key-cape confirms the secret format +- the RPF-WP-0054-T05 probe passes + +2026-10-10 (later): +- **Broker lane.** The issuer role is bound (CCR-2026-0043) and the T05 probe + passed (railiance-platform `21b8af3`). Only key-cape's format confirmation is + left. The playbook now includes the issuer-role login, a 15m token with + unlimited uses that `warden case run` must group with the attended session. +- **Env spec.** net-kingdom adopted `{client_id:env}` as a 1.0 patch + (`9a9b8e3`). The plan renders `KEYCAPE_FORGEJO_CLIENT_SECRET`. +- **Forgejo case.** Rendered and built by ops-mason. It is not yet + `materialized`: MASON-WP-0010-T03 waits on the railiance-platform CCRs and + their founder apply. + +2026-10-10: `keycape-client-secret-generate` activated on railiance-platform's +go-ahead (message 20973a9c) after key-cape confirmed the format (4e0213b3). +`warden case check-type` on the live type now reports `plannable=True`. The +only thing T03 still waits on is the Forgejo case reaching `materialized` +(MASON-WP-0010-T03). + +2026-10-10: net-kingdom moved the type to 1.1 (`e2174ce`) and re-pinned the +Forgejo case. 1.1 adds the verification `consumer-enforces-groups` and requires +the hand-over to enforce `access_groups`. `check-type` on 1.1 reports +`plannable=True`, and a materialized copy renders six steps and the new check. +The case-ref digest covers the whole case file, including `status`, so it +changes when the case becomes `materialized`. `warden case run` therefore takes +it from the file at run time, and the broker re-derives it from the same file. + +```task +id: WARDEN-WP-0044-T04 +status: wait +depends_on: [WARDEN-WP-0044-T03] +blocking_reason: "Catalog integration follows a proven run." +priority: medium +state_hub_task_id: "4a0fa4a7-5ca9-518a-ab92-a79a5e2cf93e" +``` + +### T04 — Catalog entries from types + +Let a type contribute reviewed catalog entries once, so that every case of that +type routes without a new catalog edit. Cases add parameter values, not routes. +`warden route find` should be able to answer "which SecurityCaseType covers this +need?" before anyone builds by hand. + + +2026-10-10 consolidation source prototype (not an accepted owner adapter API): +`src/warden/case_receipt_state.py` provides a pure immutable receipt reducer, +with exact case/type/adapter/catalog/acceptance/run-sheet pins, running intent +before dispatch, uncertain recovery after a lost result, no success replay, +explicit retry-safe failed-before-change retry and created-only reverse undo. +It executes nothing and accepts no command output. The caller must durably +persist running before any future side effect; storage/authenticated owner +receipts/live adapters are not implemented. Focused offline tests cover changed +pins, lost receipts, retry/refusal, uncertain rollback and shared-object +preservation. Source proposal: `docs/security-case-execution-proposal.md`. + +T03 remains open: typed owner execution/receipt contracts require review, the +Forgejo structural materialization needs owner receipts, and CLI dispatch, +contained sessions, native verification and provenance remain unimplemented. +There is a circularity to resolve with net-kingdom/ops-mason: materialization +cannot require mint-secret if mint-secret is itself a follow-through step whose +planning requires materialized. No case status is changed by this prototype. + + +2026-10-11: the exact execution-design memo version1 has confirmed native human +acceptance (approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca). The approved proposal +bytes remain unchanged. `src/warden/case_run.py` now implements a closed v1 local +execution seam, independent admission/evidence callbacks, private transactional +SQLite receipt storage, process locking, running-before-dispatch, crash recovery +to uncertainty, explicit safe retry, separately required broker-grant and parent-session revocation +proofs, native owner reconciliation and created-only +reverse rollback. Thirty-seven kernel tests and fourteen reducer tests pass; +ruff checks pass. See `docs/security-case-execution-kernel-v1.md` for exact scope +and receipt provenance. No installed native adapter or live CLI execution is +claimed. Current prose types stay plan-only; acknowledged descriptor/type machine +seams and authenticated owner/caller receipts are still required. T03 progresses +for implementation but cannot finish before ordered native Forgejo acceptance. + +2026-10-16 source preparation (isolated checkout; no native acceptance): +T03's browser parameter mismatch now has a producer-validator-backed projection +in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2 +parameter definitions and all four reviewed producer modules are pinned; full case +validation admits reviewed security-sensitive metadata while unknown/fixed/secret +inputs and type/producer drift refuse. Only the reviewed descriptor target +reaches the owner callback; no parameter journal payload is added. Actual +Executor/producer tests cover positive versions and refusal/containment paths. +Acknowledged machine descriptors, authenticated owner evidence, real Forgejo +materialization and contained native sessions remain mandatory and unimplemented; +T03 stays progress, T04 stays wait. No type or case status changed. + +2026-10-16 producer source refresh: independently reviewed NetKingdom +`015694ee414c802dc2ef1fa0120dfed610086e69` optional descriptor-map repair. +Only the validator/evidence source pins change; exact parameter and transitive +source identity remain enforced. Actual canonical producer, Executor and reducer +suite passes 87 checks, including 14 new malformed-map integration cases; all +four missing/changed modules still refuse. Review: +`docs/reviews/2026-10-16-browser-producer-pin-refresh.md`. No adapter attestation, +native acceptance, credential use or privileged dispatch. T03 remains progress. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/.custodian-brief.md.worktree b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/.custodian-brief.md.worktree new file mode 100644 index 00000000..34573d26 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/.custodian-brief.md.worktree @@ -0,0 +1,60 @@ + +# Custodian Brief — ops-warden + +**Domain:** infotech +**Last synced:** 2026-10-16 00:05 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Adopt unknown -> fail_closed behind signing-target classification coverage +Progress: 1/4 done | workplan_id: `c8ee441e-1be1-5219-910c-e79ff23cc9ec` + +**Open tasks:** +- ! Tasks `611f0901` + *(wait: Warden's own declaration cannot classify the repair-path target workloads; the owner must declare the continuity actor/target and its z2-continuity resolution (see 2026-09-28 review).)* +- ! Tasks `54ad4864` + *(wait: Owners of agt-codex-interhub-bootstrap (ops-hub) and agt-state-hub-bridge (ops-bridge) must publish authoritative workload identity declarations; atm-backup-daily declared by activity-core and compiled 2026-10-09 (z2-protected, admission unsatisfied: M1 below M2 floor).)* +- ! Tasks `93eaf1f2` + *(wait: Gated on T01 and on layer model v0.8 (or successor) reaching accepted; never convert against a proposed standard.)* + +### Layer model v0.7 conformance — state the deadline, bind the agent boundary, steward the estate's newest rule +Progress: 5/6 done | workplan_id: `ae3ff76f-883d-5e2f-b6aa-144d61e8fdef` + +**Open tasks:** +- ! Tasks `7d1b3c82` + *(wait: Two items to route, not absorb: ops-mason has published no stance map; the second is another repository's to own.)* + +### Follow through on KnownSecurityCases along their type's route +Progress: 2/4 done | workplan_id: `f7e81013-6868-5d99-8528-ba75a442eca9` + +**Open tasks:** +- ! T03 — Run and verify a case `4a0fa4a7` + *(wait: Catalog integration follows a proven run.)* +- ► T02 — Plan a case run `bcbf6c91` + +### Tamper-resistant credential governance + mass rotation/lockdown (Strand B) +Progress: 2/3 done | workplan_id: `21528e8d-a049-523d-9ae1-da7a27cb8bbf` + +**Open tasks:** +- · Task: Graded lockdown / break-glass with explicit trust-root `cae498ee` + +### Repoint the whynot-design npm lane to Forgejo +Progress: 2/3 done | workplan_id: `42a097db-1c24-558e-a724-030bb2b4443e` + +**Open tasks:** +- ! Prove routing and publication `a8b1b855` + *(wait: 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.)* + +## Inbox Hygiene + +**Stale unread:** 2 message(s) older than 3 day(s) — triage at session start. +**Missing thread_id:** 3 unread message(s) lack supersession chains. + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/WORK-RECORDS.md.worktree b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/WORK-RECORDS.md.worktree new file mode 100644 index 00000000..8b5226ae --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/WORK-RECORDS.md.worktree @@ -0,0 +1,188 @@ +# Work Records — ops-warden + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | WARDEN-WP-ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md | +| workplan | WARDEN-WP-ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md | +| workplan | WARDEN-WP-ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md | +| workplan | WARDEN-WP-ADHOC-2026-08-17 | finished | — | workplans/ADHOC-2026-08-17.md | +| workplan | WARDEN-WP-ADHOC-2026-09-08 | finished | — | workplans/ADHOC-2026-09-08.md | +| workplan | WARDEN-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md | +| workplan | WARDEN-WP-0016 | finished | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| workplan | WARDEN-WP-0017 | finished | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| workplan | WARDEN-WP-0018 | finished | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| workplan | WARDEN-WP-0019 | finished | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| workplan | WARDEN-WP-0020 | finished | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| workplan | WARDEN-WP-0021 | finished | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| workplan | WARDEN-WP-0022 | finished | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| workplan | WARDEN-WP-0023 | finished | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| workplan | WARDEN-WP-0024 | finished | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| workplan | WARDEN-WP-0025 | finished | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| workplan | WARDEN-WP-0026 | finished | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| workplan | WARDEN-WP-0027 | blocked | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| workplan | WARDEN-WP-0028 | finished | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md | +| workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md | +| workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| workplan | WARDEN-WP-0034 | active | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| workplan | WARDEN-WP-0037 | blocked | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | +| workplan | WARDEN-WP-0039 | finished | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| workplan | WARDEN-WP-0040 | active | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| workplan | WARDEN-WP-0043 | finished | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| workplan | WARDEN-WP-0044 | active | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | +| task | WARDEN-WP-ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T04 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T01 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T02 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T03 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T04 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T05 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T06 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | +| task | WARDEN-WP-0016-T01 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T02 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T03 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T04 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0017-T01 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0017-T02 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0017-T03 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0018-T01 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0018-T02 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0018-T03 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0019-T01 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| task | WARDEN-WP-0019-T02 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| task | WARDEN-WP-0020-T01 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T02 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T03 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T04 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T05 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0021-T01 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T02 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T03 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T04 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T05 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0022-T01 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T02 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T03 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T04 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0023-T01 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T02 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T03 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T04 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T05 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T06 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T07 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0024-T01 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T02 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T03 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T04 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T05 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T06 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T07 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0025-T01 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T02 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T03 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T04 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T05 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0026-T01 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T02 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T03 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T04 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T05 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T06 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T07 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0027-T01 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0027-T02 | todo | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0027-T03 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0028-T01 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T02 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T03 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T04 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T05 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T06 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T07 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0029-T01 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T02 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T03 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T04 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T05 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0030-T01 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T02 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T03 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T04 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T05 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0031-T01 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T02 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T03 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T04 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T05 | cancel | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0032-T01 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T02 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T03 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T04 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T06 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T07 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T04 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T05 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T06 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0034-T01 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T02 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T03 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T04 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T05 | wait | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T06 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0035-T01 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| task | WARDEN-WP-0035-T02 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| task | WARDEN-WP-0036-T01 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| task | WARDEN-WP-0036-T02 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| task | WARDEN-WP-0037-T01 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0037-T02 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0037-T03 | wait | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0038-T01 | done | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | +| task | WARDEN-WP-0039-T01 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0039-T02 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0039-T03 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0040-T01 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T02 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T03 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T04 | done | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0041-T01 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T02 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T03 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T04 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0042-T01 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| task | WARDEN-WP-0042-T02 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| task | WARDEN-WP-0043-T01 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0043-T02 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0043-T03 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0044-T01 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T02 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T03 | progress | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T04 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md | +| intake | WARDEN-IN-0002 | closed | — | intakes/intakes.md | +| intake | WARDEN-IN-0003 | closed | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/originals.json b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/originals.json new file mode 100644 index 00000000..21406cf6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-backup/originals.json @@ -0,0 +1,10 @@ +[ + { + "path": ".custodian-brief.md", + "index": "100644 34573d262327057c11628ce8a7ca78292968545b 0\t.custodian-brief.md" + }, + { + "path": "WORK-RECORDS.md", + "index": "100644 34cf268862326a5832a7d0be9e84eb296e5601bb 0\tWORK-RECORDS.md" + } +] diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-commit.json b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-commit.json new file mode 100644 index 00000000..02e7c8c9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-commit.json @@ -0,0 +1,25 @@ +{ + "exit_code": 0, + "commit": "117fec9241e7b985dfb187b992e173d0a5dc3700", + "base": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2", + "files": [ + { + "path": "src/warden/case_run.py", + "sha256": "77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768" + }, + { + "path": "tests/test_case_original_context_consistency.py", + "sha256": "10c5187cbc49222017319249ccb408816497ec6e006859d23003f3b2b375445b" + }, + { + "path": "docs/security-case-execution-kernel-v1.md", + "sha256": "e5570f542ec3ef4e6c11332cfe48fea16bf90cde5b9a5d926f5a068b7cafdbc7" + }, + { + "path": "workplans/WARDEN-WP-0044-security-case-follow-through.md", + "sha256": "721a90b2ed8c896257c34a1451eb642cb8824c8263e37c483fb61b36adc54088" + } + ], + "stdout": "[main 117fec9] fix(case): validate original intent and bind its running receipt\n 4 files changed, 421 insertions(+), 17 deletions(-)\n create mode 100644 tests/test_case_original_context_consistency.py\n", + "stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 4, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-inputs.json b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-inputs.json new file mode 100644 index 00000000..3549a2b0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/publication-inputs.json @@ -0,0 +1,23 @@ +{ + "owner": "ops-warden", + "base_revision": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2", + "message": "fix(case): validate original intent and bind its running receipt", + "files": [ + { + "path": "src/warden/case_run.py", + "sha256": "77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768" + }, + { + "path": "tests/test_case_original_context_consistency.py", + "sha256": "10c5187cbc49222017319249ccb408816497ec6e006859d23003f3b2b375445b" + }, + { + "path": "docs/security-case-execution-kernel-v1.md", + "sha256": "e5570f542ec3ef4e6c11332cfe48fea16bf90cde5b9a5d926f5a068b7cafdbc7" + }, + { + "path": "workplans/WARDEN-WP-0044-security-case-follow-through.md", + "sha256": "721a90b2ed8c896257c34a1451eb642cb8824c8263e37c483fb61b36adc54088" + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/retained-evidence-integrity.json b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/retained-evidence-integrity.json new file mode 100644 index 00000000..d4e025af --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/retained-evidence-integrity.json @@ -0,0 +1,14 @@ +[ + { + "packet": "original-context-r1", + "manifest_sha256": "sha256:c91e5a764712ec78d98fa3b74c652de734c2f4f524c290b851a2fa1c79d40f4f", + "all_manifest_files_unchanged": true, + "mismatches": [] + }, + { + "packet": "original-context-compare-r2", + "manifest_sha256": "sha256:b98cedced069a744614d1086fda38b6db0fafb8a43d6bbcee0ad5e25cb860e05", + "all_manifest_files_unchanged": true, + "mismatches": [] + } +] diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/unrelated-WORK-RECORDS.index b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/unrelated-WORK-RECORDS.index new file mode 100644 index 00000000..34cf2688 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/unrelated-WORK-RECORDS.index @@ -0,0 +1,187 @@ +# Work Records — ops-warden + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | WARDEN-WP-ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md | +| workplan | WARDEN-WP-ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md | +| workplan | WARDEN-WP-ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md | +| workplan | WARDEN-WP-ADHOC-2026-08-17 | finished | — | workplans/ADHOC-2026-08-17.md | +| workplan | WARDEN-WP-ADHOC-2026-09-08 | finished | — | workplans/ADHOC-2026-09-08.md | +| workplan | WARDEN-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md | +| workplan | WARDEN-WP-0016 | finished | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| workplan | WARDEN-WP-0017 | finished | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| workplan | WARDEN-WP-0018 | finished | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| workplan | WARDEN-WP-0019 | finished | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| workplan | WARDEN-WP-0020 | finished | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| workplan | WARDEN-WP-0021 | finished | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| workplan | WARDEN-WP-0022 | finished | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| workplan | WARDEN-WP-0023 | finished | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| workplan | WARDEN-WP-0024 | finished | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| workplan | WARDEN-WP-0025 | finished | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| workplan | WARDEN-WP-0026 | finished | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| workplan | WARDEN-WP-0027 | blocked | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| workplan | WARDEN-WP-0028 | finished | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md | +| workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md | +| workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| workplan | WARDEN-WP-0034 | backlog | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| workplan | WARDEN-WP-0037 | blocked | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | +| workplan | WARDEN-WP-0039 | finished | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| workplan | WARDEN-WP-0040 | backlog | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| workplan | WARDEN-WP-0043 | finished | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| workplan | WARDEN-WP-0044 | active | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | +| task | WARDEN-WP-ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T04 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T01 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T02 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T03 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T04 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T05 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T06 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | +| task | WARDEN-WP-0016-T01 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T02 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T03 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T04 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0017-T01 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0017-T02 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0017-T03 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0018-T01 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0018-T02 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0018-T03 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0019-T01 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| task | WARDEN-WP-0019-T02 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| task | WARDEN-WP-0020-T01 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T02 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T03 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T04 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T05 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0021-T01 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T02 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T03 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T04 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T05 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0022-T01 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T02 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T03 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T04 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0023-T01 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T02 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T03 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T04 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T05 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T06 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T07 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0024-T01 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T02 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T03 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T04 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T05 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T06 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T07 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0025-T01 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T02 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T03 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T04 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T05 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0026-T01 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T02 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T03 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T04 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T05 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T06 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T07 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0027-T01 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0027-T02 | todo | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0027-T03 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0028-T01 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T02 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T03 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T04 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T05 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T06 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T07 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0029-T01 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T02 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T03 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T04 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T05 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0030-T01 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T02 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T03 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T04 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T05 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0031-T01 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T02 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T03 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T04 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T05 | cancel | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0032-T01 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T02 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T03 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T04 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T06 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T07 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T04 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T05 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T06 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0034-T01 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T02 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T03 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T04 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T05 | wait | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T06 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0035-T01 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| task | WARDEN-WP-0035-T02 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| task | WARDEN-WP-0036-T01 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| task | WARDEN-WP-0036-T02 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| task | WARDEN-WP-0037-T01 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0037-T02 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0037-T03 | wait | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0039-T01 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0039-T02 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0039-T03 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0040-T01 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T02 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T03 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T04 | done | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0041-T01 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T02 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T03 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T04 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0042-T01 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| task | WARDEN-WP-0042-T02 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| task | WARDEN-WP-0043-T01 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0043-T02 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0043-T03 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0044-T01 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T02 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T03 | progress | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T04 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md | +| intake | WARDEN-IN-0002 | closed | — | intakes/intakes.md | +| intake | WARDEN-IN-0003 | closed | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/unrelated-WORK-RECORDS.worktree b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/unrelated-WORK-RECORDS.worktree new file mode 100644 index 00000000..8b5226ae --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-owner-followthrough-r3/unrelated-WORK-RECORDS.worktree @@ -0,0 +1,188 @@ +# Work Records — ops-warden + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | WARDEN-WP-ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md | +| workplan | WARDEN-WP-ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md | +| workplan | WARDEN-WP-ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md | +| workplan | WARDEN-WP-ADHOC-2026-08-17 | finished | — | workplans/ADHOC-2026-08-17.md | +| workplan | WARDEN-WP-ADHOC-2026-09-08 | finished | — | workplans/ADHOC-2026-09-08.md | +| workplan | WARDEN-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md | +| workplan | WARDEN-WP-0016 | finished | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| workplan | WARDEN-WP-0017 | finished | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| workplan | WARDEN-WP-0018 | finished | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| workplan | WARDEN-WP-0019 | finished | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| workplan | WARDEN-WP-0020 | finished | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| workplan | WARDEN-WP-0021 | finished | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| workplan | WARDEN-WP-0022 | finished | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| workplan | WARDEN-WP-0023 | finished | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| workplan | WARDEN-WP-0024 | finished | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| workplan | WARDEN-WP-0025 | finished | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| workplan | WARDEN-WP-0026 | finished | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| workplan | WARDEN-WP-0027 | blocked | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| workplan | WARDEN-WP-0028 | finished | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md | +| workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md | +| workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| workplan | WARDEN-WP-0034 | active | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| workplan | WARDEN-WP-0037 | blocked | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | +| workplan | WARDEN-WP-0039 | finished | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| workplan | WARDEN-WP-0040 | active | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| workplan | WARDEN-WP-0043 | finished | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| workplan | WARDEN-WP-0044 | active | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | +| task | WARDEN-WP-ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-08-17-T04 | done | — | workplans/ADHOC-2026-08-17.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T01 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T02 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T03 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T04 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T05 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-09-08-T06 | done | — | workplans/ADHOC-2026-09-08.md | +| task | WARDEN-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | +| task | WARDEN-WP-0016-T01 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T02 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T03 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0016-T04 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | +| task | WARDEN-WP-0017-T01 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0017-T02 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0017-T03 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | +| task | WARDEN-WP-0018-T01 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0018-T02 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0018-T03 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | +| task | WARDEN-WP-0019-T01 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| task | WARDEN-WP-0019-T02 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md | +| task | WARDEN-WP-0020-T01 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T02 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T03 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T04 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0020-T05 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md | +| task | WARDEN-WP-0021-T01 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T02 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T03 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T04 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0021-T05 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md | +| task | WARDEN-WP-0022-T01 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T02 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T03 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0022-T04 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md | +| task | WARDEN-WP-0023-T01 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T02 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T03 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T04 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T05 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T06 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0023-T07 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md | +| task | WARDEN-WP-0024-T01 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T02 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T03 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T04 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T05 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T06 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0024-T07 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | +| task | WARDEN-WP-0025-T01 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T02 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T03 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T04 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0025-T05 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | +| task | WARDEN-WP-0026-T01 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T02 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T03 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T04 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T05 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T06 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0026-T07 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | +| task | WARDEN-WP-0027-T01 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0027-T02 | todo | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0027-T03 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0028-T01 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T02 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T03 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T04 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T05 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T06 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0028-T07 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | +| task | WARDEN-WP-0029-T01 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T02 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T03 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T04 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0029-T05 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | +| task | WARDEN-WP-0030-T01 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T02 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T03 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T04 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0030-T05 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0031-T01 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T02 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T03 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T04 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T05 | cancel | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0032-T01 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T02 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T03 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T04 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T06 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0032-T07 | done | — | workplans/WARDEN-WP-0032-security-zones.md | +| task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T04 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T05 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T06 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0034-T01 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T02 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T03 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T04 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T05 | wait | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0034-T06 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | +| task | WARDEN-WP-0035-T01 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| task | WARDEN-WP-0035-T02 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | +| task | WARDEN-WP-0036-T01 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| task | WARDEN-WP-0036-T02 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | +| task | WARDEN-WP-0037-T01 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0037-T02 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0037-T03 | wait | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | +| task | WARDEN-WP-0038-T01 | done | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | +| task | WARDEN-WP-0039-T01 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0039-T02 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0039-T03 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +| task | WARDEN-WP-0040-T01 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T02 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T03 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0040-T04 | done | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | +| task | WARDEN-WP-0041-T01 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T02 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T03 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0041-T04 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | +| task | WARDEN-WP-0042-T01 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| task | WARDEN-WP-0042-T02 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | +| task | WARDEN-WP-0043-T01 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0043-T02 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0043-T03 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0044-T01 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T02 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T03 | progress | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T04 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md | +| intake | WARDEN-IN-0002 | closed | — | intakes/intakes.md | +| intake | WARDEN-IN-0003 | closed | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/docs/security-case-execution-kernel-v1.md b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/docs/security-case-execution-kernel-v1.md new file mode 100644 index 00000000..c8490db8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/docs/security-case-execution-kernel-v1.md @@ -0,0 +1,176 @@ +# Closed case execution kernel v1 — 2026-10-11 + +Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal +at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +was accepted through memo `infd-20261010-security-case-execution`, version 1, +approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition +disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original +approved proposal remains byte-identical. That decision accepts source design; +it grants no execution session, credential or deployment authority. + +`src/warden/case_run.py` advances the pure reducer into an executable injected +owner harness with private durable receipts. Descriptor version +`warden.case.execution.v1` is a closed local implementation seam, not an accepted +NetKingdom type schema. It contains fixed owner/adapter/source digest, actor, +lane, target parameter, effect, reviewed created-object identifiers declared +retry safety and explicit required revocation scopes (parent-session and broker-grant). +A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused. +An acknowledged type must additionally pin the descriptor bytes under its +`execution_descriptors` map and independent owner evidence must authenticate +that exact machine-seam revision. Existing descriptive types lack this map and +remain plan-only. Their acknowledged labels do not authorize this new seam. + +There are no installed owner adapters, native provenance verifier or dispatch CLI +in this change. Callables are injected only through a fixed adapter registry, +with immutable owner reference/source pins. A real integration must supply +independent owner/native verification or explicit attended manual evidence; +a bare JSON receipt identifier is insufficient. Fixture verifiers in tests +are deliberately fake and attest no native case. Caller sessions are independently +verified for exact principal/actor/lane at admission and again before dispatch; +input actor labels alone are never sufficient. Agent mint is refused, including +when WARDEN_AGENT_ID appears after initial human admission. + +Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference +and run-sheet identity. Actual ops-mason materialization evidence is required +against the case source, separately from its status label. Case parameters supply +an exact metadata target. Source/session changes cannot inherit journal success. +Owner results have a closed metadata shape, result evidence fingerprint and +separate independently authenticated grant/parent-session revocation evidence +fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and +created objects outside the reviewed scope become uncertain, never success. +Python and OS stdout/stderr, including inherited child-process output, are +discarded without buffering or retention under a serialized fd containment +context. Independent provenance verification uses the same containment and +sanitizes exceptions. All source inputs are capped at 256 KiB before parsing. +Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other +owner interruptions are rethrown with a fixed message while retaining running +intent for crash recovery. + +The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks +and insecure modes, uses SQLite FULL synchronization and commits running intent +before calling the adapter. A separate process lock prevents concurrent mutation. +A crash between dispatch and final receipt leaves running; the next lock holder +persists uncertain before refusing replay. Both intent and outcome remain in +history. Each persisted attempt has a unique identity bound to all exact pins, +operation (run/reconcile/rollback), sequence and nonce. Adapter results and both +revocation proof purposes carry that identity. Prior-attempt evidence cannot +verify a safe retry, even if a caller rewrites result labels. Descriptor-created +objects are capped at 32 and histories at 128 transitions, with dispatch stopped +before archival is required. A throw or lost response is not failed-before-change. A safe retry needs +verified failed-before-change, unchanged pins, descriptor retry-safe and explicit +retry. Success and no-change cannot replay. + +Reconciliation is an explicit fixed owner callback, never the mutation callback. +It may verify an already-present native CAS result as no-change/success, or prove +failed-before-change; it never tells Warden to regenerate a credential. Uncertain +or failed-after-change requires this owner-native inspection before any retry. +Owner proof and revocation remain mandatory. Rollback visits only objects in a +verified creation receipt, in reverse order; no-change has no undo and shared +objects are excluded. Unknown/lost undo blocks another rollback. + +The kernel operates individual admitted steps. Full ordered run-sheet orchestration, +verification inventory, contained login integration, native owner adapters and +CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is +implemented. Current Forgejo source is not materialized and has no acknowledged +machine descriptors. Its first native run remains an acceptance requirement, +with real materialization, separately authenticated broker-grant and parent-session +revocation receipts, session/owner receipts and consumer enforcement. This +implementation neither promotes a type nor changes any case status. + +Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open +an independent SQLite connection inside dispatch to prove running was committed, +crash after mutation and reopen a fresh journal to prove no duplicate invocation, +lose a response then reconcile without mint, require explicit retry, check exact +pins/session expiry, withhold malicious output and unconfirmed revocation, and +prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and +checks pass. No live token or target action occurred. + +## Browser input projection — 2026-10-16 source preparation + +An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser +cases before admission. It calls the real NetKingdom type/case validator over +verified producer bytes in a disposable Python process (`-I`, empty environment, +timeout, no inherited output). The validator, composer, capability contract and lazy evidence +modules have exact source pins; missing or changed code refuses. There is no +automatic discovery or download. A producer upgrade needs reviewed pin changes. + +Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest +of each complete parameter definition list prevents reclassifying secrets as +metadata, loosening constraints, or adding fields under a recognized version. +The complete case passes producer validation, including required fields, +constraints and credential-shape checks. Explicit fixed/default overrides, +unknown fields and secret references refuse. Security-sensitive browser metadata +can therefore be present without being passed to an owner adapter: the only +projection is its descriptor's exact public/operational target identifier. +No raw parameter values are added to invocations or persisted receipts. + +`require_implemented=False` validates the acknowledged type's source, not its +execution authority. Existing descriptor/type/source/catalog/caller provenance +gates stay mandatory. Neither historical browser type contains the required +exact machine-descriptor map, and neither is promoted by this change. There are +still no installed native owner adapters, authenticated provenance verifier, +contained session/ordered run orchestration or live CLI dispatch. A journal digest +is a fingerprint and cannot authenticate native evidence. Completion still needs +owner-admitted descriptors and actual structural, mint/delivery, consumer and +revocation/rollback evidence. Local fixture success is not native acceptance. + +Tests use the actual producer checkout (override sibling location with +`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected +test-only owner adapter/evidence verifier. Their descriptor extensions and case +materialized labels exist only in memory. Canonical case/type bytes stay intact. + +Producer integration refinement: the accepted NetKingdom browser-case patch +`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05` +is now the tested producer revision. Its evidence module is imported lazily only +by `check-evidence`, outside this admission path; Warden nevertheless pins and +copies it with the complete reviewed producer set. Missing or modified bytes in +any of the four modules refuse admission. The test covers every dependency. + + +## Original-attempt context candidate — 2026-10-17 + +Source-review candidate only; no native holder, adapter installation or execution +permission is introduced. The local reconcile callback changes explicitly from +`reconcile(fresh_invocation)` to +`reconcile(fresh_invocation, original_attempt_context)`. Existing one-argument +callbacks fail closed; no signature probing or silent fallback is used. Exact +adapter source/descriptor admission remains required for a real integration. + +Journal initialization adds `attempt_context_v1(receipt_seq, invocation_digest)`. +The companion fingerprint is inserted in the same SQLite transaction as each +invocation-bearing receipt, before dispatch. Existing v1 receipt JSON, IDs and +source pins are unchanged. The fingerprint hashes only a fixed validated field +projection: exact Invocation class, bounded step/owner/target/principal/lane/ +attempt identifiers, closed actor/operation values and the six Pins digests. +No arbitrary attributes, parameters, OwnerResult payload, evidence identifiers, +credentials or raw callback output are added to this table. Original fields are +reconstructed from current exact admission and the original intent's attempt ID; +they must match that stored fingerprint. Legacy rows are never backfilled and +missing/mismatched context refuses before an owner callback. + +The original context identifies the original run intent row/digest and the latest +same-attempt evidence/session/grant fingerprints. Recovery rows lacking invocation +and later interrupted reconciliation attempts do not replace the original run. +The fresh reconciliation attempt remains distinct, with its normal current owner +and session/grant verification. Before any reconciled result can be accepted, +independent evidence verification must also accept purpose +`original-attempt-context::`, +source equal to the exact OriginalAttemptContext digest, and the returned owner +evidence reference. This is a new source interface, not existing native assent. +Journal hashes are lookup/correspondence context, never independent authentication. + +No actual independent original-attempt holder/readback or verifier is supplied. +NetKingdom's registration adapter still has no reconcile callback and cannot undo +a prior-process creation. Missing original owner proof stays uncertain, including +when the local intent/fingerprint is intact; no native result is inferred from +matching JSON or current target bytes. Ordinary exact pins continue to apply: +a changed caller/session/run-sheet requires separately reviewed migration and +cannot inherit old journal state. Interrupted rollback remains its existing +rollback-uncertain owner gate, not a new supported automatic recovery path. + +The focused tests reuse existing synthetic owner/issuer trust and explicitly do +not provide a native JSON trust holder. They cover atomic context/intent visibility, +a distinct interpreter with no original Adapter closure, refusal without independent +original proof, legacy/missing/wrong-attempt/current-context refusal, interrupted +reconciliation continuity, paired-proof replay refusal and transaction failure +before effect. Existing reducer/owner/session/revocation checks remain required. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/pytest.ini b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/pytest.ini new file mode 100644 index 00000000..fcccae19 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = src diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/__init__.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/__init__.py new file mode 100644 index 00000000..4525f1e2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/__init__.py @@ -0,0 +1,3 @@ +"""OpsWarden — SSH CA and certificate lifecycle manager.""" + +__version__ = "0.1.0" diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_inputs.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_inputs.py new file mode 100644 index 00000000..9c876d79 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_inputs.py @@ -0,0 +1,98 @@ +"""Reviewed browser metadata projection; no native trust or command dispatch. + +The producer validator and all reviewed producer modules are pinned below. +Their verified bytes run in a disposable tree, without caller environment or +ambient Python imports. Type parameters must match the exact reviewed version. +Machine descriptor extensions still require Executor's independent admission. +""" + +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import tempfile + +from .case_receipt_state import Refused + +# Reviewed producer: net-kingdom 015694ee414c802dc2ef1fa0120dfed610086e69. +# Review: docs/reviews/2026-10-16-browser-producer-pin-refresh.md +SOURCE_PINS = { + "tools/security-case-types/security_case_evidence.py": "da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75", + "tools/security-case-types/security_case_validator.py": "1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b", + "tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f", + "tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", +} +PARAMETER_PINS = { + "1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c", + "1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6", +} + +_VALIDATE = """ +import json, sys +from pathlib import Path +sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types')) +import security_case_validator as v +case, typ = json.load(sys.stdin) +issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False) +print('valid' if not v.has_errors(issues) else 'invalid') +""" + + +class BrowserInputs: + """Explicit owner configuration, never inferred from a case-supplied path.""" + + def __init__(self, netkingdom_root: Path): + self.root = Path(netkingdom_root) + + def target(self, case: dict, typ: dict, target_parameter: str) -> str: + try: + spec = typ["spec"] + parameters = spec["parameters"] + fingerprint = hashlib.sha256( + json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + if ( + spec["id"] != "oidc-browser-relying-party" + or PARAMETER_PINS.get(spec["version"]) != fingerprint + ): + raise ValueError + declared = {item["name"]: item for item in parameters} + definition = declared.get(target_parameter, {}) + if ( + definition.get("sensitivity") not in ("public", "operational") + or definition.get("case_settable") is not True + ): + raise ValueError + # No secret references or newly classified metadata are grandfathered. + for name in case["spec"]["parameters"]: + item = declared.get(name, {}) + if item.get("case_settable") is not True or item.get( + "sensitivity" + ) not in ("public", "operational", "security_sensitive"): + raise ValueError + with tempfile.TemporaryDirectory( + prefix="warden-case-validator-" + ) as directory: + for relative, expected in SOURCE_PINS.items(): + source = (self.root / relative).read_bytes() + if hashlib.sha256(source).hexdigest() != expected: + raise ValueError + dest = Path(directory) / relative + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(source) + result = subprocess.run( + [sys.executable, "-I", "-c", _VALIDATE, directory], + input=json.dumps([case, typ]), + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + env={}, + timeout=10, + check=True, + ) + if result.stdout != "valid\n": + raise ValueError + return case["spec"]["parameters"][target_parameter] + except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError): + raise Refused("browser metadata failed exact-version validation") from None diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_receipt_state.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_receipt_state.py new file mode 100644 index 00000000..03a53e99 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_receipt_state.py @@ -0,0 +1,146 @@ +"""Proposed, pure case receipt reducer for WARDEN-WP-0044-T03. + +This prototype executes nothing and implements no accepted owner adapter API. +Callers MUST durably persist a returned running receipt before a side effect. +Only metadata identifiers/digests enter this model; no command output is accepted. +""" +from __future__ import annotations + +from dataclasses import dataclass, replace +from enum import Enum +import re + +_ID = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}\Z") +_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") + + +class Refused(ValueError): + """A transition requires an independent owner reconciliation or new review.""" + + +class Outcome(str, Enum): + RUNNING = "running" + SUCCESS = "verified-success" + NO_CHANGE = "verified-no-change" + FAILED_BEFORE = "failed-before-change" + FAILED_AFTER = "failed-after-change" + UNCERTAIN = "uncertain" + ROLLBACK_RUNNING = "rollback-running" + ROLLED_BACK = "verified-rolled-back" + ROLLBACK_UNCERTAIN = "rollback-uncertain" + + +@dataclass(frozen=True) +class Pins: + """Exact reviewed case/type/adapter/catalog/acceptance/run-sheet digests.""" + + case: str + case_type: str + adapter: str + catalog: str + acceptance: str + run_sheet: str + + def __post_init__(self) -> None: + if not all(_DIGEST.fullmatch(value) for value in vars(self).values()): + raise Refused("pins require sha256 digests") + + +@dataclass(frozen=True) +class Receipt: + attempt: int + outcome: Outcome + created: tuple[str, ...] = () + + +@dataclass(frozen=True) +class StepState: + step_id: str + owner: str + pins: Pins + allowed_created: tuple[str, ...] = () + retry_safe: bool = False + receipts: tuple[Receipt, ...] = () + + def __post_init__(self) -> None: + if not all(_ID.fullmatch(v) for v in (self.step_id, self.owner, *self.allowed_created)): + raise Refused("only metadata identifiers are supported") + if len(set(self.allowed_created)) != len(self.allowed_created): + raise Refused("duplicate reviewed object identifier") + + @property + def last(self) -> Receipt | None: + return self.receipts[-1] if self.receipts else None + + +def _same_pins(state: StepState, pins: Pins) -> None: + if state.pins != pins: + raise Refused("pins changed; a newly reviewed sheet is required") + + +def _append(state: StepState, outcome: Outcome, created: tuple[str, ...] = ()) -> StepState: + receipt = Receipt(len(state.receipts) + 1, outcome, created) + return replace(state, receipts=(*state.receipts, receipt)) + + +def begin(state: StepState, pins: Pins, *, retry: bool = False) -> StepState: + """Return intent to dispatch. Persist before calling an owner adapter.""" + _same_pins(state, pins) + last = state.last + if last is None: + if retry: + raise Refused("no failed attempt to retry") + elif not (last.outcome is Outcome.FAILED_BEFORE and state.retry_safe and retry): + raise Refused("step cannot dispatch or replay without owner reconciliation") + return _append(state, Outcome.RUNNING) + + +def finish(state: StepState, pins: Pins, outcome: Outcome, + *, created: tuple[str, ...] = ()) -> StepState: + """Accept only an adapter's verified, closed metadata result, never output.""" + _same_pins(state, pins) + if not state.last or state.last.outcome is not Outcome.RUNNING: + raise Refused("no running side effect") + if not isinstance(outcome, Outcome) or outcome not in (Outcome.SUCCESS, Outcome.NO_CHANGE, Outcome.FAILED_BEFORE, + Outcome.FAILED_AFTER, Outcome.UNCERTAIN): + raise Refused("unsupported adapter result") + if len(set(created)) != len(created) or not set(created) <= set(state.allowed_created): + raise Refused("created objects are outside the reviewed case") + if created and outcome not in (Outcome.SUCCESS, Outcome.FAILED_AFTER): + raise Refused("this result cannot claim created objects") + return _append(state, outcome, created) + + +def recover(state: StepState) -> StepState: + """Lost result after persisted dispatch is uncertain, never safe to retry.""" + if state.last and state.last.outcome is Outcome.RUNNING: + return _append(state, Outcome.UNCERTAIN) + if state.last and state.last.outcome is Outcome.ROLLBACK_RUNNING: + return _append(state, Outcome.ROLLBACK_UNCERTAIN, state.last.created) + return state + + +def rollback_objects(state: StepState, pins: Pins) -> tuple[str, ...]: + """Reviewed identifiers only, reverse created order; no shared-object undo.""" + _same_pins(state, pins) + last = state.last + if last is None or last.outcome is Outcome.NO_CHANGE: + return () + if last.outcome is not Outcome.SUCCESS: + raise Refused("rollback needs a verified successful creation receipt") + return tuple(reversed(last.created)) + + +def begin_rollback(state: StepState, pins: Pins) -> StepState: + objects = rollback_objects(state, pins) + if not objects: + raise Refused("no case-created objects to undo") + return _append(state, Outcome.ROLLBACK_RUNNING, objects) + + +def finish_rollback(state: StepState, pins: Pins, *, verified: bool) -> StepState: + _same_pins(state, pins) + if not state.last or state.last.outcome is not Outcome.ROLLBACK_RUNNING: + raise Refused("no running rollback") + outcome = Outcome.ROLLED_BACK if verified else Outcome.ROLLBACK_UNCERTAIN + return _append(state, outcome, state.last.created) diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_run.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_run.py new file mode 100644 index 00000000..f144c9d7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/src/warden/case_run.py @@ -0,0 +1,916 @@ +"""Closed local execution kernel v1; no installed adapters or live CLI dispatch. + +An owner must acknowledge each descriptor and provide an independent provenance +verifier. Descriptive case types remain plan-only. Nothing executes shell prose. +""" + +from __future__ import annotations + +from contextlib import contextmanager, redirect_stderr, redirect_stdout +from dataclasses import asdict, dataclass, replace +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import sqlite3 +import threading +import uuid +from typing import Callable + +import yaml +from .case_inputs import BrowserInputs +from .case_receipt_state import ( + Outcome, + Pins, + Receipt, + Refused, + StepState, + begin, + finish, + recover, + begin_rollback, + finish_rollback, +) + +IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z") +MAX_SOURCE_BYTES = 262144 +MAX_HISTORY = 128 +OUTPUT_LOCK = threading.RLock() + + +def bounded_source(value: bytes): + if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES: + raise Refused("source exceeds bounded input contract") + + +@contextmanager +def contained_output(): + """Discard Python and inherited OS stdout/stderr; serialize process fd changes.""" + with OUTPUT_LOCK: + saved = [] + null = os.open(os.devnull, os.O_WRONLY) + try: + for target in (1, 2): + saved.append((target, os.dup(target))) + os.dup2(null, target) + with redirect_stdout(_Discard()), redirect_stderr(_Discard()): + try: + yield + except Exception: + raise Refused("owner callback failed") from None + except BaseException: + raise OwnerInterrupted() from None + finally: + for target, original in saved: + os.dup2(original, target) + os.close(original) + os.close(null) + + +DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") + + +def digest(value: bytes) -> str: + return "sha256:" + hashlib.sha256(value).hexdigest() + + +@dataclass(frozen=True, slots=True) +class Descriptor: + version: str + step: str + owner: str + adapter: str + adapter_source: str + actor: str + lane: str + target_parameter: str + effect: str + allowed_created: tuple[str, ...] + retry_safe: bool + revocation_scopes: tuple[str, ...] + + @classmethod + def decode(cls, source: bytes) -> Descriptor: + bounded_source(source) + try: + obj = json.loads(source) + if set(obj) != set(cls.__dataclass_fields__): + raise ValueError + if not isinstance(obj["allowed_created"], list): + raise ValueError + obj["allowed_created"] = tuple(obj["allowed_created"]) + if not isinstance(obj["revocation_scopes"], list): + raise ValueError + obj["revocation_scopes"] = tuple(obj["revocation_scopes"]) + result = cls(**obj) + identifiers = ( + result.step, + result.owner, + result.adapter, + result.lane, + result.target_parameter, + *result.allowed_created, + ) + if result.version != "warden.case.execution.v1" or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers + ): + raise ValueError + if not DIGEST.fullmatch(result.adapter_source) or result.actor not in ( + "human", + "agent", + ): + raise ValueError + if ( + result.effect not in ("read", "change", "mint") + or type(result.retry_safe) is not bool + ): + raise ValueError + scopes = set(result.revocation_scopes) + if ( + len(scopes) != len(result.revocation_scopes) + or not scopes <= {"parent-session", "broker-grant"} + or "parent-session" not in scopes + ): + raise ValueError + if result.effect == "mint" and "broker-grant" not in scopes: + raise ValueError + if len(result.allowed_created) > 32: + raise ValueError + if len(set(result.allowed_created)) != len(result.allowed_created): + raise ValueError + return result + except (ValueError, TypeError, KeyError): + raise Refused("invalid closed execution descriptor") from None + + +@dataclass(frozen=True, slots=True) +class OwnerResult: + outcome: Outcome + evidence: str + created: tuple[str, ...] = () + revoked: bool = False + session_revocation_evidence: str | None = None + grant_revocation_evidence: str | None = None + attempt_id: str = "" + operation: str = "" + + +@dataclass(frozen=True, slots=True) +class Invocation: + step: str + owner: str + target: str + actor: str + principal: str + lane: str + pins: Pins + attempt_id: str = "" + operation: str = "admission" + + +def invocation_fingerprint(invocation: Invocation) -> str: + """Fingerprint a fixed validated projection; retain no owner payload or refs. + + Invocation is not a generic metadata container. Accept only this exact class, + its reviewed identifier fields and closed Pins. Never serialize arbitrary + attributes, case parameters, OwnerResult.evidence or callback output. + """ + if type(invocation) is not Invocation or type(invocation.pins) is not Pins: + raise Refused("exact invocation metadata required") + identifiers = ( + invocation.step, + invocation.owner, + invocation.target, + invocation.principal, + invocation.lane, + invocation.attempt_id, + ) + if ( + not all( + type(value) is str and IDENTIFIER.fullmatch(value) for value in identifiers + ) + or invocation.actor not in ("human", "agent") + or invocation.operation not in ("run", "rollback", "reconcile") + ): + raise Refused("bounded invocation metadata required") + pins = [ + invocation.pins.case, + invocation.pins.case_type, + invocation.pins.adapter, + invocation.pins.catalog, + invocation.pins.acceptance, + invocation.pins.run_sheet, + ] + if not all(type(value) is str and DIGEST.fullmatch(value) for value in pins): + raise Refused("exact invocation pins required") + projection = [ + "warden.case.invocation-fingerprint.v1", + *identifiers, + invocation.actor, + invocation.operation, + *pins, + ] + return digest(json.dumps(projection, separators=(",", ":")).encode()) + + +@dataclass(frozen=True, slots=True) +class OriginalAttemptContext: + """Local lookup context, never independently authenticated owner evidence. + + Original fields are reconstructed from current exact admission and checked + against the fingerprint atomically stored with the original running intent. + Journal fingerprints cannot establish native creation or authorize an undo. + """ + + original: Invocation + intent_sequence: int + intent_digest: str + result_sequence: int + evidence_digest: str | None + session_revocation_digest: str | None + grant_revocation_digest: str | None + + @property + def digest(self) -> str: + projection = [ + "warden.case.original-context.v1", + invocation_fingerprint(self.original), + self.intent_sequence, + self.intent_digest, + self.result_sequence, + self.evidence_digest, + self.session_revocation_digest, + self.grant_revocation_digest, + ] + return digest(json.dumps(projection, separators=(",", ":")).encode()) + + +@dataclass(frozen=True, slots=True) +class Adapter: + reference: str + source: str + invoke: Callable[[Invocation], OwnerResult] + verify: Callable[[Invocation, OwnerResult], bool] + rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None + reconcile: Callable[[Invocation, OriginalAttemptContext], OwnerResult] | None = None + + +class Journal: + """Private SQLite journal plus process lock; transaction commits before dispatch.""" + + def __init__(self, directory: Path): + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + if ( + directory.is_symlink() + or directory.stat().st_uid != os.getuid() + or directory.stat().st_mode & 0o077 + ): + raise Refused("journal directory is not private") + self.directory = directory + for name in ("journal.sqlite", "execution.lock"): + path = directory / name + fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077: + raise Refused("journal file is not private") + finally: + os.close(fd) + self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None) + self.db.execute("PRAGMA synchronous=FULL") + self.db.execute( + "CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + # Additive local storage v1; no existing receipt payload is rewritten or + # backfilled. Legacy attempts without this atomic companion fail closed. + self.db.execute( + "CREATE TABLE IF NOT EXISTS attempt_context_v1 " + "(receipt_seq INTEGER PRIMARY KEY, invocation_digest TEXT NOT NULL)" + ) + + @contextmanager + def locked(self): + with (self.directory / "execution.lock").open("rb") as fh: + try: + fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused("case execution already active") from None + try: + yield + finally: + fcntl.flock(fh, fcntl.LOCK_UN) + + def load(self, binding: str, initial: StepState) -> StepState: + row = self.db.execute( + "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + return initial + obj = json.loads(row[0]) + expected = { + "step_id", + "owner", + "pins", + "allowed_created", + "retry_safe", + "receipts", + "journal_schema", + "evidence_digest", + "session_revoked", + "session_revocation_digest", + "grant_revocation_digest", + "attempt_id", + "operation", + } + if ( + set(obj) != expected + or obj["journal_schema"] != "warden.case.step-journal.v1" + ): + raise Refused("unknown journal receipt schema") + state = StepState( + obj["step_id"], + obj["owner"], + Pins(**obj["pins"]), + tuple(obj["allowed_created"]), + obj["retry_safe"], + tuple( + Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"])) + for r in obj["receipts"] + ), + ) + if ( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ) != ( + initial.step_id, + initial.owner, + initial.pins, + initial.allowed_created, + initial.retry_safe, + ): + raise Refused("journal pins changed") + return state + + def save( + self, + binding: str, + state: StepState, + evidence: OwnerResult | None = None, + invocation: Invocation | None = None, + ): + # Validate/hash only the fixed field contract before opening the write. + fingerprint = ( + invocation_fingerprint(invocation) if invocation is not None else None + ) + self.db.execute("BEGIN IMMEDIATE") + try: + inserted = self.db.execute( + "INSERT INTO receipts(binding,payload) VALUES (?,?)", + ( + binding, + json.dumps( + asdict(state) + | { + "journal_schema": "warden.case.step-journal.v1", + "attempt_id": invocation.attempt_id if invocation else None, + "operation": invocation.operation if invocation else None, + "evidence_digest": digest(evidence.evidence.encode()) + if evidence + else None, + "session_revoked": evidence.revoked if evidence else None, + "session_revocation_digest": digest( + evidence.session_revocation_evidence.encode() + ) + if evidence and evidence.session_revocation_evidence + else None, + "grant_revocation_digest": digest( + evidence.grant_revocation_evidence.encode() + ) + if evidence and evidence.grant_revocation_evidence + else None, + }, + separators=(",", ":"), + ), + ), + ) + if fingerprint is not None: + self.db.execute( + "INSERT INTO attempt_context_v1(receipt_seq,invocation_digest) " + "VALUES (?,?)", + (inserted.lastrowid, fingerprint), + ) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK") + raise + + def original_context( + self, binding: str, invocation: Invocation + ) -> OriginalAttemptContext: + """Locate original run intent across recovery/reconcile rows, never trust it. + + Called under the existing journal lock after exact current admission and + state validation. No owner receipt/reference/payload is hydrated here. + """ + rows = self.db.execute( + "SELECT seq,payload FROM receipts WHERE binding=? " + "ORDER BY seq DESC LIMIT ?", + (binding, MAX_HISTORY + 1), + ).fetchall() + if len(rows) > MAX_HISTORY: + raise Refused("bounded original context requires owner archival") + try: + decoded = [(seq, raw, json.loads(raw)) for seq, raw in rows] + intent = next( + ( + row + for row in decoded + if row[2].get("operation") == "run" + and row[2].get("receipts") + and row[2]["receipts"][-1].get("outcome") == "running" + ), + None, + ) + if intent is None: + raise Refused("original run context absent; no legacy backfill") + sequence, raw, payload = intent + attempt = payload.get("attempt_id") + original = replace(invocation, attempt_id=attempt, operation="run") + fingerprint = invocation_fingerprint(original) + # Match both old journal binding and independently re-admitted fields. + if ( + payload.get("journal_schema") != "warden.case.step-journal.v1" + or payload.get("pins") != asdict(invocation.pins) + or payload.get("step_id") != invocation.step + or payload.get("owner") != invocation.owner + or binding != f"{invocation.step}:{invocation.target}" + ): + raise Refused("original context differs from current admission") + latest = next( + row + for row in decoded + if row[2].get("attempt_id") == attempt + and row[2].get("operation") == "run" + ) + for seq, _, row in (intent, latest): + stored = self.db.execute( + "SELECT invocation_digest FROM attempt_context_v1 " + "WHERE receipt_seq=?", + (seq,), + ).fetchone() + if ( + stored is None + or stored[0] != fingerprint + or row.get("pins") != asdict(invocation.pins) + or row.get("step_id") != invocation.step + or row.get("owner") != invocation.owner + ): + raise Refused("original context fingerprint absent or changed") + proofs = [ + latest[2].get(field) + for field in ( + "evidence_digest", + "session_revocation_digest", + "grant_revocation_digest", + ) + ] + if any( + value is not None + and (type(value) is not str or not DIGEST.fullmatch(value)) + for value in proofs + ): + raise Refused("original evidence fingerprint invalid") + return OriginalAttemptContext( + original, sequence, digest(raw.encode()), latest[0], *proofs + ) + except (TypeError, ValueError, KeyError, AttributeError, StopIteration): + raise Refused("original context unavailable or malformed") from None + + +class Executor: + """Injected fixed owner adapters and provenance verifier; none ship by default. + + evidence verifier must authenticate owner custody/provenance or explicitly + attest attended manual evidence; a bare JSON reference is never sufficient. + It receives purpose, exact source digest, and named evidence reference. + """ + + def __init__( + self, + journal: Journal, + adapters: dict[str, Adapter], + evidence_verifier: Callable[[str, str, str], bool], + *, + browser_inputs: BrowserInputs | None = None, + ): + self.journal, self.adapters, self.evidence_verifier = ( + journal, + dict(adapters), + evidence_verifier, + ) + self._admitted = {} + self.browser_inputs = browser_inputs + + def admit( + self, + case_source: bytes, + type_source: bytes, + descriptor_source: bytes, + catalog_source: bytes, + *, + principal: str, + actor: str, + lane: str, + acceptance_evidence: str, + materialization_evidence: str, + descriptor_evidence: str, + session_evidence: str, + ) -> tuple[Descriptor, Invocation, StepState]: + for source in (case_source, type_source, descriptor_source, catalog_source): + bounded_source(source) + descriptor = Descriptor.decode(descriptor_source) + try: + case = yaml.safe_load(case_source)["spec"] + case_type = yaml.safe_load(type_source)["spec"] + if not isinstance(case, dict) or not isinstance(case_type, dict): + raise ValueError + except (yaml.YAMLError, KeyError, TypeError, ValueError): + raise Refused("invalid case or type source shape") from None + if case.get("status") != "materialized" or not case.get("acceptance"): + raise Refused("case is not accepted and structurally materialized") + if case_type.get("status") not in ("acknowledged", "implemented"): + raise Refused("type is not acknowledged") + if case.get("type") != f"{case_type['id']}@{case_type['version']}": + raise Refused("case type pin mismatch") + if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest( + descriptor_source + ): + raise Refused( + "type has no exact acknowledged machine descriptor; plan only" + ) + for purpose, source, evidence in ( + ("case-acceptance", digest(case_source), acceptance_evidence), + ( + "ops-mason-materialization", + digest(case_source), + materialization_evidence, + ), + ( + "owner-descriptor-acceptance:" + descriptor.owner, + digest(descriptor_source), + descriptor_evidence, + ), + ("type-machine-seam-acceptance", digest(type_source), descriptor_evidence), + ("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence), + ( + f"caller-session:{actor}:{principal}:{lane}", + digest(case_source), + session_evidence, + ), + ): + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, source, evidence) is not True + ): + raise Refused("independent owner evidence not verified") + if ( + not IDENTIFIER.fullmatch(principal) + or actor != descriptor.actor + or lane != descriptor.lane + ): + raise Refused("principal actor or lane mismatch") + if descriptor.effect == "mint" and ( + actor != "human" or os.getenv("WARDEN_AGENT_ID") + ): + raise Refused("agent mint is forbidden") + parameters = case.get("parameters", {}) + declared = { + item["name"]: item + for item in case_type.get("parameters", []) + if isinstance(item, dict) and isinstance(item.get("name"), str) + } + if ( + not isinstance(parameters, dict) + or descriptor.target_parameter not in declared + ): + raise Refused("target parameter is not declared by acknowledged type") + if case_type.get("id") == "oidc-browser-relying-party": + if type(self.browser_inputs) is not BrowserInputs: + raise Refused("browser metadata validator is not configured") + target = self.browser_inputs.target( + yaml.safe_load(case_source), + yaml.safe_load(type_source), + descriptor.target_parameter, + ) + else: + for name in parameters: + definition = declared.get(name) + if ( + definition is None + or definition.get("sensitivity") not in ("public", "operational") + or definition.get("case_settable") is False + ): + raise Refused("unknown, fixed or secret-sensitivity case parameter") + target = parameters.get(descriptor.target_parameter) + if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): + raise Refused("target must be exact case metadata") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("unknown adapter or owner source pin") + sheet = json.dumps( + [ + descriptor.step, + descriptor.owner, + target, + actor, + principal, + lane, + acceptance_evidence, + materialization_evidence, + descriptor_evidence, + session_evidence, + ], + separators=(",", ":"), + ).encode() + pins = Pins( + digest(case_source), + digest(type_source), + digest(descriptor_source), + digest(catalog_source), + digest(acceptance_evidence.encode()), + digest(sheet), + ) + invocation = Invocation( + descriptor.step, descriptor.owner, target, actor, principal, lane, pins + ) + initial = StepState( + descriptor.step, + descriptor.owner, + pins, + descriptor.allowed_created, + descriptor.retry_safe, + ) + self._admitted[invocation] = (descriptor, initial, session_evidence) + return descriptor, invocation, initial + + def _result( + self, adapter: Adapter, invocation: Invocation, result: OwnerResult + ) -> OwnerResult: + if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome): + raise Refused("unknown owner result shape") + if type(result.created) is not tuple or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created + ): + raise Refused("owner created-object result invalid") + if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch( + result.evidence + ): + raise Refused("owner evidence identifier invalid") + if adapter.verify(invocation, result) is not True or result.revoked is not True: + raise Refused("owner result or session revocation unverified") + if (result.attempt_id, result.operation) != ( + invocation.attempt_id, + invocation.operation, + ): + raise Refused("owner receipt belongs to another attempt") + descriptor = self._admitted[ + replace(invocation, attempt_id="", operation="admission") + ][0] + proof_fields = { + "parent-session": ( + f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.session_revocation_evidence, + ), + "broker-grant": ( + f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.grant_revocation_evidence, + ), + } + proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes] + for purpose, evidence in proofs: + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, invocation.pins.case, evidence) + is not True + ): + raise Refused("independent grant or parent-session revocation absent") + return result + + def _verify_evidence(self, purpose, source, evidence): + try: + with contained_output(): + return self.evidence_verifier(purpose, source, evidence) is True + except Exception: + raise Refused("independent evidence verification failed") from None + + def _admission( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ): + if self._admitted.get(invocation, ())[:2] != (descriptor, initial): + raise Refused("execution requires verified admission on this executor") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("adapter changed after admission") + session = self._admitted[invocation][2] + if ( + self._verify_evidence( + f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}", + invocation.pins.case, + session, + ) + is not True + ): + raise Refused("caller session no longer verified") + if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"): + raise Refused("agent mint is forbidden") + return adapter + + def _attempt(self, invocation: Invocation, state: StepState, operation: str): + identity = json.dumps( + [asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex], + separators=(",", ":"), + ).encode() + return replace( + invocation, + attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(), + operation=operation, + ) + + def run( + self, + descriptor: Descriptor, + invocation: Invocation, + initial: StepState, + *, + retry=False, + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + recovered = recover(state) + if recovered != state: + self.journal.save(binding, recovered) + state = recovered + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin(state, invocation.pins, retry=retry) + dispatched = self._attempt(invocation, running, "run") + self.journal.save(binding, running, invocation=dispatched) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, dispatched, adapter.invoke(dispatched) + ) + completed = finish( + running, invocation.pins, result.outcome, created=result.created + ) + evidence = result + except Exception: + completed = finish(running, invocation.pins, Outcome.UNCERTAIN) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def rollback( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + if recover(state) != state: + self.journal.save(binding, recover(state)) + raise Refused("lost rollback result requires owner reconciliation") + if adapter.rollback is None: + raise Refused("owner has no reviewed undo adapter") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin_rollback(state, invocation.pins) + dispatched = self._attempt(invocation, running, "rollback") + self.journal.save(binding, running, invocation=dispatched) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.rollback(dispatched, running.last.created), + ) + verified = result.outcome is Outcome.ROLLED_BACK and not result.created + evidence = result if verified else None + except Exception: + verified = False + completed = finish_rollback(running, invocation.pins, verified=verified) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def reconcile( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + """Explicit owner-native state inspection; never redispatches the mutation.""" + adapter = self._admission(descriptor, invocation, initial) + if adapter.reconcile is None: + raise Refused("owner reconciliation adapter absent") + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = recover(self.journal.load(binding, initial)) + if not state.last or state.last.outcome not in ( + Outcome.UNCERTAIN, + Outcome.FAILED_AFTER, + ): + raise Refused("no uncertain mutation to reconcile") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + original = self.journal.original_context(binding, invocation) + state = replace( + state, + receipts=( + *state.receipts, + Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN), + ), + ) + dispatched = self._attempt(invocation, state, "reconcile") + self.journal.save(binding, state, invocation=dispatched) + with contained_output(): + result = adapter.reconcile(dispatched, original) + # Local context is only a lookup key. Require the independent + # owner verifier to bind its exact original/fresh attempt pair + # and context to the returned evidence before accepting a result. + if ( + type(result) is not OwnerResult + or self._verify_evidence( + f"original-attempt-context:{original.original.attempt_id}:" + f"{dispatched.attempt_id}", + original.digest, + result.evidence, + ) + is not True + ): + raise Refused("independent original context evidence absent") + result = self._result(adapter, dispatched, result) + if result.outcome not in ( + Outcome.NO_CHANGE, + Outcome.SUCCESS, + Outcome.FAILED_BEFORE, + ): + raise Refused("owner did not reconcile target state") + # Validate created-object scope using the same closed result reducer. + finish( + begin( + StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ), + state.pins, + ), + state.pins, + result.outcome, + created=result.created, + ) + reconciled = StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ( + *state.receipts, + Receipt(len(state.receipts) + 1, result.outcome, result.created), + ), + ) + self.journal.save(binding, reconciled, result, dispatched) + return reconciled + + +class OwnerInterrupted(KeyboardInterrupt): + """Sanitized interruption preserves persisted running/crash recovery semantics.""" + + def __init__(self): + super().__init__("owner execution interrupted") + + +class _Discard: + """Never retain or forward adapter output, including unbounded malicious text.""" + + def write(self, text): + return len(text) + + def flush(self): + pass diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_original_context.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_original_context.py new file mode 100644 index 00000000..852e3f51 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_original_context.py @@ -0,0 +1,291 @@ +"""Context persistence/refusal only, without a native holder or trusted JSON. + +Existing fixture owner/issuer trust is synthetic. The new fresh-process trial +proves context survives; missing independent proof must retain uncertainty. +""" + +from dataclasses import asdict, replace +import json +from pathlib import Path +import sqlite3 +import subprocess +import sys +import pytest +from test_case_run import bound, rig as rig +from warden.case_receipt_state import Outcome, Pins, Refused +from warden.case_run import ( + Executor, + Invocation, + Journal, + OwnerResult, + invocation_fingerprint, +) + + +def lose(rig, hard=False): + executor, args, calls, sources, kwargs = rig + old = executor.adapters[args[0].adapter] + + def lost(inv): + calls.append(inv) + if hard: + raise KeyboardInterrupt + raise TimeoutError("owner output must not persist") + + executor.adapters[old.reference] = replace(old, invoke=lost) + if hard: + with pytest.raises(KeyboardInterrupt): + executor.run(*args) + else: + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + return executor, args, calls, sources, kwargs, old + + +def test_context_committed_with_intent_before_effect_without_new_raw_fields(rig): + executor, args, calls, _, _ = rig + old = executor.adapters[args[0].adapter] + + def inspect(inv): + with sqlite3.connect(executor.journal.directory / "journal.sqlite") as other: + payload, fingerprint = other.execute( + "SELECT r.payload,c.invocation_digest FROM receipts r JOIN attempt_context_v1 c ON c.receipt_seq=r.seq ORDER BY r.seq DESC LIMIT 1" + ).fetchone() + row = json.loads(payload) + assert ( + row["attempt_id"] == inv.attempt_id + and row["receipts"][-1]["outcome"] == "running" + ) + assert fingerprint == invocation_fingerprint(inv) + assert inv.principal not in payload and inv.principal not in fingerprint + assert set(row) == { + "step_id", + "owner", + "pins", + "allowed_created", + "retry_safe", + "receipts", + "journal_schema", + "evidence_digest", + "session_revoked", + "session_revocation_digest", + "grant_revocation_digest", + "attempt_id", + "operation", + } + return old.invoke(inv) + + executor.adapters[old.reference] = replace(old, invoke=inspect) + assert executor.run(*args).last.outcome is Outcome.SUCCESS and len(calls) == 1 + + +def test_fresh_process_recovers_identity_but_unproved_reconcile_stays_uncertain(rig): + executor, args, calls, sources, kwargs, old = lose(rig, hard=True) + original = calls[-1] + code = """import json,sys +from pathlib import Path +from warden.case_run import Journal,Invocation +from warden.case_receipt_state import Pins +obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1])) +with j.locked(): + c=j.original_context('mint-secret:fixture-client',Invocation(**obj)) + print(json.dumps({'id':c.original.attempt_id,'op':c.original.operation,'evidence':c.evidence_digest})) +""" + env = { + "PATH": "/usr/bin:/bin", + "PYTHONPATH": str(Path(__file__).parents[1] / "src"), + "PYTHONDONTWRITEBYTECODE": "1", + } + result = subprocess.run( + [sys.executable, "-B", "-c", code, str(executor.journal.directory)], + input=json.dumps(asdict(args[1])), + text=True, + capture_output=True, + env=env, + timeout=15, + check=True, + ) + recovered = json.loads(result.stdout) + assert recovered == {"id": original.attempt_id, "op": "run", "evidence": None} + seen = [] + + def reconcile(fresh, context): + seen.append((fresh, context)) + assert fresh.attempt_id != context.original.attempt_id == original.attempt_id + assert fresh.operation == "reconcile" and context.original.operation == "run" + return bound( + fresh, OwnerResult(Outcome.SUCCESS, "owner-receipt", ("case-policy",), True) + ) + + def reject_original(purpose, source, evidence): + return ( + False + if purpose.startswith("original-attempt-context:") + else executor.evidence_verifier(purpose, source, evidence) + ) + + reopened = Executor( + Journal(executor.journal.directory), + {old.reference: replace(old, reconcile=reconcile)}, + reject_original, + ) + admitted = reopened.admit(*sources, **kwargs) + with pytest.raises(Refused): + reopened.reconcile(*admitted) + assert len(seen) == len(calls) == 1 + assert ( + reopened.journal.load("mint-secret:fixture-client", admitted[2]).last.outcome + is Outcome.UNCERTAIN + ) + with pytest.raises(Refused): + reopened.run(*admitted, retry=True) + with pytest.raises(Refused): + reopened.rollback(*admitted) + + +def test_legacy_context_not_backfilled_and_refused_before_callback(rig): + executor, args, calls, _, _, old = lose(rig) + executor.journal.db.execute("DROP TABLE attempt_context_v1") + executor.journal = Journal(executor.journal.directory) + seen = [] + executor.adapters[old.reference] = replace(old, reconcile=lambda *v: seen.append(v)) + with pytest.raises(Refused): + executor.reconcile(*args) + assert not seen and len(calls) == 1 + assert executor.journal.db.execute( + "SELECT COUNT(*) FROM attempt_context_v1" + ).fetchone() == (0,) + + +@pytest.mark.parametrize( + "field,value", + [ + ("principal", "other"), + ("actor", "agent"), + ("lane", "other-lane"), + ("target", "other-target"), + ("owner", "other-owner"), + ("pins", Pins(*["sha256:" + "f" * 64] * 6)), + ], +) +def test_wrong_readmitted_original_context_refused(rig, field, value): + executor, args, _, _, _, _ = lose(rig) + with executor.journal.locked(), pytest.raises(Refused): + executor.journal.original_context( + "mint-secret:fixture-client", replace(args[1], **{field: value}) + ) + + +def test_wrong_original_attempt_or_missing_row_link_refuses_callback(rig): + executor, args, _, _, _, old = lose(rig) + row = executor.journal.db.execute( + "SELECT seq,payload FROM receipts ORDER BY seq LIMIT 1" + ).fetchone() + payload = json.loads(row[1]) + payload["attempt_id"] = "attempt-wrong" + executor.journal.db.execute( + "UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0]) + ) + seen = [] + executor.adapters[old.reference] = replace(old, reconcile=lambda *v: seen.append(v)) + with pytest.raises(Refused): + executor.reconcile(*args) + assert not seen + executor.journal.db.execute( + "DELETE FROM attempt_context_v1 WHERE receipt_seq=?", (row[0],) + ) + with pytest.raises(Refused): + executor.reconcile(*args) + assert not seen + + +def test_context_write_failure_rolls_back_intent_and_prevents_effect(rig): + executor, args, calls, _, _ = rig + executor.journal.db.execute( + "CREATE TRIGGER fail_context BEFORE INSERT ON attempt_context_v1 BEGIN SELECT RAISE(ABORT,'fixture'); END" + ) + with pytest.raises(sqlite3.IntegrityError): + executor.run(*args) + assert not calls + assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( + 0, + ) + + +@pytest.mark.parametrize( + "bad", + [ + {}, + object(), + Invocation( + "step", + "owner", + "target", + "human", + "actor", + "lane", + Pins(*["sha256:" + "1" * 64] * 6), + attempt_id={"owner": "payload"}, + operation="run", + ), + ], +) +def test_arbitrary_context_never_serialized_or_persisted(rig, bad): + executor, args, calls, _, _ = rig + with pytest.raises(Refused): + executor.journal.save("binding", args[2], invocation=bad) + assert not calls + assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( + 0, + ) + + +def test_interrupted_reconcile_preserves_original_run_for_followup(rig): + executor, args, calls, _, _, old = lose(rig) + seen = [] + + def interrupted(fresh, original): + seen.append((fresh, original)) + raise KeyboardInterrupt + + executor.adapters[old.reference] = replace(old, reconcile=interrupted) + for _ in range(2): + with pytest.raises(KeyboardInterrupt): + executor.reconcile(*args) + assert seen[0][0].attempt_id != seen[1][0].attempt_id + assert seen[0][1].original == seen[1][1].original == calls[0] + assert seen[0][1].digest == seen[1][1].digest and len(calls) == 1 + + +def test_prior_context_proof_cannot_authenticate_new_reconcile(rig): + executor, args, calls, _, _, _ = lose(rig) + old = executor.evidence_verifier + purposes = [] + + def verify(purpose, source, evidence): + if purpose.startswith("original-attempt-context:"): + purposes.append((purpose, source, evidence)) + return purpose.endswith(":attempt-from-another-reconcile") + return old(purpose, source, evidence) + + executor.evidence_verifier = verify + with pytest.raises(Refused): + executor.reconcile(*args) + assert len(purposes) == 1 and purposes[0][0].split(":")[1] == calls[0].attempt_id + assert purposes[0][0].split(":")[2] != calls[0].attempt_id + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.UNCERTAIN + ) + + +def test_one_argument_legacy_callback_cannot_ignore_original_context(rig): + executor, args, _, _, _, old = lose(rig) + seen = [] + + def legacy(inv): + seen.append(inv) + + executor.adapters[old.reference] = replace(old, reconcile=legacy) + with pytest.raises(Refused): + executor.reconcile(*args) + assert not seen diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_receipt_state.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_receipt_state.py new file mode 100644 index 00000000..74ee9ea6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_receipt_state.py @@ -0,0 +1,124 @@ +"""Offline prototype regression cases; no owner adapter, credential or live use.""" +from dataclasses import FrozenInstanceError, replace +import pytest +from warden.case_receipt_state import ( + Outcome, Pins, Refused, StepState, begin, begin_rollback, finish, + finish_rollback, recover, rollback_objects, +) + + +@pytest.fixture +def pins(): + return Pins(*["sha256:" + character * 64 for character in "abcdef"]) + + +@pytest.fixture +def state(pins): + return StepState("mint-secret", "railiance-platform", pins, ("case-policy", "case-role")) + + +def test_records_running_before_dispatch_without_mutating_history(state, pins): + running = begin(state, pins) + assert state.receipts == () + assert running.last.outcome is Outcome.RUNNING + with pytest.raises(FrozenInstanceError): + running.last.attempt = 8 + + +def test_changed_exact_revision_refuses_dispatch(state, pins): + with pytest.raises(Refused): + begin(state, replace(pins, adapter="sha256:" + "0" * 64)) + + +def test_lost_receipt_becomes_uncertain_and_cannot_retry(state, pins): + resumed = recover(begin(state, pins)) + assert resumed.last.outcome is Outcome.UNCERTAIN + assert len(resumed.receipts) == 2 + with pytest.raises(Refused): + begin(replace(resumed, retry_safe=True), pins, retry=True) + + +@pytest.mark.parametrize("outcome", [Outcome.SUCCESS, Outcome.NO_CHANGE]) +def test_verified_success_or_no_change_never_replayed(state, pins, outcome): + done = finish(begin(state, pins), pins, outcome) + with pytest.raises(Refused): + begin(done, pins, retry=True) + + +def test_retry_requires_verified_no_change_failure_and_safe_adapter(state, pins): + failed = finish(begin(state, pins), pins, Outcome.FAILED_BEFORE) + with pytest.raises(Refused): + begin(failed, pins, retry=True) + safe = replace(failed, retry_safe=True) + with pytest.raises(Refused): + begin(safe, pins) + retry = begin(safe, pins, retry=True) + assert retry.last.outcome is Outcome.RUNNING + assert len(retry.receipts) == 3 + + +def test_failure_after_change_does_not_become_safe_retry_or_rollback(state, pins): + failed = finish(begin(state, pins), pins, Outcome.FAILED_AFTER, created=("case-policy",)) + with pytest.raises(Refused): + begin(replace(failed, retry_safe=True), pins, retry=True) + with pytest.raises(Refused): + rollback_objects(failed, pins) + + +def test_rollback_only_created_not_all_reviewed_objects(state, pins): + done = finish(begin(state, pins), pins, Outcome.SUCCESS, created=("case-role",)) + assert rollback_objects(done, pins) == ("case-role",) + assert "case-policy" not in rollback_objects(done, pins) + undo = begin_rollback(done, pins) + verified = finish_rollback(undo, pins, verified=True) + assert verified.last.outcome is Outcome.ROLLED_BACK + with pytest.raises(Refused): + begin_rollback(verified, pins) + + +def test_rollback_reverse_order_and_uncertain_undo_blocks_replay(state, pins): + done = finish(begin(state, pins), pins, Outcome.SUCCESS, + created=("case-policy", "case-role")) + assert rollback_objects(done, pins) == ("case-role", "case-policy") + uncertain = recover(begin_rollback(done, pins)) + assert uncertain.last.outcome is Outcome.ROLLBACK_UNCERTAIN + with pytest.raises(Refused): + begin_rollback(uncertain, pins) + + +def test_unverified_rollback_cannot_claim_success(state, pins): + done = finish(begin(state, pins), pins, Outcome.SUCCESS, created=("case-role",)) + failed = finish_rollback(begin_rollback(done, pins), pins, verified=False) + assert failed.last.outcome is Outcome.ROLLBACK_UNCERTAIN + + +def test_refuses_shared_objects_or_false_no_change_creation(state, pins): + running = begin(state, pins) + for outcome, created in [(Outcome.SUCCESS, ("shared-group",)), + (Outcome.NO_CHANGE, ("case-policy",)), + (Outcome.FAILED_BEFORE, ("case-policy",)), + (Outcome.SUCCESS, ("case-policy", "case-policy"))]: + with pytest.raises(Refused): + finish(running, pins, outcome, created=created) + + +def test_no_raw_payload_or_output_receipt_interface(state, pins): + with pytest.raises(TypeError): + finish(begin(state, pins), pins, Outcome.SUCCESS, output="sensitive output") + with pytest.raises(Refused): + StepState("step", "owner", pins, ("path with spaces or output",)) + with pytest.raises(Refused): + Pins("not-a-digest", pins.case_type, pins.adapter, pins.catalog, pins.acceptance, pins.run_sheet) + + +def test_out_of_order_result_and_noop_recovery(state, pins): + assert recover(state) is state + with pytest.raises(Refused): + finish(state, pins, Outcome.SUCCESS) + with pytest.raises(Refused): + finish_rollback(state, pins, verified=True) + + +def test_result_must_be_closed_enum_not_string(state, pins): + with pytest.raises(Refused): + finish(begin(state, pins), pins, "verified-success") diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_run.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_run.py new file mode 100644 index 00000000..3c0b05da --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/candidate/tests/test_case_run.py @@ -0,0 +1,644 @@ +"""Injected owner harness; these receipts are not native owner attestations.""" + +from dataclasses import asdict, replace +import json +import os +import subprocess +import sys +import sqlite3 +import pytest +import yaml +from warden.case_receipt_state import Outcome, Refused +from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest + + +@pytest.fixture +def rig(tmp_path): + desc = Descriptor( + "warden.case.execution.v1", + "mint-secret", + "owner", + "owner/mint@v1", + "sha256:" + "1" * 64, + "human", + "issuer-lane", + "client_id", + "mint", + ("case-policy", "case-role"), + True, + ("parent-session", "broker-grant"), + ) + descriptor = json.dumps(asdict(desc)).encode() + typ = yaml.safe_dump( + { + "spec": { + "id": "test", + "version": "1", + "status": "acknowledged", + "parameters": [ + { + "name": "client_id", + "sensitivity": "operational", + "case_settable": True, + } + ], + "execution_descriptors": {"mint-secret": digest(descriptor)}, + } + } + ).encode() + case = yaml.safe_dump( + { + "spec": { + "id": "case-test", + "type": "test@1", + "status": "materialized", + "acceptance": "acceptance", + "parameters": {"client_id": "fixture-client"}, + } + } + ).encode() + calls = [] + + def invoke(inv): + calls.append(inv) + return bound( + inv, + OwnerResult( + Outcome.SUCCESS, + "owner-receipt", + ("case-policy", "case-role"), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ) + + adapter = Adapter( + "owner/mint@v1", + desc.adapter_source, + invoke, + lambda inv, result: result.evidence == "owner-receipt", + lambda inv, objects: bound( + inv, + OwnerResult( + Outcome.ROLLED_BACK, + "owner-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ), + lambda inv, original: bound( + inv, + OwnerResult( + Outcome.NO_CHANGE, + "owner-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ), + ) + verified = { + ("case-acceptance", digest(case), "acceptance"), + ("ops-mason-materialization", digest(case), "materialization"), + ("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"), + ("type-machine-seam-acceptance", digest(typ), "descriptor"), + ("catalog-routing-acceptance", digest(b"catalog"), "descriptor"), + ("caller-session:human:operator:issuer-lane", digest(case), "session"), + ("session-revocation:issuer-lane", digest(case), "session-revoked"), + ("grant-revocation:issuer-lane", digest(case), "grant-revoked"), + } + + def verifier(purpose, source, evidence): + if purpose.startswith("original-attempt-context:"): + # Existing fixture trust only; exact lookup context, not a real holder. + context = executor.journal.original_context( + "mint-secret:fixture-client", admitted[1] + ) + parts = purpose.split(":") + return ( + len(parts) == 3 + and parts[1] == context.original.attempt_id + and parts[2] != context.original.attempt_id + and source == context.digest + and evidence == "owner-receipt" + ) + if purpose.startswith(("session-revocation:", "grant-revocation:")): + scope, lane, operation, attempt = purpose.split(":") + return ( + source == digest(case) + and lane == "issuer-lane" + and operation in ("run", "rollback", "reconcile") + and evidence + == ("session-" if scope == "session-revocation" else "grant-") + attempt + ) + return (purpose, source, evidence) in verified + + executor = Executor( + Journal(tmp_path / "private"), {"owner/mint@v1": adapter}, verifier + ) + kwargs = dict( + principal="operator", + actor="human", + lane="issuer-lane", + acceptance_evidence="acceptance", + materialization_evidence="materialization", + descriptor_evidence="descriptor", + session_evidence="session", + ) + admitted = executor.admit(case, typ, descriptor, b"catalog", **kwargs) + return (executor, admitted, calls, (case, typ, descriptor, b"catalog"), kwargs) + + +def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig): + executor, args, calls, _, _ = rig + old = executor.adapters[args[0].adapter] + + def inspect(inv): + other = sqlite3.connect(executor.journal.directory / "journal.sqlite") + payload = json.loads( + other.execute( + "SELECT payload FROM receipts ORDER BY seq DESC LIMIT 1" + ).fetchone()[0] + ) + assert payload["receipts"][-1]["outcome"] == "running" + return old.invoke(inv) + + executor.adapters[old.reference] = replace(old, invoke=inspect) + assert executor.run(*args).last.outcome is Outcome.SUCCESS + with pytest.raises(Refused): + executor.run(*args) + assert len(calls) == 1 + + +def test_crash_and_fresh_process_resume_never_duplicates_effect(rig): + executor, args, calls, sources, kwargs = rig + old = executor.adapters[args[0].adapter] + + def crash(inv): + calls.append(inv) + raise KeyboardInterrupt + + executor.adapters[old.reference] = replace(old, invoke=crash) + with pytest.raises(KeyboardInterrupt): + executor.run(*args) + fresh = Executor( + Journal(executor.journal.directory), + {old.reference: old}, + executor.evidence_verifier, + ) + admitted = fresh.admit(*sources, **kwargs) + with pytest.raises(Refused): + fresh.run(*admitted, retry=True) + state = fresh.journal.load("mint-secret:fixture-client", admitted[2]) + assert state.last.outcome is Outcome.UNCERTAIN + assert len(calls) == 1 + + +def test_lost_result_preserved_and_cas_reconciliation_does_not_mint(rig): + executor, args, calls, _, _ = rig + old = executor.adapters[args[0].adapter] + + def lost(inv): + calls.append(inv) + raise TimeoutError("SECRET-HIDDEN") + + executor.adapters[old.reference] = replace(old, invoke=lost) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE + with pytest.raises(Refused): + executor.run(*args, retry=True) + assert len(calls) == 1 + assert ( + b"SECRET-HIDDEN" + not in (executor.journal.directory / "journal.sqlite").read_bytes() + ) + + +def test_failed_before_change_requires_explicit_retry_and_safe_owner(rig): + executor, args, calls, _, _ = rig + old = executor.adapters[args[0].adapter] + + def no_change(inv): + calls.append(inv) + return bound( + inv, + OwnerResult( + Outcome.FAILED_BEFORE, + "owner-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ) + + executor.adapters[old.reference] = replace(old, invoke=no_change) + assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE + with pytest.raises(Refused): + executor.run(*args) + assert executor.run(*args, retry=True).last.outcome is Outcome.FAILED_BEFORE + assert len(calls) == 2 + + +@pytest.mark.parametrize( + "result", + [ + OwnerResult( + Outcome.SUCCESS, + "owner-receipt", + ("shared-policy",), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + OwnerResult( + Outcome.SUCCESS, + "owner-receipt", + (), + False, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + OwnerResult( + Outcome.SUCCESS, + "unknown-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + {"outcome": "verified-success", "stdout": "SECRET-HIDDEN"}, + ], +) +def test_unknown_secret_payload_created_scope_or_missing_revocation_never_succeeds( + rig, result, capsys +): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + + def malicious(inv): + print("SECRET-HIDDEN" * 1000) + return result + + executor.adapters[old.reference] = replace(old, invoke=malicious) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + assert capsys.readouterr().out == "" + assert ( + b"SECRET-HIDDEN" + not in (executor.journal.directory / "journal.sqlite").read_bytes() + ) + + +def test_rollback_reverses_only_verified_case_created_objects(rig): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + undos = [] + + def undo(inv, objects): + undos.append(objects) + return old.rollback(inv, objects) + + executor.adapters[old.reference] = replace(old, rollback=undo) + executor.run(*args) + assert executor.rollback(*args).last.outcome is Outcome.ROLLED_BACK + assert undos == [("case-role", "case-policy")] + assert "shared-policy" not in undos[0] + + +def test_uncertain_rollback_cannot_replay(rig): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + executor.run(*args) + executor.adapters[old.reference] = replace( + old, + rollback=lambda inv, objs: bound( + inv, + OwnerResult( + Outcome.ROLLED_BACK, + "owner-receipt", + (), + False, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ), + ) + assert executor.rollback(*args).last.outcome is Outcome.ROLLBACK_UNCERTAIN + with pytest.raises(Refused): + executor.rollback(*args) + + +@pytest.mark.parametrize( + "change", ["materialization", "type-prose", "actor", "lane", "source", "target"] +) +def test_exact_admission_requires_owner_evidence_and_closed_source_pins(rig, change): + executor, _, _, sources, kwargs = rig + case, typ, descriptor, catalog = sources + kwargs = dict(kwargs) + if change == "materialization": + kwargs["materialization_evidence"] = "bare-json" + if change == "type-prose": + obj = yaml.safe_load(typ) + obj["spec"].pop("execution_descriptors") + typ = yaml.safe_dump(obj).encode() + if change == "actor": + kwargs["actor"] = "agent" + if change == "lane": + kwargs["lane"] = "other-lane" + if change == "source": + obj = json.loads(descriptor) + obj["adapter_source"] = "sha256:" + "2" * 64 + descriptor = json.dumps(obj).encode() + if change == "target": + obj = yaml.safe_load(case) + obj["spec"]["parameters"]["client_id"] = "other-target" + case = yaml.safe_dump(obj).encode() + with pytest.raises(Refused): + executor.admit(case, typ, descriptor, catalog, **kwargs) + + +def test_agent_env_cannot_dispatch_after_human_admission(rig, monkeypatch): + executor, args, calls, _, _ = rig + monkeypatch.setenv("WARDEN_AGENT_ID", "test-agent") + with pytest.raises(Refused): + executor.run(*args) + assert calls == [] + + +def test_forged_invocation_cannot_bypass_admission(rig): + executor, args, calls, _, _ = rig + with pytest.raises(Refused): + executor.run(args[0], replace(args[1], target="other-target"), args[2]) + assert calls == [] + + +def test_journal_rejects_unsafe_directory_and_symlink(tmp_path): + unsafe = tmp_path / "unsafe" + unsafe.mkdir(mode=493) + with pytest.raises(Refused): + Journal(unsafe) + private = tmp_path / "private" + private.mkdir(mode=448) + (private / "journal.sqlite").symlink_to(tmp_path / "outside") + with pytest.raises(OSError): + Journal(private) + + +def test_process_lock_blocks_parallel_dispatch(rig): + executor, args, calls, _, _ = rig + with executor.journal.locked(): + with pytest.raises(Refused): + executor.run(*args) + assert calls == [] + + +def test_changed_catalog_pins_cannot_inherit_old_success(rig): + executor, args, calls, sources, kwargs = rig + executor.run(*args) + with pytest.raises(Refused): + executor.admit(*sources[:3], b"changed-catalog", **kwargs) + assert len(calls) == 1 + + +def test_private_journal_does_not_retain_owner_payload_or_accept_unknown_schema(rig): + executor, args, _, _, _ = rig + executor.run(*args) + row = executor.journal.db.execute( + "SELECT seq,payload FROM receipts ORDER BY seq DESC LIMIT 1" + ).fetchone() + payload = json.loads(row[1]) + assert payload["journal_schema"] == "warden.case.step-journal.v1" + assert payload["evidence_digest"] == digest(b"owner-receipt") + assert payload["session_revoked"] is True + assert "owner-receipt" not in row[1] + payload["stdout"] = "UNTRUSTED-OUTPUT" + executor.journal.db.execute( + "UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0]) + ) + with pytest.raises(Refused): + executor.run(*args) + + +def test_expired_caller_session_cannot_use_cached_admission(rig): + executor, args, calls, _, _ = rig + old = executor.evidence_verifier + executor.evidence_verifier = lambda purpose, source, evidence: ( + False + if purpose.startswith("caller-session:") + else old(purpose, source, evidence) + ) + with pytest.raises(Refused): + executor.run(*args) + assert calls == [] + + +@pytest.mark.parametrize("index", [0, 1, 2, 3]) +def test_source_sizes_are_bounded_before_parsing(rig, index): + executor, _, _, sources, kwargs = rig + oversized = list(sources) + oversized[index] = b"x" * 262145 + with pytest.raises(Refused, match="bounded"): + executor.admit(*oversized, **kwargs) + + +def test_owner_fd_and_inherited_subprocess_output_is_contained(rig, capfd): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + + def leaking(inv): + os.write(1, b"SYNTHETIC-SECRET") + os.write(2, b"SYNTHETIC-SECRET") + subprocess.run([sys.executable, "-c", 'print("SYNTHETIC-SECRET")'], check=True) + return old.invoke(inv) + + executor.adapters[old.reference] = replace(old, invoke=leaking) + assert executor.run(*args).last.outcome is Outcome.SUCCESS + observed = capfd.readouterr() + assert "SYNTHETIC-SECRET" not in observed.out + observed.err + + +def test_evidence_provider_output_and_error_never_escape(rig, capfd): + executor, _, calls, sources, kwargs = rig + + def broken(*args): + print("SYNTHETIC-SECRET") + os.write(2, b"SYNTHETIC-SECRET") + raise RuntimeError("SYNTHETIC-SECRET") + + executor.evidence_verifier = broken + with pytest.raises(Refused) as caught: + executor.admit(*sources, **kwargs) + assert "SYNTHETIC-SECRET" not in str(caught.value) + observed = capfd.readouterr() + assert "SYNTHETIC-SECRET" not in observed.out + observed.err + assert calls == [] + + +@pytest.mark.parametrize("missing", ["session", "grant"]) +def test_broker_grant_and_parent_session_have_separate_required_evidence(rig, missing): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + result = old.invoke(args[1]) + if missing == "session": + result = replace(result, session_revocation_evidence=None) + else: + result = replace(result, grant_revocation_evidence=None) + executor.adapters[old.reference] = replace(old, invoke=lambda inv: result) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + + +def test_mint_descriptor_cannot_omit_broker_grant_scope(rig): + descriptor = json.loads(rig[3][2]) + descriptor["revocation_scopes"] = ["parent-session"] + with pytest.raises(Refused): + Descriptor.decode(json.dumps(descriptor).encode()) + + +def test_unsafe_owner_step_cannot_retry_even_after_verified_failed_before( + rig, tmp_path +): + executor, args, _, sources, kwargs = rig + descriptor = replace(args[0], retry_safe=False) + descriptor_source = json.dumps(asdict(descriptor)).encode() + type_doc = yaml.safe_load(sources[1]) + type_doc["spec"]["execution_descriptors"][descriptor.step] = digest( + descriptor_source + ) + type_source = yaml.safe_dump(type_doc).encode() + old_verifier = executor.evidence_verifier + + def verifier(purpose, source, evidence): + if purpose == "owner-descriptor-acceptance:owner": + return source == digest(descriptor_source) and evidence == "descriptor" + if purpose == "type-machine-seam-acceptance": + return source == digest(type_source) and evidence == "descriptor" + return old_verifier(purpose, source, evidence) + + adapter = executor.adapters[descriptor.adapter] + + def failed(inv): + return bound( + inv, + OwnerResult( + Outcome.FAILED_BEFORE, + "owner-receipt", + (), + True, + "session-revoked", + "grant-revoked", + ), + ) + + fresh = Executor( + Journal(tmp_path / "unsafe-retry"), + {adapter.reference: replace(adapter, invoke=failed)}, + verifier, + ) + admitted = fresh.admit( + sources[0], type_source, descriptor_source, sources[3], **kwargs + ) + assert fresh.run(*admitted).last.outcome is Outcome.FAILED_BEFORE + with pytest.raises(Refused): + fresh.run(*admitted, retry=True) + + +def bound(inv, result): + return replace( + result, + attempt_id=inv.attempt_id, + operation=inv.operation, + session_revocation_evidence=("session-" + inv.attempt_id) + if result.session_revocation_evidence + else None, + grant_revocation_evidence=("grant-" + inv.attempt_id) + if result.grant_revocation_evidence + else None, + ) + + +def test_old_grant_and_session_proofs_cannot_verify_a_new_retry(rig): + executor, args, _, _, _ = rig + adapter = executor.adapters[args[0].adapter] + retained = [] + + def attempts(inv): + if not retained: + result = bound( + inv, + OwnerResult( + Outcome.FAILED_BEFORE, + "owner-receipt", + (), + True, + "session-revoked", + "grant-revoked", + ), + ) + retained.append(result) + return result + # Even a caller rewriting labels cannot authenticate prior revocation + # evidence for the newly persisted attempt identity. + return replace(retained[0], attempt_id=inv.attempt_id, operation=inv.operation) + + executor.adapters[adapter.reference] = replace(adapter, invoke=attempts) + assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE + assert executor.run(*args, retry=True).last.outcome is Outcome.UNCERTAIN + + +def test_owner_interrupt_payload_is_sanitized_with_running_intent_retained(rig, capfd): + executor, args, _, _, _ = rig + adapter = executor.adapters[args[0].adapter] + + def interrupted(inv): + os.write(2, b"SYNTHETIC-SECRET") + raise KeyboardInterrupt("SYNTHETIC-SECRET") + + executor.adapters[adapter.reference] = replace(adapter, invoke=interrupted) + with pytest.raises(KeyboardInterrupt) as caught: + executor.run(*args) + assert "SYNTHETIC-SECRET" not in str(caught.value) + assert "SYNTHETIC-SECRET" not in "".join(capfd.readouterr()) + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.RUNNING + ) + + +def test_attempt_identity_changes_with_exact_catalog_revision_and_new_journal( + rig, tmp_path +): + executor, args, calls, sources, kwargs = rig + executor.run(*args) + original_attempt = calls[-1].attempt_id + new_catalog = b"independently-reviewed-catalog-v2" + old_verifier = executor.evidence_verifier + + def verifier(purpose, source, evidence): + if purpose == "catalog-routing-acceptance": + return source == digest(new_catalog) and evidence == "descriptor" + return old_verifier(purpose, source, evidence) + + fresh = Executor(Journal(tmp_path / "new-revision"), executor.adapters, verifier) + revised = fresh.admit(*sources[:3], new_catalog, **kwargs) + assert fresh.run(*revised).last.outcome is Outcome.SUCCESS + assert calls[-1].attempt_id != original_attempt + assert calls[-1].pins.catalog != calls[0].pins.catalog + + +def test_secret_sensitivity_parameter_cannot_reach_admission_or_journal(rig): + executor, _, _, sources, kwargs = rig + type_doc = yaml.safe_load(sources[1]) + type_doc["spec"]["parameters"][0]["sensitivity"] = "secret" + source = yaml.safe_dump(type_doc).encode() + old = executor.evidence_verifier + executor.evidence_verifier = lambda purpose, digest_value, evidence: ( + True + if purpose == "type-machine-seam-acceptance" + else old(purpose, digest_value, evidence) + ) + with pytest.raises(Refused, match="secret-sensitivity"): + executor.admit(sources[0], source, sources[2], sources[3], **kwargs) diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/failed-preparation-r1.json b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/failed-preparation-r1.json new file mode 100644 index 00000000..13196643 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/failed-preparation-r1.json @@ -0,0 +1 @@ +{"phase":"candidate test setup","outcome":"failed before tests","reason":"Relative packet paths were applied while cwd was already candidate; no source edit or tests executed."} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/focused-tests.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/focused-tests.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/focused-tests.stdout.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/focused-tests.stdout.txt new file mode 100644 index 00000000..0cd77e14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/focused-tests.stdout.txt @@ -0,0 +1,2 @@ +.................................................................... [100%] +68 passed in 2.06s diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/format.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/format.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/format.stdout.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/format.stdout.txt new file mode 100644 index 00000000..8e4d5041 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/format.stdout.txt @@ -0,0 +1 @@ +3 files already formatted diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/lint.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/lint.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/lint.stdout.txt b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/lint.stdout.txt new file mode 100644 index 00000000..1f5f344d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/lint.stdout.txt @@ -0,0 +1 @@ +All checks passed! diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/manifest.json new file mode 100644 index 00000000..7763365a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/manifest.json @@ -0,0 +1,108 @@ +{ + "files": [ + { + "path": "candidate/docs/security-case-execution-kernel-v1.md", + "sha256": "sha256:1299999413739c92a9d41a591baeeb1e8cd912e425db30468e15f7cccedee8ec" + }, + { + "path": "candidate/pytest.ini", + "sha256": "sha256:363381771127e3479fe49773659cc2a8dc2b2f727459ebd057ac39e79e98a94e" + }, + { + "path": "candidate/src/warden/__init__.py", + "sha256": "sha256:047e0feba479474235f0707a207655ecfddfb33cd4f54069134419b8ca4ea24a" + }, + { + "path": "candidate/src/warden/case_inputs.py", + "sha256": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7" + }, + { + "path": "candidate/src/warden/case_receipt_state.py", + "sha256": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc" + }, + { + "path": "candidate/src/warden/case_run.py", + "sha256": "sha256:3b1e923b34fbdd7e3166c3963451363099f8cab27f677de5dba70313561172be" + }, + { + "path": "candidate/tests/test_case_original_context.py", + "sha256": "sha256:dd3a365be56254755b72ff9119215239e8b7e3311ce39bcc5c4dd0b9baf213ac" + }, + { + "path": "candidate/tests/test_case_receipt_state.py", + "sha256": "sha256:6ca261660744ea187b7a7aa79cfa5e0d05e7e53af72f9c12d6c9ed375a4130e9" + }, + { + "path": "candidate/tests/test_case_run.py", + "sha256": "sha256:004b1730a88c28ff3c047070cf8ed2606737b2a328873e3c2d8dcf0c7c573bac" + }, + { + "path": "failed-preparation-r1.json", + "sha256": "sha256:b8d3b020565dafa13af6ebe935cc830d4d35d30a720b79a7498863d9b28af6b2" + }, + { + "path": "focused-tests.stderr.txt", + "sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "focused-tests.stdout.txt", + "sha256": "sha256:c744439c895a0dfd417791c9cf21d3c55cb3d416ccc724bad6fd66da615d5c70" + }, + { + "path": "format.stderr.txt", + "sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "format.stdout.txt", + "sha256": "sha256:eee76f865de5a9f2456950f710ae5d5f9b5bd0ea02aa54f614c5eed6642a0bfd" + }, + { + "path": "lint.stderr.txt", + "sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "lint.stdout.txt", + "sha256": "sha256:82b3e6a6c090a57601d22943bd23fca9218d1031dbe5a7b754092f9a156b4f18" + }, + { + "path": "original-context.patch", + "sha256": "sha256:99bc0e6e4e3e1d86267f15cb668f4e0e8147c3c4b227d8ffed917d8e91e42df9" + }, + { + "path": "owner-review.json", + "sha256": "sha256:048756d19b76c064b9ab17f475ce42420261b450976714b21e819d4b3e7ea57b" + }, + { + "path": "patch-check.json", + "sha256": "sha256:358a1dfa2e454d8386e1c1ba0e03f607774a83169acd03ec7230527f6a0c7247" + }, + { + "path": "preimages/docs/security-case-execution-kernel-v1.md", + "sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb" + }, + { + "path": "preimages/src/warden/case_run.py", + "sha256": "sha256:afb5ce9c59b9748f85c9f1d4af04b255087632c58de8dcd8c9426fc1ed4db068" + }, + { + "path": "preimages/tests/test_case_run.py", + "sha256": "sha256:1e70ceb858af0eb3a126132a3c3606706120c0caf58ca9d45deb43f217705c7e" + }, + { + "path": "retained-preparation-failures.json", + "sha256": "sha256:6bf6d4e17224a67366a66e405469b612096f79060744e1e4efbcc9070b58e7de" + }, + { + "path": "source-references.json", + "sha256": "sha256:87b82dfdca444710f4079011f1ad80e1b6524485a6c6b5dc6bfc204fef734b11" + }, + { + "path": "superseded-mixed-base.patch", + "sha256": "sha256:af34199a9940508e3ccc3c3eeb7800e8e760af3943a74aec78941f0fc61dce68" + }, + { + "path": "verification.json", + "sha256": "sha256:e38169127cc4eb700878198c85d43c7141efc4827dde682c0eb9a1866e3c6207" + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/original-context.patch b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/original-context.patch new file mode 100644 index 00000000..ae672ea8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/original-context.patch @@ -0,0 +1,636 @@ +--- a/src/warden/case_run.py ++++ b/src/warden/case_run.py +@@ -170,6 +170,83 @@ + operation: str = "admission" + + ++def invocation_fingerprint(invocation: Invocation) -> str: ++ """Fingerprint a fixed validated projection; retain no owner payload or refs. ++ ++ Invocation is not a generic metadata container. Accept only this exact class, ++ its reviewed identifier fields and closed Pins. Never serialize arbitrary ++ attributes, case parameters, OwnerResult.evidence or callback output. ++ """ ++ if type(invocation) is not Invocation or type(invocation.pins) is not Pins: ++ raise Refused("exact invocation metadata required") ++ identifiers = ( ++ invocation.step, ++ invocation.owner, ++ invocation.target, ++ invocation.principal, ++ invocation.lane, ++ invocation.attempt_id, ++ ) ++ if ( ++ not all( ++ type(value) is str and IDENTIFIER.fullmatch(value) for value in identifiers ++ ) ++ or invocation.actor not in ("human", "agent") ++ or invocation.operation not in ("run", "rollback", "reconcile") ++ ): ++ raise Refused("bounded invocation metadata required") ++ pins = [ ++ invocation.pins.case, ++ invocation.pins.case_type, ++ invocation.pins.adapter, ++ invocation.pins.catalog, ++ invocation.pins.acceptance, ++ invocation.pins.run_sheet, ++ ] ++ if not all(type(value) is str and DIGEST.fullmatch(value) for value in pins): ++ raise Refused("exact invocation pins required") ++ projection = [ ++ "warden.case.invocation-fingerprint.v1", ++ *identifiers, ++ invocation.actor, ++ invocation.operation, ++ *pins, ++ ] ++ return digest(json.dumps(projection, separators=(",", ":")).encode()) ++ ++ ++@dataclass(frozen=True, slots=True) ++class OriginalAttemptContext: ++ """Local lookup context, never independently authenticated owner evidence. ++ ++ Original fields are reconstructed from current exact admission and checked ++ against the fingerprint atomically stored with the original running intent. ++ Journal fingerprints cannot establish native creation or authorize an undo. ++ """ ++ ++ original: Invocation ++ intent_sequence: int ++ intent_digest: str ++ result_sequence: int ++ evidence_digest: str | None ++ session_revocation_digest: str | None ++ grant_revocation_digest: str | None ++ ++ @property ++ def digest(self) -> str: ++ projection = [ ++ "warden.case.original-context.v1", ++ invocation_fingerprint(self.original), ++ self.intent_sequence, ++ self.intent_digest, ++ self.result_sequence, ++ self.evidence_digest, ++ self.session_revocation_digest, ++ self.grant_revocation_digest, ++ ] ++ return digest(json.dumps(projection, separators=(",", ":")).encode()) ++ ++ + @dataclass(frozen=True, slots=True) + class Adapter: + reference: str +@@ -177,7 +254,7 @@ + invoke: Callable[[Invocation], OwnerResult] + verify: Callable[[Invocation, OwnerResult], bool] + rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None +- reconcile: Callable[[Invocation], OwnerResult] | None = None ++ reconcile: Callable[[Invocation, OriginalAttemptContext], OwnerResult] | None = None + + + class Journal: +@@ -204,6 +281,12 @@ + self.db.execute("PRAGMA synchronous=FULL") + self.db.execute( + "CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)" ++ ) ++ # Additive local storage v1; no existing receipt payload is rewritten or ++ # backfilled. Legacy attempts without this atomic companion fail closed. ++ self.db.execute( ++ "CREATE TABLE IF NOT EXISTS attempt_context_v1 " ++ "(receipt_seq INTEGER PRIMARY KEY, invocation_digest TEXT NOT NULL)" + ) + + @contextmanager +@@ -280,9 +363,13 @@ + evidence: OwnerResult | None = None, + invocation: Invocation | None = None, + ): ++ # Validate/hash only the fixed field contract before opening the write. ++ fingerprint = ( ++ invocation_fingerprint(invocation) if invocation is not None else None ++ ) + self.db.execute("BEGIN IMMEDIATE") + try: +- self.db.execute( ++ inserted = self.db.execute( + "INSERT INTO receipts(binding,payload) VALUES (?,?)", + ( + binding, +@@ -311,10 +398,98 @@ + ), + ), + ) ++ if fingerprint is not None: ++ self.db.execute( ++ "INSERT INTO attempt_context_v1(receipt_seq,invocation_digest) " ++ "VALUES (?,?)", ++ (inserted.lastrowid, fingerprint), ++ ) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK") + raise ++ ++ def original_context( ++ self, binding: str, invocation: Invocation ++ ) -> OriginalAttemptContext: ++ """Locate original run intent across recovery/reconcile rows, never trust it. ++ ++ Called under the existing journal lock after exact current admission and ++ state validation. No owner receipt/reference/payload is hydrated here. ++ """ ++ rows = self.db.execute( ++ "SELECT seq,payload FROM receipts WHERE binding=? " ++ "ORDER BY seq DESC LIMIT ?", ++ (binding, MAX_HISTORY + 1), ++ ).fetchall() ++ if len(rows) > MAX_HISTORY: ++ raise Refused("bounded original context requires owner archival") ++ try: ++ decoded = [(seq, raw, json.loads(raw)) for seq, raw in rows] ++ intent = next( ++ ( ++ row ++ for row in decoded ++ if row[2].get("operation") == "run" ++ and row[2].get("receipts") ++ and row[2]["receipts"][-1].get("outcome") == "running" ++ ), ++ None, ++ ) ++ if intent is None: ++ raise Refused("original run context absent; no legacy backfill") ++ sequence, raw, payload = intent ++ attempt = payload.get("attempt_id") ++ original = replace(invocation, attempt_id=attempt, operation="run") ++ fingerprint = invocation_fingerprint(original) ++ # Match both old journal binding and independently re-admitted fields. ++ if ( ++ payload.get("journal_schema") != "warden.case.step-journal.v1" ++ or payload.get("pins") != asdict(invocation.pins) ++ or payload.get("step_id") != invocation.step ++ or payload.get("owner") != invocation.owner ++ or binding != f"{invocation.step}:{invocation.target}" ++ ): ++ raise Refused("original context differs from current admission") ++ latest = next( ++ row ++ for row in decoded ++ if row[2].get("attempt_id") == attempt ++ and row[2].get("operation") == "run" ++ ) ++ for seq, _, row in (intent, latest): ++ stored = self.db.execute( ++ "SELECT invocation_digest FROM attempt_context_v1 " ++ "WHERE receipt_seq=?", ++ (seq,), ++ ).fetchone() ++ if ( ++ stored is None ++ or stored[0] != fingerprint ++ or row.get("pins") != asdict(invocation.pins) ++ or row.get("step_id") != invocation.step ++ or row.get("owner") != invocation.owner ++ ): ++ raise Refused("original context fingerprint absent or changed") ++ proofs = [ ++ latest[2].get(field) ++ for field in ( ++ "evidence_digest", ++ "session_revocation_digest", ++ "grant_revocation_digest", ++ ) ++ ] ++ if any( ++ value is not None ++ and (type(value) is not str or not DIGEST.fullmatch(value)) ++ for value in proofs ++ ): ++ raise Refused("original evidence fingerprint invalid") ++ return OriginalAttemptContext( ++ original, sequence, digest(raw.encode()), latest[0], *proofs ++ ) ++ except (TypeError, ValueError, KeyError, AttributeError, StopIteration): ++ raise Refused("original context unavailable or malformed") from None + + + class Executor: +@@ -660,6 +835,7 @@ + raise Refused("no uncertain mutation to reconcile") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") ++ original = self.journal.original_context(binding, invocation) + state = replace( + state, + receipts=( +@@ -670,9 +846,22 @@ + dispatched = self._attempt(invocation, state, "reconcile") + self.journal.save(binding, state, invocation=dispatched) + with contained_output(): +- result = self._result( +- adapter, dispatched, adapter.reconcile(dispatched) +- ) ++ result = adapter.reconcile(dispatched, original) ++ # Local context is only a lookup key. Require the independent ++ # owner verifier to bind its exact original/fresh attempt pair ++ # and context to the returned evidence before accepting a result. ++ if ( ++ type(result) is not OwnerResult ++ or self._verify_evidence( ++ f"original-attempt-context:{original.original.attempt_id}:" ++ f"{dispatched.attempt_id}", ++ original.digest, ++ result.evidence, ++ ) ++ is not True ++ ): ++ raise Refused("independent original context evidence absent") ++ result = self._result(adapter, dispatched, result) + if result.outcome not in ( + Outcome.NO_CHANGE, + Outcome.SUCCESS, +--- a/tests/test_case_run.py ++++ b/tests/test_case_run.py +@@ -89,7 +89,7 @@ + grant_revocation_evidence="grant-revoked", + ), + ), +- lambda inv: bound( ++ lambda inv, original: bound( + inv, + OwnerResult( + Outcome.NO_CHANGE, +@@ -113,6 +113,19 @@ + } + + def verifier(purpose, source, evidence): ++ if purpose.startswith("original-attempt-context:"): ++ # Existing fixture trust only; exact lookup context, not a real holder. ++ context = executor.journal.original_context( ++ "mint-secret:fixture-client", admitted[1] ++ ) ++ parts = purpose.split(":") ++ return ( ++ len(parts) == 3 ++ and parts[1] == context.original.attempt_id ++ and parts[2] != context.original.attempt_id ++ and source == context.digest ++ and evidence == "owner-receipt" ++ ) + if purpose.startswith(("session-revocation:", "grant-revocation:")): + scope, lane, operation, attempt = purpose.split(":") + return ( +--- /dev/null ++++ b/tests/test_case_original_context.py +@@ -0,0 +1,291 @@ ++"""Context persistence/refusal only, without a native holder or trusted JSON. ++ ++Existing fixture owner/issuer trust is synthetic. The new fresh-process trial ++proves context survives; missing independent proof must retain uncertainty. ++""" ++ ++from dataclasses import asdict, replace ++import json ++from pathlib import Path ++import sqlite3 ++import subprocess ++import sys ++import pytest ++from test_case_run import bound, rig as rig ++from warden.case_receipt_state import Outcome, Pins, Refused ++from warden.case_run import ( ++ Executor, ++ Invocation, ++ Journal, ++ OwnerResult, ++ invocation_fingerprint, ++) ++ ++ ++def lose(rig, hard=False): ++ executor, args, calls, sources, kwargs = rig ++ old = executor.adapters[args[0].adapter] ++ ++ def lost(inv): ++ calls.append(inv) ++ if hard: ++ raise KeyboardInterrupt ++ raise TimeoutError("owner output must not persist") ++ ++ executor.adapters[old.reference] = replace(old, invoke=lost) ++ if hard: ++ with pytest.raises(KeyboardInterrupt): ++ executor.run(*args) ++ else: ++ assert executor.run(*args).last.outcome is Outcome.UNCERTAIN ++ return executor, args, calls, sources, kwargs, old ++ ++ ++def test_context_committed_with_intent_before_effect_without_new_raw_fields(rig): ++ executor, args, calls, _, _ = rig ++ old = executor.adapters[args[0].adapter] ++ ++ def inspect(inv): ++ with sqlite3.connect(executor.journal.directory / "journal.sqlite") as other: ++ payload, fingerprint = other.execute( ++ "SELECT r.payload,c.invocation_digest FROM receipts r JOIN attempt_context_v1 c ON c.receipt_seq=r.seq ORDER BY r.seq DESC LIMIT 1" ++ ).fetchone() ++ row = json.loads(payload) ++ assert ( ++ row["attempt_id"] == inv.attempt_id ++ and row["receipts"][-1]["outcome"] == "running" ++ ) ++ assert fingerprint == invocation_fingerprint(inv) ++ assert inv.principal not in payload and inv.principal not in fingerprint ++ assert set(row) == { ++ "step_id", ++ "owner", ++ "pins", ++ "allowed_created", ++ "retry_safe", ++ "receipts", ++ "journal_schema", ++ "evidence_digest", ++ "session_revoked", ++ "session_revocation_digest", ++ "grant_revocation_digest", ++ "attempt_id", ++ "operation", ++ } ++ return old.invoke(inv) ++ ++ executor.adapters[old.reference] = replace(old, invoke=inspect) ++ assert executor.run(*args).last.outcome is Outcome.SUCCESS and len(calls) == 1 ++ ++ ++def test_fresh_process_recovers_identity_but_unproved_reconcile_stays_uncertain(rig): ++ executor, args, calls, sources, kwargs, old = lose(rig, hard=True) ++ original = calls[-1] ++ code = """import json,sys ++from pathlib import Path ++from warden.case_run import Journal,Invocation ++from warden.case_receipt_state import Pins ++obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1])) ++with j.locked(): ++ c=j.original_context('mint-secret:fixture-client',Invocation(**obj)) ++ print(json.dumps({'id':c.original.attempt_id,'op':c.original.operation,'evidence':c.evidence_digest})) ++""" ++ env = { ++ "PATH": "/usr/bin:/bin", ++ "PYTHONPATH": str(Path(__file__).parents[1] / "src"), ++ "PYTHONDONTWRITEBYTECODE": "1", ++ } ++ result = subprocess.run( ++ [sys.executable, "-B", "-c", code, str(executor.journal.directory)], ++ input=json.dumps(asdict(args[1])), ++ text=True, ++ capture_output=True, ++ env=env, ++ timeout=15, ++ check=True, ++ ) ++ recovered = json.loads(result.stdout) ++ assert recovered == {"id": original.attempt_id, "op": "run", "evidence": None} ++ seen = [] ++ ++ def reconcile(fresh, context): ++ seen.append((fresh, context)) ++ assert fresh.attempt_id != context.original.attempt_id == original.attempt_id ++ assert fresh.operation == "reconcile" and context.original.operation == "run" ++ return bound( ++ fresh, OwnerResult(Outcome.SUCCESS, "owner-receipt", ("case-policy",), True) ++ ) ++ ++ def reject_original(purpose, source, evidence): ++ return ( ++ False ++ if purpose.startswith("original-attempt-context:") ++ else executor.evidence_verifier(purpose, source, evidence) ++ ) ++ ++ reopened = Executor( ++ Journal(executor.journal.directory), ++ {old.reference: replace(old, reconcile=reconcile)}, ++ reject_original, ++ ) ++ admitted = reopened.admit(*sources, **kwargs) ++ with pytest.raises(Refused): ++ reopened.reconcile(*admitted) ++ assert len(seen) == len(calls) == 1 ++ assert ( ++ reopened.journal.load("mint-secret:fixture-client", admitted[2]).last.outcome ++ is Outcome.UNCERTAIN ++ ) ++ with pytest.raises(Refused): ++ reopened.run(*admitted, retry=True) ++ with pytest.raises(Refused): ++ reopened.rollback(*admitted) ++ ++ ++def test_legacy_context_not_backfilled_and_refused_before_callback(rig): ++ executor, args, calls, _, _, old = lose(rig) ++ executor.journal.db.execute("DROP TABLE attempt_context_v1") ++ executor.journal = Journal(executor.journal.directory) ++ seen = [] ++ executor.adapters[old.reference] = replace(old, reconcile=lambda *v: seen.append(v)) ++ with pytest.raises(Refused): ++ executor.reconcile(*args) ++ assert not seen and len(calls) == 1 ++ assert executor.journal.db.execute( ++ "SELECT COUNT(*) FROM attempt_context_v1" ++ ).fetchone() == (0,) ++ ++ ++@pytest.mark.parametrize( ++ "field,value", ++ [ ++ ("principal", "other"), ++ ("actor", "agent"), ++ ("lane", "other-lane"), ++ ("target", "other-target"), ++ ("owner", "other-owner"), ++ ("pins", Pins(*["sha256:" + "f" * 64] * 6)), ++ ], ++) ++def test_wrong_readmitted_original_context_refused(rig, field, value): ++ executor, args, _, _, _, _ = lose(rig) ++ with executor.journal.locked(), pytest.raises(Refused): ++ executor.journal.original_context( ++ "mint-secret:fixture-client", replace(args[1], **{field: value}) ++ ) ++ ++ ++def test_wrong_original_attempt_or_missing_row_link_refuses_callback(rig): ++ executor, args, _, _, _, old = lose(rig) ++ row = executor.journal.db.execute( ++ "SELECT seq,payload FROM receipts ORDER BY seq LIMIT 1" ++ ).fetchone() ++ payload = json.loads(row[1]) ++ payload["attempt_id"] = "attempt-wrong" ++ executor.journal.db.execute( ++ "UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0]) ++ ) ++ seen = [] ++ executor.adapters[old.reference] = replace(old, reconcile=lambda *v: seen.append(v)) ++ with pytest.raises(Refused): ++ executor.reconcile(*args) ++ assert not seen ++ executor.journal.db.execute( ++ "DELETE FROM attempt_context_v1 WHERE receipt_seq=?", (row[0],) ++ ) ++ with pytest.raises(Refused): ++ executor.reconcile(*args) ++ assert not seen ++ ++ ++def test_context_write_failure_rolls_back_intent_and_prevents_effect(rig): ++ executor, args, calls, _, _ = rig ++ executor.journal.db.execute( ++ "CREATE TRIGGER fail_context BEFORE INSERT ON attempt_context_v1 BEGIN SELECT RAISE(ABORT,'fixture'); END" ++ ) ++ with pytest.raises(sqlite3.IntegrityError): ++ executor.run(*args) ++ assert not calls ++ assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( ++ 0, ++ ) ++ ++ ++@pytest.mark.parametrize( ++ "bad", ++ [ ++ {}, ++ object(), ++ Invocation( ++ "step", ++ "owner", ++ "target", ++ "human", ++ "actor", ++ "lane", ++ Pins(*["sha256:" + "1" * 64] * 6), ++ attempt_id={"owner": "payload"}, ++ operation="run", ++ ), ++ ], ++) ++def test_arbitrary_context_never_serialized_or_persisted(rig, bad): ++ executor, args, calls, _, _ = rig ++ with pytest.raises(Refused): ++ executor.journal.save("binding", args[2], invocation=bad) ++ assert not calls ++ assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( ++ 0, ++ ) ++ ++ ++def test_interrupted_reconcile_preserves_original_run_for_followup(rig): ++ executor, args, calls, _, _, old = lose(rig) ++ seen = [] ++ ++ def interrupted(fresh, original): ++ seen.append((fresh, original)) ++ raise KeyboardInterrupt ++ ++ executor.adapters[old.reference] = replace(old, reconcile=interrupted) ++ for _ in range(2): ++ with pytest.raises(KeyboardInterrupt): ++ executor.reconcile(*args) ++ assert seen[0][0].attempt_id != seen[1][0].attempt_id ++ assert seen[0][1].original == seen[1][1].original == calls[0] ++ assert seen[0][1].digest == seen[1][1].digest and len(calls) == 1 ++ ++ ++def test_prior_context_proof_cannot_authenticate_new_reconcile(rig): ++ executor, args, calls, _, _, _ = lose(rig) ++ old = executor.evidence_verifier ++ purposes = [] ++ ++ def verify(purpose, source, evidence): ++ if purpose.startswith("original-attempt-context:"): ++ purposes.append((purpose, source, evidence)) ++ return purpose.endswith(":attempt-from-another-reconcile") ++ return old(purpose, source, evidence) ++ ++ executor.evidence_verifier = verify ++ with pytest.raises(Refused): ++ executor.reconcile(*args) ++ assert len(purposes) == 1 and purposes[0][0].split(":")[1] == calls[0].attempt_id ++ assert purposes[0][0].split(":")[2] != calls[0].attempt_id ++ assert ( ++ executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome ++ is Outcome.UNCERTAIN ++ ) ++ ++ ++def test_one_argument_legacy_callback_cannot_ignore_original_context(rig): ++ executor, args, _, _, _, old = lose(rig) ++ seen = [] ++ ++ def legacy(inv): ++ seen.append(inv) ++ ++ executor.adapters[old.reference] = replace(old, reconcile=legacy) ++ with pytest.raises(Refused): ++ executor.reconcile(*args) ++ assert not seen +--- a/docs/security-case-execution-kernel-v1.md ++++ b/docs/security-case-execution-kernel-v1.md +@@ -125,3 +125,52 @@ + by `check-evidence`, outside this admission path; Warden nevertheless pins and + copies it with the complete reviewed producer set. Missing or modified bytes in + any of the four modules refuse admission. The test covers every dependency. ++ ++ ++## Original-attempt context candidate — 2026-10-17 ++ ++Source-review candidate only; no native holder, adapter installation or execution ++permission is introduced. The local reconcile callback changes explicitly from ++`reconcile(fresh_invocation)` to ++`reconcile(fresh_invocation, original_attempt_context)`. Existing one-argument ++callbacks fail closed; no signature probing or silent fallback is used. Exact ++adapter source/descriptor admission remains required for a real integration. ++ ++Journal initialization adds `attempt_context_v1(receipt_seq, invocation_digest)`. ++The companion fingerprint is inserted in the same SQLite transaction as each ++invocation-bearing receipt, before dispatch. Existing v1 receipt JSON, IDs and ++source pins are unchanged. The fingerprint hashes only a fixed validated field ++projection: exact Invocation class, bounded step/owner/target/principal/lane/ ++attempt identifiers, closed actor/operation values and the six Pins digests. ++No arbitrary attributes, parameters, OwnerResult payload, evidence identifiers, ++credentials or raw callback output are added to this table. Original fields are ++reconstructed from current exact admission and the original intent's attempt ID; ++they must match that stored fingerprint. Legacy rows are never backfilled and ++missing/mismatched context refuses before an owner callback. ++ ++The original context identifies the original run intent row/digest and the latest ++same-attempt evidence/session/grant fingerprints. Recovery rows lacking invocation ++and later interrupted reconciliation attempts do not replace the original run. ++The fresh reconciliation attempt remains distinct, with its normal current owner ++and session/grant verification. Before any reconciled result can be accepted, ++independent evidence verification must also accept purpose ++`original-attempt-context::`, ++source equal to the exact OriginalAttemptContext digest, and the returned owner ++evidence reference. This is a new source interface, not existing native assent. ++Journal hashes are lookup/correspondence context, never independent authentication. ++ ++No actual independent original-attempt holder/readback or verifier is supplied. ++NetKingdom's registration adapter still has no reconcile callback and cannot undo ++a prior-process creation. Missing original owner proof stays uncertain, including ++when the local intent/fingerprint is intact; no native result is inferred from ++matching JSON or current target bytes. Ordinary exact pins continue to apply: ++a changed caller/session/run-sheet requires separately reviewed migration and ++cannot inherit old journal state. Interrupted rollback remains its existing ++rollback-uncertain owner gate, not a new supported automatic recovery path. ++ ++The focused tests reuse existing synthetic owner/issuer trust and explicitly do ++not provide a native JSON trust holder. They cover atomic context/intent visibility, ++a distinct interpreter with no original Adapter closure, refusal without independent ++original proof, legacy/missing/wrong-attempt/current-context refusal, interrupted ++reconciliation continuity, paired-proof replay refusal and transaction failure ++before effect. Existing reducer/owner/session/revocation checks remain required. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/owner-review.json b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/owner-review.json new file mode 100644 index 00000000..3232361e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/owner-review.json @@ -0,0 +1,97 @@ +{ + "schema": "custodian.warden-original-context-source-review.v1", + "status": "artifact-only candidate for root review; not applied, published, synced or admitted", + "scope": "WARDEN-WP-0044-T03 source context prerequisite for NK-WP-0048-T15; not a native holder or original registration recovery implementation.", + "owner_heads": { + "ops-warden": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "net-kingdom": "503a4f617502baf38f9a09be11308a015072ef92", + "audit-core": "64509d2c4039ca1249f9e8aabf2bae030e3640cf" + }, + "changed_paths": [ + { + "path": "src/warden/case_run.py", + "verified_base_commit": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "before_sha256": "sha256:afb5ce9c59b9748f85c9f1d4af04b255087632c58de8dcd8c9426fc1ed4db068", + "after_sha256": "sha256:3b1e923b34fbdd7e3166c3963451363099f8cab27f677de5dba70313561172be", + "current_canonical_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671", + "current_matches_HEAD": false + }, + { + "path": "tests/test_case_run.py", + "verified_base_commit": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "before_sha256": "sha256:1e70ceb858af0eb3a126132a3c3606706120c0caf58ca9d45deb43f217705c7e", + "after_sha256": "sha256:004b1730a88c28ff3c047070cf8ed2606737b2a328873e3c2d8dcf0c7c573bac", + "current_canonical_sha256": "sha256:bc7264848e76e3576821523fec0c4925c4446eaf470f859fe245c98ea072d139", + "current_matches_HEAD": false + }, + { + "path": "tests/test_case_original_context.py", + "verified_base_commit": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "before_sha256": null, + "after_sha256": "sha256:dd3a365be56254755b72ff9119215239e8b7e3311ce39bcc5c4dd0b9baf213ac", + "current_canonical_sha256": null, + "current_matches_HEAD": true + }, + { + "path": "docs/security-case-execution-kernel-v1.md", + "verified_base_commit": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "before_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb", + "after_sha256": "sha256:1299999413739c92a9d41a591baeeb1e8cd912e425db30468e15f7cccedee8ec", + "current_canonical_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb", + "current_matches_HEAD": true + } + ], + "why_smallest_authorized_slice": "Current Journal saves attempt_id and proof fingerprints but discards full original invocation context; Executor.reconcile passes only a fresh attempt. Preserve exact correspondence using a fingerprint-only companion table rather than persisting arbitrary Invocation or owner payload. Reconstruct original from independently readmitted exact fields and refuse legacy/missing/mismatch. Actual creation must still be independently authenticated.", + "new_local_interfaces": { + "storage": "Additive attempt_context_v1(receipt_seq INTEGER PRIMARY KEY, invocation_digest TEXT NOT NULL); same SQLite transaction as existing receipt insert. No v1 payload or historical-row rewrite/backfill.", + "fixed_projection": "Only exact dataclass Invocation and Pins; explicit bounded identifier fields, closed actor/operation, six exact digest fields. The table stores only SHA256; no raw projected strings.", + "callback": "Adapter.reconcile(fresh Invocation, OriginalAttemptContext) -> OwnerResult; explicit source API change. Legacy one-argument callables fail before their function body.", + "proof": "Independent verifier must authenticate original-attempt-context::, exact context digest, returned owner evidence, then normal Adapter.verify/current-attempt identity/session/grant checks. A permissive test verifier does not attest native creation." + }, + "acceptance_evidence": { + "focused_tests": 68, + "existing_kernel_reducer_cases": 51, + "new_context_cases": 17, + "actual_distinct_interpreter": true, + "actual_sqlite_separate_connection_before_dispatch": true, + "lint": "owner-installed Ruff check and format check pass", + "scope": "Synthetic source adapters/owner evidence only; fresh interpreter proves local context persistence and no-proof refusal, not native original-attempt readback." + }, + "safety_and_compatibility": { + "default_adapter_registry_empty": true, + "new_native_holder": false, + "new_replay_or_undo_authority": false, + "netkingdom_adapter_unchanged": "No reconcile callback; post-restart undo still refused.", + "v1_receipt_payload_fields_unchanged": true, + "old_source_bindings_preserved": true, + "missing_legacy_context": "Fail closed; no backfill.", + "caller_session_migration": "Existing exact run-sheet/pins checks stay; different current session/reference cannot inherit previous state. No new grant/scope migration.", + "rollback_uncertainty": "Existing rollback-uncertain gate unchanged; no automatic original rollback recovery.", + "source_trust": "Fingerprint-only Journal context is not independent authority. Actual owner/context verifier/readback must be admitted for meaningful recovery." + }, + "remaining_exact_owner_work": [ + "Root independently reviews this local storage/API change before canonical application/publication.", + "Warden admits exact adapter/verifier source and purpose contract; no installed native verifier is present.", + "NetKingdom/actual evidence owner supplies authenticated original payload/readback, source/tenant/principal provenance, exact created-only original success and current native revision checks; no matching holder is inferred from Audit custody.", + "Original and fresh parent-session/grant revocation remain separately verified. Current session/pin changes need separately scoped migration design if required.", + "Only after that source integration should exact separately authorized native restart/lost-receipt and created-only undo acceptance be proposed." + ], + "failures_retained": "failed-preparation-r1.json and retained-preparation-failures.json preserve wrong-cwd, unavailable PATH Ruff and fixture-import/plugin preparation failures. None are native executions.", + "canonical_changes": false, + "native_calls": false, + "credentials_read": false, + "new_workplans": false, + "tests_mirroring_generic_backend_added": false, + "base_commit": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "concurrent_edit_conflict": { + "status": "STOP before canonical apply; root must select/reconcile concurrent variant", + "files": [ + "src/warden/case_run.py", + "tests/test_case_run.py" + ], + "concurrent_variant": "Actual canonical source now adds tenant/realm, full OriginalIntent serialization, intents table and reconcile_original. These edits appeared after the artifact-only candidate copied the initially clean source. This agent did not write them.", + "candidate_base": "Exact Git HEAD preimages, not current concurrent dirty source. original-context.patch is now rebased to that clean verified HEAD snapshot.", + "superseded_patch": "superseded-mixed-base.patch retained for error history; DO NOT APPLY. It was assembled using the concurrent dirty preimage and could remove those changes.", + "authority": "No canonical code is applied, published, synced or approved by this packet." + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/patch-check.json b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/patch-check.json new file mode 100644 index 00000000..41cf258e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/patch-check.json @@ -0,0 +1,8 @@ +{ + "base_commit": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "cwd": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages", + "exit_code": 0, + "stdout": "", + "stderr": "", + "scope": "Read-only check against exact HEAD fixture, not permission to overwrite concurrent canonical changes." +} diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/docs/security-case-execution-kernel-v1.md b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/docs/security-case-execution-kernel-v1.md new file mode 100644 index 00000000..35289d77 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/docs/security-case-execution-kernel-v1.md @@ -0,0 +1,127 @@ +# Closed case execution kernel v1 — 2026-10-11 + +Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal +at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +was accepted through memo `infd-20261010-security-case-execution`, version 1, +approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition +disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original +approved proposal remains byte-identical. That decision accepts source design; +it grants no execution session, credential or deployment authority. + +`src/warden/case_run.py` advances the pure reducer into an executable injected +owner harness with private durable receipts. Descriptor version +`warden.case.execution.v1` is a closed local implementation seam, not an accepted +NetKingdom type schema. It contains fixed owner/adapter/source digest, actor, +lane, target parameter, effect, reviewed created-object identifiers declared +retry safety and explicit required revocation scopes (parent-session and broker-grant). +A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused. +An acknowledged type must additionally pin the descriptor bytes under its +`execution_descriptors` map and independent owner evidence must authenticate +that exact machine-seam revision. Existing descriptive types lack this map and +remain plan-only. Their acknowledged labels do not authorize this new seam. + +There are no installed owner adapters, native provenance verifier or dispatch CLI +in this change. Callables are injected only through a fixed adapter registry, +with immutable owner reference/source pins. A real integration must supply +independent owner/native verification or explicit attended manual evidence; +a bare JSON receipt identifier is insufficient. Fixture verifiers in tests +are deliberately fake and attest no native case. Caller sessions are independently +verified for exact principal/actor/lane at admission and again before dispatch; +input actor labels alone are never sufficient. Agent mint is refused, including +when WARDEN_AGENT_ID appears after initial human admission. + +Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference +and run-sheet identity. Actual ops-mason materialization evidence is required +against the case source, separately from its status label. Case parameters supply +an exact metadata target. Source/session changes cannot inherit journal success. +Owner results have a closed metadata shape, result evidence fingerprint and +separate independently authenticated grant/parent-session revocation evidence +fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and +created objects outside the reviewed scope become uncertain, never success. +Python and OS stdout/stderr, including inherited child-process output, are +discarded without buffering or retention under a serialized fd containment +context. Independent provenance verification uses the same containment and +sanitizes exceptions. All source inputs are capped at 256 KiB before parsing. +Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other +owner interruptions are rethrown with a fixed message while retaining running +intent for crash recovery. + +The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks +and insecure modes, uses SQLite FULL synchronization and commits running intent +before calling the adapter. A separate process lock prevents concurrent mutation. +A crash between dispatch and final receipt leaves running; the next lock holder +persists uncertain before refusing replay. Both intent and outcome remain in +history. Each persisted attempt has a unique identity bound to all exact pins, +operation (run/reconcile/rollback), sequence and nonce. Adapter results and both +revocation proof purposes carry that identity. Prior-attempt evidence cannot +verify a safe retry, even if a caller rewrites result labels. Descriptor-created +objects are capped at 32 and histories at 128 transitions, with dispatch stopped +before archival is required. A throw or lost response is not failed-before-change. A safe retry needs +verified failed-before-change, unchanged pins, descriptor retry-safe and explicit +retry. Success and no-change cannot replay. + +Reconciliation is an explicit fixed owner callback, never the mutation callback. +It may verify an already-present native CAS result as no-change/success, or prove +failed-before-change; it never tells Warden to regenerate a credential. Uncertain +or failed-after-change requires this owner-native inspection before any retry. +Owner proof and revocation remain mandatory. Rollback visits only objects in a +verified creation receipt, in reverse order; no-change has no undo and shared +objects are excluded. Unknown/lost undo blocks another rollback. + +The kernel operates individual admitted steps. Full ordered run-sheet orchestration, +verification inventory, contained login integration, native owner adapters and +CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is +implemented. Current Forgejo source is not materialized and has no acknowledged +machine descriptors. Its first native run remains an acceptance requirement, +with real materialization, separately authenticated broker-grant and parent-session +revocation receipts, session/owner receipts and consumer enforcement. This +implementation neither promotes a type nor changes any case status. + +Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open +an independent SQLite connection inside dispatch to prove running was committed, +crash after mutation and reopen a fresh journal to prove no duplicate invocation, +lose a response then reconcile without mint, require explicit retry, check exact +pins/session expiry, withhold malicious output and unconfirmed revocation, and +prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and +checks pass. No live token or target action occurred. + +## Browser input projection — 2026-10-16 source preparation + +An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser +cases before admission. It calls the real NetKingdom type/case validator over +verified producer bytes in a disposable Python process (`-I`, empty environment, +timeout, no inherited output). The validator, composer, capability contract and lazy evidence +modules have exact source pins; missing or changed code refuses. There is no +automatic discovery or download. A producer upgrade needs reviewed pin changes. + +Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest +of each complete parameter definition list prevents reclassifying secrets as +metadata, loosening constraints, or adding fields under a recognized version. +The complete case passes producer validation, including required fields, +constraints and credential-shape checks. Explicit fixed/default overrides, +unknown fields and secret references refuse. Security-sensitive browser metadata +can therefore be present without being passed to an owner adapter: the only +projection is its descriptor's exact public/operational target identifier. +No raw parameter values are added to invocations or persisted receipts. + +`require_implemented=False` validates the acknowledged type's source, not its +execution authority. Existing descriptor/type/source/catalog/caller provenance +gates stay mandatory. Neither historical browser type contains the required +exact machine-descriptor map, and neither is promoted by this change. There are +still no installed native owner adapters, authenticated provenance verifier, +contained session/ordered run orchestration or live CLI dispatch. A journal digest +is a fingerprint and cannot authenticate native evidence. Completion still needs +owner-admitted descriptors and actual structural, mint/delivery, consumer and +revocation/rollback evidence. Local fixture success is not native acceptance. + +Tests use the actual producer checkout (override sibling location with +`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected +test-only owner adapter/evidence verifier. Their descriptor extensions and case +materialized labels exist only in memory. Canonical case/type bytes stay intact. + +Producer integration refinement: the accepted NetKingdom browser-case patch +`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05` +is now the tested producer revision. Its evidence module is imported lazily only +by `check-evidence`, outside this admission path; Warden nevertheless pins and +copies it with the complete reviewed producer set. Missing or modified bytes in +any of the four modules refuse admission. The test covers every dependency. diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/src/warden/case_run.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/src/warden/case_run.py new file mode 100644 index 00000000..f16ab2af --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/src/warden/case_run.py @@ -0,0 +1,727 @@ +"""Closed local execution kernel v1; no installed adapters or live CLI dispatch. + +An owner must acknowledge each descriptor and provide an independent provenance +verifier. Descriptive case types remain plan-only. Nothing executes shell prose. +""" + +from __future__ import annotations + +from contextlib import contextmanager, redirect_stderr, redirect_stdout +from dataclasses import asdict, dataclass, replace +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import sqlite3 +import threading +import uuid +from typing import Callable + +import yaml +from .case_inputs import BrowserInputs +from .case_receipt_state import ( + Outcome, + Pins, + Receipt, + Refused, + StepState, + begin, + finish, + recover, + begin_rollback, + finish_rollback, +) + +IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z") +MAX_SOURCE_BYTES = 262144 +MAX_HISTORY = 128 +OUTPUT_LOCK = threading.RLock() + + +def bounded_source(value: bytes): + if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES: + raise Refused("source exceeds bounded input contract") + + +@contextmanager +def contained_output(): + """Discard Python and inherited OS stdout/stderr; serialize process fd changes.""" + with OUTPUT_LOCK: + saved = [] + null = os.open(os.devnull, os.O_WRONLY) + try: + for target in (1, 2): + saved.append((target, os.dup(target))) + os.dup2(null, target) + with redirect_stdout(_Discard()), redirect_stderr(_Discard()): + try: + yield + except Exception: + raise Refused("owner callback failed") from None + except BaseException: + raise OwnerInterrupted() from None + finally: + for target, original in saved: + os.dup2(original, target) + os.close(original) + os.close(null) + + +DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") + + +def digest(value: bytes) -> str: + return "sha256:" + hashlib.sha256(value).hexdigest() + + +@dataclass(frozen=True, slots=True) +class Descriptor: + version: str + step: str + owner: str + adapter: str + adapter_source: str + actor: str + lane: str + target_parameter: str + effect: str + allowed_created: tuple[str, ...] + retry_safe: bool + revocation_scopes: tuple[str, ...] + + @classmethod + def decode(cls, source: bytes) -> Descriptor: + bounded_source(source) + try: + obj = json.loads(source) + if set(obj) != set(cls.__dataclass_fields__): + raise ValueError + if not isinstance(obj["allowed_created"], list): + raise ValueError + obj["allowed_created"] = tuple(obj["allowed_created"]) + if not isinstance(obj["revocation_scopes"], list): + raise ValueError + obj["revocation_scopes"] = tuple(obj["revocation_scopes"]) + result = cls(**obj) + identifiers = ( + result.step, + result.owner, + result.adapter, + result.lane, + result.target_parameter, + *result.allowed_created, + ) + if result.version != "warden.case.execution.v1" or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers + ): + raise ValueError + if not DIGEST.fullmatch(result.adapter_source) or result.actor not in ( + "human", + "agent", + ): + raise ValueError + if ( + result.effect not in ("read", "change", "mint") + or type(result.retry_safe) is not bool + ): + raise ValueError + scopes = set(result.revocation_scopes) + if ( + len(scopes) != len(result.revocation_scopes) + or not scopes <= {"parent-session", "broker-grant"} + or "parent-session" not in scopes + ): + raise ValueError + if result.effect == "mint" and "broker-grant" not in scopes: + raise ValueError + if len(result.allowed_created) > 32: + raise ValueError + if len(set(result.allowed_created)) != len(result.allowed_created): + raise ValueError + return result + except (ValueError, TypeError, KeyError): + raise Refused("invalid closed execution descriptor") from None + + +@dataclass(frozen=True, slots=True) +class OwnerResult: + outcome: Outcome + evidence: str + created: tuple[str, ...] = () + revoked: bool = False + session_revocation_evidence: str | None = None + grant_revocation_evidence: str | None = None + attempt_id: str = "" + operation: str = "" + + +@dataclass(frozen=True, slots=True) +class Invocation: + step: str + owner: str + target: str + actor: str + principal: str + lane: str + pins: Pins + attempt_id: str = "" + operation: str = "admission" + + +@dataclass(frozen=True, slots=True) +class Adapter: + reference: str + source: str + invoke: Callable[[Invocation], OwnerResult] + verify: Callable[[Invocation, OwnerResult], bool] + rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None + reconcile: Callable[[Invocation], OwnerResult] | None = None + + +class Journal: + """Private SQLite journal plus process lock; transaction commits before dispatch.""" + + def __init__(self, directory: Path): + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + if ( + directory.is_symlink() + or directory.stat().st_uid != os.getuid() + or directory.stat().st_mode & 0o077 + ): + raise Refused("journal directory is not private") + self.directory = directory + for name in ("journal.sqlite", "execution.lock"): + path = directory / name + fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077: + raise Refused("journal file is not private") + finally: + os.close(fd) + self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None) + self.db.execute("PRAGMA synchronous=FULL") + self.db.execute( + "CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + + @contextmanager + def locked(self): + with (self.directory / "execution.lock").open("rb") as fh: + try: + fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused("case execution already active") from None + try: + yield + finally: + fcntl.flock(fh, fcntl.LOCK_UN) + + def load(self, binding: str, initial: StepState) -> StepState: + row = self.db.execute( + "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + return initial + obj = json.loads(row[0]) + expected = { + "step_id", + "owner", + "pins", + "allowed_created", + "retry_safe", + "receipts", + "journal_schema", + "evidence_digest", + "session_revoked", + "session_revocation_digest", + "grant_revocation_digest", + "attempt_id", + "operation", + } + if ( + set(obj) != expected + or obj["journal_schema"] != "warden.case.step-journal.v1" + ): + raise Refused("unknown journal receipt schema") + state = StepState( + obj["step_id"], + obj["owner"], + Pins(**obj["pins"]), + tuple(obj["allowed_created"]), + obj["retry_safe"], + tuple( + Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"])) + for r in obj["receipts"] + ), + ) + if ( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ) != ( + initial.step_id, + initial.owner, + initial.pins, + initial.allowed_created, + initial.retry_safe, + ): + raise Refused("journal pins changed") + return state + + def save( + self, + binding: str, + state: StepState, + evidence: OwnerResult | None = None, + invocation: Invocation | None = None, + ): + self.db.execute("BEGIN IMMEDIATE") + try: + self.db.execute( + "INSERT INTO receipts(binding,payload) VALUES (?,?)", + ( + binding, + json.dumps( + asdict(state) + | { + "journal_schema": "warden.case.step-journal.v1", + "attempt_id": invocation.attempt_id if invocation else None, + "operation": invocation.operation if invocation else None, + "evidence_digest": digest(evidence.evidence.encode()) + if evidence + else None, + "session_revoked": evidence.revoked if evidence else None, + "session_revocation_digest": digest( + evidence.session_revocation_evidence.encode() + ) + if evidence and evidence.session_revocation_evidence + else None, + "grant_revocation_digest": digest( + evidence.grant_revocation_evidence.encode() + ) + if evidence and evidence.grant_revocation_evidence + else None, + }, + separators=(",", ":"), + ), + ), + ) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK") + raise + + +class Executor: + """Injected fixed owner adapters and provenance verifier; none ship by default. + + evidence verifier must authenticate owner custody/provenance or explicitly + attest attended manual evidence; a bare JSON reference is never sufficient. + It receives purpose, exact source digest, and named evidence reference. + """ + + def __init__( + self, + journal: Journal, + adapters: dict[str, Adapter], + evidence_verifier: Callable[[str, str, str], bool], + *, + browser_inputs: BrowserInputs | None = None, + ): + self.journal, self.adapters, self.evidence_verifier = ( + journal, + dict(adapters), + evidence_verifier, + ) + self._admitted = {} + self.browser_inputs = browser_inputs + + def admit( + self, + case_source: bytes, + type_source: bytes, + descriptor_source: bytes, + catalog_source: bytes, + *, + principal: str, + actor: str, + lane: str, + acceptance_evidence: str, + materialization_evidence: str, + descriptor_evidence: str, + session_evidence: str, + ) -> tuple[Descriptor, Invocation, StepState]: + for source in (case_source, type_source, descriptor_source, catalog_source): + bounded_source(source) + descriptor = Descriptor.decode(descriptor_source) + try: + case = yaml.safe_load(case_source)["spec"] + case_type = yaml.safe_load(type_source)["spec"] + if not isinstance(case, dict) or not isinstance(case_type, dict): + raise ValueError + except (yaml.YAMLError, KeyError, TypeError, ValueError): + raise Refused("invalid case or type source shape") from None + if case.get("status") != "materialized" or not case.get("acceptance"): + raise Refused("case is not accepted and structurally materialized") + if case_type.get("status") not in ("acknowledged", "implemented"): + raise Refused("type is not acknowledged") + if case.get("type") != f"{case_type['id']}@{case_type['version']}": + raise Refused("case type pin mismatch") + if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest( + descriptor_source + ): + raise Refused( + "type has no exact acknowledged machine descriptor; plan only" + ) + for purpose, source, evidence in ( + ("case-acceptance", digest(case_source), acceptance_evidence), + ( + "ops-mason-materialization", + digest(case_source), + materialization_evidence, + ), + ( + "owner-descriptor-acceptance:" + descriptor.owner, + digest(descriptor_source), + descriptor_evidence, + ), + ("type-machine-seam-acceptance", digest(type_source), descriptor_evidence), + ("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence), + ( + f"caller-session:{actor}:{principal}:{lane}", + digest(case_source), + session_evidence, + ), + ): + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, source, evidence) is not True + ): + raise Refused("independent owner evidence not verified") + if ( + not IDENTIFIER.fullmatch(principal) + or actor != descriptor.actor + or lane != descriptor.lane + ): + raise Refused("principal actor or lane mismatch") + if descriptor.effect == "mint" and ( + actor != "human" or os.getenv("WARDEN_AGENT_ID") + ): + raise Refused("agent mint is forbidden") + parameters = case.get("parameters", {}) + declared = { + item["name"]: item + for item in case_type.get("parameters", []) + if isinstance(item, dict) and isinstance(item.get("name"), str) + } + if ( + not isinstance(parameters, dict) + or descriptor.target_parameter not in declared + ): + raise Refused("target parameter is not declared by acknowledged type") + if case_type.get("id") == "oidc-browser-relying-party": + if type(self.browser_inputs) is not BrowserInputs: + raise Refused("browser metadata validator is not configured") + target = self.browser_inputs.target( + yaml.safe_load(case_source), + yaml.safe_load(type_source), + descriptor.target_parameter, + ) + else: + for name in parameters: + definition = declared.get(name) + if ( + definition is None + or definition.get("sensitivity") not in ("public", "operational") + or definition.get("case_settable") is False + ): + raise Refused("unknown, fixed or secret-sensitivity case parameter") + target = parameters.get(descriptor.target_parameter) + if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): + raise Refused("target must be exact case metadata") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("unknown adapter or owner source pin") + sheet = json.dumps( + [ + descriptor.step, + descriptor.owner, + target, + actor, + principal, + lane, + acceptance_evidence, + materialization_evidence, + descriptor_evidence, + session_evidence, + ], + separators=(",", ":"), + ).encode() + pins = Pins( + digest(case_source), + digest(type_source), + digest(descriptor_source), + digest(catalog_source), + digest(acceptance_evidence.encode()), + digest(sheet), + ) + invocation = Invocation( + descriptor.step, descriptor.owner, target, actor, principal, lane, pins + ) + initial = StepState( + descriptor.step, + descriptor.owner, + pins, + descriptor.allowed_created, + descriptor.retry_safe, + ) + self._admitted[invocation] = (descriptor, initial, session_evidence) + return descriptor, invocation, initial + + def _result( + self, adapter: Adapter, invocation: Invocation, result: OwnerResult + ) -> OwnerResult: + if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome): + raise Refused("unknown owner result shape") + if type(result.created) is not tuple or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created + ): + raise Refused("owner created-object result invalid") + if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch( + result.evidence + ): + raise Refused("owner evidence identifier invalid") + if adapter.verify(invocation, result) is not True or result.revoked is not True: + raise Refused("owner result or session revocation unverified") + if (result.attempt_id, result.operation) != ( + invocation.attempt_id, + invocation.operation, + ): + raise Refused("owner receipt belongs to another attempt") + descriptor = self._admitted[ + replace(invocation, attempt_id="", operation="admission") + ][0] + proof_fields = { + "parent-session": ( + f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.session_revocation_evidence, + ), + "broker-grant": ( + f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.grant_revocation_evidence, + ), + } + proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes] + for purpose, evidence in proofs: + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, invocation.pins.case, evidence) + is not True + ): + raise Refused("independent grant or parent-session revocation absent") + return result + + def _verify_evidence(self, purpose, source, evidence): + try: + with contained_output(): + return self.evidence_verifier(purpose, source, evidence) is True + except Exception: + raise Refused("independent evidence verification failed") from None + + def _admission( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ): + if self._admitted.get(invocation, ())[:2] != (descriptor, initial): + raise Refused("execution requires verified admission on this executor") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("adapter changed after admission") + session = self._admitted[invocation][2] + if ( + self._verify_evidence( + f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}", + invocation.pins.case, + session, + ) + is not True + ): + raise Refused("caller session no longer verified") + if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"): + raise Refused("agent mint is forbidden") + return adapter + + def _attempt(self, invocation: Invocation, state: StepState, operation: str): + identity = json.dumps( + [asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex], + separators=(",", ":"), + ).encode() + return replace( + invocation, + attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(), + operation=operation, + ) + + def run( + self, + descriptor: Descriptor, + invocation: Invocation, + initial: StepState, + *, + retry=False, + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + recovered = recover(state) + if recovered != state: + self.journal.save(binding, recovered) + state = recovered + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin(state, invocation.pins, retry=retry) + dispatched = self._attempt(invocation, running, "run") + self.journal.save(binding, running, invocation=dispatched) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, dispatched, adapter.invoke(dispatched) + ) + completed = finish( + running, invocation.pins, result.outcome, created=result.created + ) + evidence = result + except Exception: + completed = finish(running, invocation.pins, Outcome.UNCERTAIN) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def rollback( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + if recover(state) != state: + self.journal.save(binding, recover(state)) + raise Refused("lost rollback result requires owner reconciliation") + if adapter.rollback is None: + raise Refused("owner has no reviewed undo adapter") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin_rollback(state, invocation.pins) + dispatched = self._attempt(invocation, running, "rollback") + self.journal.save(binding, running, invocation=dispatched) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.rollback(dispatched, running.last.created), + ) + verified = result.outcome is Outcome.ROLLED_BACK and not result.created + evidence = result if verified else None + except Exception: + verified = False + completed = finish_rollback(running, invocation.pins, verified=verified) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def reconcile( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + """Explicit owner-native state inspection; never redispatches the mutation.""" + adapter = self._admission(descriptor, invocation, initial) + if adapter.reconcile is None: + raise Refused("owner reconciliation adapter absent") + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = recover(self.journal.load(binding, initial)) + if not state.last or state.last.outcome not in ( + Outcome.UNCERTAIN, + Outcome.FAILED_AFTER, + ): + raise Refused("no uncertain mutation to reconcile") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + state = replace( + state, + receipts=( + *state.receipts, + Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN), + ), + ) + dispatched = self._attempt(invocation, state, "reconcile") + self.journal.save(binding, state, invocation=dispatched) + with contained_output(): + result = self._result( + adapter, dispatched, adapter.reconcile(dispatched) + ) + if result.outcome not in ( + Outcome.NO_CHANGE, + Outcome.SUCCESS, + Outcome.FAILED_BEFORE, + ): + raise Refused("owner did not reconcile target state") + # Validate created-object scope using the same closed result reducer. + finish( + begin( + StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ), + state.pins, + ), + state.pins, + result.outcome, + created=result.created, + ) + reconciled = StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ( + *state.receipts, + Receipt(len(state.receipts) + 1, result.outcome, result.created), + ), + ) + self.journal.save(binding, reconciled, result, dispatched) + return reconciled + + +class OwnerInterrupted(KeyboardInterrupt): + """Sanitized interruption preserves persisted running/crash recovery semantics.""" + + def __init__(self): + super().__init__("owner execution interrupted") + + +class _Discard: + """Never retain or forward adapter output, including unbounded malicious text.""" + + def write(self, text): + return len(text) + + def flush(self): + pass diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/tests/test_case_run.py b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/tests/test_case_run.py new file mode 100644 index 00000000..8125e2c7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/preimages/tests/test_case_run.py @@ -0,0 +1,631 @@ +"""Injected owner harness; these receipts are not native owner attestations.""" + +from dataclasses import asdict, replace +import json +import os +import subprocess +import sys +import sqlite3 +import pytest +import yaml +from warden.case_receipt_state import Outcome, Refused +from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest + + +@pytest.fixture +def rig(tmp_path): + desc = Descriptor( + "warden.case.execution.v1", + "mint-secret", + "owner", + "owner/mint@v1", + "sha256:" + "1" * 64, + "human", + "issuer-lane", + "client_id", + "mint", + ("case-policy", "case-role"), + True, + ("parent-session", "broker-grant"), + ) + descriptor = json.dumps(asdict(desc)).encode() + typ = yaml.safe_dump( + { + "spec": { + "id": "test", + "version": "1", + "status": "acknowledged", + "parameters": [ + { + "name": "client_id", + "sensitivity": "operational", + "case_settable": True, + } + ], + "execution_descriptors": {"mint-secret": digest(descriptor)}, + } + } + ).encode() + case = yaml.safe_dump( + { + "spec": { + "id": "case-test", + "type": "test@1", + "status": "materialized", + "acceptance": "acceptance", + "parameters": {"client_id": "fixture-client"}, + } + } + ).encode() + calls = [] + + def invoke(inv): + calls.append(inv) + return bound( + inv, + OwnerResult( + Outcome.SUCCESS, + "owner-receipt", + ("case-policy", "case-role"), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ) + + adapter = Adapter( + "owner/mint@v1", + desc.adapter_source, + invoke, + lambda inv, result: result.evidence == "owner-receipt", + lambda inv, objects: bound( + inv, + OwnerResult( + Outcome.ROLLED_BACK, + "owner-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ), + lambda inv: bound( + inv, + OwnerResult( + Outcome.NO_CHANGE, + "owner-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ), + ) + verified = { + ("case-acceptance", digest(case), "acceptance"), + ("ops-mason-materialization", digest(case), "materialization"), + ("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"), + ("type-machine-seam-acceptance", digest(typ), "descriptor"), + ("catalog-routing-acceptance", digest(b"catalog"), "descriptor"), + ("caller-session:human:operator:issuer-lane", digest(case), "session"), + ("session-revocation:issuer-lane", digest(case), "session-revoked"), + ("grant-revocation:issuer-lane", digest(case), "grant-revoked"), + } + + def verifier(purpose, source, evidence): + if purpose.startswith(("session-revocation:", "grant-revocation:")): + scope, lane, operation, attempt = purpose.split(":") + return ( + source == digest(case) + and lane == "issuer-lane" + and operation in ("run", "rollback", "reconcile") + and evidence + == ("session-" if scope == "session-revocation" else "grant-") + attempt + ) + return (purpose, source, evidence) in verified + + executor = Executor( + Journal(tmp_path / "private"), {"owner/mint@v1": adapter}, verifier + ) + kwargs = dict( + principal="operator", + actor="human", + lane="issuer-lane", + acceptance_evidence="acceptance", + materialization_evidence="materialization", + descriptor_evidence="descriptor", + session_evidence="session", + ) + admitted = executor.admit(case, typ, descriptor, b"catalog", **kwargs) + return (executor, admitted, calls, (case, typ, descriptor, b"catalog"), kwargs) + + +def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig): + executor, args, calls, _, _ = rig + old = executor.adapters[args[0].adapter] + + def inspect(inv): + other = sqlite3.connect(executor.journal.directory / "journal.sqlite") + payload = json.loads( + other.execute( + "SELECT payload FROM receipts ORDER BY seq DESC LIMIT 1" + ).fetchone()[0] + ) + assert payload["receipts"][-1]["outcome"] == "running" + return old.invoke(inv) + + executor.adapters[old.reference] = replace(old, invoke=inspect) + assert executor.run(*args).last.outcome is Outcome.SUCCESS + with pytest.raises(Refused): + executor.run(*args) + assert len(calls) == 1 + + +def test_crash_and_fresh_process_resume_never_duplicates_effect(rig): + executor, args, calls, sources, kwargs = rig + old = executor.adapters[args[0].adapter] + + def crash(inv): + calls.append(inv) + raise KeyboardInterrupt + + executor.adapters[old.reference] = replace(old, invoke=crash) + with pytest.raises(KeyboardInterrupt): + executor.run(*args) + fresh = Executor( + Journal(executor.journal.directory), + {old.reference: old}, + executor.evidence_verifier, + ) + admitted = fresh.admit(*sources, **kwargs) + with pytest.raises(Refused): + fresh.run(*admitted, retry=True) + state = fresh.journal.load("mint-secret:fixture-client", admitted[2]) + assert state.last.outcome is Outcome.UNCERTAIN + assert len(calls) == 1 + + +def test_lost_result_preserved_and_cas_reconciliation_does_not_mint(rig): + executor, args, calls, _, _ = rig + old = executor.adapters[args[0].adapter] + + def lost(inv): + calls.append(inv) + raise TimeoutError("SECRET-HIDDEN") + + executor.adapters[old.reference] = replace(old, invoke=lost) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE + with pytest.raises(Refused): + executor.run(*args, retry=True) + assert len(calls) == 1 + assert ( + b"SECRET-HIDDEN" + not in (executor.journal.directory / "journal.sqlite").read_bytes() + ) + + +def test_failed_before_change_requires_explicit_retry_and_safe_owner(rig): + executor, args, calls, _, _ = rig + old = executor.adapters[args[0].adapter] + + def no_change(inv): + calls.append(inv) + return bound( + inv, + OwnerResult( + Outcome.FAILED_BEFORE, + "owner-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ) + + executor.adapters[old.reference] = replace(old, invoke=no_change) + assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE + with pytest.raises(Refused): + executor.run(*args) + assert executor.run(*args, retry=True).last.outcome is Outcome.FAILED_BEFORE + assert len(calls) == 2 + + +@pytest.mark.parametrize( + "result", + [ + OwnerResult( + Outcome.SUCCESS, + "owner-receipt", + ("shared-policy",), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + OwnerResult( + Outcome.SUCCESS, + "owner-receipt", + (), + False, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + OwnerResult( + Outcome.SUCCESS, + "unknown-receipt", + (), + True, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + {"outcome": "verified-success", "stdout": "SECRET-HIDDEN"}, + ], +) +def test_unknown_secret_payload_created_scope_or_missing_revocation_never_succeeds( + rig, result, capsys +): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + + def malicious(inv): + print("SECRET-HIDDEN" * 1000) + return result + + executor.adapters[old.reference] = replace(old, invoke=malicious) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + assert capsys.readouterr().out == "" + assert ( + b"SECRET-HIDDEN" + not in (executor.journal.directory / "journal.sqlite").read_bytes() + ) + + +def test_rollback_reverses_only_verified_case_created_objects(rig): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + undos = [] + + def undo(inv, objects): + undos.append(objects) + return old.rollback(inv, objects) + + executor.adapters[old.reference] = replace(old, rollback=undo) + executor.run(*args) + assert executor.rollback(*args).last.outcome is Outcome.ROLLED_BACK + assert undos == [("case-role", "case-policy")] + assert "shared-policy" not in undos[0] + + +def test_uncertain_rollback_cannot_replay(rig): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + executor.run(*args) + executor.adapters[old.reference] = replace( + old, + rollback=lambda inv, objs: bound( + inv, + OwnerResult( + Outcome.ROLLED_BACK, + "owner-receipt", + (), + False, + session_revocation_evidence="session-revoked", + grant_revocation_evidence="grant-revoked", + ), + ), + ) + assert executor.rollback(*args).last.outcome is Outcome.ROLLBACK_UNCERTAIN + with pytest.raises(Refused): + executor.rollback(*args) + + +@pytest.mark.parametrize( + "change", ["materialization", "type-prose", "actor", "lane", "source", "target"] +) +def test_exact_admission_requires_owner_evidence_and_closed_source_pins(rig, change): + executor, _, _, sources, kwargs = rig + case, typ, descriptor, catalog = sources + kwargs = dict(kwargs) + if change == "materialization": + kwargs["materialization_evidence"] = "bare-json" + if change == "type-prose": + obj = yaml.safe_load(typ) + obj["spec"].pop("execution_descriptors") + typ = yaml.safe_dump(obj).encode() + if change == "actor": + kwargs["actor"] = "agent" + if change == "lane": + kwargs["lane"] = "other-lane" + if change == "source": + obj = json.loads(descriptor) + obj["adapter_source"] = "sha256:" + "2" * 64 + descriptor = json.dumps(obj).encode() + if change == "target": + obj = yaml.safe_load(case) + obj["spec"]["parameters"]["client_id"] = "other-target" + case = yaml.safe_dump(obj).encode() + with pytest.raises(Refused): + executor.admit(case, typ, descriptor, catalog, **kwargs) + + +def test_agent_env_cannot_dispatch_after_human_admission(rig, monkeypatch): + executor, args, calls, _, _ = rig + monkeypatch.setenv("WARDEN_AGENT_ID", "test-agent") + with pytest.raises(Refused): + executor.run(*args) + assert calls == [] + + +def test_forged_invocation_cannot_bypass_admission(rig): + executor, args, calls, _, _ = rig + with pytest.raises(Refused): + executor.run(args[0], replace(args[1], target="other-target"), args[2]) + assert calls == [] + + +def test_journal_rejects_unsafe_directory_and_symlink(tmp_path): + unsafe = tmp_path / "unsafe" + unsafe.mkdir(mode=493) + with pytest.raises(Refused): + Journal(unsafe) + private = tmp_path / "private" + private.mkdir(mode=448) + (private / "journal.sqlite").symlink_to(tmp_path / "outside") + with pytest.raises(OSError): + Journal(private) + + +def test_process_lock_blocks_parallel_dispatch(rig): + executor, args, calls, _, _ = rig + with executor.journal.locked(): + with pytest.raises(Refused): + executor.run(*args) + assert calls == [] + + +def test_changed_catalog_pins_cannot_inherit_old_success(rig): + executor, args, calls, sources, kwargs = rig + executor.run(*args) + with pytest.raises(Refused): + executor.admit(*sources[:3], b"changed-catalog", **kwargs) + assert len(calls) == 1 + + +def test_private_journal_does_not_retain_owner_payload_or_accept_unknown_schema(rig): + executor, args, _, _, _ = rig + executor.run(*args) + row = executor.journal.db.execute( + "SELECT seq,payload FROM receipts ORDER BY seq DESC LIMIT 1" + ).fetchone() + payload = json.loads(row[1]) + assert payload["journal_schema"] == "warden.case.step-journal.v1" + assert payload["evidence_digest"] == digest(b"owner-receipt") + assert payload["session_revoked"] is True + assert "owner-receipt" not in row[1] + payload["stdout"] = "UNTRUSTED-OUTPUT" + executor.journal.db.execute( + "UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0]) + ) + with pytest.raises(Refused): + executor.run(*args) + + +def test_expired_caller_session_cannot_use_cached_admission(rig): + executor, args, calls, _, _ = rig + old = executor.evidence_verifier + executor.evidence_verifier = lambda purpose, source, evidence: ( + False + if purpose.startswith("caller-session:") + else old(purpose, source, evidence) + ) + with pytest.raises(Refused): + executor.run(*args) + assert calls == [] + + +@pytest.mark.parametrize("index", [0, 1, 2, 3]) +def test_source_sizes_are_bounded_before_parsing(rig, index): + executor, _, _, sources, kwargs = rig + oversized = list(sources) + oversized[index] = b"x" * 262145 + with pytest.raises(Refused, match="bounded"): + executor.admit(*oversized, **kwargs) + + +def test_owner_fd_and_inherited_subprocess_output_is_contained(rig, capfd): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + + def leaking(inv): + os.write(1, b"SYNTHETIC-SECRET") + os.write(2, b"SYNTHETIC-SECRET") + subprocess.run([sys.executable, "-c", 'print("SYNTHETIC-SECRET")'], check=True) + return old.invoke(inv) + + executor.adapters[old.reference] = replace(old, invoke=leaking) + assert executor.run(*args).last.outcome is Outcome.SUCCESS + observed = capfd.readouterr() + assert "SYNTHETIC-SECRET" not in observed.out + observed.err + + +def test_evidence_provider_output_and_error_never_escape(rig, capfd): + executor, _, calls, sources, kwargs = rig + + def broken(*args): + print("SYNTHETIC-SECRET") + os.write(2, b"SYNTHETIC-SECRET") + raise RuntimeError("SYNTHETIC-SECRET") + + executor.evidence_verifier = broken + with pytest.raises(Refused) as caught: + executor.admit(*sources, **kwargs) + assert "SYNTHETIC-SECRET" not in str(caught.value) + observed = capfd.readouterr() + assert "SYNTHETIC-SECRET" not in observed.out + observed.err + assert calls == [] + + +@pytest.mark.parametrize("missing", ["session", "grant"]) +def test_broker_grant_and_parent_session_have_separate_required_evidence(rig, missing): + executor, args, _, _, _ = rig + old = executor.adapters[args[0].adapter] + result = old.invoke(args[1]) + if missing == "session": + result = replace(result, session_revocation_evidence=None) + else: + result = replace(result, grant_revocation_evidence=None) + executor.adapters[old.reference] = replace(old, invoke=lambda inv: result) + assert executor.run(*args).last.outcome is Outcome.UNCERTAIN + + +def test_mint_descriptor_cannot_omit_broker_grant_scope(rig): + descriptor = json.loads(rig[3][2]) + descriptor["revocation_scopes"] = ["parent-session"] + with pytest.raises(Refused): + Descriptor.decode(json.dumps(descriptor).encode()) + + +def test_unsafe_owner_step_cannot_retry_even_after_verified_failed_before( + rig, tmp_path +): + executor, args, _, sources, kwargs = rig + descriptor = replace(args[0], retry_safe=False) + descriptor_source = json.dumps(asdict(descriptor)).encode() + type_doc = yaml.safe_load(sources[1]) + type_doc["spec"]["execution_descriptors"][descriptor.step] = digest( + descriptor_source + ) + type_source = yaml.safe_dump(type_doc).encode() + old_verifier = executor.evidence_verifier + + def verifier(purpose, source, evidence): + if purpose == "owner-descriptor-acceptance:owner": + return source == digest(descriptor_source) and evidence == "descriptor" + if purpose == "type-machine-seam-acceptance": + return source == digest(type_source) and evidence == "descriptor" + return old_verifier(purpose, source, evidence) + + adapter = executor.adapters[descriptor.adapter] + + def failed(inv): + return bound( + inv, + OwnerResult( + Outcome.FAILED_BEFORE, + "owner-receipt", + (), + True, + "session-revoked", + "grant-revoked", + ), + ) + + fresh = Executor( + Journal(tmp_path / "unsafe-retry"), + {adapter.reference: replace(adapter, invoke=failed)}, + verifier, + ) + admitted = fresh.admit( + sources[0], type_source, descriptor_source, sources[3], **kwargs + ) + assert fresh.run(*admitted).last.outcome is Outcome.FAILED_BEFORE + with pytest.raises(Refused): + fresh.run(*admitted, retry=True) + + +def bound(inv, result): + return replace( + result, + attempt_id=inv.attempt_id, + operation=inv.operation, + session_revocation_evidence=("session-" + inv.attempt_id) + if result.session_revocation_evidence + else None, + grant_revocation_evidence=("grant-" + inv.attempt_id) + if result.grant_revocation_evidence + else None, + ) + + +def test_old_grant_and_session_proofs_cannot_verify_a_new_retry(rig): + executor, args, _, _, _ = rig + adapter = executor.adapters[args[0].adapter] + retained = [] + + def attempts(inv): + if not retained: + result = bound( + inv, + OwnerResult( + Outcome.FAILED_BEFORE, + "owner-receipt", + (), + True, + "session-revoked", + "grant-revoked", + ), + ) + retained.append(result) + return result + # Even a caller rewriting labels cannot authenticate prior revocation + # evidence for the newly persisted attempt identity. + return replace(retained[0], attempt_id=inv.attempt_id, operation=inv.operation) + + executor.adapters[adapter.reference] = replace(adapter, invoke=attempts) + assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE + assert executor.run(*args, retry=True).last.outcome is Outcome.UNCERTAIN + + +def test_owner_interrupt_payload_is_sanitized_with_running_intent_retained(rig, capfd): + executor, args, _, _, _ = rig + adapter = executor.adapters[args[0].adapter] + + def interrupted(inv): + os.write(2, b"SYNTHETIC-SECRET") + raise KeyboardInterrupt("SYNTHETIC-SECRET") + + executor.adapters[adapter.reference] = replace(adapter, invoke=interrupted) + with pytest.raises(KeyboardInterrupt) as caught: + executor.run(*args) + assert "SYNTHETIC-SECRET" not in str(caught.value) + assert "SYNTHETIC-SECRET" not in "".join(capfd.readouterr()) + assert ( + executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome + is Outcome.RUNNING + ) + + +def test_attempt_identity_changes_with_exact_catalog_revision_and_new_journal( + rig, tmp_path +): + executor, args, calls, sources, kwargs = rig + executor.run(*args) + original_attempt = calls[-1].attempt_id + new_catalog = b"independently-reviewed-catalog-v2" + old_verifier = executor.evidence_verifier + + def verifier(purpose, source, evidence): + if purpose == "catalog-routing-acceptance": + return source == digest(new_catalog) and evidence == "descriptor" + return old_verifier(purpose, source, evidence) + + fresh = Executor(Journal(tmp_path / "new-revision"), executor.adapters, verifier) + revised = fresh.admit(*sources[:3], new_catalog, **kwargs) + assert fresh.run(*revised).last.outcome is Outcome.SUCCESS + assert calls[-1].attempt_id != original_attempt + assert calls[-1].pins.catalog != calls[0].pins.catalog + + +def test_secret_sensitivity_parameter_cannot_reach_admission_or_journal(rig): + executor, _, _, sources, kwargs = rig + type_doc = yaml.safe_load(sources[1]) + type_doc["spec"]["parameters"][0]["sensitivity"] = "secret" + source = yaml.safe_dump(type_doc).encode() + old = executor.evidence_verifier + executor.evidence_verifier = lambda purpose, digest_value, evidence: ( + True + if purpose == "type-machine-seam-acceptance" + else old(purpose, digest_value, evidence) + ) + with pytest.raises(Refused, match="secret-sensitivity"): + executor.admit(sources[0], source, sources[2], sources[3], **kwargs) diff --git a/docs/changes/netkingdom-target-2026-10-17/original-context-r1/retained-preparation-failures.json b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/retained-preparation-failures.json new file mode 100644 index 00000000..c4a1f806 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/original-context-r1/retained-preparation-failures.json @@ -0,0 +1,22 @@ +[ + { + "phase": "initial command path", + "outcome": "failed before source edit/tests", + "detail": "Relative packet paths from candidate cwd; corrected working directory. Original failed-preparation-r1.json retained." + }, + { + "phase": "lint executable lookup", + "outcome": "ruff command unavailable in PATH", + "detail": "Used existing owner .venv/bin/ruff without installing anything." + }, + { + "phase": "first owner lint", + "outcome": "12F401/F811 test-fixture import issues", + "detail": "Source code lint passed; changed new test fixture reuse to pytest plugin registration; no control/source behavior changed." + }, + { + "phase": "pytest plugin reuse attempt", + "outcome": "51 existing tests passed,17 new setup errors;2 lint E402", + "detail": "test module plugin registration did not expose fixture. Restored explicit fixture re-export; source control unchanged." + } +]