mail-address-semantics/mas_modes/runner.py
tegwick ecb1ba47f6 MAS-WP-0013 finished: mode contract flows runnable from the bundle alone
conformance/FLOWS.md defines setup, steps, captures and expectations, all
observable at the contract boundary. flows.json carries setups (peer ids),
fixed test keys, statute, scenario and explicit assign steps;
scenarios.json holds the callback scenarios (the stub loads them).
CONTRACT.md defines the case.assign response. Runner reads only the
bundle and rejects undefined keys; new flow s-006 (held over cap).

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
2026-10-19 12:49:15 +02:00

303 lines
12 KiB
Python

"""Run the mode-contract conformance bundle against this email side.
Message fixtures and the transition table are checked against the schema
and `contract.allowed`; flows run through `harness.Harness` with the real
gateway, the callback stub and the stub outside master. Used by `mas test`
and pytest.
"""
from __future__ import annotations
import json
import time
import urllib.error
import urllib.request
import uuid
from collections import Counter
from dataclasses import dataclass, field
from importlib import resources
from typing import Optional
from . import _load, message_errors, message_fixtures, transitions
from .contract import allowed
from .harness import Harness
from .signing import AuthError, sign, signature, verify
@dataclass
class Report:
passed: int = 0
failures: list = field(default_factory=list)
@property
def ok(self) -> bool:
return not self.failures
def check_messages(report: Report):
fx = message_fixtures()
for c in fx["valid"]:
errs = message_errors(c["message"])
(report.failures.append((c["case_id"], f"should be valid: {errs}")) if errs
else _ok(report))
for c in fx["invalid"]:
(report.failures.append((c["case_id"], f"should be invalid ({c['why']})"))
if not message_errors(c["message"]) else _ok(report))
t = transitions()
statuses = set(t["terminal"]) | set(t["transitions"]) - {"(new)"}
for old in [None, *sorted(statuses)]:
for new in sorted(statuses):
want = new in t["transitions"].get(old or "(new)", [])
if allowed(old, new) != want:
report.failures.append((f"transition {old}->{new}", f"expected allowed={want}"))
else:
_ok(report)
def _ok(report):
report.passed += 1
def check_auth(report: Report):
"""Signing vectors and verification cases (CONTRACT.md section 7)."""
fx = _load("conformance/auth.json")
for v in fx["vectors"]:
got = signature(bytes.fromhex(v["key_hex"]), v["timestamp"], v["body"].encode())
(report.failures.append((v["id"], f"signature {got}")) if got != v["signature"] else _ok(report))
for c in fx["verify"]:
keys = {p: bytes.fromhex(k) for p, k in c["keys"].items()}
try:
got = verify(c["headers"], c["body"].encode(), keys, now=c["now"])
except AuthError as e:
got = "error:" + e.code
want = c["peer"] if "peer" in c else "error:" + c["error"]
(report.failures.append((c["id"], f"got {got}, expected {want}")) if got != want else _ok(report))
def _inject(h, spec, ctx):
"""Send a crafted request straight to an email endpoint (authorization flows)."""
if spec["endpoint"] == "status":
reqs = [m for m in h.callback.received if m["type"] == "handoff.request"]
url = reqs[-1]["status_url"]
if spec.get("token") == "bogus":
url = url.rsplit("/", 1)[0] + "/not-a-token"
else:
url = h.email.base_url + {"master": "/master", "events": "/mas-events"}[spec["endpoint"]]
msg = {"mas_modes_version": "0.1", "id": uuid.uuid4().hex, **_fill(spec["message"], ctx)}
if msg.get("case") == "current": # the case (and handoff) the flow is working on
if spec["endpoint"] == "status":
msg["case"], msg["handoff_id"] = reqs[-1]["case"], reqs[-1]["handoff_id"]
else:
msg["case"] = {"address": ctx["case_address"], "key": ctx["case_key"]}
body = json.dumps(msg).encode()
headers = {"Content-Type": "application/json"}
mode = spec.get("sign", "valid")
if mode != "none":
peer = spec["as"]
key = h.keys.get(peer, b"k" * 32) if mode != "wrong-key" else b"x" * 32
ts = int(time.time()) - (400 if mode == "stale" else 0)
headers.update(sign(body, peer, key, ts))
req = urllib.request.Request(url, data=body, method="POST", headers=headers)
try:
with urllib.request.urlopen(req, timeout=10) as r:
return r.status, json.loads(r.read().decode() or "null")
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read().decode() or "null")
STEPS = {"mail", "mode", "advance", "master", "inject"}
EXPECT = {"event_accepted", "mode_statuses", "mode_received", "mode_received_last",
"master_received", "relayed", "mails_sent", "last_mail_contains", "response_ok",
"response_reason", "response_held", "case_address", "http_status", "error"}
def undefined_keys(flow: dict) -> list[str]:
"""Steps or expectations not defined in conformance/FLOWS.md."""
bad = []
for n, step in enumerate(flow["steps"], 1):
bad += [f"step {n}: {k}" for k in set(step) - STEPS - {"expect"}]
bad += [f"step {n}: expect {k}" for k in set(step.get("expect", {})) - EXPECT]
return bad
def _fill(value, ctx):
if isinstance(value, str):
for k, v in ctx.items():
if isinstance(v, str):
value = value.replace("{" + k + "}", v)
return value
if isinstance(value, dict):
return {k: _fill(v, ctx) for k, v in value.items()}
return value
def _roles(setup: dict) -> dict:
"""A flow setup (peer ids only) as harness role configs (with stub URLs)."""
def url_for(v):
if isinstance(v, str) and v.startswith("<mode:"):
return "{callback}"
if isinstance(v, dict):
return {k: url_for(x) for k, x in v.items()}
return v
roles = {}
for rtype, cfg in setup["roles"].items():
cfg = url_for(dict(cfg))
if cfg["role"] == "master":
cfg["modes"] = {m: {"url": "{callback}", **e} for m, e in cfg.get("modes", {}).items()}
cfg["fields"] = tuple(cfg.get("fields", ("phone", "time")))
else:
cfg["master_url"] = "{master}"
roles[rtype] = cfg
return roles
def _capture(ctx, items):
for m in items:
if not isinstance(m, dict):
continue
if isinstance(m.get("case"), dict):
ctx["case"] = m["case"]
ctx["case_id"] = m["case"]["address"].split("=", 1)[-1].rsplit("@", 1)[0]
if m.get("type") in ("case.message", None) and m.get("message_ref"):
ctx["message_ref"] = m["message_ref"]
if m.get("type") == "case.intake":
ctx["intake_ref"] = m["intake_ref"]
if m.get("type") in ("handoff.request", "handoff.requested"):
ctx["handoff_id"] = m["handoff_id"]
def _inject(h, spec, ctx):
"""Send a crafted request straight to an email endpoint (FLOWS.md, inject)."""
if spec["endpoint"] == "status":
reqs = [m for m in h.callback.received if m["type"] == "handoff.request"]
url = reqs[-1]["status_url"]
if spec.get("token") == "bogus":
url = url.rsplit("/", 1)[0] + "/not-a-token"
else:
url = h.email.base_url + {"master": "/master", "events": "/mas-events"}[spec["endpoint"]]
msg = {"mas_modes_version": "0.1", "id": uuid.uuid4().hex, **_fill(spec["message"], ctx)}
if msg.get("case") == "current":
msg["case"] = ctx["case"]
body = json.dumps(msg).encode()
headers = {"Content-Type": "application/json"}
mode = spec.get("sign", "valid")
if mode != "none":
peer = spec["as"]
key = h.keys.get(peer, b"k" * 32) if mode != "wrong-key" else b"x" * 32
ts = int(time.time()) - (400 if mode == "stale" else 0)
headers.update(sign(body, peer, key, ts))
req = urllib.request.Request(url, data=body, method="POST", headers=headers)
try:
with urllib.request.urlopen(req, timeout=10) as r:
return r.status, json.loads(r.read().decode() or "null")
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read().decode() or "null")
def _master(h, cmd, ctx):
c = cmd["command"]
if c == "assign":
return h.master.assign(cmd.get("intake_ref", ctx.get("intake_ref")), cmd["resource_id"])
if c == "send":
return h.master.send(ctx["case"], cmd["in_reply_to"], cmd["body"], cmd.get("subject"))
if c == "cancel":
return h.master.cancel(ctx["case"], cmd["handoff_id"])
if c == "policy":
return h.master.policy(ctx["case"], cmd.get("autonomous_send"), cmd.get("transfers"))
raise KeyError(f"unknown master command {c!r}")
def run_flow(flow: dict, doc: Optional[dict] = None) -> list[str]:
"""Run one flow from the bundle (FLOWS.md). Returns the problems found."""
doc = doc or _load("conformance/flows.json")
bad = undefined_keys(flow)
if bad:
return [f"undefined key(s): {bad}"]
keys = {p: bytes.fromhex(k) for p, k in doc["peers"].items()}
with resources.as_file(resources.files(__package__).joinpath("conformance", doc["statute"])) as st:
h = Harness(st, _roles(doc["setups"][flow["setup"]]), scenario=flow["scenario"],
auto_assign=False, max_sent=flow.get("max_sent"), keys=keys)
ctx: dict = {}
problems = []
try:
for n, step in enumerate(flow["steps"], 1):
seen = (len(h.callback.received), len(h.master.received))
event, response = None, None
if "mail" in step:
m = _fill(step["mail"], ctx)
event = h.mail(m["to"], m.get("text", ""), subject=m.get("subject", "Help needed"),
dmarc=m.get("dmarc", "pass"))
elif "mode" in step:
h.callback.run()
elif "advance" in step:
h.advance(step["advance"])
elif "master" in step:
response = _master(h, _fill(step["master"], ctx), ctx)
elif "inject" in step:
response = _inject(h, step["inject"], ctx)
_capture(ctx, h.callback.received[seen[0]:] + h.master.received[seen[1]:])
if response is not None:
_capture(ctx, [response[1]])
for key, want in step.get("expect", {}).items():
got = _observe(h, key, event, response)
if key == "master_received":
got = {t: got.get(t, 0) for t in want}
elif key == "last_mail_contains":
got, want = (want in (got or "")), True
if got != want:
problems.append(f"step {n} {key}: got {got!r}, expected {want!r}")
finally:
h.close()
return problems
def _observe(h, key, event, response):
body = (response[1] if response else None) or {}
if key == "event_accepted":
return event is not None and event.status is not None and 200 <= event.status < 300
if key == "mode_statuses":
return [m["status"] for hd in h.callback.handoffs.values() for m in hd.sent]
if key == "mode_received":
return len(h.callback.received)
if key == "mode_received_last":
return h.callback.received[-1]["type"] if h.callback.received else None
if key == "master_received":
return dict(Counter(m["type"] for m in h.master.received))
if key == "relayed":
return [m["status"] for m in h.master.of_type("handoff.status")]
if key == "mails_sent":
return len(h.outbox())
if key == "last_mail_contains":
mails = h.outbox()
return mails[-1]["body"] if mails else None
if key == "response_ok":
return body.get("ok")
if key == "response_reason":
return body.get("reason")
if key == "response_held":
return body.get("held")
if key == "case_address":
return (body.get("case") or {}).get("address")
if key == "http_status":
return response[0] if response else None
if key == "error":
return body.get("error")
raise KeyError(f"unknown expectation {key!r}")
def run_mode_corpus() -> Report:
report = Report()
check_messages(report)
check_auth(report)
doc = _load("conformance/flows.json")
for flow in doc["flows"]:
problems = run_flow(flow, doc)
if problems:
report.failures.append((flow["id"], "; ".join(problems)))
else:
report.passed += 1
return report
if __name__ == "__main__":
r = run_mode_corpus()
print(json.dumps({"passed": r.passed, "failures": r.failures}, indent=2))