For implementers

The AMAP specification, at the wire level

AMAP defines a wire contract between a trusted runtime, which holds the mail credentials and applies policy, and an agent-side connector, which holds nothing credentialed. The two sides share one thing: a per-agent filesystem namespace. This page summarises what each side must do. The authority is the specification itself.

Source of truth. The AMAP Internet-Draft (v3.1.0 DRAFT; its source is draft/draft-amap.xml), the schemas and the fixtures are the normative specification. The peer-origin profile is in spec/peer-origin.md. Where this page and the specification disagree, the specification wins. A reference such as "§ Inbound" names a section of the Internet-Draft. The older spec/contract.md is frozen history, not the specification.

Three actors, one invariant

Runtime trusted

Holds the mail-send credentials, each agent's identity and the policy, and keeps the record. Reaches the mail provider and the volume.

Connector agent-side

One per agent. Holds nothing credentialed. Reaches only its own namespace on the volume and its agent's input and output.

Agent untrusted

Holds only what the connector surfaces. Reaches the connector and its own sandbox.

The load-bearing invariant (§ Architecture): no security property may depend on the agent or connector behaving. Enforcement is entirely runtime-side. A compromised agent or connector can at most write a well-formed request that the runtime then refuses. That is why the channel is a runtime-owned filesystem namespace rather than an API the agent could call: a file drop carries no capability.

The per-agent namespace

The runtime owns the tree and mounts each agent's namespace into that agent's sandbox (§ Volume Layout). A connector can reach only its own <agent-id>/. The runtime assigns that id, and attribution is the namespace a file arrived in, never an agent_id field inside it.

Per-agent namespace and who writes what The agent-id directory contains outbound, written by the connector with results and processed written by the runtime; inbound and peer, both runtime-written and mountable read-only; audit, runtime-only; and an optional directory.json, runtime-written. <agent-id>/ runtime-assigned; attribution is the namespace, never a field outbound/ req-<id>.json · req-<id>.attachments/ connector writes: the only outbound act results/<id>.json runtime writes: one verdict per consumed request processed/ · ext/<name>/ runtime archive · connector side channel (nothing security-relevant) inbound/ origin class: mail notices/notice-<id>.json · <notice-id>.attachments/ messages/notice-<id>.json · cursor.json runtime writes; MAY be mounted read-only peer/ origin class: same-principal peer (profile) same layout and sidecar rules as inbound/ runtime writes, by a separate write authority audit/ log.jsonl runtime only; append-only; never the connector directory.json runtime writes; OPTIONAL; which peers this agent may address written by the connector (agent side) written by the runtime Every write is atomic (.tmp then rename). One connector writes outbound/ and one runtime drains it. Runtime-private staging lives outside the mounted volume, where the agent cannot reach it.
The per-agent tree from § Volume Layout, with the peer tree from the peer-origin profile §1. Colour shows who writes.

Two lanes, two trees

Mail and same-principal peer traffic arrive in separate trees: inbound/ for mail, and peer/ under the peer-origin profile. The tree a document is read from is the runtime's sole assertion of its origin class. A connector derives the class from the tree and never from any member of the document, and kind must equal the tree name (§ Volume Layout, tree rules; peer-origin §2). The two schemas cross-lock:

Either way it is dropped. Write authority for each tree must be structurally enforced, never configuration the writer reads.

The documents

10 document types cross the seam. The rule for which are open and which are closed is direction, not taste (§ Versioning and Conformance, since v3.0.0):

DocumentWhereWritten bySchemaEnvelope
Deliver-noticeinbound/notices/Runtimedeliver-notice.schema.jsonOpen
Peer-noticepeer/notices/Runtimepeer-notice.schema.jsonOpen
Inbound message (spooled body)inbound/messages/Runtimeinbound-message.schema.jsonOpen
Resultoutbound/results/Runtimeresult.schema.jsonOpen
Peer directorydirectory.jsonRuntimedirectory.schema.jsonOpen
Fleet rosterroster.json, where the deployment designatesRuntimeroster.schema.jsonOpen
Submit-requestoutbound/req-<id>.jsonConnectorsubmit-request.schema.jsonClosed, at every level
Binding recordidentity.json (volume root)Runtime or provisionerbinding-record.schema.jsonClosed, as an anti-secret-leak lint

The binding record is closed for a different reason from the submit-request. Its closure catches a credential written into the file by mistake: the imap_password fixture tests exactly that. It is not a compatibility surface. The record is also informational, not authoritative. The agent can edit it, so it is never consulted for enforcement (§ Binding Record).

One trap to avoid: to is a property of the spooled inbound message, not of the deliver-notice's message object. Because the notice envelope is open, code that reads message.to off a notice will not be caught by the schema. The recipient list lives on the spool document (§ Inbound).

Outbound: the submit-request and its result

Attachments and dispositions

An inbound attachment descriptor carries disposition ∈ {clean, stripped, quarantined, unscanned}. The disposition is the runtime's assertion, not a scanner verdict. How a runtime decides clean is runtime policy, but a runtime that cannot make the determination must not assert clean (§ Inbound).

Enforcement by absence

The runtime writes one notice per delivered message. Quarantined or unverified mail produces no notice at all: it is logged runtime-side and never seen by the agent (§ Inbound). A withheld body is simply not spooled, and a read against it returns "not resolved", with no fallback fetch.

Versioning

Two axes, not one:

Read contract_version before any schema validation, comparing majors as non-negative integers (§ Versioning and Conformance). An absent, lower or higher major is a version refusal that fails closed. It is a distinct, logged outcome, never folded into the malformed-document path. The refusal is asymmetric:

Because runtime-authored documents are open, adding an optional member to them is genuinely additive (MINOR). Adding any member to the submit-request is MAJOR.

Filesystem obligations

Many obligations are about the filesystem rather than the documents (§ Volume Layout):

Conformance, and what a green gate does not prove

The specification's Conformance section defines two conformance classes, runtime and connector. The peer-origin profile adds a sub-class for each. Claiming either without the fixture run and the prescribed operational check for that class is not conformance.

Run the gate with the Python standard library only. It needs no network access and no counterpart:

python3 fixtures/validate.py
# 119 fixtures checked, 0 unexpected.

Every valid/ fixture must pass and every invalid/ fixture must fail: 42 and 77 respectively. The schema is chosen by filename prefix, so a document you validate yourself needs its type chosen explicitly. The in-browser validator does that for you.

A green run does not prove the obligations a document validator cannot see. The NOT-CHECKED list in fixtures/validate.py is the authoritative inventory. It includes:

It also cannot tell you which document classes your producer never submitted. A conformance claim has to account for coverage as well as outcome.

Reference implementations

amap-router-local

The reference runtime, with its own test suite. On top of the seam it adds recipient policy, the two lanes, the reply ledger and delivery-status notices. Implementations in this family call a conforming runtime a router; the specification itself says "runtime" (§ Architecture).

amap-connector-claude

The Claude Code connector. It uses the Python standard library only, with zero third-party dependencies. It gives the agent ordinary MCP tools: a read-only inbox, and a submit tool that writes a gated request.

How changes are made today

These rules apply while the work lives in this repository. If the IETF takes it up, a working group will hold change control.

  1. A capability change starts in the specification. It lands as the Internet-Draft's prose, schemas/ and fixtures/ in one change, before either side builds it. Neither a runtime nor a connector may invent a wire field locally.
  2. The fixtures go in the same change. A field with no fixture is a field no implementer can prove they handle.
  3. Additive means MINOR; breaking an existing fixture means MAJOR, and a MAJOR is taken deliberately by both sides. A version mismatch fails closed.
  4. Some edits travel the other way. The standards editor edits the Internet-Draft directly, and those edits are canonical. A change in meaning then moves into the schemas and fixtures.

Read CONTRIBUTING.md and WORKFLOWS.md before opening a pull request. Report security problems in the specification privately, as described in SECURITY.md.


See also: validator · MCP, A2A and AMAP · glossary.