impl/js (Node, zero deps, written from the specs): parser, v0.2 statute-aware rules, signing, outside master. Passes addresses.json, statute-aware.json and auth.json unchanged; its master passes the 11 slave-setup mode flows against the Python email side (ExternalMaster). Differential testing vs mas_core found two unstated stand-in rules, now SCP-9 in the v0.2 draft with fixtures si-011..013, dt-109; ABNF comments no longer say v0.1. Runner: external masters, step errors fail the flow. 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
78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
"""An outside master in another process, driven through its control API.
|
|
|
|
Same interface as `stubs.master.StubMaster`, so the harness and the flow
|
|
runner can swap it in. Used for the independent JS master in
|
|
`impl/js/src/master.mjs` (MAS-WP-0009).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
JS_MASTER = ROOT / "impl/js/src/master.mjs"
|
|
|
|
|
|
def _call(url: str, body: Optional[dict] = None):
|
|
data = None if body is None else json.dumps(body).encode()
|
|
req = urllib.request.Request(url, data=data, method="GET" if body is None else "POST",
|
|
headers={"Content-Type": "application/json"})
|
|
with urllib.request.urlopen(req, timeout=15) as r:
|
|
return json.loads(r.read().decode())
|
|
|
|
|
|
class ExternalMaster:
|
|
def __init__(self, argv: list[str]):
|
|
self.proc = subprocess.Popen(argv, stdout=subprocess.PIPE, text=True)
|
|
info = json.loads(self.proc.stdout.readline())
|
|
self.events_url, self.control_url = info["events_url"], info["control_url"]
|
|
self._email_url = None
|
|
|
|
@classmethod
|
|
def js(cls, key: bytes, peer_id: str = "cms-1") -> "ExternalMaster":
|
|
return cls(["node", str(JS_MASTER), "--peer-id", peer_id, "--key-hex", key.hex()])
|
|
|
|
@property
|
|
def email_url(self):
|
|
return self._email_url
|
|
|
|
@email_url.setter
|
|
def email_url(self, url):
|
|
self._email_url = url
|
|
_call(self.control_url + "/config", {"email_url": url})
|
|
|
|
@property
|
|
def received(self) -> list[dict]:
|
|
return _call(self.control_url + "/received")
|
|
|
|
def of_type(self, type_: str) -> list[dict]:
|
|
return [m for m in self.received if m["type"] == type_]
|
|
|
|
def command(self, type_: str, case: Optional[dict] = None, **fields):
|
|
out = _call(self.control_url + "/command",
|
|
{"type": type_, **({"case": case} if case else {}),
|
|
**{k: v for k, v in fields.items() if v is not None}})
|
|
return out["status"], out["body"]
|
|
|
|
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=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"):
|
|
return self.command("handoff.cancel", case, handoff_id=handoff_id, reason=reason)
|
|
|
|
def pump(self):
|
|
return []
|
|
|
|
def close(self):
|
|
self.proc.terminate()
|
|
self.proc.wait(timeout=10)
|