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
84 lines
3.6 KiB
Python
84 lines
3.6 KiB
Python
"""A minimal outside master (CMS / ticketing stand-in) for email in the slave role.
|
|
|
|
It records every message email sends it, issues case identifiers, and
|
|
drives email through the contract: assign, policy, mail.send, cancel.
|
|
Nothing happens on its own: `pump()` performs queued auto-assignments, so
|
|
tests stay deterministic and no handler calls back into email.
|
|
|
|
master = StubMaster({"email": key}, email_url="http://127.0.0.1:PORT/master", auto_assign=True)
|
|
base = serve(master.server()) # POST /events (signed by email)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import itertools
|
|
import threading
|
|
from typing import Optional
|
|
|
|
from .. import message_errors
|
|
from ..contract import Idempotent, json_server, message, post, refuse
|
|
|
|
|
|
class StubMaster:
|
|
def __init__(self, keys: dict[str, bytes], email_url: Optional[str] = None,
|
|
auto_assign: bool = False, id_prefix: str = "EXT-", peer_id: str = "cms-1",
|
|
email_peer: str = "email"):
|
|
self.keys, self.peer_id, self.email_peer = keys, peer_id, email_peer
|
|
self.email_url = email_url
|
|
self.auto_assign = auto_assign
|
|
self.ids = (f"{id_prefix}{n}" for n in itertools.count(100))
|
|
self.received: list[dict] = []
|
|
self.pending_intakes: list[str] = []
|
|
self.lock = threading.Lock()
|
|
self._once = Idempotent()
|
|
|
|
def receive(self, msg: dict, peer: Optional[str] = None, rest: str = "") -> tuple[int, dict]:
|
|
if peer != self.email_peer:
|
|
return refuse("wrong-peer", "events come from email only", 403)
|
|
return self._once(msg, self._receive)
|
|
|
|
def _receive(self, msg):
|
|
errors = message_errors(msg)
|
|
if errors:
|
|
return refuse("schema", "; ".join(errors), 400)
|
|
with self.lock:
|
|
self.received.append(msg)
|
|
if msg["type"] == "case.intake":
|
|
self.pending_intakes.append(msg["intake_ref"])
|
|
return 200, {"ok": True}
|
|
|
|
def of_type(self, type_: str) -> list[dict]:
|
|
return [m for m in self.received if m["type"] == type_]
|
|
|
|
# -- commands to email -------------------------------------------------------
|
|
|
|
def command(self, type_: str, case: Optional[dict] = None, **fields) -> tuple[Optional[int], object]:
|
|
return post(self.email_url, message(type_, case, **fields),
|
|
(self.peer_id, self.keys[self.email_peer]))
|
|
|
|
def pump(self) -> list[tuple]:
|
|
"""Assign case ids to queued intakes (when auto_assign is on)."""
|
|
out = []
|
|
while self.auto_assign:
|
|
with self.lock:
|
|
if not self.pending_intakes:
|
|
break
|
|
ref = self.pending_intakes.pop(0)
|
|
out.append(self.assign(ref, next(self.ids)))
|
|
return out
|
|
|
|
def assign(self, intake_ref: str, resource_id: str):
|
|
return self.command("case.assign", intake_ref=intake_ref, resource_id=resource_id)
|
|
|
|
def policy(self, case: dict, autonomous_send: Optional[bool] = None, transfers=None):
|
|
return self.command("case.policy", case, autonomous_send=autonomous_send, transfers=transfers)
|
|
|
|
def send(self, case: dict, in_reply_to: str, body: str, subject: Optional[str] = None):
|
|
return self.command("mail.send", case, in_reply_to=in_reply_to, body=body, subject=subject)
|
|
|
|
def cancel(self, case: dict, handoff_id: str, reason: str = "timed-out"):
|
|
"""The master owns timeouts in the slave role: it ends a handoff itself."""
|
|
return self.command("handoff.cancel", case, handoff_id=handoff_id, reason=reason)
|
|
|
|
def server(self, host="127.0.0.1", port=0):
|
|
return json_server({"/events": self.receive}, self.keys, host=host, port=port)
|