ops-warden/tests/test_case_run.py
tegwick a14376cf62 feat(case): KERNEL-2/3 lane resolution and mode-aware admission; CATALOG-1 draft lanes
nk48 1.3 owner review (owner-review-ops-warden.json):
- KERNEL-2: Executor.admit parses catalog_source and resolves descriptor.lane;
  refuses a missing, non-active, undelegated or ungraded lane, an owner outside
  owner_repo, and a mint/change effect on a lane not graded risk high.
  catalog-routing-acceptance still covers the whole catalog digest.
- KERNEL-3: dispatch mode is read from the type's follow_through step
  (warden_executes/attended). The step must declare both booleans and a lane
  and agree with the descriptor's lane and owner. Attended: human actor,
  WARDEN_AGENT_ID unset, parent-session revocation scope, caller lane equal to
  the step lane. Owner: native delegation, owner_repo == descriptor.owner, no
  Warden-executed lane and no Warden-brokered mint.
- CATALOG-1: draft entries keycape-attended-case-change (OW-1.3-C1),
  platform-secret-delivery, consumer-owner-deployment, apps-consumer-handover.
  Not promoted; promotion waits for owner confirmation.
- Fixture catalogs and type steps in tests; published routing_lanes coverage
  and the workload-join count follow the four new not-applicable lanes.

PARAMETER_PINS untouched (PIN-1 waits for founder acknowledgment).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 903097@bnt-lap001
Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
2026-10-17 12:08:49 +02:00

745 lines
25 KiB
Python

"""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
def lane_entry(lane="issuer-lane", owner="owner", **over):
"""Fixture catalog entry (TEST ONLY; never a real routing pointer)."""
entry = {
"id": lane,
"title": "fixture lane",
"need_keywords": [lane],
"owner_repo": owner,
"subsystem": "fixture",
"warden_executes": False,
"wiki_ref": "wiki/fixture.md",
"canon_ref": "fixture/canon.md",
"reviewed": "2026-10-17",
"status": "active",
"risk": "high",
"workload_ref": {"applicability": "not-applicable", "reason": "fixture"},
"delegation": {
"mode": "native",
"intended_owner": owner,
"reviewed": "2026-10-17",
},
}
entry.update(over)
return entry
def fixture_catalog(*entries):
return yaml.safe_dump({"entries": list(entries) or [lane_entry()]}).encode()
CATALOG = fixture_catalog()
MINT_STEP = {
"id": "mint-secret",
"owner": "owner",
"lane": "issuer-lane",
"warden_executes": True,
"attended": False,
}
def make_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)},
"follow_through": [dict(MINT_STEP)],
}
}
).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(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, CATALOG, **kwargs)
return (executor, admitted, calls, (case, typ, descriptor, CATALOG), kwargs)
@pytest.fixture
def rig(tmp_path):
return make_rig(tmp_path)
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], fixture_catalog(lane_entry(title="changed")), **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 = fixture_catalog(lane_entry(title="independently reviewed 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)
def _lose_result(executor, args, calls):
old = executor.adapters[args[0].adapter]
def lost(inv):
calls.append(inv)
raise TimeoutError("lost")
seen = []
def by_original(ctx):
seen.append(ctx)
return old.reconcile(ctx.attempt)
executor.adapters[old.reference] = replace(
old, invoke=lost, reconcile=None, reconcile_original=by_original
)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
return seen
def test_original_intent_survives_restart_separate_from_reconcile_attempt(rig):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
executor.journal = Journal(executor.journal.directory) # restart: fresh handle
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
(ctx,) = seen
original = ctx.original.invocation
assert original.attempt_id == calls[0].attempt_id
assert original.operation == "run" and ctx.attempt.operation == "reconcile"
assert ctx.attempt.attempt_id != original.attempt_id
assert original.pins == args[1].pins
assert (original.principal, original.tenant, original.realm) == (
args[1].principal,
args[1].tenant,
args[1].realm,
)
assert ctx.original.source == executor.adapters[args[0].adapter].source
def test_missing_original_intent_refuses_and_stays_uncertain(rig):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
executor.journal.db.execute("DELETE FROM intents")
with pytest.raises(Refused, match="original intent record missing"):
executor.reconcile(*args)
assert seen == []
binding = f"{args[1].step}:{args[1].target}"
assert executor.journal.load(binding, args[2]).last.outcome is Outcome.UNCERTAIN
@pytest.mark.parametrize(
"change", [{"tenant": "other-tenant"}, {"realm": "other-realm"}, {"lane": "x"}]
)
def test_original_intent_mismatch_refuses(rig, change):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
with pytest.raises(Refused):
executor.reconcile(args[0], replace(args[1], **change), args[2])
assert seen == []
def test_original_intent_source_mismatch_refuses(rig):
executor, args, calls, _, _ = rig
_lose_result(executor, args, calls)
binding = f"{args[1].step}:{args[1].target}"
with pytest.raises(Refused, match="source mismatch"):
executor.journal.original_intent(binding, calls[0], "other-source")