Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a198e4-7903-75b3-aaf3-c89ddac9437d
390 lines
12 KiB
Python
390 lines
12 KiB
Python
"""Ordered sheet over admitted steps. Fixture adapters only; nothing native."""
|
|
|
|
from dataclasses import asdict, replace
|
|
import json
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from warden.case_receipt_state import Outcome, Refused
|
|
from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest
|
|
from warden.case_sheet import rollback_sheet, run_sheet
|
|
from tests.test_case_run import lane_entry
|
|
|
|
|
|
SOURCE = "sha256:" + "1" * 64
|
|
|
|
|
|
def _result(inv, outcome, created=()):
|
|
return OwnerResult(
|
|
outcome,
|
|
"owner-receipt",
|
|
created,
|
|
True,
|
|
session_revocation_evidence="session-revoked",
|
|
grant_revocation_evidence="grant-revoked",
|
|
attempt_id=inv.attempt_id,
|
|
operation=inv.operation,
|
|
)
|
|
|
|
|
|
def _spec(step, mode, outcome=Outcome.SUCCESS, created=True, lane=None):
|
|
return {
|
|
"step": step,
|
|
"mode": mode,
|
|
"outcome": outcome,
|
|
"created": created,
|
|
"lane": lane,
|
|
}
|
|
|
|
|
|
def admit_sheet(tmp_path, specs, *, case_id="case-sheet", executor=None):
|
|
"""Admit fixture steps on one executor. No step is dispatched."""
|
|
modes = {
|
|
"warden": (
|
|
"issuer-lane",
|
|
True,
|
|
False,
|
|
"mint",
|
|
("parent-session", "broker-grant"),
|
|
None,
|
|
),
|
|
"attended": ("login-lane", False, True, "change", ("parent-session",), "login"),
|
|
"owner": ("issuer-lane", False, False, "change", ("parent-session",), None),
|
|
}
|
|
descriptors = {}
|
|
follow = []
|
|
entries = {}
|
|
adapters = {}
|
|
calls = []
|
|
undos = []
|
|
for spec in specs:
|
|
lane, executes, attended, effect, scopes, kind = modes[spec["mode"]]
|
|
lane = spec["lane"] or lane
|
|
if spec["mode"] == "attended" and spec["lane"]:
|
|
kind = "login"
|
|
object_id = "obj-" + spec["step"]
|
|
desc = Descriptor(
|
|
"warden.case.execution.v1",
|
|
spec["step"],
|
|
"owner",
|
|
"owner/" + spec["step"] + "@v1",
|
|
SOURCE,
|
|
"human",
|
|
lane,
|
|
"client_id",
|
|
effect,
|
|
(object_id,),
|
|
False,
|
|
scopes,
|
|
)
|
|
encoded = json.dumps(asdict(desc)).encode()
|
|
descriptors[spec["step"]] = (desc, encoded)
|
|
follow.append(
|
|
{
|
|
"id": spec["step"],
|
|
"owner": "owner",
|
|
"lane": lane,
|
|
"warden_executes": executes,
|
|
"attended": attended,
|
|
}
|
|
)
|
|
entry = lane_entry(lane)
|
|
if kind:
|
|
entry["lane"] = kind
|
|
entries[lane] = entry
|
|
|
|
def invoke(inv, spec=spec, object_id=object_id):
|
|
calls.append(spec["step"])
|
|
created = (
|
|
(object_id,)
|
|
if spec["created"] and spec["outcome"] is Outcome.SUCCESS
|
|
else ()
|
|
)
|
|
if spec["outcome"] == "raise":
|
|
raise RuntimeError("synthetic lost reply")
|
|
return _result(inv, spec["outcome"], created)
|
|
|
|
def undo(inv, objects, spec=spec):
|
|
undos.append(spec["step"])
|
|
if spec.get("undo") == "raise":
|
|
raise RuntimeError("synthetic lost undo")
|
|
return _result(inv, Outcome.ROLLED_BACK, ())
|
|
|
|
adapters[desc.adapter] = Adapter(
|
|
desc.adapter, SOURCE, invoke, lambda *_: True, undo
|
|
)
|
|
|
|
typ = yaml.safe_dump(
|
|
{
|
|
"spec": {
|
|
"id": "test",
|
|
"version": "1",
|
|
"status": "acknowledged",
|
|
"parameters": [
|
|
{
|
|
"name": "client_id",
|
|
"sensitivity": "operational",
|
|
"case_settable": True,
|
|
}
|
|
],
|
|
"execution_descriptors": {
|
|
step: digest(encoded) for step, (_, encoded) in descriptors.items()
|
|
},
|
|
"follow_through": follow,
|
|
}
|
|
}
|
|
).encode()
|
|
case = yaml.safe_dump(
|
|
{
|
|
"spec": {
|
|
"id": case_id,
|
|
"type": "test@1",
|
|
"status": "materialized",
|
|
"acceptance": "acceptance",
|
|
"parameters": {"client_id": "fixture-client"},
|
|
}
|
|
}
|
|
).encode()
|
|
catalog = yaml.safe_dump({"entries": list(entries.values())}).encode()
|
|
if executor is None:
|
|
executor = Executor(Journal(tmp_path / "private"), adapters, lambda *_: True)
|
|
else:
|
|
executor.adapters.update(adapters)
|
|
admitted = []
|
|
for spec in specs:
|
|
desc, encoded = descriptors[spec["step"]]
|
|
admitted.append(
|
|
executor.admit(
|
|
case,
|
|
typ,
|
|
encoded,
|
|
catalog,
|
|
principal="operator",
|
|
actor="human",
|
|
lane=desc.lane,
|
|
acceptance_evidence="acceptance",
|
|
materialization_evidence="materialization",
|
|
descriptor_evidence="descriptor",
|
|
session_evidence="session",
|
|
)
|
|
)
|
|
return executor, tuple(admitted), calls, undos
|
|
|
|
|
|
def _session(events):
|
|
def session(lane, dispatch):
|
|
events.append(("session", lane))
|
|
dispatch()
|
|
events.append(("session-end", lane))
|
|
|
|
return session
|
|
|
|
|
|
def test_stops_before_later_steps_when_a_step_does_not_close(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, calls, _ = admit_sheet(
|
|
tmp_path,
|
|
[
|
|
_spec("mint-secret", "warden", Outcome.FAILED_BEFORE, created=False),
|
|
_spec("bind-env", "warden"),
|
|
],
|
|
)
|
|
seen = []
|
|
receipt = run_sheet(
|
|
executor,
|
|
steps,
|
|
verifications=(("consumer-enforces-groups", lambda: seen.append(1)),),
|
|
)
|
|
assert calls == ["mint-secret"]
|
|
assert seen == []
|
|
assert receipt.closed is False
|
|
assert receipt.steps == (("mint-secret", "failed-before-change"),)
|
|
assert receipt.verifications == ()
|
|
|
|
|
|
def test_attended_steps_on_one_lane_share_one_session(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, calls, _ = admit_sheet(
|
|
tmp_path,
|
|
[
|
|
_spec("ensure-groups", "attended"),
|
|
_spec("bind-env", "attended"),
|
|
_spec("mint-secret", "warden"),
|
|
],
|
|
)
|
|
events = []
|
|
|
|
def session(lane, dispatch):
|
|
events.append(("session", lane, tuple(calls)))
|
|
dispatch()
|
|
events.append(("session-end", lane, tuple(calls)))
|
|
|
|
receipt = run_sheet(executor, steps, session=session)
|
|
assert events == [
|
|
("session", "login-lane", ()),
|
|
("session-end", "login-lane", ("ensure-groups", "bind-env")),
|
|
]
|
|
assert calls == ["ensure-groups", "bind-env", "mint-secret"]
|
|
assert receipt.closed is True
|
|
assert receipt.verifications == ()
|
|
|
|
|
|
def test_different_lanes_and_a_break_are_separate_sessions(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, calls, _ = admit_sheet(
|
|
tmp_path,
|
|
[
|
|
_spec("ensure-groups", "attended"),
|
|
_spec("mint-secret", "warden"),
|
|
_spec("register-client", "attended", lane="other-login"),
|
|
],
|
|
)
|
|
events = []
|
|
run_sheet(executor, steps, session=_session(events))
|
|
assert [lane for kind, lane in events if kind == "session"] == [
|
|
"login-lane",
|
|
"other-login",
|
|
]
|
|
assert calls == ["ensure-groups", "mint-secret", "register-client"]
|
|
|
|
|
|
def test_attended_dispatch_outside_the_session_is_refused(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, calls, _ = admit_sheet(
|
|
tmp_path, [_spec("ensure-groups", "attended")]
|
|
)
|
|
with pytest.raises(Refused, match="contained session"):
|
|
run_sheet(executor, steps)
|
|
assert calls == []
|
|
|
|
def silent(lane, dispatch):
|
|
return None
|
|
|
|
with pytest.raises(Refused, match="left the contained session"):
|
|
run_sheet(executor, steps, session=silent)
|
|
assert calls == []
|
|
|
|
|
|
def test_verification_runs_only_after_every_step_closes(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, _, _ = admit_sheet(
|
|
tmp_path, [_spec("mint-secret", "warden"), _spec("bind-env", "owner")]
|
|
)
|
|
checks = []
|
|
|
|
def check(outcome):
|
|
def run():
|
|
checks.append(outcome)
|
|
return outcome
|
|
|
|
return run
|
|
|
|
receipt = run_sheet(
|
|
executor,
|
|
steps,
|
|
verifications=(
|
|
("custody-allowed", check(Outcome.SUCCESS)),
|
|
("consumer-enforces-groups", check(Outcome.FAILED_BEFORE)),
|
|
("not-reached", check(Outcome.SUCCESS)),
|
|
),
|
|
)
|
|
assert [item.value for item in checks] == [
|
|
"verified-success",
|
|
"failed-before-change",
|
|
]
|
|
assert receipt.closed is False
|
|
assert receipt.verifications[-1][0] == "consumer-enforces-groups"
|
|
|
|
|
|
def test_unknown_verification_result_is_uncertain_and_not_stored_as_text(
|
|
tmp_path, monkeypatch
|
|
):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, _, _ = admit_sheet(tmp_path, [_spec("mint-secret", "warden")])
|
|
|
|
def leak():
|
|
raise RuntimeError("synthetic-secret-value")
|
|
|
|
receipt = run_sheet(executor, steps, verifications=(("readback", leak),))
|
|
assert receipt.closed is False
|
|
assert receipt.verifications == (("readback", "uncertain"),)
|
|
assert "synthetic-secret-value" not in repr(receipt)
|
|
|
|
def wrong():
|
|
return "verified-success"
|
|
|
|
again, again_steps, _, _ = admit_sheet(
|
|
tmp_path / "again", [_spec("mint-secret", "warden")]
|
|
)
|
|
receipt = run_sheet(again, again_steps, verifications=(("readback", wrong),))
|
|
assert receipt.verifications == (("readback", "uncertain"),)
|
|
|
|
|
|
def test_refuses_unadmitted_duplicate_and_foreign_pins(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, calls, _ = admit_sheet(tmp_path, [_spec("mint-secret", "warden")])
|
|
moved = (steps[0][0], replace(steps[0][1], target="other-target"), steps[0][2])
|
|
with pytest.raises(Refused, match="not admitted"):
|
|
run_sheet(executor, (moved,))
|
|
with pytest.raises(Refused, match="duplicate"):
|
|
run_sheet(executor, (steps[0], steps[0]))
|
|
_, others, _, _ = admit_sheet(
|
|
tmp_path,
|
|
[_spec("bind-env", "owner")],
|
|
case_id="other-case",
|
|
executor=executor,
|
|
)
|
|
with pytest.raises(Refused, match="do not share"):
|
|
run_sheet(executor, (steps[0], others[0]))
|
|
assert calls == []
|
|
|
|
|
|
def test_rollback_skips_no_change_and_stops_on_uncertain_undo(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, _, undos = admit_sheet(
|
|
tmp_path,
|
|
[
|
|
_spec("mint-secret", "warden"),
|
|
_spec("bind-env", "owner", Outcome.NO_CHANGE, created=False),
|
|
dict(_spec("register-client", "owner"), undo="raise"),
|
|
],
|
|
)
|
|
assert run_sheet(executor, steps).closed is True
|
|
done = rollback_sheet(executor, steps)
|
|
assert undos == ["register-client"]
|
|
assert done[-1].last.outcome is Outcome.ROLLBACK_UNCERTAIN
|
|
assert "mint-secret" not in undos
|
|
|
|
|
|
def test_uncertain_step_blocks_sheet_rollback(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, _, undos = admit_sheet(
|
|
tmp_path,
|
|
[
|
|
_spec("mint-secret", "warden"),
|
|
_spec("bind-env", "owner", outcome="raise", created=False),
|
|
],
|
|
)
|
|
receipt = run_sheet(executor, steps)
|
|
assert receipt.steps[-1][1] == "uncertain"
|
|
with pytest.raises(Refused, match="owner reconciliation"):
|
|
rollback_sheet(executor, steps)
|
|
assert undos == []
|
|
|
|
|
|
def test_attended_rollback_stays_inside_the_session(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
|
executor, steps, _, undos = admit_sheet(
|
|
tmp_path,
|
|
[_spec("ensure-groups", "attended"), _spec("bind-env", "attended")],
|
|
)
|
|
run_sheet(executor, steps, session=_session([]))
|
|
with pytest.raises(Refused, match="contained session"):
|
|
rollback_sheet(executor, steps)
|
|
assert undos == []
|
|
events = []
|
|
done = rollback_sheet(executor, steps, session=_session(events))
|
|
assert [lane for kind, lane in events if kind == "session"] == ["login-lane"]
|
|
assert undos == ["bind-env", "ensure-groups"]
|
|
assert all(state.last.outcome is Outcome.ROLLED_BACK for state in done)
|