mail-address-semantics/mas_modes/harness.py
tegwick bdf200a600 MAS-WP-0011 finished: peer authentication in mode contract v0.1
CONTRACT.md section 7 (founder-reviewed): pairwise HMAC-SHA256 over
TIMESTAMP.BODY in MAS-Modes-* headers, 300 s window; master commands only
from the configured master; statuses only at the handoff's tokenized URL
from its peer; primed webhooks must name a known peer. mas_modes.signing,
signed contract.post, authenticating json_server, EmailMode/stubs/gateway
signing. conformance/auth.json vectors + 7 authorization flows.

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 11:25:24 +02:00

126 lines
5.5 KiB
Python

"""Wire the email side, the stubs and the gateway together on loopback HTTP.
Used by the tests and by the contract flow runner. Mail enters through the
real gateway (`build_event` from raw RFC 822, with trusted
Authentication-Results), so the whole path is exercised. Time is a fake
clock that only moves on `advance()`. Every peer gets its own pairwise key
with email: `callback-1` (the stub) and `cms-1` (the stub master), plus
`gateway` when events are signed.
"""
from __future__ import annotations
import secrets
import sys
from pathlib import Path
from typing import Optional
from mas_gateway.core import Resolver, gateway, message_meta_from_rfc822
from .contract import serve
from .email_mode import EmailMode, RoleConfig
from .stubs.callback import CallbackStub
from .stubs.master import StubMaster
ROOT = Path(__file__).resolve().parents[1]
CUSTOMER = "customer@example.net"
def reference_ticket_service():
"""The reference ticket service (examples/), the master-role store."""
path = str(ROOT / "examples/ticket_service")
if path not in sys.path:
sys.path.append(path)
from ticket_service import TicketService
return TicketService
class FakeClock:
def __init__(self, t: float = 1_000_000.0):
self.t = t
def __call__(self) -> float:
return self.t
class Harness:
"""roles: resource type -> {"role": "master"|"slave", ...RoleConfig fields}.
The strings "{callback}" and "{master}" in them are replaced by the stub URLs.
Without explicit settings, master roles hand callbacks to the stub and
slave roles take commands from the stub master."""
def __init__(self, statute_file, roles: dict, scenario: str = "completed",
auto_assign: bool = True, max_sent: Optional[int] = None, ticket_ids=None,
sign_events: bool = False):
self.resolver = Resolver.with_statute_files([statute_file])
[self.domain] = self.resolver.pinned
self.clock = FakeClock()
self.keys = {peer: secrets.token_bytes(32) for peer in ("callback-1", "cms-1", "gateway")}
self.callback = CallbackStub({"email": self.keys["callback-1"]}, default=scenario)
self._servers = []
cb_url = self.callback_url = self._serve(self.callback.server()) + "/handoffs"
self.master = StubMaster({"email": self.keys["cms-1"]}, auto_assign=auto_assign)
master_url = self._serve(self.master.server()) + "/events"
def fill(v):
if isinstance(v, str):
return v.replace("{callback}", cb_url).replace("{master}", master_url)
if isinstance(v, dict):
return {k: fill(x) for k, x in v.items()}
return v
defaults = {"master": {"modes": {"callback": {"url": "{callback}", "peer": "callback-1"}}},
"slave": {"master_url": "{master}", "master_peer": "cms-1"}}
configs = {t: RoleConfig(**fill({**defaults[c["role"]], **c})) for t, c in roles.items()}
self.tickets = None
if any(c.role == "master" for c in configs.values()):
kw = {"id_factory": (lambda it=iter(ticket_ids): next(it))} if ticket_ids else {}
self.tickets = reference_ticket_service()(require_dmarc_pass=True, max_sent=max_sent, **kw)
self.signer = ("gateway", self.keys["gateway"]) if sign_events else None
self.email = EmailMode(configs, dict(self.keys), self.tickets,
event_peer="gateway" if sign_events else None,
statutes=dict(self.resolver.pinned), clock=self.clock,
max_sent=max_sent)
self.email.base_url = self._serve(self.email.server())
self.master.email_url = self.email.base_url + "/master"
self.app_url = self.email.base_url + "/mas-events"
def _serve(self, server):
self._servers.append(server)
return serve(server)
def close(self):
for s in self._servers:
s.shutdown()
# -- driving -------------------------------------------------------------------
def mail(self, local_part: str, text: str = "", subject: str = "Help needed",
sender: str = CUSTOMER, dmarc: Optional[str] = "pass", headers: str = ""):
to = f"{local_part}@{self.domain}"
ar = (f"Authentication-Results: {self.domain}; dmarc={dmarc} header.from={sender.split('@')[1]}\r\n"
if dmarc else "")
raw = (f"{ar}From: <{sender}>\r\nTo: {to}\r\nSubject: {subject}\r\n"
f"Message-ID: <{abs(hash((to, subject, text, self.clock.t)))}@example.net>\r\n"
f"{headers}\r\n{text}\r\n").encode()
meta = message_meta_from_rfc822(raw, trusted_authserv_id=self.domain)
[d] = gateway([to], meta, self.resolver, self.app_url, self.signer)
return d
def advance(self, seconds: float):
self.clock.t += seconds
return self.email.tick()
def outbox(self) -> list[dict]:
"""Every mail email decided to send, in either role."""
mails = self.email.mailer.outbox()
if self.tickets is not None:
mails = self.tickets.outbox() + mails
return mails
def thread(self, ticket_id: str) -> list[str]:
"""Mode entries recorded in a master-role ticket thread."""
t = self.tickets.ticket(ticket_id)
return [m["body"] for m in t["messages"] if m["disposition"].startswith("mode-")]
def statuses(self, handoff_id: str) -> list[str]:
return [m["status"] for m in self.callback.handoffs[handoff_id].sent]