docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
1fc8eed19c
commit
e63355aa4c
50 changed files with 9232 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
...................................................... [100%]
|
||||
54 passed in 1.92s
|
||||
|
|
@ -0,0 +1 @@
|
|||
2 files already formatted
|
||||
|
|
@ -0,0 +1 @@
|
|||
All checks passed!
|
||||
|
|
@ -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 <module>\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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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 <type.yaml>` (`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 <case.yaml>` 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 <case.yaml> [--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 `<param> == <value>` 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_ID>_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: <case_id>@sha256:<hex>` 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 <case.yaml>` 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 <id> --case-ref <case-ref> --case-file <case.yaml>` 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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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 <type.yaml>` (`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 <case.yaml>` 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 <case.yaml> [--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 `<param> == <value>` 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_ID>_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: <case_id>@sha256:<hex>` 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 <case.yaml>` 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 <id> --case-ref <case-ref> --case-file <case.yaml>` 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.
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# 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.
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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:<original-attempt-id>:<fresh-reconcile-attempt-id>`,
|
||||
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.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[pytest]
|
||||
pythonpath = src
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""OpsWarden — SSH CA and certificate lifecycle manager."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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."}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
.................................................................... [100%]
|
||||
68 passed in 2.06s
|
||||
|
|
@ -0,0 +1 @@
|
|||
3 files already formatted
|
||||
|
|
@ -0,0 +1 @@
|
|||
All checks passed!
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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:<original-attempt-id>:<fresh-reconcile-attempt-id>`,
|
||||
+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.
|
||||
|
|
@ -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:<original-id>:<fresh-id>, 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."
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue