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
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:
- Email side. An email side for the statute in
statute.json(namespacemodes.example), configured with the flow'ssetup, i.e.setups[flow.setup].roles. Its own peer id isemail, and it knows the keys inpeers. 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 toticket=IDworks. - Callback stub. Peer
callback-1speaks the contract and playsscenarios.json, withflow.scenarioas its default. It sends nothing until amodestep. - Outside master stub. Peer
cms-1records every message it gets, and sends commands only inmastersteps. - Clock. A controllable clock for the email side's timeouts. Only
advancesteps move it. Signatures use real time. - Send cap. When
flow.max_sentis 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_peeris 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:
- every message the mode stub and the master stub received;
- the response to a
masterorinjectstep.
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.