mail-address-semantics/mas_modes/conformance/FLOWS.md
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

5 KiB

Conformance flows (mode contract v0.1)

flows.json describes complete conversations that an email-side implementation must handle. A run needs only this bundle: flows.json, scenarios.json and statute.json. Every expectation is observable at the contract boundary: messages the stub parties receive, responses to contract requests, and mail the email side sends. Nothing depends on how an implementation stores data or answers the gateway.

Running a flow

For each flow, start fresh:

  1. Email side. An email side for the statute in statute.json (namespace modes.example), configured with the flow's setup, i.e. setups[flow.setup].roles. Its own peer id is email, and it knows the keys in peers. A master role needs a store for its cases. The reference uses its ticket service, but any store that creates a case on intake mail and correlates mail to ticket=ID works.
  2. Callback stub. Peer callback-1 speaks the contract and plays scenarios.json, with flow.scenario as its default. It sends nothing until a mode step.
  3. Outside master stub. Peer cms-1 records every message it gets, and sends commands only in master steps.
  4. Clock. A controllable clock for the email side's timeouts. Only advance steps move it. Signatures use real time.
  5. Send cap. When flow.max_sent is set, the email side's send cap is that number, and an SMTP sink counts what is actually sent. Without it there is no cap.

Role configuration fields are those of CONTRACT.md §3. In a setup:

  • modes: {MODE: {"peer": ID}} means the mode endpoint of that peer;
  • master_peer is the outside master, which implies its event URL;
  • "<mode:ID>" stands for the handoff URL of peer ID.

The implementation wires peer ids to its own URLs. The peers keys are public test keys, never to be used in a deployment.

Steps

Step Meaning
{"mail": {to, text, dmarc?, subject?}} Inbound mail from customer@example.net to TO@modes.example, through the normal gateway path. dmarc (default "pass") is the trusted Authentication-Results verdict; null means no verdict.
{"mode": "run"} The callback stub sends every status that is due, until it is waiting, silent or terminal.
{"advance": N} Move the email side's clock N seconds and let it run its timeouts.
{"master": {"command": C, ...}} The stub master sends a signed contract command: assign (case.assign; resource_id, plus intake_ref, by default the captured one), send (mail.send; in_reply_to, body), cancel (handoff.cancel; handoff_id), or policy (case.policy). The case is the captured case.
{"inject": {endpoint, as?, sign?, token?, message}} Send a crafted request straight to the email side: endpoint is master, status (the captured handoff's status URL) or events. as is the claimed peer id. sign is valid (default), none, stale (timestamp 400 s old) or wrong-key. token: "bogus" replaces the status URL token. In message, "case": "current" means the captured case, and the envelope (mas_modes_version, id) is added.

Strings in steps may contain the captures {case_id}, {message_ref}, {intake_ref} and {handoff_id}.

Captures

Captures are updated after every step, from what that step produced, in this order:

  1. every message the mode stub and the master stub received;
  2. the response to a master or inject step.

The latest value wins.

Capture From
case (address, key) the case of any of those messages or responses
{case_id} the resource id in the captured case address (TYPE=ID@D)
{message_ref} message_ref of case.message, or of the case.assign response
{intake_ref} intake_ref of case.intake
{handoff_id} handoff_id of handoff.request (mode) or handoff.requested (master)

Expectations

expect is checked after the step. Every key listed must match.

Key Value
event_accepted the email side answered the step's inbound mail event with 2xx
mode_statuses statuses of every handoff.status the mode stub sent, in order
mode_received number of contract messages the mode stub received
mode_received_last type of the last one
master_received {type: count} of messages the master stub received; only the listed types are compared
relayed statuses of every handoff.status the master stub received, in order
mails_sent number of mails the email side sent (held and refused mail excluded)
last_mail_contains the body of the last mail sent contains this text
response_ok ok in the response to the step's command
response_reason reason in that response
response_held held in that response (CONTRACT.md §6)
case_address case.address in that response
http_status HTTP status of that response
error error in that response

A runner MUST reject a flow that uses a step or expectation key not defined here.