Network Working Group D. Rapp Internet-Draft T. Adams Intended status: Standards Track Proofpoint, Inc. Expires: 24 April 2027 21 October 2026 The Agent Mediated Messaging Protocol (AMMP) draft-rapp-ammp-00 Abstract The Agent Mediated Messaging Protocol (AMMP) is a wire contract, carried over a filesystem namespace, between an untrusted per-agent connector and a trusted, credentialed, store-and-forward messaging runtime. It gives a sandboxed AI agent the ability to send and receive messages (email, and under an optional profile, messages to other agents) through a runtime that does not trust it: every outbound message begins as an inert request file that the runtime copies into private staging, validates independently, and applies policy to before anything is transmitted, and every inbound message is authenticated and screened by the runtime before a notice describing it appears in the agent's namespace. This document specifies the directory layout and its filesystem obligations — ownership, write and delete rights, and the path, symlink, and concurrency discipline a runtime observes; the JSON message shapes for submit requests, results, deliver notices, spooled message bodies, and an optional binding record; the correlation and versioning rules, including a direction-aware unknown-member tolerance rule; and two conformance classes. An optional profile gives agent-to-agent traffic within one principal its own tree, and specifies how a Runtime signs and verifies that traffic when it travels between hosts as mail. AMMP is deliberately not a mail protocol: it neither defines nor replaces SMTP, IMAP, or provider-side infrastructure, and composes with them rather than displacing them. Its single design center is that no security property depends on the agent — or on the connector speaking for it — behaving correctly. About This Draft This document and the accompanying schemas and conformance fixtures are published under the Apache License 2.0. Discussion Venues This note is to be removed before publishing as an RFC. Rapp & Adams Expires 24 April 2027 [Page 1] Internet-Draft AMMP October 2026 Source for this draft, its schemas, its conformance fixtures and an issue tracker can be found at https://github.com/proofpoint/amap- spec. Status of This Memo This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79. Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet- Drafts is at https://datatracker.ietf.org/drafts/current/. Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." This Internet-Draft will expire on 24 April 2027. Copyright Notice Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved. This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/ license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License. Table of Contents 1. Introduction — Motivation and Problem Statement . . . . . . . 5 1.1. Why the existing options are insufficient . . . . . . . . 6 1.2. The approach . . . . . . . . . . . . . . . . . . . . . . 6 1.3. Why a filesystem namespace rather than an API, RPC, or queue . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.4. Design goals . . . . . . . . . . . . . . . . . . . . . . 7 1.5. Non-goals . . . . . . . . . . . . . . . . . . . . . . . . 8 1.6. Why This Work Is Brought to the IETF . . . . . . . . . . 8 2. Conventions and Terminology . . . . . . . . . . . . . . . . . 10 2.1. Terminology . . . . . . . . . . . . . . . . . . . . . . . 10 3. Architecture and Trust Model . . . . . . . . . . . . . . . . 13 Rapp & Adams Expires 24 April 2027 [Page 2] Internet-Draft AMMP October 2026 3.1. Actors . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.2. The load-bearing invariant . . . . . . . . . . . . . . . 14 3.3. Why a filesystem namespace . . . . . . . . . . . . . . . 14 3.4. Relationship to Other Agent Protocols (Informative) . . . 15 3.5. Runtimes Built from Several Components (Informative) . . 16 4. Volume Layout . . . . . . . . . . . . . . . . . . . . . . . . 17 4.1. Obligations . . . . . . . . . . . . . . . . . . . . . . . 18 4.2. Deletion rights . . . . . . . . . . . . . . . . . . . . . 24 5. Outbound: Submit Request . . . . . . . . . . . . . . . . . . 27 5.1. Field semantics and obligations . . . . . . . . . . . . . 28 6. Outbound: Result . . . . . . . . . . . . . . . . . . . . . . 30 6.1. Field semantics and obligations . . . . . . . . . . . . . 30 7. Inbound: Deliver Notice and Inbound Message . . . . . . . . . 32 7.1. Field semantics and obligations . . . . . . . . . . . . . 33 7.2. Inbound attachment Descriptors . . . . . . . . . . . . . 37 7.3. The Inbound Message (body spool) . . . . . . . . . . . . 38 8. Task Correlation . . . . . . . . . . . . . . . . . . . . . . 40 9. Binding Record . . . . . . . . . . . . . . . . . . . . . . . 40 9.1. Field semantics and obligations . . . . . . . . . . . . . 41 10. The Peer Directory . . . . . . . . . . . . . . . . . . . . . 41 11. The Fleet Roster . . . . . . . . . . . . . . . . . . . . . . 44 12. The Peer-Origin Profile (Optional) . . . . . . . . . . . . . 45 12.1. Scope . . . . . . . . . . . . . . . . . . . . . . . . . 45 12.2. The Peer Tree . . . . . . . . . . . . . . . . . . . . . 46 12.3. Origin Class Follows the Tree . . . . . . . . . . . . . 47 12.4. Addressing . . . . . . . . . . . . . . . . . . . . . . . 49 12.5. Correlation and Replies . . . . . . . . . . . . . . . . 50 12.6. Peer Traffic Between Hosts . . . . . . . . . . . . . . . 53 12.6.1. The Statement . . . . . . . . . . . . . . . . . . . 53 12.6.2. Carriage, and the Candidate the Verifier Sees . . . 54 12.6.3. Verification . . . . . . . . . . . . . . . . . . . . 57 12.6.4. Outcomes . . . . . . . . . . . . . . . . . . . . . . 59 12.6.5. The Replay Store, Windows and Recovery . . . . . . . 60 12.6.6. Correlation and Edges Between Hosts . . . . . . . . 63 12.6.7. Keys . . . . . . . . . . . . . . . . . . . . . . . . 64 12.6.8. Control Notices . . . . . . . . . . . . . . . . . . 66 12.6.9. Optional: Sender Age, and Deployment Admission Policy . . . . . . . . . . . . . . . . . . . . . . . 67 12.6.10. The Intake Location . . . . . . . . . . . . . . . . 68 12.7. Sender Exposure . . . . . . . . . . . . . . . . . . . . 69 12.8. Delivery Outcomes, Retention, Deletion and Presentation . . . . . . . . . . . . . . . . . . . . . . 70 13. Versioning and Conformance . . . . . . . . . . . . . . . . . 71 13.1. Versioning . . . . . . . . . . . . . . . . . . . . . . . 71 13.2. Required versus optional . . . . . . . . . . . . . . . . 74 13.3. Conformance . . . . . . . . . . . . . . . . . . . . . . 77 14. Security Considerations . . . . . . . . . . . . . . . . . . . 80 14.1. Path authority and traversal . . . . . . . . . . . . . . 81 Rapp & Adams Expires 24 April 2027 [Page 3] Internet-Draft AMMP October 2026 14.2. Symlinks, descriptors, and open discipline . . . . . . . 81 14.3. Time-of-check to time-of-use, and ingest by copy . . . . 82 14.4. Concurrency . . . . . . . . . . . . . . . . . . . . . . 83 14.5. Attribution spoofing . . . . . . . . . . . . . . . . . . 83 14.6. Untrusted content . . . . . . . . . . . . . . . . . . . 83 14.7. Recipient policy and header injection . . . . . . . . . 84 14.8. The outbound digest is not a trust boundary . . . . . . 85 14.9. Confirmation-oracle avoidance . . . . . . . . . . . . . 85 14.10. Byte-absence and artifact-absence as the access gate . . 85 14.11. Fail-closed version handling, and the cost of tolerance . . . . . . . . . . . . . . . . . . . . . . . 86 14.12. Inbound integrity and the writability of the inbound tree . . . . . . . . . . . . . . . . . . . . . . . . . 87 14.13. Retention, deletion, and lost verdicts . . . . . . . . . 87 14.14. What is wire-verifiable, and what is a Runtime obligation . . . . . . . . . . . . . . . . . . . . . . 87 14.15. The extension space, and outcomes as claims . . . . . . 89 14.16. Special-use domain names . . . . . . . . . . . . . . . . 89 14.17. The Peer-Origin Profile . . . . . . . . . . . . . . . . 89 15. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 91 15.1. Message Header Fields . . . . . . . . . . . . . . . . . 91 15.2. Possible Future Registries . . . . . . . . . . . . . . . 91 16. References . . . . . . . . . . . . . . . . . . . . . . . . . 91 16.1. Normative References . . . . . . . . . . . . . . . . . . 91 16.2. Informative References . . . . . . . . . . . . . . . . . 93 Appendix A. Submit-Request Schema . . . . . . . . . . . . . . . 94 Appendix B. Result Schema . . . . . . . . . . . . . . . . . . . 96 Appendix C. Deliver-Notice Schema . . . . . . . . . . . . . . . 97 Appendix D. Inbound-Message Schema . . . . . . . . . . . . . . . 99 Appendix E. Binding-Record Schema . . . . . . . . . . . . . . . 101 Appendix F. Peer-Origin Profile Schemas . . . . . . . . . . . . 102 Appendix G. Peer-Directory Schema . . . . . . . . . . . . . . . 115 Appendix H. Fleet-Roster Schema . . . . . . . . . . . . . . . . 117 Appendix I. Golden Examples . . . . . . . . . . . . . . . . . . 119 I.1. Valid Submit Request with attachments . . . . . . . . . . 119 I.2. Valid Deliver Notice with a clean attachment Descriptor . . . . . . . . . . . . . . . . . . . . . . . 119 I.3. Valid Inbound Message . . . . . . . . . . . . . . . . . . 120 I.4. Valid Result — a policy hold with per-attachment diagnostics . . . . . . . . . . . . . . . . . . . . . . . 121 I.5. Invalid Deliver Notice — a content_ref that fails the binding . . . . . . . . . . . . . . . . . . . . . . . . . 121 I.6. Valid Deliver Notice carrying an unrecognized member . . 122 I.7. Invalid Submit Request carrying an unrecognized member . 122 I.8. Invalid Peer Notice — a deliver-kind document under the peer tree . . . . . . . . . . . . . . . . . . . . . . . . 123 Appendix J. Conformance Suite . . . . . . . . . . . . . . . . . 123 Rapp & Adams Expires 24 April 2027 [Page 4] Internet-Draft AMMP October 2026 Appendix K. A Deferred Seam-Visible "Handled" Signal (Non-Normative) . . . . . . . . . . . . . . . . . . . . . 135 Appendix L. Historical Background . . . . . . . . . . . . . . . 137 L.1. draft-rapp-ammp-00 — brought to the IETF (contract 3.1.0) . . . . . . . . . . . . . . . . . . . . . . . . . 137 L.2. How the versioning rules came about . . . . . . . . . . . 138 L.3. 3.1.0 — the protocol is AMAP; the outbound extension space; the peer-origin profile . . . . . . . . . . . . . . . . . 140 L.4. 3.0.0 — direction-aware tolerance, and six closed safety holes . . . . . . . . . . . . . . . . . . . . . . . . . . 143 L.5. 2.3.0 — inbound byte access in force . . . . . . . . . . 146 L.6. 2.2.0 — deletion rights, read-only inbound . . . . . . . 146 L.7. 2.1.0 — the body spool, and three optional Result members . . . . . . . . . . . . . . . . . . . . . . . . . 146 L.8. 2.0.0 — attachments, and contract_version required . . . 147 L.9. Fixture identifier normalization . . . . . . . . . . . . 147 Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 147 1. Introduction — Motivation and Problem Statement AI agents increasingly need to correspond, by email above all. They are tasked over it, they return results over it, and they correspond over it with people and with systems that speak no other protocol. An agent is nonetheless an untrusted process: it can be prompt- injected by the very content it is asked to read, it can be defective, and it can be compromised outright. Granting such a process email capability directly is dangerous along three concrete axes. *Inbound.* Delivered mail is untrusted content routed directly into the agent's reasoning. A message body, a subject line, a sender display name, or an attachment filename can carry instructions the agent was never meant to follow. The delivery path is therefore an injection vector by construction, and no amount of care inside the agent removes that property. *Outbound.* An agent that can send mail can exfiltrate anything it has read, to any address it chooses, and can do so under an identity that recipients associate with its operator. Outbound mail is simultaneously a data-loss channel and an impersonation channel. *Credentials and identity.* Any mailbox credential, API token, or send endpoint held inside the agent's sandbox can be read, copied, or abused by whatever compromises the agent. Worse, an agent holding a credential has no identity of its own that a recipient — or its own operator — can verify independently of that credential. Rapp & Adams Expires 24 April 2027 [Page 5] Internet-Draft AMMP October 2026 1.1. Why the existing options are insufficient Handing the agent mailbox credentials, or a mail-send API, makes the agent a _trusted_ participant in the mail system. That is precisely the wrong posture for a component whose defining risk is that it may be compromised: the trust must be withdrawn exactly when it is least possible to detect that it should be. Wrapping the credential in a helper process that the agent calls does not change the analysis by itself, because a callable send capability is still a capability the agent holds; what matters is whether the capability crosses the boundary at all. The common alternative is per-deployment glue: a bespoke script or service wired between one agent harness and one mail system. Such glue may even be correct, but it produces no reviewable contract, no interoperability, and no shared conformance bar. Nothing tells a second implementer what the first one guarantees, and nothing tells either of them what a third-party reviewer should check. Adjacent agent protocols do not fill the gap, because they are solving a different problem: integration and interoperation _among credentialed participants_, rather than isolation of an _untrusted_ one. Section 3.4 positions AMMP against that landscape in detail. 1.2. The approach The premise is isolation. The agent runs in a sandbox, and the only path from that sandbox to its correspondence is the one this document specifies. Isolation is what makes an agent safe to connect, and it is also what makes it operable: a process that is contained can be replaced, replayed and audited as a unit. This document specifies what crosses the boundary, not the sandbox itself. AMMP interposes a *trusted runtime* between the agent and the systems that carry its messages, holding each message before forwarding it. The runtime holds every credential, owns the agent's sending identity, and makes every policy decision. The agent interacts with the runtime only through a channel that carries *no capability*: it writes an inert request file into a directory, and the runtime — independently, on its own schedule, over its own private copy of those bytes — decides what, if anything, happens next. In the other direction the runtime authenticates and screens each message before writing a notice describing it into the agent's namespace, and withholds the bytes of anything it will not vouch for. The payoff, which the rest of this document formalizes, is the load- bearing invariant: Rapp & Adams Expires 24 April 2027 [Page 6] Internet-Draft AMMP October 2026 No security property depends on the agent — or on the connector speaking for it — behaving correctly. Enforcement is entirely runtime-side. A compromised agent or connector can at most write a well-formed request that the runtime then refuses. AMMP is the wire contract at exactly that trust boundary, and is owned by neither side of it. That ownership choice is what allows a runtime and a connector built by different parties, on different schedules, to interoperate and to co-evolve against a versioned contract with an offline conformance suite. 1.3. Why a filesystem namespace rather than an API, RPC, or queue The medium _is_ the boundary. An API, an RPC endpoint, or a message queue is a live capability: something the agent invokes, whose invocation is itself the grant. Its security therefore rests on authenticating and authorizing each call, on the client library behaving, and on the transport being what it claims to be. A file written into a directory grants nothing at all. It has no callee, no session, and no side effect until a separate, trusted process chooses to read it — at a time of that process's choosing, into memory of its own, under validation of its own. The runtime is free to ignore the file, to refuse it, to hold it for human review, or to consume it and answer with a verdict. Section 3.3 develops this point, along with the obligations it puts on the runtime in exchange. 1.4. Design goals * *Capability-free isolation.* The channel available to an untrusted agent grants no send capability, no credential, and no reach outside its own namespace. * *Vendor-neutral interoperability.* A runtime and a connector built independently interoperate because both are built against this contract rather than against each other. * *Versioned co-evolution with offline conformance.* Either side proves conformance against a fixture suite with no counterpart present, and version changes are deliberate, legible, and fail closed rather than silent. * *Composition with real email.* Interactions remain federated, of- record, and human-inclusive, rather than confined to a closed agent-only channel. * *Portability across agent runtimes.* Nothing in the contract presumes a particular agent harness, language, or tool interface. Rapp & Adams Expires 24 April 2027 [Page 7] Internet-Draft AMMP October 2026 1.5. Non-goals * *AMMP is not a mail transport.* It neither defines nor replaces SMTP, IMAP, or any provider-side mail infrastructure. It describes what a runtime and a connector exchange _locally_; whatever the runtime does with a mail provider is out of scope. * *AMMP is not agent-to-agent RPC.* The core contract is local to one deployment's filesystem, between two components on either side of a single trust boundary. The optional peer-origin profile (Section 12) lets Agents of one principal write to one another, but as messages: between hosts they travel as ordinary mail, signed by the sending Runtime. The profile defines no call or return semantics, no discovery mechanism, and no transport of its own. * *AMMP is not a policy language.* What a runtime's policy decides — which recipients are permitted, which content is held for review, how a sender's standing is computed — is deliberately out of scope. The wire defines only the shapes of requests and verdicts, never the computation that produces them. Every obligation in this document is grounded in what the contract actually provides. Where a capability is sketched but not in force, it is marked as such (Appendix K), and where a property is a runtime obligation rather than a wire-verifiable fact, this document says so (Section 14.14). 1.6. Why This Work Is Brought to the IETF This section is to be removed before publishing as an RFC. AMMP began as an implementation: a Runtime and Connectors built separately against a shared contract, with an offline conformance suite so that each side could be tested without the other. It has been exercised in deployments of several cooperating Agents from more than one vendor, but every deployment so far has run under a single operator. The authors bring it to the IETF for the things that implementation alone cannot supply: * *Review.* The design rests on a security argument (Section 3.2), and that argument needs review by people who did not make it, particularly from the mail, security and identity communities. * *Interoperation.* A contract owned by neither side is useful only if independent implementers build both sides. The authors are looking for implementers and for interoperation testing across organizations. Rapp & Adams Expires 24 April 2027 [Page 8] Internet-Draft AMMP October 2026 * *Collaborators.* The work should be shaped by more than one organization, and by the people who operate mail at scale. * *Alignment with related work.* AMMP deliberately adds no new transport and no new identity scheme: email stays SMTP, and how an Agent's identity is established is left to work designed for that purpose. Where AMMP touches such work, it should follow it rather than invent its own. Much of this document records why a rule is the way it is, because the reasoning is what a reviewer needs in order to disagree with it. None of it is offered as settled. The questions below are ones the authors already know are open; others will surely be raised, and every one of them deserves a fresh look now that more people are reading the work. * *The wire version.* The wire version starts at "2" rather than "1", because implementations already emit and check "2" (Section 13.1). Whether to reset it is a fair question. * *Registries.* Whether the wire version, the Result reason_code vocabulary, or the Control Notice's outcome and cause vocabularies should be governed by IANA (Section 15). * *The name of the attestation member.* In this document it is the identity stamp a Runtime applied to an outbound message (Section 6.1). In IETF usage, attestation usually means remote attestation of platform state, and the member may need another name. * *Identity between Runtimes.* The peer-origin profile signs traffic between hosts with Ed25519 keys configured per Runtime (Section 12.6.7). Whether that identity should instead be carried by an existing mechanism is open, and the candidates differ in what they would replace: a workload-identity system such as SPIFFE would replace the configured keys (distribution and attestation), while a standard signed envelope such as JWS with EdDSA (RFC 7515, RFC 8037) or COSE (RFC 9052) would replace the statement's own encoding, which is close to a detached JWS already. DKIM and ARC sign on behalf of a mail domain rather than a Runtime, so they neither bind a message to the issuing Runtime nor carry the statement's members. * *Document structure.* Whether the peer-origin profile belongs in this document or in a companion document. Rapp & Adams Expires 24 April 2027 [Page 9] Internet-Draft AMMP October 2026 * *Internationalized addresses.* Addresses are restricted to a narrow ASCII addr-spec (Section 5.1), and internationalized addresses are out of scope for now. * *A consumption signal.* Whether a Connector should be able to tell its Runtime that a Notice has been handled (Appendix K). 2. Conventions and Terminology The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. Every message defined by this document is a JSON text as defined by [RFC8259], encoded in UTF-8 [RFC3629]. The schemas in Appendix A through Appendix H are written in the JSON Schema 2020-12 dialect [JSON-SCHEMA-2020-12]. Long lines in the JSON examples and schemas in this document are folded using the single-backslash strategy of [RFC8792]. Each folded block begins with that strategy's header line, which is part of the folded content: it is what allows the original to be reconstituted mechanically, so it is kept inside the block. Sections and subsections explicitly marked _(informative)_ or _(non- normative)_ carry no requirements; Section 3.4, Section 3.5, Appendix K, and Appendix L are the only such parts of this document. 2.1. Terminology * *Admitting Component* — in the peer-origin profile, a Runtime component that admits messages from a transport not restricted to authorized peer senders; a mail deliverer is the usual example. It does not hold write authority over the peer tree (Section 12.3). * *Agent* — the untrusted process whose messaging capability this protocol mediates: email, and under the peer-origin profile, messages to other Agents. It holds no credential, has no send path of its own, and reaches the protocol only through its Connector. Nothing in this document places a requirement on the Agent, because no security property may depend on it. * *Binding Record* — the optional, informational artifact at the Volume root naming which mailbox the Volume is bound to (Section 9). Rapp & Adams Expires 24 April 2027 [Page 10] Internet-Draft AMMP October 2026 * *Connector* — the per-Agent component that speaks the Agent-side half of this contract: it writes Submit Requests, reads Results and Notices, and surfaces them to its Agent in whatever form its harness uses. It is semi-trusted at best; for the purpose of every security property in this document it is treated as untrusted, and the requirements placed on it are well-formedness and non-crashing obligations, never obligations another party's safety rests on. * *Control Notice* — in the peer-origin profile, the signed report a receiving Runtime sends to a configured sending Runtime about a Peer Statement it did not place (Section 12.6.8). * *Descriptor* — one entry in an attachments[] array, describing an attachment (its display filename, media type, size, and — depending on direction — digest, Disposition, or content reference). A Descriptor is metadata, not a path. * *Disposition* — the Runtime's assertion about an inbound attachment's handling: clean, stripped, quarantined, or unscanned (Section 7.2). * *Drop* — to discard an artifact without surfacing it. Where this document says a consumer drops an artifact, the consumer MUST NOT surface the artifact to its Agent and MUST NOT act on it. A drop is a consumer's decision about one artifact; it is unrelated to the outbound tree, into which a Connector writes requests. * *Fail Closed* — to refuse to process an artifact rather than proceed on a partial or guessed interpretation of it. * *Inbound Message* — the Runtime-authored artifact carrying a delivered message's body text and headers, spooled under a Notice's identifier and fetched on demand (Section 7.3). * *Lane* — one class of traffic between an Agent and its Runtime, or between two Agents: the mail lane (inbound/ and outbound mail) and, under the peer-origin profile, the peer lane (peer/). The Peer Directory states, per peer, which lanes are open. * *Namespace* — the per-Agent subtree //, mounted into that Agent's sandbox. It is the unit of isolation and the sole authority for attributing an artifact to an Agent. * *Notice* (Deliver Notice) — the Runtime-authored artifact announcing that a message was delivered to the Agent's mailbox (Section 7). It is a pointer and summary, not the message. Rapp & Adams Expires 24 April 2027 [Page 11] Internet-Draft AMMP October 2026 * *Ordinal* — the integer naming a Sidecar blob. It equals the index of the corresponding Descriptor in its array, and is the sole binding authority between a Descriptor and its bytes. * *Origin Class* — the class of origin a consumer assigns to an inbound document, derived only from the Tree it was read from: mail for inbound/, and same-principal peer origin for peer/ (Section 4.1, Section 12.3). * *Peer Notice* — the Runtime-authored artifact, in the peer-origin profile, announcing a message from a same-principal Agent; the Deliver Notice shape with kind "peer", placed under peer/notices/ (Section 12.2). * *Peer Statement* — the signed JSON object a sending Runtime attaches, in an AMAP-Peer-Statement header field, to peer traffic it sends to another host (Section 12.6.1). * *Result* — the Runtime-authored verdict for one consumed Submit Request (Section 6). * *Runtime* — the trusted, credentialed, Agent-agnostic, store-and- forward messaging component that speaks the other half: it holds the messaging credentials, owns each Agent's identity, makes every policy decision, and is the only party that transmits or receives messages. It holds each message before forwarding it: an outbound message is copied into private staging and checked before anything is transmitted, and an inbound message is screened before a Notice describing it appears. * *Sandbox* — the isolation boundary within which an Agent and its Connector run. The Runtime mounts the Agent's Namespace into it and nothing else of the Volume; no credential, send path or other messaging capability exists inside it. This document does not specify the sandbox mechanism: it presumes one, and every property it states holds only as well as the Sandbox holds. * *Sidecar* — attachment bytes stored beside a Submit Request or a Notice, in a directory named after that artifact, with each blob named by a 0-based ordinal. * *Staging* — Runtime-private storage outside the Volume, unreachable from any Agent's mount, into which the Runtime copies Agent-authored bytes before acting on them (Section 4.1). * *Submit Request* — the Connector-authored artifact requesting that a message be sent (Section 5). It is inert: it carries no capability and effects nothing by existing. Rapp & Adams Expires 24 April 2027 [Page 12] Internet-Draft AMMP October 2026 * *Tree* — one of the Runtime-owned inbound-class subtrees of a Namespace: inbound/, and, under the peer-origin profile, peer/. Each Tree is an Origin Class. * *Volume* — the runtime-owned filesystem tree carrying this protocol, containing one Namespace per Agent plus an optional Binding Record at its root. Its mount path is a deployment choice; this document writes / for it and nothing in the protocol derives from its name. No implementation may depend on that name or any other. 3. Architecture and Trust Model 3.1. Actors +=============+===============+====================+==============+ | Actor | Trust | Holds | Reaches | +=============+===============+====================+==============+ | *Runtime* | trusted | messaging | the mail | | (host-side) | | credentials, Agent | provider, | | | | identity, policy, | the Volume | | | | the record of what | | | | | was sent | | +-------------+---------------+--------------------+--------------+ | *Connector* | semi-trusted, | nothing | its own | | (per Agent) | Agent-side | credentialed | Namespace on | | | | | the Volume; | | | | | its Agent's | | | | | input and | | | | | output | +-------------+---------------+--------------------+--------------+ | *Agent* | *untrusted* | only what the | the | | | | Connector surfaces | Connector; | | | | | its sandbox | +-------------+---------------+--------------------+--------------+ Table 1 The Runtime is trusted by construction: it is the party holding the credentials, and a compromise of it is a compromise of the messaging capability itself. The Connector is the interesting case. It runs Agent-side, is reachable by whatever compromises the Agent, and is therefore treated as untrusted for every property that protects the Runtime, other Agents, or the outside world. Requirements this document places on a Connector fall into two distinct classes, and the difference matters when reading them: Rapp & Adams Expires 24 April 2027 [Page 13] Internet-Draft AMMP October 2026 * *Well-formedness and liveness obligations* — emit valid documents, tolerate a missing artifact, do not crash on hostile input. A Connector that violates one harms itself and its own Agent, not any other party. * *Obligations that protect an Agent from its own inbox* — the untrusted-content posture of Section 7. These are necessarily Connector-implemented, because the Connector is the only component on the Agent's side of the boundary, and they are stated as such rather than as properties the Runtime relies on. No requirement in this document places a Runtime-side, other-Agent- side, or outside-world safety property in a Connector's hands. The Connector's obligation not to rewrite a published Submit Request might appear to be one; Section 4.1's ingest-by-copy rule removes that dependency, leaving the obligation as pure well-formedness. 3.2. The load-bearing invariant *Invariant.* No security property may depend on the Agent or the 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. Everything else in this document is downstream of that sentence. It is why the Agent's declared identity is a cross-check rather than an attribution source (Section 4.1); why the Runtime re-derives every path from data it controls rather than from a file's contents (Section 4.1); why recipient policy is evaluated Runtime-side over the exact recipient set that will be transmitted to (Section 5.1); why the digest an Agent supplies for its own attachment bytes is an integrity aid and not a trust boundary (Section 5.1); and why the absence of bytes, rather than a flag inside a document, is the access gate on inbound attachments (Section 7.2). The invariant also bounds what this document can promise. A wire contract cannot make a Runtime honest, and a fixture suite cannot observe a filesystem. Section 14.14 states which properties are wire-verifiable and which are Runtime obligations that a green conformance run does not prove. 3.3. Why a filesystem namespace The Runtime mounts each Agent's Namespace into that Agent's sandbox, and the Agent's entire protocol surface is that directory. This is a deliberate choice of medium, with three consequences. Rapp & Adams Expires 24 April 2027 [Page 14] Internet-Draft AMMP October 2026 *Writing a file carries no capability.* It invokes nothing. There is no callee to authenticate, no session to hijack, no method whose mere reachability is a grant. The request exists as bytes until a trusted process elects to look at them. Compare an API or queue, where the act of enqueuing is itself the privileged act and every security property depends on the authorization check in front of it being correct and unbypassable. *Isolation is structural rather than enforced by policy.* A Connector scoped to // cannot name another Agent's Namespace at all. Attribution follows from _where a file appeared_, not from what the file says about itself, so spoofing attribution requires defeating the mount rather than lying in a JSON member. *The medium's costs are real and are paid explicitly.* A shared filesystem between a trusted and an untrusted party is itself an attack surface: symlinks, directory swaps, special files, hard links, and unbounded sizes are all available to the untrusted side, and every path-based check the trusted side performs is racy against a concurrent rename. This document therefore does not treat "it is just a directory" as a simplification. Section 4.1 states the path, open, copy, and concurrency discipline a Runtime observes in exchange for the medium's advantages, and Section 14 revisits each as a threat. 3.4. Relationship to Other Agent Protocols (Informative) _This subsection is informative. It contains no requirements and creates no dependency on any external protocol. Nothing here claims that AMMP replaces, subsumes, or competes with any protocol named below._ AMMP occupies a narrow position: it is the local seam at a single trust boundary, where one side holds every credential and the other holds none. The adjacent protocols in the agent ecosystem sit at different layers, and the common question of "why not use one of them here" has a specific answer in each case. *Model Context Protocol (MCP)* [MCP] connects an agent host to tools and context through JSON-RPC servers. It is an integration interface: live capabilities are exposed across it and exercised by the agent through it. That makes MCP complementary to AMMP rather than an alternative. A Connector often surfaces AMMP's operations to its Agent through MCP, or through whatever tool interface its harness uses natively; AMMP is the capability-free seam sitting behind that surface. The contrast worth stating is exactly the one that motivates the two coexisting: an MCP-style interface grants live capability across the boundary it spans, while AMMP's channel Rapp & Adams Expires 24 April 2027 [Page 15] Internet-Draft AMMP October 2026 deliberately grants none — the Agent-side surface can be as rich as the harness likes, because what crosses the trust boundary underneath it is an inert file. MCP originated at Anthropic. *Agent2Agent (A2A)* [A2A] is networked agent-to-agent RPC between mutually authenticated services, with agent cards, tasks, and HTTP/SSE/JSON-RPC transport. It presumes both endpoints are credentialed participants that can authenticate to each other. AMMP's design center is the opposite: an untrusted agent that holds no credentials at all, with enforcement placed entirely on the trusted side of a local boundary. The two are orthogonal, and compose without friction — an agent that speaks A2A to its peers can use AMMP for its mail capability, and neither protocol needs to know about the other. A2A originated at Google and is hosted by the Linux Foundation. *The two protocols abbreviated "ACP" are not the same protocol*, and the collision is worth disambiguating in a document a reviewer may arrive at with one of them in mind. * The *Agent Communication Protocol* (IBM/BeeAI) is of historical interest here: it merged into A2A under the Linux Foundation, announced in August 2025, with its repository archived [ACP-MERGER]. * The *Agent Client Protocol* [ACP-CLIENT] is a distinct and currently active protocol at a different layer — an interface between a host or editor and an agent — originated by Zed Industries. It is unrelated to the merged Agent Communication Protocol and unrelated to AMMP. 3.5. Runtimes Built from Several Components (Informative) _This subsection is informative. It contains no requirements._ The Runtime is defined by its role, not as a single process. A Runtime may be built from several trusted components: for example, one that drains Agent Namespaces and applies policy, and another that holds the mail credentials and talks to the provider. Every obligation this document places on the Runtime binds the composite as seen from the Agent's Namespace. How the components divide the work, and how they communicate, is Runtime mechanism and out of scope. They may communicate by any means, for example SMTP [RFC5321] between hosts or LMTP [RFC2033] within one; this document specifies only the Agent boundary. Some existing obligations are easy to lose when the work is divided: Rapp & Adams Expires 24 April 2027 [Page 16] Internet-Draft AMMP October 2026 * exactly one Result per consumed request, including when an inner component fails or never answers (Section 6.1); * accepted only once the provider has accepted the message for relay (Section 6.1); * the recipient set handed to the provider is exactly the set on which recipient policy was evaluated (Section 5); * nothing quarantined or unverified becomes a Notice (Section 7), and clean is asserted only for bytes the publishing component has verified, with every content_ref bound to the Notice it sits in (Section 7.2); * attribution is carried between components by Runtime configuration, never by anything the Agent wrote (Section 4.1); and * only the component facing the Agent's Namespace writes the Agent's lanes. 4. Volume Layout The Runtime owns the Volume and mounts each Agent's Namespace into that Agent's sandbox. A Connector has access *only* within its own /, and only to the paths below. Within that Namespace, write access may be narrower still: the inbound/ subtree is Runtime- owned and MAY be mounted read-only. The Volume root's own name is a *deployment choice and is not part of this protocol* — nothing derives from it, and no implementation may depend on it. / below stands for whatever a deployment calls it. Rapp & Adams Expires 24 April 2027 [Page 17] Internet-Draft AMMP October 2026 / identity.json # runtime/provisioner: Binding Record (S9) / outbound/ req-.json # agent -> runtime: Submit Request req-.attachments/ # agent -> runtime: Sidecar bytes, # ordinals 0 1 2 ... results/.json # runtime -> agent: verdict for req processed/req-.json # runtime: consumed requests ext// # connector-owned side channel a # runtime MAY read by out-of-band # agreement; is a STABLE, # opaque connector id inbound/ notices/notice-.json # runtime -> agent: delivered event notices/.attachments/ # bytes asserted clean, # ordinals 0 1 2 ... (S7.2) notices/processed/ # OPTIONAL - connector archive, # writable-inbound deployments only messages/notice-.json # runtime -> agent: full message cursor.json # runtime: inbound poll cursor peer/ # OPTIONAL - peer-origin profile: # same layout as inbound/ directory.json # OPTIONAL - runtime: Peer Directory audit/ log.jsonl # runtime: append-only action log Figure 1: Volume layout (illustrative — not a fixture) There is exactly one inbound Sidecar directory — inbound/ notices/.attachments/ — and both a Deliver Notice's and an Inbound Message's content_ref resolve there. No second, message- specific Sidecar directory exists. 4.1. Obligations *Attribution by Namespace.* is assigned by the Runtime, not declared by the Agent. Attribution is _the Namespace a file arrived in_; an agent_id member inside the file is a cross-check only. A Runtime MUST reject a Submit Request whose agent_id, when present, does not match the Namespace it was found in, and MUST NOT use that member to attribute the request. A Runtime MUST know the of every Namespace it serves: from its own configuration or, where its own layout carries it, from the Namespace's path, and MUST NOT take it from anything the Agent writes. A Runtime that cannot name the Namespace in which it found a Submit Request MUST reject that request rather than accept its agent_id unchecked. Rapp & Adams Expires 24 April 2027 [Page 18] Internet-Draft AMMP October 2026 *Isolation is structural.* A Connector scoped to / cannot read another Agent's drafts or Notices. *All writes are atomic.* Every artifact in the tree above is published by writing a .tmp sibling and then renaming it into place (os.replace or the equivalent). Readers MUST ignore *.tmp entries. *Single writer, single drainer.* At any moment a Namespace has exactly one Connector instance writing outbound/ and exactly one Runtime instance draining it. Every concurrency-sensitive rule in this document — the atomic .tmp-then-rename discipline above, the commit sentinel below, and the derivation of req_id from max(pending, processed, results) + 1 (Section 5.1) — is sound only under that assumption and provides no mutual exclusion of its own. Each side MUST ensure single-instance operation over a Namespace by a mechanism outside this protocol, such as an OS-level lock held for the process lifetime. A Runtime MUST NOT run two concurrent drains of one Namespace, and a Connector MUST NOT run two concurrent writers into one. Behavior under violation is unspecified: duplicated delivery, clobbered published requests, and false verdicts are all reachable. *Filename identifiers are the authority for paths.* An identifier taken from a file's _contents_ — a req_id member in a request body, for instance — MUST NOT determine a write path. The Runtime derives identifiers from the validated filename, on a restricted character set. *Write-side path discipline.* Every write, rename, directory creation, or archive move into or within a Namespace MUST satisfy all three of the following *conjunctively*. Each closes a distinct gap the other two leave open, and none is sufficient alone. * *(a) Full parent-chain pinning.* The write resolves its full parent chain through pinned directory descriptors from the Namespace root — openat, mkdirat, renameat off a descriptor the Runtime opened once per Namespace — and MUST NOT re-resolve a path string for the act itself. A path-based containment check followed by a path-based act is a race, not a check: the parent directory the check inspected and the parent directory the act touches are not guaranteed to be the same object, because the Agent can swap a parent directory for a symlink in between. Applying a no-follow flag to the _final_ path component alone does not close this gap — it constrains only the last component, not a swapped parent — and rename and directory-creation calls have no no-follow equivalent at all, so a bare "writes do not follow symlinks" requirement is not sufficient on its own. Rapp & Adams Expires 24 April 2027 [Page 19] Internet-Draft AMMP October 2026 * *(b) No-follow while building the chain.* Each directory component that goes into that parent chain MUST itself have been opened with O_NOFOLLOW|O_DIRECTORY at the moment it was added to the pinned chain. A descriptor counts as pinned only if the open that produced it refused to follow a symlink at that step. Requirement (a) without this is equally insufficient, and the gap is the symmetric one: nothing otherwise stops the Runtime from _acquiring_ a "pinned" descriptor by following an Agent-planted symlink in the first place — the Agent replaces a Namespace subdirectory with a symlink before the Runtime's once-per- Namespace open, or before a later re-open after a restart — and a descriptor obtained that way is pinned to the wrong object, so every write built off it inherits the redirection no matter how faithfully (a) is followed from there. * *(c) Non-following final component.* The final path component MUST NOT be followed if a symlink exists at that name: the open MUST carry O_NOFOLLOW and MUST fail closed on ELOOP rather than writing through whatever the name currently resolves to. Two cases differ in what else they require, and both are covered: - *A newly created file* — the .tmp sibling the atomicity rule above already requires, and any Sidecar blob — MUST additionally be created with O_EXCL (O_WRONLY|O_CREAT|O_EXCL, never O_TRUNC in place of O_EXCL), failing closed on EEXIST. - *An append-mode or directory-creating write that has no .tmp sibling* — audit/log.jsonl is the case this document names — cannot use O_EXCL, because the target legitimately already exists. It MUST instead open with O_NOFOLLOW|O_APPEND|O_NONBLOCK (or use mkdirat with the parent pinned per (b)), MUST NOT block on a special file, and MUST verify through the resulting descriptor (fstat, never a pre- open stat) that the object is a regular file it may append to; it SHOULD refuse a target whose link count exceeds 1. The non- blocking open is load-bearing and is not a performance concern: O_NOFOLLOW does not apply to a FIFO — a FIFO is not a symlink — and opening one for writing blocks until a reader appears, so without O_NONBLOCK the fstat is never reached and an Agent that plants a FIFO at this name stalls the drain indefinitely. The first branch's O_EXCL refuses that case on its own; the second branch has no O_EXCL to lean on and states it explicitly. Requiring O_EXCL in the second branch would forbid appending to an existing log at all; requiring nothing there would leave the one write this document names that has no .tmp sibling unprotected. Rapp & Adams Expires 24 April 2027 [Page 20] Internet-Draft AMMP October 2026 Requirement (c) is needed in both of its branches because an Agent can pre-plant a symlink at a filename the Runtime is about to write. req_id is Agent-chosen, and every Runtime-written filename in the layout tree above — results/.json, the .tmp sibling of processed/req-.json, inbound/notices/notice-.json, inbound/messages/notice-.json, audit/log.jsonl — is reachable to an Agent that can predict or observe it. Absent (c), such a name gets its bytes written through the planted symlink with the Runtime's own privileges, even though the parent chain that reached the containing directory was itself correctly pinned under (a) and (b). This discipline governs every Runtime-side write named in the layout tree, not only the initial consumption of an Agent-authored artifact. *Read-side discipline.* The same rule applied to reads. When the Runtime opens any path the Agent can influence — every req-.json, every Sidecar ordinal, and, in a deployment that leaves inbound/ writable, anything it later reads back from there — it MUST open without following symbolic links at any component (per-component O_NOFOLLOW, or an equivalently pinned directory-descriptor walk from the Namespace root, matching the write-side rule above), MUST NOT block on a special file (a non-blocking open), MUST verify that the opened object is a regular file through the open descriptor (fstat and an S_ISREG test — never a pre-open stat, which is a race), SHOULD refuse a file whose link count exceeds 1, and MUST enforce a deployment-configured per-file size cap (a bounded read) and a per- request Sidecar count cap. The too_large value in the Result vocabulary (Section 6.1) presumes that cap: enforcing one is an obligation, not an option. A Connector's one Agent-influenceable read path — resolving a notice_id to inbound/messages/notice-.json — MUST be built only from a pattern-validated notice_id (the schemas pin the pattern) and MUST confine resolution to the Connector's own inbound/ tree (Section 7.3). *Sidecar ordinal binding.* Sidecar blobs are named by Runtime- assigned 0-based ordinals equal to the Descriptor's array index, which is the sole binding authority. Nothing else names a blob; a Descriptor's filename is display-only, and MUST NOT be used as a path. Rapp & Adams Expires 24 April 2027 [Page 21] Internet-Draft AMMP October 2026 *Commit sentinel.* All Sidecar bytes are written — .tmp sibling, then rename — *before* req-.json; the JSON file's presence is the only publish signal. The Runtime MUST verify that every referenced ordinal exists and MUST re-verify size and digest before acting on the request, against the snapshot it took at ingest (below), never against the live path. *Inbound Sidecar directories are keyed on the Runtime-minted notice identifier.* A Runtime MUST NOT name one after the provider message.id. The provider identifier is external and is not character-set-safe, so using one as a path component would let a sender influence where the Runtime writes. The same rule extends to the body artifact: inbound/messages/notice-.json MUST be keyed on the notice identifier, and MUST NOT be keyed on message.id. Inbound bytes are published only at inbound/notices/.attachments/, the single Sidecar directory named in the layout tree. *Runtime-private Staging lives outside the mounted Volume* — at a path the Agent's mount cannot reach at all. *Ingest by copy.* Before validating, evaluating policy on, or composing from any Agent-authored artifact — req-.json and every Sidecar ordinal — the Runtime MUST copy the bytes into Runtime- private Staging and MUST act only on that private copy. All checks — schema validation, recipient policy, size and sha256 verification — and all message composition MUST run over that snapshot, and the Runtime MUST NOT re-read any Agent-writable path after a decision that depends on its content. Verify-then-use against a live Agent- writable path is a race, not a check. This obligation is what makes the Connector's "MUST NOT delete or rewrite a published request" (Section 4.2) a pure well-formedness rule: a Runtime MAY treat an observed post-publish change as malformed, but no safety property depends on the Connector honoring immutability — which is what the invariant of Section 3.2 requires. (This governs the properties that protect the Runtime, other Agents, and the outside world. Obligations that protect an Agent from its own inbox are necessarily Connector-implemented and are stated as such, not as properties the Runtime relies on a Connector for.) *Byte-absence is the gate.* The Runtime MUST NOT place non-clean bytes in an Agent's Namespace. content_ref is the Runtime's access grant, present only for bytes it has published as clean (Section 7.2). Rapp & Adams Expires 24 April 2027 [Page 22] Internet-Draft AMMP October 2026 *Tree rules.* Every Runtime-owned inbound-class tree is an *origin class*: a consumer MUST derive the class of a document from the tree it was read from, and MUST NOT derive it from any member of the document. notice_id uniqueness is per tree, and where a tree has more than one writer each MUST mint so that cross-writer collision is negligible — at least 128 bits of entropy under the identifier character set, or a writer-specific prefix — and a consumer keying state across trees MUST include the tree in that key. content_ref resolves only within the notices/ directory of the tree the enclosing document was read from. Write authority per tree is distinct and MUST be structurally enforced; it MUST NOT depend on configuration the writer itself reads. inbound/ is the one tree the core protocol defines; a profile, such as the peer-origin profile of Section 12, MAY define others. *Inbound is Runtime-owned, and read-only is a valid posture.* The inbound/ tree is Runtime-owned end to end. A Runtime MAY expose it to the sandbox read-only — for instance as a read-only mount, which is the mechanism that actually makes a delivered Notice unforgeable, since file permission bits do not bind a same-uid Agent, which can change a mode it owns. A Connector MUST NOT require write access anywhere under inbound/: it MUST start, relay Notices, and resolve bodies when every write, rename, and directory creation under inbound/ fails. inbound/notices/processed/ is OPTIONAL — a conventional archive location a Connector MAY use only where a deployment leaves inbound writable; nothing on either side may depend on its existence or its contents. *Consumption tracking is Connector-private.* How a Connector records which Notices it has already relayed to its Agent is Connector- private state, outside this protocol. Such state lives in the Connector's own storage outside the Volume, or in Connector-owned paths under outbound/ not enumerated in the layout tree — which MUST NOT use the req- prefix, reserved for Submit Requests and their Sidecars, so that a Runtime's scan of outbound/ never encounters Connector-private state. Either way, the Runtime neither reads nor relies on it. A seam-visible consumption signal is deliberately not specified by this document; Appendix K records the reasoning. *Outbound extension space.* outbound/ext// is a Connector-owned side channel, one directory per , that a Runtime MAY read by out-of-band agreement. * is a stable Connector id: the Connector chooses it once, and it MUST NOT change when the Connector's repository, package, or deployment is renamed or moved. A Runtime that reads the directory MUST treat as opaque — it is an identifier, never a name to be parsed, resolved, or matched against a repository or path.* Because it is also a path segment in an Agent-writable tree, MUST be a single path segment of 1 to Rapp & Adams Expires 24 April 2027 [Page 23] Internet-Draft AMMP October 2026 64 characters drawn from lowercase ASCII letters, digits, hyphen and underscore, beginning with a letter or digit, so that it names the same directory on case-sensitive and case-insensitive filesystems alike. This document specifies no shape for anything under it; it lies outside the req- prefix the Runtime scans, and nothing security- relevant may be built on it. Its first use is a Connector's per- Notice *outcome* file, written by the receiving Agent's own uid and therefore forgeable by construction. An outcome is a statement by the receiving side about what it did with a Notice; a party MUST NOT treat an outcome as proof of delivery or of non-delivery, and the Runtime's own delivery record is authoritative over it (Section 14.15). There is deliberately no inbound extension space: an inbound tree is a Runtime assertion about origin, and a Connector cannot know what a vendor-named assertion means. 4.2. Deletion rights This table names who may delete each artifact. It is *permissive*: a party exercising a right it already effectively had — a Runtime deleting from its own Runtime-owned tree, for instance — changes nothing about what conformant behavior looks like. +=======================+===========+===========+===================+ | Artifact |Written by |May be |Basis | | | |deleted by | | +=======================+===========+===========+===================+ | outbound/ |Connector |Runtime |Consumption — the | | req-.json | | |move to | | (pending) and | | |processed/, or | | req-.attachments/ | | |deletion once the | | | | |Result is written. | | | | |The Connector MUST | | | | |NOT delete or | | | | |rewrite a | | | | |published request; | | | | |the JSON file's | | | | |presence is the | | | | |commit signal, and | | | | |un-publishing is | | | | |unspecified. | +-----------------------+-----------+-----------+-------------------+ | outbound/processed/ |Runtime |Runtime at |The Runtime's own | | req-.json | |will; |archive. *Caveat:* | | | |Connector |Section 5.1 | | | |after |derives req_id | | | |reading the|from max(pending, | | | |matching |processed, | | | |Result |results) + 1, so a | Rapp & Adams Expires 24 April 2027 [Page 24] Internet-Draft AMMP October 2026 | | | |Connector that | | | | |deletes here (or | | | | |in results/) MUST | | | | |preserve req_id | | | | |monotonicity by | | | | |other durable | | | | |means, such as its | | | | |own counter. | | | | |Correspondingly, a | | | | |Runtime MUST NOT | | | | |rely on processed/ | | | | |contents as | | | | |durable state (for | | | | |deduplication, | | | | |say) — the | | | | |Connector may | | | | |delete there, and | | | | |the Runtime's own | | | | |record lives | | | | |outside this | | | | |protocol. | +-----------------------+-----------+-----------+-------------------+ | outbound/ |Runtime |Connector, |No signal in this | | results/.json | |once read; |protocol tells the | | | |Runtime by |Runtime that a | | | |retention |Connector has read | | | | |a verdict, so | | | | |Runtime-side | | | | |deletion SHOULD be | | | | |age-based and | | | | |generous: a Result | | | | |deleted before it | | | | |is read is a lost | | | | |verdict. | +-----------------------+-----------+-----------+-------------------+ | inbound/notices/ |Runtime |Runtime |Runtime retention | | notice-.json, | |only. |policy. Absent a | | inbound/messages/ | |Where a |seam-visible | | notice-.json, and | |deployment |consumption signal | | their .attachments/ | |leaves |(Appendix K), | | Sidecar directories | |inbound |retention MUST be | | | |writable, |conservative and | | | |the |age-based. A | | | |Connector |Connector already | | | |MAY move or|tolerates the | | | |delete per |disappearance of | | | |the |these artifacts: a | | | |optional |read against a | Rapp & Adams Expires 24 April 2027 [Page 25] Internet-Draft AMMP October 2026 | | |processed/ |missing one | | | |convention,|returns "not | | | |but MUST |resolved" | | | |NOT depend |(Section 7.3). | | | |on the | | | | |ability. | | +-----------------------+-----------+-----------+-------------------+ | peer/ notices, |Runtime |Runtime |The same retention | | messages and their | |only, as |rule as the | | .attachments/ Sidecar | |for the |inbound row | | directories (peer- | |inbound row|(Section 12.8). | | origin profile) | |above. | | +-----------------------+-----------+-----------+-------------------+ | inbound/cursor.json |Runtime |Runtime |Internal state. | +-----------------------+-----------+-----------+-------------------+ | audit/log.jsonl |Runtime |Runtime |The Connector MUST | | | |(rotation) |NOT delete it. | +-----------------------+-----------+-----------+-------------------+ | identity.json |Runtime or |Runtime or |Consumers already | | |provisioner|provisioner|tolerate its | | | | |absence | | | | |(Section 9). | +-----------------------+-----------+-----------+-------------------+ | outbound/ext// |Connector |Connector, |A Connector-owned | | | |at will; |side channel | | | |Runtime, |(Section 4.1). A | | | |only what |Runtime MUST NOT | | | |its out-of-|delete the | | | |band |directory itself | | | |agreement |or anything else | | | |says it |under it, and an | | | |consumes |operator reset is | | | |(an outcome|not consumption. | | | |it has | | | | |read, for | | | | |example) | | +-----------------------+-----------+-----------+-------------------+ | directory.json |Runtime |Runtime |Advisory and | | | | |Runtime-published | | | | |(Section 10). It | | | | |is not read back | | | | |as authorisation, | | | | |so deleting or | | | | |replacing it | | | | |carries no state. | +-----------------------+-----------+-----------+-------------------+ Table 2 Rapp & Adams Expires 24 April 2027 [Page 26] Internet-Draft AMMP October 2026 5. Outbound: Submit Request An Agent's only outbound action is writing an inert request file into its Namespace. The file carries no send capability; the Runtime enforces policy before anything leaves. A Submit Request is written to //outbound/ req-.json as a JSON text [RFC8259]. Its Sidecar bytes, if any, are written first, into req-.attachments/, one blob per Descriptor, named by that Descriptor's 0-based array index (Section 4.1, commit sentinel). NOTE: '\' line wrapping per RFC 8792 { "contract_version": "2", "req_id": "00000001", "agent_id": "agent.research", "ts": "2026-07-20T04:10:56Z", "in_reply_to": "1721457600000000000-4242", "draft": { "to": ["owner@example.com"], "cc": [], "subject": "Run report", "body_text": "The run finished; details below.", "reply_to_message_id": null, "attachments": [ { "filename": "report.pdf", "media_type": "application/pdf", "size_bytes": 48213, "sha256": "970e441b6c4ecd37abf2541e1276c680bdec8481cef9368ff\ 1d010fd6b784b07" } ] } } Figure 2: draft/examples/submit-request.json (illustrative — not a fixture) The normative shape is Appendix A. The Submit Request envelope is *closed*: additionalProperties is false at every level, and a Runtime MUST reject a request carrying any unrecognized member. Section 13.1 explains why this direction is closed while the Runtime-authored artifacts are open. Rapp & Adams Expires 24 April 2027 [Page 27] Internet-Draft AMMP October 2026 5.1. Field semantics and obligations * *contract_version* (REQUIRED) is the wire major version, the string "2". A Runtime MUST check it before schema validation and MUST fail closed on an absent, lower, or higher major (Section 13.1). * *req_id* (REQUIRED) is *monotonic per Namespace* and MUST NOT be reused for the life of the Namespace's outbound/ tree; derive it from max(pending, processed, results) + 1. Reuse collides message identifiers and triggers provider-side idempotency deduplication. req_id is pinned to a filename-safe character set by the schema, but it is not the path authority: the Runtime derives the path identifier from the validated filename (Section 4.1). * *agent_id* (OPTIONAL), if present, MUST match the Namespace the request was found in, and a Runtime MUST reject a mismatch. A Runtime MUST NOT use it as the attribution source (Section 4.1). * *ts* (OPTIONAL) is the Connector's timestamp for the request. It is Agent-authored and therefore advisory. * *in_reply_to* (OPTIONAL) correlates this request with prior inbound work. It carries either a Runtime-minted notice_id or a mail message identifier [RFC5322]. It is a *hint from an untrusted party* and MUST NOT by itself widen what the request is permitted to do — the same posture Section 8 fixes for task_id. * The Runtime derives the message identity and the From/Reply-To of the composed message itself; identity is Runtime-owned, and the Agent does *not* choose its sending identity. * *draft.to* is REQUIRED and carries at least one recipient; *draft.cc* is OPTIONAL and MAY be empty. *draft.subject* and *draft.body_text* are REQUIRED and MAY be empty strings. body_text is *plain text*; this document defines no alternative body representation. * *draft.reply_to_message_id* (OPTIONAL, nullable) names the message this draft replies to, as an [RFC5322] message identifier. It is composed directly into an In-Reply-To: header, which is why it carries a control-character pattern in the schema (below). Rapp & Adams Expires 24 April 2027 [Page 28] Internet-Draft AMMP October 2026 * *Recipient policy is a Runtime decision.* The Agent's chosen draft.to and draft.cc are *not* inherently trusted. Allowlisting, task-counterpart binding, and any other recipient rule are evaluated Runtime-side. A populated allowlist plus a fresh recipient is an exfiltration surface that the Runtime, not the Connector, must close. * *Recipient syntax and header-injection discipline.* Each draft.to and draft.cc element is exactly one bare addr-spec: no display name, address list, quoted local part, comment, IP literal, or folding whitespace. The schema pins the pattern. This profile is deliberately narrower than [RFC5322], because those excluded forms are exactly what makes allowlist evaluation non-mechanical; internationalized (EAI/SMTPUTF8) addresses are out of scope for now and may be added later. A Runtime MUST reject — with a top- level rejected Result — a request whose recipient entries do not parse under this profile, MUST evaluate recipient policy over exactly the recipient set it will transmit to, and MUST NOT transmit to any address not present in draft.to or draft.cc. Separately, a Runtime MUST NOT allow any Agent-authored string — subject, a recipient entry, an attachment filename, an attachment media_type, draft.reply_to_message_id, in_reply_to, agent_id, ts, or body_text where it abuts headers — to introduce a header boundary in the composed message. CR, LF, and NUL MUST be rejected or stripped before composition *even where schema validation was bypassed upstream*: the schema-level control- character patterns on subject, filename, media_type, reply_to_message_id, in_reply_to, agent_id, and ts are the first line of defense, not the only one. Two of those fields are easily overlooked: reply_to_message_id and media_type are composed _directly into headers_ — In-Reply-To: and a MIME part's Content- Type: [RFC2045] respectively — rather than into a body or a display field. * *draft.attachments[]* (OPTIONAL) is an array of Descriptors, each carrying filename, media_type, size_bytes, and sha256. *size_bytes and sha256 are REQUIRED on every Descriptor*, computed by the Connector over the *decoded* bytes it wrote, with sha256 in lowercase hexadecimal. There is *no outbound content_ref*. The Runtime re-derives each Sidecar's path from req- plus the Descriptor's array index, which is the sole binding authority; an Agent-supplied path string would be pure disagreement and attack surface, so it is absent from the schema entirely and additionalProperties: false rejects it. attachments: [] is valid and is equivalent to the member being absent; the Sidecar directory MUST then be absent. Rapp & Adams Expires 24 April 2027 [Page 29] Internet-Draft AMMP October 2026 sha256 here is an integrity and deduplication aid, *not an outbound trust boundary*: the Agent controls both the bytes and the hash, so a mismatch catches corruption, never a hostile Agent. Its value to the Runtime is that it detects an inconsistent request, not that it authenticates one. 6. Outbound: Result A Result is the Runtime's verdict for one consumed Submit Request, written to //outbound/results/.json. { "contract_version": "2", "req_id": "00000001", "outcome": "accepted", "detail": null, "reason_code": "allowlisted", "ts": "2026-08-09T19:14:00Z", "recipients": ["owner@example.com"], "message_id": "<20260809191400.1@example.org>", "job_id": null, "attestation": { "form": "self-stamp", "token": "selfstamp.v1.abcdef", "claims": { "agent": "agent.research", "acts_for": "owner@example.com", "issued_at": "2026-08-09T19:14:00Z" } }, "attachments": [ { "index": 0, "outcome": "accepted", "detail": null } ] } Figure 3: draft/examples/result.json (illustrative — not a fixture) The normative shape is Appendix B. The Result envelope is *open*: a consumer MUST tolerate members it does not recognize, at every level (Section 13.1). 6.1. Field semantics and obligations * *The Runtime writes exactly one Result per Submit Request it consumes.* A request the Runtime never consumes gets no Result, and no signal in this protocol distinguishes "not yet" from "never." Rapp & Adams Expires 24 April 2027 [Page 30] Internet-Draft AMMP October 2026 * *outcome* (REQUIRED) is one of accepted, queued_for_human, or rejected. This is a *closed vocabulary*: it gates which processing path a document takes, so a consumer cannot safely treat an unrecognized value as opaque, and introducing a new value is a major change (Section 13.1). * *Any policy miss — allowlist, rate limit, DLP hold, halt — MUST produce queued_for_human.* A Runtime MUST NOT send such a request, and MUST NOT drop it without a Result. * *accepted* means the provider accepted the message for relay. Downstream delivery or quarantine is out of the Runtime's hands and is surfaced later via an inbound delivery-status notification, if any. * *detail* (OPTIONAL, nullable) is human-readable text. It carries no machine semantics. * *reason_code* (OPTIONAL) records _which policy path_ authorized or refused the request; human text stays in detail. A Runtime *SHOULD* set a reason on every Result, successes included. The suggested vocabulary — recipient_bound, allowlisted, no_verified_counterparty, recipient_not_allowlisted, dlp_hold, no_screener, halted, rate_limited, unsupported_contract_version — is *extensible and not enum-locked in the schema*. Two consumer obligations follow: a consumer MUST tolerate a reason_code it does not recognize, treating it as opaque and never as an error, and a consumer MUST NOT infer the outcome from it. outcome is the sole authoritative verdict; reason_code only explains which path produced it, and a Runtime may add codes at any time without a protocol change. unsupported_contract_version is the code a Runtime sets on the rejected Result it writes for a version- refused Submit Request (Section 13.1). * *ts* (OPTIONAL) is the Runtime-set, of-record time of the verdict, in ISO-8601 form. * *recipients* (OPTIONAL) records the recipient set the Runtime actually transmitted to. * *message_id* (OPTIONAL, nullable) is the [RFC5322] message identifier the Runtime assigned to the composed message. * *job_id* (OPTIONAL, nullable) is a provider-side job identifier, opaque to this protocol. Rapp & Adams Expires 24 April 2027 [Page 31] Internet-Draft AMMP October 2026 * *attestation* (OPTIONAL) is the identity stamp the Runtime applied to the outbound message. Its form is self-stamp or verifiable, distinguishing a stamp a stranger cannot verify from one that can be verified independently. A Connector _surfaces_ an attestation, and does not verify it; verification is a Runtime concern. * *attachments[]* (OPTIONAL) carries one diagnostic entry per submitted attachment: index, an outcome of accepted, dlp_held, rejected_type, or too_large, and an optional detail. *index MUST equal the entry's own position in the array* — contiguous, unique, and covering 0 through n-1 exactly once. Array position is the binding authority for attachments in both directions (Section 4.1). This constraint is not expressible in JSON Schema and is enforced by the conformance gate as a named post-check rather than by the schema (Appendix J). *Two paths, kept apart.* A _malformed_ attachment — a size or digest mismatch, a missing or extra Sidecar, a duplicate index — is not a policy decision: it fails the whole request with a top- level rejected. A _policy hold_ — DLP, a disallowed type, a size cap, including a fail-closed hold when no screener is available — is queued_for_human. Per-attachment outcomes are diagnostic detail under *one* governing top-level outcome, and a Runtime MUST NOT send part of a request on their strength. 7. Inbound: Deliver Notice and Inbound Message The Runtime writes one Deliver Notice per *delivered* message. Quarantined or unverified mail produces *no Notice*: a Runtime MUST NOT write a Notice for it, and logs it on its own side instead. The Notice is a _pointer and summary_; the body is fetched on demand from a separate spooled artifact (Section 7.3) and is *untrusted content*. Rapp & Adams Expires 24 April 2027 [Page 32] Internet-Draft AMMP October 2026 { "contract_version": "2", "notice_id": "1721457600000000000-4242", "ts": "2026-07-20T04:00:00Z", "kind": "deliver", "message": { "id": "42", "from": "Alex Doe ", "subject": "status?", "preview": "hey, how is the run going", "mailbox": "inbox", "task_id": "0f1e2d3c4b5a6978", "thread_id": "T-9182", "in_reply_to": "<20260719120000.7@example.com>", "references": ["<20260718090000.3@example.com>"], "sender_standing": "known", "provenance": "external", "verdict": { "auth": "dmarc-pass", "screen": "clean", "entitlement": "task-counterparty" }, "attachments": [ { "filename": "invoice.pdf", "media_type": "application/pdf", "size_bytes": 12345, "disposition": "unscanned" } ] } } Figure 4: draft/examples/deliver-notice.json (illustrative — not a fixture) The normative shape is Appendix C. The Deliver Notice envelope is *open*: a consumer MUST tolerate members it does not recognize, at every level (Section 13.1). 7.1. Field semantics and obligations *The drop rule.* A Connector relays only deliver Notices. It MUST drop — not surface — a Notice that fails *either* of two independent checks, treating a failure of either one alone as sufficient grounds to drop: Rapp & Adams Expires 24 April 2027 [Page 33] Internet-Draft AMMP October 2026 1. *Schema failure.* The Notice fails validation against the deliver-notice schema (Appendix C) for *any reason other than an unrecognized member*. That single formulation covers a kind other than deliver, a missing required member, an enum violation on a closed vocabulary, a pattern violation (including a traversal or character-set violation in notice_id, or a shape violation in content_ref's own pattern), a conditional (allOf / if-then) violation, and a type violation alike, with no need to enumerate schema-failure categories separately. *A Connector MUST NOT drop a Notice because it carries an unrecognized member* (Section 13.1); that is the one carve-out from "fails schema validation." 2. *The content_ref index and prefix binding.* A Notice can pass check 1 — every member individually well-shaped — and still carry an attachment Descriptor whose content_ref does not equal .attachments/ for its own 0-based array position i (Section 7.2): a mismatched trailing index, or a leading notice_id prefix pointing into a _different_ Notice's Sidecar directory. A Connector MUST run this check independently of schema validation and MUST drop the Notice on failure exactly as it would a schema failure. *This is not a closure over schema validation.* Check 2 binds two members of the same document — a Descriptor's content_ref against the enclosing document's own notice_id and against the Descriptor's own array position — against each other, which is not expressible in the JSON Schema vocabulary these schemas use [JSON-SCHEMA-2020-12]. It therefore cannot be folded into "fails schema validation" no matter how the schema is phrased: it is a second, independent condition, not a special case of the first. The conformance suite enforces it as a named post-check and pins it with a fixture that is schema-valid on its own and fails only this check (Appendix J). A *version refusal* — an absent or non-matching contract_version major — is explicitly *not* part of this drop path. It is handled first and separately (Section 13.1) as a distinct, logged outcome: a Connector checks the version before it ever reaches schema validation, so a version-refused Notice never falls into the drop rule at all. A document with kind: "peer" found under inbound/notices/ fails check 1 against this schema, and is dropped the same way (Appendix F, the peer-origin profile's cross-lock). *Field obligations.* Rapp & Adams Expires 24 April 2027 [Page 34] Internet-Draft AMMP October 2026 * *contract_version*, *notice_id*, *ts*, *kind*, and *message* are REQUIRED. notice_id is Runtime-minted and is pinned by the schema to a character set that is safe as a path component and as a content_ref prefix. * Required members of message are id, from, subject, preview, and mailbox. The rest is enrichment (Section 13.2). * *message.id* is the provider's message identifier. It is *external and not character-set-safe*, and MUST NOT be used as a path component anywhere (Section 4.1). It is the mail store's identifier for the message (an IMAP UID, for example), not the [RFC5322] Message-ID; an Agent replies to a delivered message by naming its notice identifier in in_reply_to (Section 5). * *message.from is attacker-controlled display data.* Its display- name half is chosen freely by the sender — "security@yourbank.example " is a legal value — and it MUST NOT be used for any trust decision, nor rendered to a human or to an Agent as verified identity. Verified sender identity is only ever what the Runtime summarizes into sender_standing and verdict; it is not the from string itself. The one exception in this protocol is the message.from of a Peer Notice, which the Runtime asserts (Section 12.4). * *Presentation obligations.* A Connector that presents a message body to its Agent MUST convey two things, whatever words it uses: quoted or forwarded material inside the body carries no authority of its own — a message's authority extends to what its author wrote, never to what they relayed — and that an attachment's disposition is the Runtime's assertion about handling and not a scanner verdict, so a Connector MUST NOT imply that bytes have been screened and MUST NOT contradict the per-attachment status the Runtime published. A Connector MUST also convey that an attachment's filename and media_type are sender-chosen display strings on the mail tree; on a tree whose sender the Runtime authenticated it SHOULD. On the mail tree a Connector MUST additionally convey that the sender is not authenticated beyond whatever standing the Runtime has already summarized — carried in sender_standing and verdict, named here as the locus and not as vocabulary to repeat to a reader — and that message.from is sender-chosen text. These are obligations about substance, not vocabulary: a Connector satisfies them with any wording its reader can act on without consulting this document, and they MUST be satisfiable by a literal string the Connector carries, requiring no lookup or computation when a body is presented. Rapp & Adams Expires 24 April 2027 [Page 35] Internet-Draft AMMP October 2026 * *The untrusted-content posture covers every sender-originated string* in the Notice and in the spooled Inbound Message: message.from, subject, preview, body_text, references, attachment filename and media_type, and the Inbound Message's from, to, cc, and subject. This is not limited to the members called out individually here. * *message.sender_standing* (OPTIONAL) is an *open, advisory vocabulary* — known, agent-tier, first-contact, read-only, and others. How the values are computed is Runtime policy and out of scope; the wire fixes only today's known vocabulary, so that a Connector can tell its Agent "stranger," "agent," or "needs sign- off." A Connector MUST tolerate a value outside this list, treating it as opaque — never as grounds to drop the Notice — and a Runtime MAY add a value as a minor change (Section 13.1). * *message.provenance* (OPTIONAL) classifies the message source — external, internal, and others — so that a Runtime can route provenance-labelled screening. It is advisory metadata, like verdict, and a Connector MUST NOT present it as license for an Agent to obey body content. It is likewise an *open, advisory vocabulary*: a Connector MUST tolerate a value outside this list, treating it as opaque, and MUST NOT drop the Notice on its account; a Runtime MAY add a value as a minor change. * *message.verdict* (OPTIONAL) carries the upstream authentication, screening, and entitlement results the Runtime consumed. A Connector treats it as advisory metadata, and MUST NOT present it as license for an Agent to obey body content. The object is open: unrecognized members inside it are tolerated, as they are everywhere else in this artifact. * *message.thread_id*, *message.in_reply_to*, and *message.references* (all OPTIONAL) carry provider and [RFC5322] threading identity. They grant correlation, and a consumer MUST NOT treat them as grounds for trust (Section 8). * *message.task_id* (OPTIONAL) is Runtime-minted; Section 8 states the obligations on it. *Direction and openness.* This artifact is Runtime-authored, and its envelope is open at every level: a Connector MUST tolerate a member it does not recognize, in message or at the top level, and MUST NOT drop the Notice on that account. Emitting an unrecognized member remains Runtime non-conformance (Section 13.1). Rapp & Adams Expires 24 April 2027 [Page 36] Internet-Draft AMMP October 2026 7.2. Inbound attachment Descriptors *message.attachments[]* (OPTIONAL) is an array of Descriptors carrying filename, media_type, and size_bytes — all REQUIRED — plus a REQUIRED disposition, and OPTIONAL sha256 and content_ref members gated on it. *disposition is the Runtime's assertion, not a scanner verdict.* Its values are clean, stripped, quarantined, and unscanned, and the vocabulary is *closed*: it gates byte publication, so a new value would be a behavioral fork a consumer cannot treat as opaque, and introducing one is a major change (Section 13.1). On what basis a Runtime asserts clean is deliberately unspecified — a screening pipeline's pass in one deployment, a closed operator-supervised topology in another. That is Runtime policy and out of scope for the wire, the same posture sender_standing takes: the wire fixes the vocabulary, never the computation. The Runtime is trusted by construction (Section 3.1); the load-bearing invariant is that the _Agent_ is not. The counterpart obligation is equally plain: *a Runtime that cannot make the determination MUST NOT assert clean.* It emits unscanned — or stripped or quarantined where it made that decision — and publishes no bytes. *Byte publication and the access grant.* A Runtime MAY publish an attachment's bytes at inbound/notices/.attachments/ *only* for a Descriptor it asserts disposition == "clean", and only then MAY it emit content_ref (a path of the form .attachments/) and sha256. Byte-absence remains the gate (Section 4.1) for anything not asserted clean. Exposing sha256 on a withheld item would be a confirmation oracle for no integrity value, which is why it is clean-gated too. Both conditionals are expressed in the schema. *The content_ref binding.* content_ref's leading notice_id prefix MUST equal the enclosing document's own notice_id, and its trailing MUST equal the Descriptor's own 0-based array index. The array index remains the sole binding authority (Section 4.1), and a consumer that resolves bytes MUST derive the path from the enclosing notice_id and the index, and MUST NOT use the content_ref string's own text as the path, since a Descriptor could otherwise point into another Notice's Sidecar directory. This is a wire-shaped, document- level obligation, enforced by the conformance gate as a named post- check (Appendix J) in the same way the Result-side attachments[i].index == i binding is. Rapp & Adams Expires 24 April 2027 [Page 37] Internet-Draft AMMP October 2026 *A grant, not a directive.* A consumer MUST tolerate a Descriptor whose bytes it does not resolve — relaying the Notice as usual — and MUST NOT treat the presence of content_ref as a requirement to fetch. A Connector that never dereferences content_ref remains fully conformant. A Connector MUST resolve published bytes only within its own Namespace's inbound/notices/ tree and MUST reject any content_ref that does not match the pinned pattern — the same confinement Section 4.1's read-side discipline requires generally. *filename and media_type here are attacker-controlled, untrusted display strings.* The untrusted-content posture extends to attachment Descriptors: a quarantined file named to look like an instruction is still an injection payload even with its bytes withheld. A clean assertion covers the bytes' screening disposition, never the truthfulness of these strings. *Inbound size_bytes is provider-declared and therefore advisory and untrusted* — it comes from the provider's structure report, and is not Runtime-verified the way outbound size_bytes is. *MIME edges.* filename is the [RFC2047] / [RFC2231]-*decoded*, Runtime-sanitized display string. An "attachment" is any part carrying Content-Disposition: attachment [RFC2183], or any named or non-inline text part — inline images and multipart/related parts count, which is the conservative reading of [RFC2045] and [RFC2046]. The declared media_type is display-only: the Runtime sniffs the bytes, and the sniffed type governs policy on the outbound gate. 7.3. The Inbound Message (body spool) The Runtime spools the delivered body at //inbound/messages/notice-.json, keyed on the Runtime-minted notice identifier — never on the provider message.id, for the reason given in Section 4.1. Rapp & Adams Expires 24 April 2027 [Page 38] Internet-Draft AMMP October 2026 { "contract_version": "2", "notice_id": "1721457600000000000-4242", "id": "42", "date": "Mon, 20 Jul 2026 04:00:00 +0000", "from": "Alex Doe ", "to": "agent@example.org", "subject": "status?", "body_text": "hey, how is the run going", "attachments": [ { "filename": "report.pdf", "media_type": "application/pdf", "size_bytes": 48213, "disposition": "unscanned" } ] } Figure 5: draft/examples/inbound-message.json (illustrative — not a fixture) The normative shape is Appendix D. Required members are contract_version, notice_id, and body_text; the rest — id, date, from, to, cc, subject, and attachments[] — is enrichment (Section 13.2). Descriptors here follow exactly the rules of Section 7.2, including the content_ref binding and the clean gating. This artifact's envelope is *open*, like the other Runtime-authored artifacts. *Resolution.* A Connector MUST resolve a notice identifier to a body from this artifact only, and MUST NOT fetch the body from the provider. The body is *untrusted content*, the same posture as the rest of message. *Enforcement by absence.* A body the Runtime withholds — because of screening or quarantine, or because the message was never delivered — is simply not spooled, and a read against a missing artifact returns "not resolved." There is no other signal and no fallback fetch. The same "not resolved" response also covers an artifact the Runtime later removes under its own retention policy (Section 4.2): from the Connector's side, retired and never-spooled are indistinguishable, and both are already-specified behavior. Rapp & Adams Expires 24 April 2027 [Page 39] Internet-Draft AMMP October 2026 8. Task Correlation The Runtime mints and owns task_id; a Connector only reads it, to correlate a Notice with prior work. The minting and thread- resolution _algorithm_ is Runtime policy and out of scope. This protocol fixes three security obligations on it. * *task_id MUST be unguessable and Runtime-minted* — an HMAC over the thread root, for instance. It MUST NOT be a sequential or otherwise guessable value, and it MUST NOT be a capability an inbound sender can assert: a guessable task_id lets a sender who has seen one predict others. * *Matching a task_id, by itself, MUST NOT grant a message entry into that task.* Authoritative correlation is header threading *plus* per-message verified sender identity. A subaddress or header token supplied by an inbound sender is a _hint_ only, and a Runtime MUST NOT treat it as authoritative: it is attacker- controllable, and it is stripped by many gateways. * *Threading grants correlation, not trust.* A message that threads onto a known task is still identity-verified per message; it does not inherit the task's standing. 9. Binding Record The Binding Record is written by the Runtime or provisioner at the *Volume root* — /identity.json, a sibling of /, not inside it — and names which mailbox this Volume is bound to. Its normative shape is Appendix E. The Binding Record names which mailbox a Volume is bound to; it is informational and not consulted for enforcement. { "contract_version": "2", "agent_id": "agent-research", "address": "agent@example.org", "provisioned_ts": "2026-07-20T04:10:56Z" } Figure 6: fixtures/valid/identity-basic.json Rapp & Adams Expires 24 April 2027 [Page 40] Internet-Draft AMMP October 2026 9.1. Field semantics and obligations * *Informational, not authoritative.* The Volume is an Agent- writable filesystem, so a compromised Agent can edit or delete this file at will. Attribution and policy stay keyed on Runtime- side configuration (Section 4.1). This record grants nothing, and a Runtime MUST NOT consult it for enforcement. * *Consumers MUST tolerate its absence*, and *MUST treat its contents as untrusted data*: a reader MUST NOT crash, or change its protocol behavior, because of a malformed, oversized, or hostile file. * Its jobs are narrow by design: operator sanity — a human confirming which mailbox a Volume is bound to — a pre-start conflict check for whatever wrote it, catching for instance one Agent's credentials pointed at another Agent's Volume, and letting a Connector surface the Agent's own address back to its Agent. * *The file is OPTIONAL* (Section 13.2): nothing in this protocol requires a Runtime to write it or a Connector to read it, and both sides MUST work correctly with it absent. * Required members, if the file is present, are agent_id, address, and provisioned_ts. label is OPTIONAL. contract_version is OPTIONAL here, as it is on the Peer Directory (Section 10), because the file is not a protocol artifact a consumer must parse to function; if present it is pinned to "2". * This artifact's envelope is *closed* (additionalProperties: false), for a different reason than the Submit Request's is: closure here is an anti-secret-leak lint, catching a provisioner that writes credential material into a file living on an Agent- readable Volume. It is not a compatibility surface. 10. The Peer Directory A Runtime MAY publish, at the Agent's Namespace root (/directory.json, beside the lanes rather than inside one), a per- instance projection of the authorisation graph naming which peers that Agent may address. Rapp & Adams Expires 24 April 2027 [Page 41] Internet-Draft AMMP October 2026 { "contract_version": "2", "router": "amap.router@fleet.example", "written_at": "2026-09-18T12:00:00Z", "delegate_to": [ "bravo@fleet.example", "charlie@fleet.example" ], "mail_to": [] } Figure 7: draft/examples/directory.json (illustrative — not a fixture) * *Advisory, not authoritative.* The directory is the Runtime's most recent word about the graph, and grants nothing. A Connector MUST NOT treat an address's presence as a prediction that a submission to it will be accepted, nor its absence as a prediction of refusal: authorisation is evaluated by the Runtime at submission time, and the Result (Section 6) is the only thing that reports what happened. A consumer that reads the directory as a prediction is wrong even when the file is current. * *A Runtime MUST NOT read directory.json back, for any purpose.* It is write-only from the Runtime's side: the graph is derived from policy, the file is a projection of the graph, and nothing derives the graph from the file. Enforcement remains the Runtime's own check against its graph, on every message. The prohibition is on the direction rather than the purpose, because the ways this decays — as an optimisation, a cache, a recovery source, or a cross-check — all have innocent intent, and the last two turn a projection into a source. A projection that can be read back becomes a cache; a cache becomes a record; a record that disagrees with policy wins by accident. * *Per-instance projection.* Each Agent's directory carries only the edges that Agent is party to. It MUST NOT carry the set of peers who may address that Agent, which is an allowlist in all but name, and MUST NOT carry the Agent's own address — attribution is the Namespace a file arrived in (Section 4), so an address an Agent reads is a claim rather than an identity. * router is the publishing Runtime's own address and MUST NOT appear in either list. Every address is one bare addr-spec (Section 5). * *Both lists are always present*, and empty when that lane is closed for this Agent; mail_to is empty in a delegation-only deployment. Absent and empty MUST NOT be conflated. Rapp & Adams Expires 24 April 2027 [Page 42] Internet-Draft AMMP October 2026 * *Written whole and atomically.* A Runtime MUST NOT publish a newly adopted instance's address to others before it has adopted that instance itself. * *A Runtime that publishes the directory MUST ensure the Agent can read it*, and SHOULD expose it read-only. Publishing to a path the Agent's Namespace does not expose is not conformant: the write succeeds, the file is correct, and no Agent can read it — a failure that reports nothing on either side. Publishing is optional, so a Runtime that cannot make the file readable MUST NOT publish it; an unreadable directory is indistinguishable from an absent one to a consumer, and worse than an absent one to an operator, who has a file on disk that looks like it is working. The trap follows from the location: the file deliberately belongs to no lane, which is what keeps a delegation-only deployment from materialising a mail tree, and is exactly what places it outside an exposure built lane by lane. * *written_at* is the only staleness signal a consumer gets: an honest marker, not a freshness guarantee. * *Where the consumer is an Agent rather than a parser, these become obligations to convey.* A Connector that surfaces the directory to its Agent instead of interpreting it satisfies the obligations above by stating them, under the presentation rule in Section 7: the Agent is the party forming a belief, so the Connector MUST convey that an address's presence is not a prediction of acceptance, that the file may be absent, that an empty list and an absent list differ, and that written_at is the only staleness signal there is. A Connector with no directory parser does not thereby satisfy them vacuously. * *The file MAY be absent* — before first adoption, on a Runtime that does not implement this section, or after a failed write — and a consumer MUST tolerate absence exactly as it tolerates a missing Binding Record (Section 9). * *A Runtime that cannot write it keeps routing.* The directory is advisory, so a failed write loses a convenience, not the ability to enforce; failing closed would convert an advisory artifact into a dependency. The Runtime MUST log the failure. The Agent's only signal remains written_at going stale. Rapp & Adams Expires 24 April 2027 [Page 43] Internet-Draft AMMP October 2026 11. The Fleet Roster A Runtime MAY publish a fleet roster: a single file, written for the whole fleet rather than per instance, naming the instances the Runtime has admitted. The Peer Directory (Section 10) answers which peers an Agent may address; the roster answers only which instances exist. It is a separate artifact, and no obligation of Section 10 applies to it except where this section states one. * *Existence is not authorisation.* A member's presence says the instance exists. It does not say this Agent may address it, and it is not a prediction that a submission to it will be accepted: authorisation is evaluated by the Runtime at submission time, and the Result (Section 6) is the only thing that reports what happened. A Connector MUST NOT treat membership as authorisation or as a prediction of acceptance. * *Not a source of identity.* The roster is fleet-wide, so the reader's own address MAY appear in it. A consumer MUST NOT derive its own identity from the roster and MUST NOT treat its own entry as a peer: attribution is the Namespace a file arrived in (Section 4), and an address read from a file is a claim rather than an identity. * *Write-only from the Runtime's side.* A Runtime MUST NOT read the roster back, for any purpose, for the reasons given for the directory in Section 10. * *Written whole and atomically.* A Runtime MUST NOT list a newly admitted instance before it has adopted that instance itself. router is the publishing Runtime's own address and MUST NOT appear in members. Every address is one bare addr-spec. * *Unknown is not empty.* members is always present. When the Runtime cannot determine membership, it MUST NOT write an empty members list; it leaves the previous file in place to age. written_at is the only staleness signal a consumer gets, and interval_s, when present, is the Runtime's nominal rewrite interval to judge it against. A consumer MUST treat an absent roster, or one it judges stale, as unknown, and MUST NOT treat it as an empty fleet. Unknown is not unusable: an Agent may still address a member of a stale roster, and the Runtime decides; what it must not do is conclude that an instance is absent from the fleet. This section fixes no freshness bound; how stale is stale is the consumer's judgement, made against interval_s where it is present. Rapp & Adams Expires 24 April 2027 [Page 44] Internet-Draft AMMP October 2026 * *Readability is split between the parties that can observe it.* A Runtime cannot observe a mount. It MUST publish the roster only into a location the deployment designates for it, and MUST NOT create that location. The deployment MUST expose that location read-only to each Agent it points at the roster, and SHOULD stop pointing Agents at the roster before, or together with, ceasing to expose it. A roster written where nothing is exposed is inert, not a violation: an Agent is only ever pointed at a roster its deployment exposes. * *Conveyed, where the consumer is an Agent.* Whatever points an Agent at the roster, whether a Connector that surfaces it or a deployment that tells the Agent where to read it, MUST convey that membership is neither authorisation nor a prediction of acceptance, that the roster is not the Agent's identity and its own entry is not a peer, and that an absent or stale roster means unknown. Stating these in text the Agent receives whenever it is pointed at the roster satisfies this; no tool is required. The roster's shape is pinned by its schema (Appendix H) and by the roster-* fixtures (Appendix J). The envelope is Runtime-authored and therefore open (Section 13.1): a consumer ignores members it does not recognize, and emitting one remains producer non-conformance. 12. The Peer-Origin Profile (Optional) This section is an optional profile. It gives agent-to-agent traffic between Agents of the same principal its own inbound tree, peer/, and states what that tree asserts, who may write it, how peers are addressed, how a reply is correlated with the message it answers, and how peer traffic is carried and verified between hosts. A deployment that implements the profile is bound by every requirement in this section; a deployment that does not is unaffected. No schema or fixture outside the profile depends on it, and the wire contract_version is the same for both. 12.1. Scope The profile is optional because peer origin is needed by deployments in which Agents task one another, and by no deployment that only handles mail. Rapp & Adams Expires 24 April 2027 [Page 45] Internet-Draft AMMP October 2026 The tree is named for what it asserts, not for what it is used for: peer/ asserts authenticated, same-principal peer origin, and the document kind is peer. What a Connector does with a peer message (delegation, review, notification) is the Connector's business. Because the kind and the tree share a name, the rule that a document's kind equals the name of the tree it was read from is mechanical. The profile adds an origin class and reuses mail threading for correlation. It defines no call or return semantics, no method or operation member, and no status vocabulary for the work itself: a reply is prose authored by an Agent, with the posture of any other peer content. This keeps the profile on the mail side of the line drawn in Section 1.5. The profile relies on two generic rules in Section 4.1: the _tree rules_ (every Runtime-owned inbound-class tree is an origin class, derived from the tree and never from a member, with notice_id uniqueness, content_ref confinement and write authority all per tree), and the _outbound extension space_ outbound/ext//, where delivery outcomes are kept (Section 12.8). There is deliberately no inbound extension space. 12.2. The Peer Tree / outbound/ # as in the Volume Layout inbound/ # mail notices/ messages/ peer/ # PROFILE: same layout, conventions notices/notice-.json # and Sidecar rules as inbound/ notices/.attachments/ messages/notice-.json audit/ Figure 8: Namespace layout with the peer tree (illustrative — not a fixture) *A sibling of inbound/, never a child.* Nesting the peer tree inside the mail tree would put the tree the mail deliverer must not write inside the tree it must write, so the write-authority partition (Section 12.3) would have to be carved out of a mount instead of being one. As a sibling, the partition coincides with mount granularity. Every obligation on inbound/ in Section 4 applies to peer/ unchanged: it is Runtime-owned; it MAY be mounted read-only; a Connector MUST NOT require write access to it; only the Runtime deletes from it; and its retention is conservative and age-based. Rapp & Adams Expires 24 April 2027 [Page 46] Internet-Draft AMMP October 2026 *Its own schema.* A Peer Notice is pinned by the peer-notice schema (Appendix F): the Deliver Notice shape with kind fixed to "peer" and message.mailbox fixed to "peer", message.from pinned to a bare addr- spec (Section 12.4), message.id, message.in_reply_to and message.references[] pinned to the notice_id character set (Section 12.5), and the optional Runtime-asserted message.sender_exposure (Section 12.7). Attachment Descriptors are exactly those of Section 7.2. The body spool uses the Inbound Message schema (Appendix D) unchanged. Because a Peer Notice appears only in a tree that a mail-only consumer never reads, the closed kind vocabulary of the Deliver Notice is untouched. *The cross-lock.* A deliver document under peer/notices/ fails the peer-notice schema, and a peer document under inbound/notices/ fails the Deliver Notice schema. Both are dropped under the rule of Section 7.1. *The placer mints notice_id.* The component that places a notice into a tree mints its notice_id. Where a tree has more than one writer, each MUST mint so that a collision between writers is negligible: at least 128 bits of entropy under the notice_id character set, or a writer-specific prefix. A consumer that keys state across trees MUST include the tree in the key. On a single host, a Runtime MAY make the notice_id equal to the peer message id (Section 12.5). Between hosts they differ, because the receiving Runtime mints the notice_id and the message id is the sender's. A consumer MUST NOT assume that they coincide. *content_ref is confined to its tree.* Bytes resolve only within the notices/ directory of the tree the enclosing document was read from, recomputed from that document's notice_id and the Descriptor's index. A Peer Notice cannot point into the mail Sidecar directory, or the reverse. *Advisory members.* provenance on a Peer Notice is redundant with the tree and MAY be omitted; if present, it is advisory, and a consumer MUST NOT derive or override the origin class from it. sender_standing, if present, SHOULD be the existing agent-tier value. 12.3. Origin Class Follows the Tree Write authority per tree is a behavioral obligation of the same kind as read-only inbound tolerance, single-writer and single-drainer operation, and ingest by copy (Section 4.1): invisible to a document validator, and checked operationally. If it were left to deployments, the separate tree would be decorative, because one writer that chose the tree by inspecting a member would be routing on a member with one level of indirection. The mechanism that enforces Rapp & Adams Expires 24 April 2027 [Page 47] Internet-Draft AMMP October 2026 it (user identities, mount sets, a host boundary) is out of scope; the property is not, and this section prescribes a check for it. *Connector obligations.* The tree a notice is read from is the Runtime's sole assertion of its origin class: inbound/ is mail, and peer/ is same-principal peer origin. A Connector MUST derive the class from the tree, and MUST NOT derive or override it from any member of the document. A document whose kind does not equal the name of the tree it was read from is dropped under the rule of Section 7.1. A Connector that cannot establish which tree a document came from MUST treat it as mail. The peer tree asserts authenticated origin: a same-principal Agent, reaching this Namespace through a path restricted to authorized peer senders. It asserts nothing about content. Peer content is authored by a peer and unscreened, and may paraphrase external mail (Section 12.7). How a Connector treats each class is out of scope; that it distinguishes the classes only by tree is not. Like the untrusted-content posture of Section 7, this obligation protects an Agent from its own inbox, and is stated as a Connector obligation rather than as a property the Runtime relies on. The invariant of Section 3.2 is unaffected. *Write-authority partition (Runtime).* Write authority over peer/ MUST be distinct from write authority over inbound/. A component that admits messages from a transport not restricted to authorized peer senders (the _admitting component_, of which a mail deliverer is the usual example) MUST NOT hold write authority over peer/. The restriction MUST be structural, and MUST NOT be configuration that the component reads. The prescribed operational check is a write attempt against peer/ from inside the admitting component, which MUST fail. The rule is one-directional: a component holding peer write authority MAY write inbound/, as the delivery failure notice of Section 12.5 requires. A Runtime MUST NOT write a notice into peer/ on the basis of any claim the message makes about itself. It MAY write there only when the message arrived through a path whose write authority is already restricted to authorized peer senders, or when the message carries a signature that verifies against a configured peer key (Section 12.6). Rapp & Adams Expires 24 April 2027 [Page 48] Internet-Draft AMMP October 2026 *Verification placement (Runtime).* Verification MUST be performed by a component holding peer write authority, which receives candidates through a location that is neither tree (Section 12.6.10). The admitting component MUST NOT perform verification. A verdict computed by one component and acted on by another would cross a trust boundary as data, which is what this rule keeps out of the decision; and the verifier processes hostile input, so it needs to be the component whose write authority the outcome concerns, not one that parses raw mail. 12.4. Addressing *A peer address is a bare addr-spec*, under the same pattern and exclusions that Section 5.1 fixes for draft.to, with no display name. Its local part is the Agent's name within its Runtime, and its domain is the Runtime's authority. A deployment confined to one host, with no mail, MAY use a non-routable domain. Traffic between hosts requires a routable domain, because it travels as mail (Section 12.6). "Routable" means deliverable by the mail system that carries the deployment's peer traffic; the domain need not resolve in the public DNS, because peer keys are configured rather than looked up (Section 12.6.7). A peer domain MUST belong to exactly one Runtime among those configured as peers, so a deployment that may peer across organizations should use domains it controls. Where an Agent has a bound mailbox (the Binding Record's address, Section 9), the deployment SHOULD use it, so that the Agent has one identity on both lanes. An addr-spec is used rather than an opaque identifier with a directory, or a URI scheme, because between hosts the transport address is an email address. Any other scheme would need a mapping onto one, and someone to run the mapping. With an addr-spec, the domain names the authority, the peer-key table is keyed by domain, and a signature has something to bind. The grammar, the schema pattern and the header-injection discipline already exist. *Sending needs no new wire shape.* A sending Agent writes an ordinary Submit Request (Section 5) whose draft.to names the peer. The Runtime's recipient policy recognizes a declared edge and routes the request to the peer tree rather than to mail. The Result vocabulary, outbound Sidecars and reply key are the existing ones. Whether a recipient is a peer is Runtime policy, and out of scope, like an allowlist. *message.from on a Peer Notice is authenticated.* It is the one from in this protocol that a consumer may treat as authenticated origin: a bare addr-spec, pinned by the schema, and asserted by the Runtime from the restricted write path or from a verified statement Rapp & Adams Expires 24 April 2027 [Page 49] Internet-Draft AMMP October 2026 (Section 12.6). A Runtime MUST NOT copy it from text the sender supplied. This is the opposite of the rule for mail from (Section 7.1). A Connector's sender check may key on it, because the Runtime asserted it. *A configured peer key vouches only for its own Runtime's domains* (Section 12.6.7). Without this, one Runtime's key could sign a message claiming an address in another Runtime's domain. *One address, one Namespace.* A peer address resolves to exactly one Namespace, as every address does in this protocol: one Namespace, one Connector instance, one Agent. Sessions are below the Connector, and this document does not define them. A peer message is a single unit addressed to one Agent. A Connector MUST NOT cause it to be acted on more than once. A Connector that cannot identify a single target MUST hold the notice rather than fan it out, and SHOULD report the condition through its own side channel (Section 12.8). "One Agent, one live session; refuse when there are several" is one conformant implementation, not the only one. A finer target (a named session) is out of scope. A coarser one (a pool) is a Runtime alias that expands to several addresses, each resolving to one Namespace, and is not a change to addressing. 12.5. Correlation and Replies *The peer message id.* The sending Runtime mints the peer message id when it accepts the submit. The id MUST be globally unique and unguessable, with at least 128 bits of entropy, and MUST be spelled under the notice_id character set (^[A-Za-z0-9_][A-Za-z0-9_.-]*$). It has exactly one spelling inside every AMMP document: message.id, message.in_reply_to and message.references[] on a Peer Notice, message_id on a peer-routed Result, and the reply key a sender submits all carry the bare id. When the id is carried in a mail header, it is encoded as , where domain is the domain of the sender's address (Section 12.6.3). The encoding is applied on the way into the header and removed on the way out, and MUST NOT appear in an AMMP document. The reverse edge and every ledger depend on two properties of the id: that it is unguessable, and that it has one spelling end to end. Bare 32-character hexadecimal satisfies both, and so does unpadded base64url (subject, between hosts, to Section 12.6.1). A mail notice's in_reply_to remains an [RFC5322] identifier; the Deliver Notice schema is unchanged. Rapp & Adams Expires 24 April 2027 [Page 50] Internet-Draft AMMP October 2026 *A reply is a Peer Notice in the reverse direction*, on the same tree, correlated with the message it answers. It is not a third kind or a third tree: the tree grants the class, and a reply deserves the class of the message it answers. There is no correlation member; mail threading already provides one, and between hosts it is [RFC5322] threading. +===========================+====================================+ | Member | On the peer lane | +===========================+====================================+ | message.id (Peer Notice) | The peer message id. | +---------------------------+------------------------------------+ | message_id (the sender's | MUST equal the peer message id, so | | Result) | that the sender learns it from its | | | own Result. reason_code SHOULD be | | | peer_routed, so that the sender | | | can tell the lanes apart (an open | | | vocabulary, Section 6.1). | +---------------------------+------------------------------------+ | message.in_reply_to (Peer | REQUIRED on a reply, and equal to | | Notice) | the id being answered. A new task | | | omits it. | +---------------------------+------------------------------------+ | message.references[] | Multi-step threads, as in mail. | | (Peer Notice) | | +---------------------------+------------------------------------+ | in_reply_to or | How a responder names the message | | draft.reply_to_message_id | it answers. | | (Submit Request) | | +---------------------------+------------------------------------+ Table 3: Correlation members on the peer lane task_id remains an optional, Runtime-minted thread handle (Section 8). Rapp & Adams Expires 24 April 2027 [Page 51] Internet-Draft AMMP October 2026 *Reverse edges.* The authorization graph is directed, yet a reply must be able to flow from B to A when only A to B is declared. A Runtime permits a reverse message when its reply key names a peer message in the opposite direction that the Runtime itself issued or placed (at submission, one it placed for the submitting Agent; at reception, one it issued to the replying Agent), within a configured window. This consults a member, but checks it against state the Runtime minted, which is the posture Section 8 takes for threading: correlation, not trust. Because ids are unguessable, the key is not a forgeable capability. Several replies to one id within the window, and a reply to a reply under the id the Runtime issued for it, are conformant. This document does not fix the window's length; it requires only that the window exists and is configured. *An unresolved reply key is refused, not stripped.* A reply key (in_reply_to, or else draft.reply_to_message_id) that the Runtime cannot resolve from its own ledger MUST produce a rejected Result with reason_code unresolved_reply. A Runtime MUST NOT strip the key and route the message on draft.to as a new peer message. A key that resolves but whose reverse edge is not permitted (no declared edge, or an expired window) MUST likewise produce a rejected Result (reason_code peer_reply_window_expired, or the Runtime's own code), and MUST NOT be re-routed. A Peer Notice MUST carry in_reply_to if and only if the placing Runtime resolved it from its own ledger to a peer message id that it issued or placed. The reason is the reason this profile exists. On the peer lane, a message the sender meant as a reply, which the Runtime quietly turned into a new task, arrives as a new instruction. That is worse than a rejection the sender can act on by resubmitting without the key. The rule is a requirement rather than a permission because a Connector may build policy on it (for example, admitting a replier it would not otherwise accept), and that is sound across a deployment only if every conformant Runtime upholds it. Such a policy MUST NOT widen anything else about the notice's treatment. Between hosts, the sending Runtime has already rejected an unresolvable key at submission. A verified statement whose in_reply_to the _receiving_ Runtime cannot resolve therefore indicates a stale or forged capability from a configured peer. The receiver MUST NOT place it in peer/ with the key, MUST NOT place it as a new peer task, and MUST log the cause, and it SHOULD tell the sending Runtime with a signed Control Notice (Section 12.6.8). The ledger binds the participants as well as the id (Section 12.6.6). The statement is not demoted to mail either: its origin was verified, only its correlation claim failed, and routing it as mail would be a third silent transformation. Rapp & Adams Expires 24 April 2027 [Page 52] Internet-Draft AMMP October 2026 *Two things called a result.* A _work result_ is the responder's content for the requester, described above. A _delivery outcome_ is mechanical feedback from a Connector to its Runtime (delivered, held, and so on). A delivery outcome is not a peer message; it is kept in outbound/ext// (Section 12.8). *Delivery failure notices.* A Runtime that reports a terminal delivery failure to the sending Agent SHOULD do so as a deliver Notice in the sender's inbound/ tree, with message.from set to the Runtime's own address (amap.router@ or equivalent), in_reply_to set to the peer message id, and a body authored by the Runtime alone. Such a notice MUST NOT be placed in peer/, and MUST NOT quote or forward any text from the receiving Connector's outcome file. The Runtime is never an allowed peer sender, so this keeps it out of every Connector's peer policy. The mail tree's untrusted-content posture is the right one for "your message did not arrive". And the outcome file is text written by the receiver, so forwarding it would let one Agent put prose in front of another under the Runtime's name. A component holding peer write authority that writes inbound/ is the one direction Section 12.3 permits. The peer message id appears in a mail-tree notice, but only to the sender, who already holds it from its own Result, so no capability is disclosed. 12.6. Peer Traffic Between Hosts Peer traffic between hosts is mail. The sending Runtime signs it with its own key; there are no per-Agent keys. Traffic on one host needs no signature, because local write permission is a stronger guarantee, although a Runtime MAY sign it too, for a uniform code path, without changing its treatment. A receiving Runtime places a message from another host in peer/ only when every rule in this section holds. Otherwise it hands the message to mail admission, drops it, or refuses it, as Section 12.6.4 specifies. 12.6.1. The Statement The sending Runtime builds a _statement_, a small JSON object, signs its exact bytes with Ed25519 (Section 5.1 of [RFC8032]), and carries the bytes and the signature in one header field (Section 12.6.2). The receiver verifies the signature over the bytes it received _before_ parsing them. Nothing is re-serialized: no canonicalization is defined, and whatever serialization the sender chose is the one that is signed. Rapp & Adams Expires 24 April 2027 [Page 53] Internet-Draft AMMP October 2026 The peer-statement schema (Appendix F) pins the statement: version v 1, closed at every level, with the members v, key_id, kind ("peer"), id, in_reply_to (optional), from, to, ts, subject, body_sha256 (the digest of the body the receiver will publish), attachments (each with sha256, size_bytes, filename and media_type, in leaf order), sender_exposure (optional, Section 12.7) and sender_since (optional, Section 12.6.9). Unlike the Runtime-authored notices (Section 13.1), the statement is closed, because it is signed bytes that another Runtime acts on. A member the receiver does not know is a member it cannot bind to what it publishes, and v is the path for evolution. The schema states the shapes. The following rules, which JSON Schema cannot express, are also part of the statement: * *Formats.* ts, sender_since and the timestamps in sender_exposure are [RFC3339] date-times in UTC, in whole seconds, with the Z designator, and each MUST denote a real instant. Digests are SHA-256 [FIPS180-4], in lowercase hexadecimal. Every number is a lexical integer, with no fraction and no exponent, so 1.0 is not 1. No string contains an unpaired surrogate (Section 8.2 of [RFC8259]). * *Ids.* A sending Runtime MUST mint the ids it sends between hosts under ^[A-Za-z0-9_][A-Za-z0-9_-]*$, which is the notice_id character set (Section 12.5) without ".", with at least 128 bits of entropy, so that is a valid msg-id (Section 3.6.4 of [RFC5322]). Hexadecimal satisfies this; a base64url id whose first character is "-" MUST be minted again. The statement schema enforces the pattern for id and in_reply_to. An id used only on one host MAY also contain ".", as the notice_id character set allows; an id sent between hosts MUST NOT. * *to* is one address: a peer message is addressed to one Agent (Section 12.4). * *sender_exposure* obeys Section 12.7: window_start is not later than window_end; last_external_delivery_ts is present if and only if external_mail_delivered is true, and then lies within the window; and asserted_by is the signing Runtime's configured Runtime address (Section 12.6.7). * *sender_since*, when present, is not later than ts. 12.6.2. Carriage, and the Candidate the Verifier Sees *The header field.* Rapp & Adams Expires 24 April 2027 [Page 54] Internet-Draft AMMP October 2026 AMAP-Peer-Statement: k=; s=; b= * s is the statement's bytes, and b is the 64-byte signature over exactly those bytes. Each is base64url without padding (Section 5 of [RFC4648]). * k is the statement's key_id, in the clear: the receiver verifies before it parses, so it must choose the key before it can read the statement. The signed key_id MUST equal k. * The field name is matched case-insensitively. The sending Runtime MUST emit exactly one such field. * The sender MUST fold the field so that no line exceeds 998 octets (Section 2.1.1 of [RFC5322]), and SHOULD fold at 78. It may insert folding whitespace anywhere, because the receiver removes all of it. * A statement MUST NOT exceed 8192 bytes. A sending Runtime that would otherwise emit a larger one SHOULD refuse the submit instead, with a rejected Result whose reason_code is peer_statement_too_large. * The sending Runtime MUST also set Message-ID to , where domain is the domain of from, and SHOULD set In-Reply-To and References to the encoded forms (Section 12.5), so that mail threads coherently and a demoted message still threads as mail. *The message shape.* A sending Runtime SHOULD send either a single text/plain part, or multipart/mixed holding one text/plain part followed by the attachments. The text part SHOULD be charset=utf-8 with base64 transfer encoding, so that transport cannot rewrite it. The sender hashes the UTF-8 encoding of the body after replacing every CRLF pair with LF. Nothing else is normalized: a bare CR, a bare LF, trailing whitespace and a leading dot are all content. *The selection rule.* The admitting component reduces a message to a candidate, and the rule is the same at every receiver: * *Leaves.* Walk the MIME tree [RFC2046] depth-first, descending only into multipart/*. Every other part is a leaf, including message/rfc822, whose octets are its part body as transmitted. * *Body.* The first leaf of type text/plain whose Content- Disposition [RFC2183] is not attachment is the body. Rapp & Adams Expires 24 April 2027 [Page 55] Internet-Draft AMMP October 2026 * *Attachments.* Every other leaf, in walk order, is an attachment: a text/html alternative, an inline image, or a second text/plain part. * *No body leaf* means the empty body. A different structure is not forbidden; it is simply likely to fail the attachment count or the body digest, and so to be demoted. *The candidate.* The intake location (Section 12.6.10) MUST hold the following, in a layout this document does not prescribe: * *Field bodies.* The body of each AMAP-Peer-Statement, AMAP-Peer- Control and Message-ID field, in message order. The admitting component MAY unfold a field body or trim its surrounding whitespace, and MUST NOT otherwise alter it. A field body over 12288 octets MUST be carried as an _oversize_ marker, and MUST NOT be truncated. The 12288-octet limit applies to the field body as transmitted, before unfolding or trimming. A field body the admitting component cannot carry as ASCII text (a non-ASCII octet, or an undecodable value) MUST be carried as an _invalid_ marker in the field's position. A marker keeps the field's identity and multiplicity: the admitting component MUST NOT omit a field, because omitting a second, malformed statement field would leave one well-formed field, and the verifier would select it. * *The body leaf*, if there is one: its octets after transfer decoding, and the value of its charset parameter, if any. * *Each attachment leaf:* its octets after transfer decoding, in order. The verifier, not the admitting component, decodes the body octets strictly. The charset label, compared case-insensitively, MUST be utf-8 or us-ascii; an absent label means us-ascii. Any other label, or octets that are invalid in the labelled charset, fail check 9 (Section 12.6.3). The verifier then replaces CRLF pairs with LF and hashes the result, and that same text is what it publishes as body_text. *What is published.* On placement, the receiver takes all of the following from the verified statement, and MUST NOT take them from headers: message.id, in_reply_to and from; subject; sender_exposure; and the attachment Descriptors' filename, media_type, size_bytes and sha256. to selects the Namespace. The receiver MUST NOT publish references from headers; it MAY derive them from its own ledger (Section 12.6.6). The byte sniffing, caps and disposition rules of Section 7.2 still apply to the attachment bytes. Rapp & Adams Expires 24 April 2027 [Page 56] Internet-Draft AMMP October 2026 *Comparisons.* Addresses (from, to, the edge graph, the ledger and Runtime addresses) compare the local part exactly and the domain case-insensitively in ASCII. Ids compare exactly, because they are opaque. After unfolding and trimming, Message-ID MUST be exactly "<", the id, "@", the domain and ">", with the id equal to the statement's id and the domain equal to the domain of from. *Logging.* A receiver MUST NOT log a raw header field value unescaped. Fields are supplied by the attacker, and k is validated against the key_id pattern before it is used for anything. 12.6.3. Verification The receiver applies these checks in this order. The first one that fails decides the outcome (Section 12.6.4). Only a candidate that passes every check is eligible for peer/. 1. *The field.* There is exactly one AMAP-Peer-Statement field; with none, the message is not a peer candidate, and is ordinary mail. A message that also carries an AMAP-Peer-Control field is dropped (Section 12.6.8). The field body is ASCII and is not oversize. With every SP, HTAB, CR and LF removed, it consists of exactly the tags k, s and b, each once, in any order, separated by ";". k matches the key_id pattern. s and b are unpadded base64url; b decodes to 64 bytes, and s to at most 8192. 2. *Known statement.* If the SHA-256 digest of the statement bytes equals that of a statement the receiver has recorded and still retains (Section 12.6.5), the candidate is a duplicate. This check needs no key, and holds whatever else has changed since: a timestamp now outside the window, a key since expired or removed, or a body modified in transit. 3. *Key.* k names exactly one configured key, valid at the receiver's clock (not-before ≤ now ≤ not-after, inclusive). 4. *Signature.* The signature is valid for that key over the received bytes, under the verification policy of Section 12.6.7. 5. *Statement.* (a) The bytes are UTF-8 without a byte-order mark, and parse as JSON with no repeated member name and no non-JSON constant. They validate against the schema, and key_id equals k. (b) Every rule of Section 12.6.1 that the schema cannot express holds. Rapp & Adams Expires 24 April 2027 [Page 57] Internet-Draft AMMP October 2026 6. *Key domain.* The domain of from is one of the domains configured for the Runtime that owns the key. A key vouches only for its own Runtime's domains (Section 12.4). 7. *Freshness.* ts lies within the window of Section 12.6.5. While peer admission is closed (no trustworthy bound, Section 12.6.5), the window is undefined: the candidate is _held_ in intake, undisposed, until admission reopens. 8. *Exposure asserter.* If sender_exposure is present, asserted_by is the configured Runtime address of the Runtime that owns the key. 9. *Body.* The body decodes under the charset rule of Section 12.6.2, and its digest equals body_sha256. The verifier MUST hash bytes it holds itself, and MUST NOT use a digest that the admitting component computed. 10. *Attachments.* The count matches; then, for each attachment in order, sha256 and size_bytes match the octets. 11. *Message-ID.* There is exactly one such field, matching Section 12.6.2. 12. *Reservation.* No record exists for the pair (the configured Runtime that owns the key, id) (Section 12.6.5). The candidate is reserved, atomically. A reserved candidate is then _decided_, and the following are not verification failures: * if to is not hosted here, the candidate is misrouted; * if in_reply_to is present, it must resolve and be permitted (Section 12.6.6); * otherwise, the receiver's own graph must have the edge from from to to (Section 12.6.6); * any deployment admission policy is applied (Section 12.6.9); * otherwise, the decision is placement. The decision MUST be persisted on the record before it has any effect (Section 12.6.5). Only a persisted placement decision is ever published; a persisted refusal or drop MUST NOT be published, whatever happens afterwards. Rapp & Adams Expires 24 April 2027 [Page 58] Internet-Draft AMMP October 2026 12.6.4. Outcomes +============+========================================+=============+ | Outcome | When | Control | | | | Notice | +============+========================================+=============+ | peer | Every check passes, and the | none | | | message is routed and permitted. | | +------------+----------------------------------------+-------------+ | held | Check 7 while peer admission is | none | | | closed (Section 12.6.5); not yet | | | | disposed of. | | +------------+----------------------------------------+-------------+ | mail, no | No AMAP-Peer-Statement field. | none | | statement | | | +------------+----------------------------------------+-------------+ | mail, | A failure of check 1 (other than | SHOULD, for | | demoted | the both-fields drop below), 3, | 5(b) and 6 | | | 4, 5, 6, 7, 8, 9, 10 or 11. | to 11 | +------------+----------------------------------------+-------------+ | drop: both | An AMAP-Peer-Statement and an | none | | fields | AMAP-Peer-Control field in one | | | | message (Section 12.6.8). | | +------------+----------------------------------------+-------------+ | drop: | Check 2 (a known statement) or | SHOULD, for | | duplicate | check 12 (a known id). | check 12 | | | | only | +------------+----------------------------------------+-------------+ | drop: | to is not hosted here. | SHOULD | | misrouted | | | +------------+----------------------------------------+-------------+ | refuse: | Section 12.6.6. | SHOULD | | unresolved | | | | reply | | | +------------+----------------------------------------+-------------+ | refuse: no | Section 12.6.6. | SHOULD | | edge | | | +------------+----------------------------------------+-------------+ | refuse: | A deployment admission policy | SHOULD, | | policy | (Section 12.6.9). | with | | | | policy_code | +------------+----------------------------------------+-------------+ Table 4: Outcomes for a candidate from another host *Mail, demoted.* Demotion is a classification, not a delivery. The candidate goes to ordinary mail admission and the deployment's lane policy; where that policy gives the pair no mail lane, it is not Rapp & Adams Expires 24 April 2027 [Page 59] Internet-Draft AMMP October 2026 delivered. The demoted notice MUST NOT carry sender_exposure, the statement or the signature, and carries the mail-admission verdict the admitting component attached. Mail admission MUST NOT treat an invalid statement as evidence of who sent the message. *Drop and refuse.* A dropped or refused candidate is neither placed nor handed to mail admission. Every outcome other than peer and "mail, no statement" MUST be logged with its cause. A refusal is not demoted, because the origin was verified and only an authorization or correlation claim failed; routing it as mail would be a silent transformation (Section 12.5). *Who is told.* A Control Notice (Section 12.6.8) MUST be sent only when every one of these holds; a configured k alone is never enough: * checks 1 to 5(a) passed, so the statement is authenticated by a currently valid configured key and is well-formed enough to name. Before check 4 the sender is unauthenticated, and notifying the claimed sender would be backscatter; between checks 4 and 5(a), the statement cannot be named; * the candidate was not a duplicate caught at check 2 (an exact retained statement: the first copy was already answered, and a replay on the path must not draw a second notice); * the candidate is a statement, not a Control Notice: nothing is ever sent about a Control Notice (Section 12.6.8); * the destination is the Runtime address configured for the key's owner, and never an address taken from the message. A "dropped, duplicate" notice (check 12) tells the sender that a record with that id already exists. That record's disposition may have been placement or refusal: the notice proves a prior reservation, not that the task failed. Whether and how the sending Agent learns of an outcome is governed by Section 12.6.8. 12.6.5. The Replay Store, Windows and Recovery *Windows.* The receiver accepts ts when lower ≤ ts ≤ now + F, both bounds inclusive, where lower = max(H − P, floor): * *F*, the allowance for clock skew, defaults to 300 seconds and MUST NOT exceed 900. Rapp & Adams Expires 24 April 2027 [Page 60] Internet-Draft AMMP October 2026 * *P*, the past window, defaults to 3600 seconds, and MUST be at least 300 and at most 86400. A message that arrives later than P is demoted, which is visible to the sender. A longer P costs replay storage and a longer key retirement (Section 12.6.7). * *H* is the latest receiver time ever observed, kept durably. The lower bound uses H rather than the current clock, so that a clock that steps backwards cannot re-admit a statement whose record has been pruned. * *floor* is set only by recovery and by window changes, both below. *Records.* Replay identity is scoped to the pair (configured Runtime, id), where the Runtime is the configured peer that owns the verifying key, so that rotating a key never resets it. Each record holds the scope, the id and ts; the SHA-256 digest of the statement bytes; the notice_id minted at reservation; a state, which is reserved, decided or done; and, once decided, the _disposition_: placement, or a refusal or drop with its cause (and policy_code), and whether a Control Notice is still pending. *Recording.* The receiver records a candidate at check 12, after checks 1 to 11 have passed. A candidate that fails any of them records nothing, so a mangled copy cannot suppress the intact one that follows. Misrouted and refused candidates are recorded too, so that their replays are dropped quietly instead of being evaluated again each time. Control Notices (Section 12.6.8) use the same store. *Reservation, decision and effect.* These are three durable steps, in order, each atomic: 1. *Reserve.* Check 12 creates the reserved record and mints a fresh notice_id. The candidate stays in the intake location until the record is done. A reservation is undecided, and authorizes nothing. 2. *Decide.* The receiver routes, correlates, permits and applies policy (Section 12.6.3), and persists the disposition on the record, which becomes decided. A refusal or drop also marks its control notice as pending. 3. *Effect.* For a placement, the notice is written under the minted notice_id; for a refusal or drop, the Control Notice is durably queued. Only then is the record done. Placement is idempotent: a notice that is already present is not written again. Rapp & Adams Expires 24 April 2027 [Page 61] Internet-Draft AMMP October 2026 A copy that arrives while another copy is reserved or decided matches it at check 2, and is dropped. On restart, the receiver MUST finish every record that is not done, exactly once, by resuming its _recorded_ state, and MUST NOT infer a disposition from the absence of a notice: * a reserved record is decided then, under the configuration in force at recovery (the receiver has no older one it can trust), and then takes effect; * a decided placement is placed if the notice is absent, and is marked done without placing it again if the notice is present; * a decided refusal or drop is not published: its pending control notice is queued, once, and the record is marked done. An implementation MUST survive a crash at each of these points: after reservation, after the decision, after a notice is placed, after a control notice is queued, and after a terminal refusal is recorded, including when the graph or policy changed while it was down. *Retention.* The receiver MUST retain each done record while H ≤ ts + P, and MAY retain it longer. It MUST NOT prune a record that is not done. A known statement replayed after its retention has lapsed fails freshness, so it is demoted and never placed. Duplicate suppression is therefore effective only within retention: this profile does not promise exactly-once delivery, and an implementation MUST NOT claim it. *Resending.* A sending Runtime whose hand-off was ambiguous MAY resend the same id, signed again with a new ts. If the first copy arrived and is retained, the resend is dropped at check 12, and the control notice tells the sender so; if the first copy did not arrive, the resend is placed. A sending Runtime that resends MUST keep the same id, including across a resend that a person approved. This permission is idempotent only within retention. Once the first copy's record has been pruned, a resend of the same id under a fresh ts finds no record, and is placed a second time. A deployment that cannot accept that MUST forbid signing again in its own deployment profile, and resend only the original bytes, which check 2 drops or freshness demotes. The permission is kept for portability, not as a recommendation. Rapp & Adams Expires 24 April 2027 [Page 62] Internet-Draft AMMP October 2026 *Recovery.* The store and H MUST survive a restart. If the receiver loses the store, or cannot show that a restored store is complete, it MUST restore this barrier: no statement that could already have been accepted under the lost coverage may be admitted again, including at the boundary, under a clock that stepped back, and under the largest F that was ever in effect. * *With H intact*, the receiver sets floor to one second after max(C, H) + Fmax, persisted before admission resumes. C is the moment from which it again holds complete evidence, H is the high- water clock at the loss, and Fmax is the largest F in effect at any point that could have admitted a statement still within the lost coverage. Fmax is taken from the retained history of F if that history is provable, and is 900 otherwise. Fmax MUST NOT be chosen from the wall-clock interval ending at C: after a clock rollback, that interval can miss a larger F that was in effect near H. Retaining the largest F ever configured is safe. Any statement accepted before the loss had ts ≤ H + F ≤ max(C, H) + Fmax, so none reaches the floor. The floor is strictly above that bound: an inclusive floor would re-admit a statement accepted at exactly now + F, and C alone would re-admit one accepted before the clock stepped back. * *With H lost as well*, no trustworthy bound exists, and the receiver MUST close peer admission: candidates reaching check 7 are held in intake, and nothing is placed or demoted on a window the receiver cannot define. Admission reopens only when the operator records a bound B that is at or after the largest receiver time before the loss. The floor is then one second after B + Fmax, with Fmax determined as above: losing H does not by itself lose the history of F. The held candidates are then evaluated in arrival order. The current wall-clock time is not a bound. *Window changes.* Narrowing P or F takes effect at once. Widening P MUST NOT re-admit anything: the receiver sets the floor to the lower bound in effect at the moment of the change, and the window then widens only as records accumulate. 12.6.6. Correlation and Edges Between Hosts *The ledger.* A Runtime keeps a record for every peer message it issued to another host or placed. Each record holds the id; the local Agent and the remote Agent; the direction (issued or placed); and an expiry, the configured reply window (Section 12.5). The ledger is scoped to peer messages: a mail notice id never resolves. Rapp & Adams Expires 24 April 2027 [Page 63] Internet-Draft AMMP October 2026 *Resolution.* A verified statement carrying in_reply_to resolves only if a record exists whose id equals in_reply_to, whose local Agent equals to, whose remote Agent equals from, and which has not expired. Possession of an id is not authority: an id presented by another Agent on the same host, or by any Agent on another host, does not resolve. *Permission.* A resolved reply to a record this Runtime issued is a reverse message, and is permitted without an edge (Section 12.5). A resolved message against a record this Runtime placed is a follow-up in the same direction, and needs the edge from from to to. A statement without in_reply_to needs the edge. *The receiver's own graph.* The edge MUST be checked against the receiver's own graph. The sending Runtime checked its edge at submission (Section 12.4), but which peers may task an Agent is the receiving Runtime's decision. If correlation fails, the outcome is "refuse: unresolved reply"; if permission fails, it is "refuse: no edge". Neither is demoted. *At submission.* The sending Runtime MUST apply the same binding: a reply key resolves only if the submitting Agent is the record's local Agent. It SHOULD also refuse at submission a message it already knows the receiver will refuse, such as one for a pair it knows has no edge or is held by policy, so that the Agent learns of it at once. The receiver remains the authority. 12.6.7. Keys *Configuration.* The operator configures, at every receiver, each peer Runtime's domain set, its Runtime address (amap.router@ or equivalent), and its key set. Trust anchors are configured, not discovered: there is no DNS publication, directory or certificate chain. A receiver MUST refuse to load a configuration in which: * a key_id occurs twice, across all peers (the receiver selects a key by k alone); * a not-before time is later than its not-after time; * a public key is not a canonical encoding of a curve point; or * a public key is of small order. A sending Runtime SHOULD make its key ids globally unique, for example by deriving them from a fingerprint of the public key. Rapp & Adams Expires 24 April 2027 [Page 64] Internet-Draft AMMP October 2026 These are configuration-time checks, a gate separate from verification. A bare Ed25519 implementation will construct and verify under a small-order or non-canonical key; the key loader, not the verifier, is what refuses one. An implementation MUST demonstrate the refusal through its loader, and MUST NOT count passing the vectors marked for rejection at verification as covering it. *Rotation.* A receiver MUST support at least two concurrent keys per peer. A rotation proceeds as follows: 1. Add the new key at every receiver. 2. Switch the sender to it. 3. If the sender stops using the old key at time T, receivers keep the old key valid until at least T + F + P, because a statement signed at T can carry a ts up to T + F, and arrive up to P after that. Validity is inclusive at both ends. Revocation is removing a key or ending its validity. Replay records are scoped to the Runtime rather than the key, so neither retiring nor revoking a key erases replay evidence (Section 12.6.5). *The signing key* is a Runtime credential (Section 3.1). It MUST NOT be readable from any Agent Namespace or by the admitting component. It MUST NOT be used to sign anything except this profile's statements and Control Notices, which their kind members keep apart. *Verification policy.* Verification is Ed25519 as specified in [RFC8032], with the cofactorless equation [S]B = R + [k]A. A verifier MUST reject a signature whose S is not less than L; a non- canonical encoding of R or A; and a key that failed the configuration checks above. A point encoding MUST be decoded canonically before any other check is applied to it: the "negative zero" encoding of x = 0 (y = p − 1 with the sign bit set) is non-canonical, and is rejected as such before a small-order check is reached. This document requires nothing about how Ed25519 is computed. A vetted library, a system binary or any other implementation conforms if it produces these results. The conformance suite holds the test vectors of Section 7.1 of [RFC8032] (TEST 1 to 3), and negative vectors marked for rejection either at verification or at configuration (Appendix J). An implementation MUST pass both kinds through the implementation it ships. Rapp & Adams Expires 24 April 2027 [Page 65] Internet-Draft AMMP October 2026 12.6.8. Control Notices A receiving Runtime tells a configured sending Runtime what it did with a statement it did not place, using a signed _Control Notice_. The peer-control schema (Appendix F) pins it: version v 1, closed, with the members v, key_id, kind ("control"), id, from, to, ts, about, outcome, cause, and policy_code (with cause policy). *Form.* A Control Notice is carried in an AMAP-Peer-Control header field with the k, s and b grammar of Section 12.6.2, signed with the same per-Runtime key. Its kind makes it impossible to read as a peer statement, and the reverse. It carries no free text: the cause vocabulary is closed, and the receiving Runtime composes any words itself. It travels as mail between Runtime addresses. *When.* A receiver SHOULD send one for every outcome Section 12.6.4 marks, and MUST send one only when the eligibility conditions of Section 12.6.4 hold in full: never for a failure before check 5(b), never for a duplicate caught at check 2, never about a Control Notice, and never merely because a key was found. There is one notice per record: the pending notice is part of the record's disposition (Section 12.6.5), MUST be queued durably before the record is done, and MUST be sent at most once, including across a crash. A receiver MAY rate-limit Control Notices; any suppression MUST be logged. *policy_code* is defined by the deployment. A receiving Runtime renders a description it defines locally. An unknown token gets a generic description, and MUST NOT be interpreted. *Addressing.* from is the receiver's own Runtime address. to MUST be the Runtime address configured for the peer that owns the verifying key, and MUST NOT be an address taken from the message. A control notice MUST NOT be sent to a Runtime that is not a configured peer. *Receiving one.* A Runtime applies checks 1 to 5 of Section 12.6.3, with AMAP-Peer-Control and the control schema in place of the statement's. It then requires all of these: from is the Runtime address of the peer that owns the key; to is its own Runtime address; ts is fresh (Section 12.6.5); the notice's pair (Runtime, id) is new; and about names a ledger record that this Runtime issued to an Agent in the signer's domains. Rapp & Adams Expires 24 April 2027 [Page 66] Internet-Draft AMMP October 2026 *What it does.* It SHOULD give the issuing Agent a failure notice authored by the Runtime in inbound/ (the delivery failure notice of Section 12.5), with no text from the Control Notice except the vocabulary values. Where it does, the failure notice is owed from the moment the record is reserved, and the record is done only once the failure notice is durable; a crash in between is finished by recovery, once. *One per message.* An issued record receives at most one failure notice. A later Control Notice about the same about, under a different id, is dropped. A "dropped, duplicate" report means that a record for the message exists at the receiver, whose disposition may have been placement; it is not proof that the task failed. *Failures.* A Control Notice that fails any rule is dropped and logged: it MUST NOT be placed, handed to mail admission, or answered. A message carrying both fields is dropped. 12.6.9. Optional: Sender Age, and Deployment Admission Policy A deployment MAY refuse authenticated peer messages for reasons of its own, such as a first-sight quarantine for a new sender, or a requirement that every delegation between hosts carry sender_exposure. The refusal is "refuse: policy", with a policy_code defined by the deployment. The policies themselves are the deployment's business, not this profile's. Measuring a sender's age at the receiver, from first sight, would make every Agent's first delegation to each host fail. The optional signed member sender_since exists to avoid that: * It is the time at which the sending Runtime first enrolled the sending Agent's identity, as that Runtime defines identity, taken from its durable registry. * It is a Runtime assertion, covered by the signature, and MUST NOT be taken from text the sender supplied. * It has the format of ts, and is not later than ts. Skew is absorbed by the age itself: a receiver measures age as its own clock minus sender_since, so a few minutes of skew change nothing under a policy measured in hours. Rapp & Adams Expires 24 April 2027 [Page 67] Internet-Draft AMMP October 2026 * *Continuity.* The sending Runtime MUST NOT reset it for a continuation of the same identity: a restart, a rebinding, a re- creation or a key rotation keeps the value. A new enrollment of the identity starts a new value. The sending Runtime decides which of the two a given event is, and MUST decide it the same way every time for that identity. * *Consistency.* A receiver that enforces a sender-age policy MUST keep the last value it accepted for each pair (Runtime, from). A later statement that asserts an _earlier_ value claims an older enrollment than the Runtime previously asserted; it is inconsistent, and the receiver refuses it with its own policy_code. A later value is a new enrollment, and the age starts again. A receiver MAY enforce a minimum sender age from it. Its absence means "not asserted", and a receiver MUST NOT read it as "new" or as "old enough". A deployment that requires it refuses its absence with its own policy_code. 12.6.10. The Intake Location The intake location is REQUIRED, as a structural property with no prescribed layout. A message admitted from a shared transport that presents as a peer or control candidate MUST be held, until it is disposed of, in a location that is neither inbound/ nor peer/, that is writable by the admitting component and readable by the verifying component, and that is unreachable from any Agent Namespace. Candidates MUST be held as the candidate of Section 12.6.2: the verifier MUST NOT parse mail. The verifier MUST apply the read-side discipline of Section 4.1 to candidates, since the admitting component is less trusted than the verifier. The verifier disposes of each candidate exactly once: it places it in peer/, minting the notice_id at reservation (Section 12.6.5); or it hands it to mail admission, which, if it delivers, places it in inbound/ under a notice_id minted there; or it drops or refuses it, logging the cause. A candidate held while peer admission is closed (Section 12.6.5) is not yet disposed of, and stays in the intake location until admission reopens. How a Runtime divides its intake (one location or several, for example by candidate kind), and where it keeps the traffic it authors itself, are implementation choices; this section constrains only the properties above. Rapp & Adams Expires 24 April 2027 [Page 68] Internet-Draft AMMP October 2026 12.7. Sender Exposure The split between Runtime assertions and sender claims (Section 7.1) applies: disposition, verdict and sender_standing are Runtime assertions, while subject and body_text (and, on the mail tree, from) are sender claims. An exposure record belongs on the assertion side. It records _delivery_, not _reading_: a Runtime can know that it delivered external mail to the sender's Namespace, but cannot know what the sender did with it. A Peer Notice asserting exposure, with the stamp in message.sender_exposure: { "contract_version": "2", "notice_id": "7b2a1c0d9e8f47a6b5c4d3e2f1a0b9c8", "ts": "2026-09-03T12:05:00Z", "kind": "peer", "message": { "id": "7b2a1c0d9e8f47a6b5c4d3e2f1a0b9c8", "from": "triage@fleet.example", "subject": "Customer asks for an export", "preview": "A customer mail this morning asks whether", "mailbox": "peer", "sender_exposure": { "asserted_by": "amap.router@fleet.example", "external_mail_delivered": true, "window_start": "2026-09-02T12:05:00Z", "window_end": "2026-09-03T12:05:00Z", "last_external_delivery_ts": "2026-09-03T08:41:10Z", "derived_from": ["1721457600000000001-4242"] } } } Figure 9: fixtures/valid/peer-exposure-true.json * *Placement.* It is a member of message, beside sender_standing, provenance and verdict, so that the Runtime's assessments of the message are kept together. The schema requires asserted_by (a bare addr-spec naming the asserting Runtime; between hosts, the signer), external_mail_delivered, window_start and window_end, and requires last_external_delivery_ts when the boolean is true. Its absence when the boolean is false is stated here, not in the schema. Rapp & Adams Expires 24 April 2027 [Page 69] Internet-Draft AMMP October 2026 * *Shape.* A boolean over an explicit window, plus the most recent timestamp. There is no count, because a count would assert a precision that a measure of delivery lacks. derived_from is optional, and present only where the Runtime observed the derivation itself (the sender's reply key named a notice in its mail tree). * *Absence means "not assessed".* A consumer MUST NOT read the member's absence as "no exposure". false, asserted, is a stronger statement than the member omitted: it says that the Runtime assessed the window and delivered no external mail. A Runtime that is the only writer of a sender's mail tree, and has written there only same-principal Agent mail and its own delivery failure notices, SHOULD assert false. Notices the Runtime authors in the mail tree (the delivery failure notices of Section 12.5) are not external mail, and MUST NOT make the boolean true. Moving window_start forward to the moment the Runtime's assessment began (for example, an instance's approval time after a reset) keeps the window on the wire equal to the span assessed. * *Not settable by the sender.* On one host, the Submit Request envelope is closed (Section 13.1), so a request carrying the member is rejected, which is stronger than stripping it. Between hosts, it is taken only from the verified statement, and is removed on demotion (Section 12.6.4). A Runtime MUST NOT accept it from any sender. * *Use.* It MUST NOT drive routing or treatment (Section 12.3). It is advisory, for audit, and covered by the signature. *Lineage declared by the sender is not included.* Such a member would ride on the Submit Request, whose envelope is closed, so adding it would be a major change; it would work only when the sender cooperates, which is the case where it is least needed; and a forgeable member invites readers to trust it. Most of its value is recovered without one: when a sender's reply key names a notice in its _mail_ tree, the Runtime knows which external message the peer message derives from, and records that as its own observation (derived_from, and the audit log). 12.8. Delivery Outcomes, Retention, Deletion and Presentation * *Delivery outcomes are not a profile artifact.* A Connector that reports what it did with a notice (delivered, held, refused, and so on) writes the report under outbound/ext//, in a shape agreed out of band with its Runtime, under the stable Connector id that Section 4.1 requires. Section 4.1 carries the one normative rule: an outcome is a claim by the receiving side, never proof of Rapp & Adams Expires 24 April 2027 [Page 70] Internet-Draft AMMP October 2026 delivery or of non-delivery, and the Runtime's own delivery record is authoritative over it (Section 14.15). If a second Connector needs outcomes, that is the point at which to define a shape for them in this profile. * *Retention.* The record is the audit log, not the spool. The Runtime logs every candidate it disposes of (id, from, to, tree, exposure, signature verdict, and any demotion or refusal cause) to the per-Agent audit/log.jsonl it owns, and to its own store of record. Spool retention for peer/ follows the mail row of the deletion table (Section 4.2) unchanged. * *Deletion rights.* The deletion table (Section 4.2) applies to peer/ artifacts exactly as to inbound ones: written by the Runtime, and deleted by the Runtime only. * *Presentation.* A Connector presenting a peer body MUST meet the presentation obligations of Section 7.1, with one prohibition and two additions. It MUST NOT convey that the sender is unauthenticated: on this tree, message.from is asserted by the Runtime from the restricted write path (Section 12.4), and saying otherwise contradicts the mechanism the tree exists to provide. It SHOULD convey that an authenticated request is still bounded by what the receiving Agent is authorized to do: a verified sender can ask for something the receiver may not perform, and a framing that reads as a grant invites the reader to treat verified identity as having settled that question. It SHOULD convey that authenticated origin is not a warranty about content: peer content is authored by a peer and unscreened, and may paraphrase external mail (Section 12.7). Authenticated _who_; unverified _what_. None of this is an enforcement mechanism. Enforcement on this tree is the write-authority partition and the origin-follows-the- tree rule (Section 12.3); presentation is what a Connector states to a reader who can ignore it. The reason to get it right is that the alternative asserts something false. 13. Versioning and Conformance 13.1. Versioning *The fail-closed rule.* contract_version is a major-version string. A consumer that reads a *higher major* than it speaks MUST *fail closed* — refuse to process the artifact — and MUST NOT silently mis- parse it. Rapp & Adams Expires 24 April 2027 [Page 71] Internet-Draft AMMP October 2026 *Version-check ordering.* A consumer MUST implement the version check by reading contract_version *before* any schema validation, comparing majors as non-negative decimal integers. An absent major, a lower major, or a higher major is a *version refusal*: fail closed, as a distinct, logged outcome, never folded into the malformed-document drop path of Section 7.1. Checking the version first is what makes fail-closed implementable at all. A validator that runs schema checks first cannot distinguish "wrong version" from "malformed," and a strict schema would reject a future-major document for the wrong reason entirely. This ordering rule is about _inbound_ artifacts a Connector consumes, which have no mechanism in this protocol to carry a distinct outcome back to the Runtime. It does not reach the _outbound_ direction: a Runtime version-refusing a Submit Request MAY carry that refusal on the ordinary request-rejection path — a top-level rejected Result with reason_code: "unsupported_contract_version" (Section 6.1) — and, per the conformance classes below, MUST do so for the version-absent request fixture. That is still a distinct, legible outcome rather than a fold into the malformed-request case: reason_code is exactly this protocol's mechanism for saying _why_ a rejected fired, and a consumer MUST already tolerate a reason_code it does not otherwise interpret. A Runtime has no equivalent document to write for a version-refused _Notice_ it never emits — hence the asymmetry: logged-only inbound, rejected outbound. *A version signal, not member rejection, is the boundary.* Rejecting a document because it carries an unrecognized member is not a version mechanism. Under closed envelopes, a consumer that meets a new member rejects the _whole document_: inbound, that is silent loss of the message; outbound, it is an accidental schema error rather than a designed refusal. And while the version string is unchanged, nothing can react to the new member. A change that a consumer of the current wire major would reject or misread as a whole document therefore requires a new wire major, so that the consumer refuses it _by version_, legibly, and both sides upgrade deliberately. Schema strictness and protocol version are separate axes, and this document does not use the first as the second. *Unknown-member tolerance is direction-aware.* The rule is stated per direction because the two directions have opposite threat models. * *Runtime-authored artifacts — Deliver Notice, Inbound Message, Result, Peer Notice — are open.* A consumer MUST ignore members it does not recognize, at every level of the document, and MUST NOT reject, drop, or fail the artifact on their account. Strictness in this direction defends nothing: the producer is trusted by Rapp & Adams Expires 24 April 2027 [Page 72] Internet-Draft AMMP October 2026 construction, and in the one posture where an attacker can author a Notice — a writable inbound tree — that attacker can author a fully schema-valid one, so strictness is not the defense there. Its failure mode is silent loss of mail. Emitting an unknown member is still a *producer* non-conformance: a Runtime MUST NOT emit members this document does not define. In the schemas, additionalProperties: false is absent from the deliver-notice, inbound-message, result and peer-notice schemas at every level — top level, message, Descriptor items, and attestation — while the clean-gating conditionals are orthogonal and remain. * *The Submit Request envelope is closed*, additionalProperties: false at every level. Closure does real security work there: it is what bars an outbound content_ref and any other smuggled member; the validator is the trusted Runtime; and a refusal is a _legible, per-request_ rejected Result, never a silent loss. A Runtime MUST reject a request carrying unrecognized members. * *Consequently:* adding an optional member to a Runtime-authored artifact is additive and needs no new wire major; adding any member to the Submit Request requires one. * The Binding Record is closed, for the separate reason given in Section 9.1. The signed Peer Statement and Control Notice are closed because they are signed bytes another Runtime acts on (Section 12.6.1); they carry their own version member, v. *Tolerance extends to values, not only members.* An optional, explicitly advisory enum on a Runtime-authored artifact — message.sender_standing and message.provenance (Section 7.1) — is scoped the same way. A consumer MUST tolerate a value it does not recognize, treating it as opaque or as the member being absent, and MUST NOT reject, drop, or fail the artifact on that account; a Runtime MAY add a value to either vocabulary without a new wire major. Without this, a Runtime adding a new sender_standing or provenance value would silently lose every Notice carrying it, against every Connector still validating the old enum — the member case again, at the value level. In the schemas, the enum constraint is absent from these two members. This is *not* a universal loosening of every enum. kind, outcome — both the top-level Result outcome and the per-attachment outcome — and the inbound Descriptor's disposition remain *closed, enum-locked vocabularies*. kind and outcome gate which processing path a document takes at all, and disposition gates byte publication (Section 7.2), so a new value in any of them is a behavioral fork a consumer cannot safely treat as opaque. Introducing one requires a new wire major. Rapp & Adams Expires 24 April 2027 [Page 73] Internet-Draft AMMP October 2026 *When the wire major changes, and when it does not.* The wire major exists to force a legible, fail-closed refusal where a _whole document_ that a counterpart already accepts would otherwise be silently misread or silently dropped. A change whose only incompatibility already travels a designed, legible, per-artifact refusal path (a rejected Result for one request, or the drop of one Notice under Section 7.1) does not require one. Changing the major for such a change would make every counterpart on the old major refuse _all_ traffic, including traffic the change does not affect, which is the failure the major exists to prevent. Revisions of this document are not wire versions: a revision that requires no new wire major leaves contract_version unchanged. *Why the wire version is "2".* The wire version starts at "2", not "1", because the value "1" is already taken. Before this document, implementations exchanged an earlier, incompatible shape under "1": it had no attachments, and its envelopes were closed. Reusing "1" for this document's shapes would give one major two incompatible meanings, and would defeat the fail-closed rule above exactly where it is needed: a consumer of the old shape would accept a document it cannot process. "2" is also what current implementations emit and check. Appendix L records the history. The authors would be glad of the community's view on whether a reset is nonetheless worth its cost. *Change control.* A capability change starts with a change to this document, its schemas, and its fixtures, _before_ either side implements it. Neither a Runtime nor a Connector may invent a wire member locally. A change that breaks an existing fixture is taken deliberately by both sides, and requires a new wire major only under the rule above. 13.2. Required versus optional contract_version is REQUIRED on the Submit Request, the Deliver Notice, the Inbound Message, the Result, the Peer Notice and the Fleet Roster. It is OPTIONAL on the Binding Record (Section 9.1) and the Peer Directory, and the signed Peer Statement and Control Notice carry their own version member, v, instead. The rest of this table reflects what implementations emit today; specified-but-unbuilt enrichment stays optional, and tightening any of it to required would be a future major bump. Rapp & Adams Expires 24 April 2027 [Page 74] Internet-Draft AMMP October 2026 +===================+=====================+=========================+ | Artifact | Required | Optional (specified; | | | | may be absent) | +===================+=====================+=========================+ | *deliver-notice* | contract_version, | message.{task_id, | | | notice_id, ts, | thread_id, | | | kind, message{id, | in_reply_to, | | | from, subject, | references, | | | preview, mailbox} | sender_standing, | | | | verdict, provenance}, | | | | message.attachments[] | | | | (items require | | | | filename, media_type, | | | | size_bytes, | | | | disposition; sha256 | | | | and content_ref | | | | optional, gated to | | | | disposition == | | | | "clean") | +-------------------+---------------------+-------------------------+ | *submit-request* | contract_version, | agent_id, ts, | | | req_id, draft{to, | in_reply_to, | | | subject, body_text} | draft.{cc, | | | | reply_to_message_id}, | | | | draft.attachments[] | | | | (items require | | | | filename, media_type, | | | | size_bytes, sha256) | +-------------------+---------------------+-------------------------+ | *result* | contract_version, | detail, reason_code, | | | req_id, outcome | ts, recipients, | | | | message_id, job_id, | | | | attestation (requires | | | | form; token and | | | | claims optional), | | | | attachments[] (items | | | | require index, | | | | outcome) | +-------------------+---------------------+-------------------------+ | *inbound-message* | _(the artifact | id, date, from, to, | | | itself is OPTIONAL | cc, subject, | | | — Section 7.3)_; if | attachments[] (same | | | present: | Descriptor rules as | | | contract_version, | deliver-notice) | | | notice_id, | | | | body_text | | +-------------------+---------------------+-------------------------+ | *binding-record* | _(the file itself | label; | Rapp & Adams Expires 24 April 2027 [Page 75] Internet-Draft AMMP October 2026 | | is OPTIONAL — | contract_version | | | Section 9)_; if | (optional — if | | | present: agent_id, | present, pinned to | | | address, | "2") | | | provisioned_ts | | +-------------------+---------------------+-------------------------+ | *directory* | _(the artifact | contract_version (if | | | itself is OPTIONAL | present, pinned to | | | — Section 10)_; if | "2") | | | present: router, | | | | written_at, | | | | delegate_to, | | | | mail_to | | +-------------------+---------------------+-------------------------+ | *peer-notice* | _(peer-origin | message.{in_reply_to, | | | profile — | references, task_id, | | | Section 12.2)_ | thread_id, | | | contract_version, | sender_standing, | | | notice_id, ts, | provenance, verdict, | | | kind: "peer", | sender_exposure, | | | message{id, from, | attachments}; | | | subject, preview, | in_reply_to is | | | mailbox: "peer"} | REQUIRED on a reply | | | | (Section 12.5). The | | | | body spool and | | | | Sidecars are as for | | | | deliver-notice. | +-------------------+---------------------+-------------------------+ | *peer-statement* | _(between hosts — | in_reply_to, | | | Section 12.6.1)_ v, | sender_exposure, | | | key_id, kind, id, | sender_since | | | from, to, ts, | | | | subject, | | | | body_sha256, | | | | attachments; | | | | carried in exactly | | | | one AMAP-Peer- | | | | Statement field | | | | with the tags k, s | | | | and b | | +-------------------+---------------------+-------------------------+ | *peer-control* | _(between hosts — | policy_code (REQUIRED | | | Section 12.6.8)_ v, | with cause policy) | | | key_id, kind, id, | | | | from, to, ts, | | | | about, outcome, | | | | cause; carried in | | | | exactly one AMAP- | | Rapp & Adams Expires 24 April 2027 [Page 76] Internet-Draft AMMP October 2026 | | Peer-Control field | | | | with the tags k, s | | | | and b | | +-------------------+---------------------+-------------------------+ | *roster* | _(the artifact | interval_s | | | itself is OPTIONAL | | | | — Section 11)_; if | | | | present: | | | | contract_version, | | | | router, written_at, | | | | members[] (items | | | | require address, | | | | state) | | +-------------------+---------------------+-------------------------+ Table 5 inbound/notices/processed/ is an OPTIONAL *directory convention* (Section 4.1), not a protocol artifact: it carries no schema, and no fixture exists or is needed for it. Emission of contract_version on the artifacts that require it is mandatory. An implementation not yet emitting it has an implementation conformance gap, not a license from this document. 13.3. Conformance *The fixtures are the other side.* fixtures/ holds golden artifacts, and a producer or consumer proves conformance by passing them with *no counterpart present* (Appendix J). That is what makes independent implementation practical: neither side needs the other to exist in order to demonstrate conformance. Not every obligation in this document is wire-shaped. A _behavioral_ obligation — one about how an implementation acts against a filesystem, rather than about the shape of a document — lies outside the fixture gate by construction and is checked operationally instead. Appendix J lists every obligation of this kind that a green run does not prove. This document defines two conformance classes. An implementation MAY claim both. Claiming either without the corresponding fixture run, and without the prescribed operational check for that class's behavioral obligations, is not conformance. *Runtime conformance class.* An implementation claiming Runtime conformance: Rapp & Adams Expires 24 April 2027 [Page 77] Internet-Draft AMMP October 2026 * MUST produce Deliver Notice documents valid against the deliver- notice schema (Appendix C) and Result documents valid against the result schema (Appendix B); MAY produce Inbound Message documents, which when produced MUST be valid against the inbound-message schema (Appendix D); and MAY produce a Binding Record valid against the binding-record schema (Appendix E). * MUST consume Submit Request documents: it MUST accept every valid request-* fixture and MUST reject every invalid request-* fixture. * MUST demonstrate its producing side by confirming that its own emitted Notices, Results, and Inbound Messages satisfy the same schemas the suite's valid notice-*, result-*, and message-* fixtures satisfy — *including the three fixtures carrying unrecognized members*. A Runtime MUST NOT itself emit unrecognized members, but its _parser_, exercised by those fixtures, is what a Connector conformance run depends on it never having to special- case. It MUST also confirm that any Binding Record it writes satisfies the valid identity-* shape. * MUST uphold the filesystem and policy obligations of Sections 4 through 8 that no document validator can observe. Appendix J lists them explicitly as what a green fixture run does *not* prove, and they are obligations of the class regardless — write- side path discipline (the conjunctive parent-chain-pinning, no- follow-while-building, and non-following-final-component rule), ingest by copy, read-side open discipline, and single-writer/ single-drainer chief among them. *Connector conformance class.* An implementation claiming Connector conformance: * MUST produce Submit Request documents valid against the submit- request schema (Appendix A), and MUST NOT emit an outbound content_ref (Section 5.1). * MUST consume Deliver Notice, Inbound Message, and Result documents: it MUST accept every valid notice-*, message-*, and result-* fixture — *including those carrying unrecognized members or unrecognized advisory-enum values* — and MUST reject, meaning drop rather than surface, every invalid notice-* fixture *except the version-refusal one*, which it MUST instead refuse via the distinct version-refusal path (Section 13.1): never folded into the drop path, and never surfaced to the Agent either. Section 7.1's drop rule is *two independent conditions, not a closure over schema validation*. "Fails schema validation for any reason other than an unrecognized member" (check 1) covers eleven Rapp & Adams Expires 24 April 2027 [Page 78] Internet-Draft AMMP October 2026 of the twelve other invalid notice-* fixtures — missing-required- member, wrong-kind, enum, pattern, and conditional violations alike. The twelfth is schema-valid on its own, producing zero errors from the gate's schema validator, and is caught only by the content_ref index and prefix binding (check 2, Section 7.2), a named post-check the schema cannot express. A Connector conformance claim requires running *both* checks; schema validation alone does not close this set. Accepting the clean-Descriptor fixture — a Descriptor asserted clean and carrying content_ref and sha256 — is part of this obligation, and acceptance means relaying the Notice, not fetching the bytes: a Connector that never dereferences content_ref satisfies it (Section 7.2). * MUST return "not resolved" for a body whose Inbound Message is absent, and MUST NOT attempt any fallback fetch (Section 7.3). * MUST tolerate a Binding Record that is absent, and MUST NOT crash or otherwise misbehave on one that is hostile or malformed (Section 9.1). * MUST NOT require write access anywhere under inbound/, demonstrated by the operational check below rather than by a fixture (Section 4.1). * MUST demonstrate its producing side by confirming that its own emitted requests satisfy the same schema the suite's valid request-* fixtures satisfy, and MUST uphold the write-ordering, atomicity, and read-discipline obligations of Section 4.1, which the fixtures cannot check. *Peer-origin sub-classes.* The peer-origin profile (Section 12) adds two sub-classes, each requiring the corresponding class above plus the profile's fixtures and operational checks. Claiming either without both is not conformance. An implementation claiming *Runtime + peer-origin* conformance, in addition to the Runtime class: * MUST produce Peer Notices valid against the peer-notice schema (Appendix F), and MUST confirm that its own emitted Peer Notices satisfy the same schema the valid peer-* fixtures satisfy; * MUST refuse to emit into peer/ any document that would fail that schema, and MUST NOT emit a peer document into inbound/ (Section 12.2); Rapp & Adams Expires 24 April 2027 [Page 79] Internet-Draft AMMP October 2026 * MUST uphold the operational obligations of the profile: the write- authority partition, proven by the write probe, and verification placement (Section 12.3); a Runtime-asserted peer from (Section 12.4); in_reply_to only when resolved, and refusal rather than stripping (Section 12.5); the intake location, and hashing of bytes by the verifier itself (Section 12.6); and sender_exposure never settable by a sender (Section 12.7); * if it verifies peer traffic from other hosts, MUST reach every step's expected outcome in the signed cases, and MUST pass the Ed25519 vectors through the implementation it ships (Appendix J). An implementation claiming *Connector + peer-origin* conformance, in addition to the Connector class: * MUST accept every valid peer-* fixture and MUST reject every invalid peer-* fixture when read from peer/notices/, and MUST reject invalid/notice-kind-peer.json when read from inbound/ notices/; * MUST derive the origin class from the tree alone (Section 12.3), and MUST act on a peer message at most once, holding rather than fanning out when it cannot identify a single target (Section 12.4); * MUST tolerate unknown members on a Peer Notice, whose envelope is open (Section 13.1). *Read-only inbound tolerance — the prescribed operational check.* Run the Connector against an inbound tree it cannot write — a genuinely read-only mount, not merely cleared permission bits, since a same-uid Agent can change a mode it owns — and confirm that it starts, relays every pending Notice, resolves bodies, and submits outbound requests, without requiring any write under inbound/ to succeed. 14. Security Considerations This section gathers the threats this protocol's obligations exist to address. It introduces no new mechanism: apart from the rule for special-use domain names (Section 14.16), which is stated only here, every requirement referenced here is stated normatively in Sections 4 through 13. The organizing principle throughout is the invariant of Section 3.2 — enforcement is Runtime-side, and no property depends on the Agent or its Connector behaving. Rapp & Adams Expires 24 April 2027 [Page 80] Internet-Draft AMMP October 2026 The threat actor assumed throughout is a *fully compromised Agent* with write access to its own Namespace, running as a uid that owns the files it creates, able to act concurrently with the Runtime, and able to observe or predict the filenames the Runtime is about to use. 14.1. Path authority and traversal An identifier drawn from a file's _contents_ MUST NOT name a write path (Section 4.1). A Runtime that trusted a req_id member inside a request body to name the path its Result is written to would let the Agent, which controls that member, direct a Runtime-privileged write anywhere the Runtime can reach. The obligation is that the Runtime derives identifiers from the _validated filename_, over a restricted character set, and re-derives every path from data it controls. The same reasoning is why inbound Sidecar directories and the body spool are keyed on the Runtime-minted notice identifier rather than the provider message.id: the provider identifier is external, arbitrary, and not character-set-safe, and using one as a path component would hand the choice of path to the sender. The schemas pin notice_id and req_id to character sets that are safe as path components; message.id is deliberately unconstrained, because it is never used as one. 14.2. Symlinks, descriptors, and open discipline A shared filesystem between a trusted and an untrusted party makes every path-based check racy. The Agent can replace any component of a path it owns with a symlink, at any moment, including between a Runtime's check and the Runtime's subsequent act. Section 4.1 states the write-side discipline as three conjunctive requirements, and the conjunction is the point: each leg closes a gap the other two leave open. * Pinning only the final component leaves the _parent_ swappable, and rename and directory-creation calls have no no-follow flag at all — so a bare "do not follow symlinks on writes" rule is not sufficient. * Pinning the parent chain without refusing symlinks _while acquiring_ it means the pinned descriptor may point at whatever the Agent planted before the Runtime opened it — including across a Runtime restart, when the chain is re-opened. * Pinning the whole chain correctly still says nothing about the final filename. Every Runtime-written filename in the layout tree is predictable or observable to the Agent, so a pre-planted Rapp & Adams Expires 24 April 2027 [Page 81] Internet-Draft AMMP October 2026 symlink at that name receives the Runtime's write with the Runtime's privileges unless the final open refuses to follow it and, where the file is newly created, refuses to reuse an existing name at all (O_EXCL). The append-only audit log is the one write with no .tmp sibling to lean on, and it is called out separately for two reasons. O_EXCL cannot apply to a file that legitimately already exists, so the final-component protection there is a no-follow, append-mode, *non- blocking* open plus an fstat through the resulting descriptor. The non-blocking part is load-bearing rather than a performance nicety: a no-follow flag does not apply to a FIFO, because a FIFO is not a symlink, and opening a FIFO for writing blocks until a reader appears. An Agent that plants a FIFO at that filename therefore stalls the Runtime's drain indefinitely — a denial of service that the exclusive-create branch happens to refuse for free, and that this branch must refuse explicitly. The read side mirrors all of it (Section 4.1): per-component no- follow opens or an equivalently pinned walk, a non-blocking open, an fstat through the open descriptor rather than a pre-open stat (which is itself a race), refusal of a multiply-linked file, and enforced size and count caps. The caps are not optional hardening: the too_large Result value presumes one, and without them an Agent can present an unbounded Sidecar or an unbounded request and exhaust the Runtime. 14.3. Time-of-check to time-of-use, and ingest by copy Even with every open performed correctly, validating a file _in place_ and then using it is a race: the Agent can rewrite the bytes between the check and the use, so the document that passed policy is not necessarily the document that gets composed and sent. The obligation that closes this is ingest by copy (Section 4.1) — the Runtime copies the request and every Sidecar into Runtime-private Staging outside the Volume, and validates, evaluates policy on, and composes exclusively from that private copy, never re-reading an Agent-writable path after a decision that depends on its content. This is also what keeps the trust model honest. Without it, the property "the request the Runtime approved is the request the Runtime sent" would rest on the Connector not rewriting a published file — an Agent-side obligation carrying a Runtime-side safety property, which the invariant of Section 3.2 forbids. With it, that Connector obligation is pure well-formedness: a Runtime MAY treat an observed post-publish change as malformed, but nothing depends on the Connector honoring it. Rapp & Adams Expires 24 April 2027 [Page 82] Internet-Draft AMMP October 2026 14.4. Concurrency Every atomicity guarantee in this protocol — the .tmp-then-rename discipline, the commit sentinel, the derivation of req_id from the maximum of the pending, processed, and result sets — assumes a single writer and a single drainer per Namespace, and none of them provides mutual exclusion (Section 4.1). Two concurrent drains can deliver the same request twice; two concurrent writers can collide on a req_id and clobber a published request, producing a verdict that names a request that no longer exists as written. The exclusion mechanism lives outside this protocol, and an implementation that does not provide one is not conformant, because behavior under violation is unspecified rather than merely degraded. 14.5. Attribution spoofing Attribution is the Namespace a file arrived in, never an identifier inside the file (Section 4.1). A compromised Agent can write any agent_id it likes into a request; the member exists only as a cross- check, and a Runtime MUST reject a mismatch rather than believe it. Structural isolation is what actually enforces this: a Connector scoped to its own Namespace cannot name another Agent's. The Binding Record deserves the same skepticism from the other direction (Section 9.1). It sits on an Agent-writable Volume, so its contents are untrusted data, it grants nothing, and it is never consulted for enforcement. 14.6. Untrusted content Inbound mail is an injection vector aimed at the Agent's reasoning, and this protocol's posture is that _every sender-originated string_ is untrusted content: bodies, subjects, previews, threading references, sender display names, and attachment Descriptors alike (Section 7.1). Two cases are worth restating because they are the ones most often assumed safe. *message.from is attacker-controlled display data.* Its display-name half is chosen freely by the sender, so a value such as "security@yourbank.example " is entirely legal and renders convincingly. It MUST NOT be used for a trust decision or shown to a human or an Agent as verified identity. Verified sender identity is only ever what the Runtime summarizes into sender_standing and verdict, or, on a Peer Notice, the message.from the Runtime asserts (Section 12.4). Rapp & Adams Expires 24 April 2027 [Page 83] Internet-Draft AMMP October 2026 *An attachment filename is an injection payload.* A Descriptor is metadata about a file the Agent may never receive the bytes of, and a clean Disposition asserts something about _bytes_, never about the truthfulness of the strings describing them. A quarantined file named to look like an instruction reaches the Agent's context intact even though its bytes were withheld. media_type is likewise a declaration, which is why policy is governed by the Runtime's own sniffing of the bytes rather than by the declared type. verdict, sender_standing, and provenance are advisory metadata for routing and presentation. None of them is a license for an Agent to obey body content, and this document deliberately does not define what an Agent should do with a message — that is Connector and deployment policy. 14.7. Recipient policy and header injection Recipient policy is the exfiltration boundary, and it is a Runtime decision (Section 5.1). The Agent's draft.to and draft.cc are requests, not instructions. Two obligations make the boundary mechanical rather than best-effort: the Runtime MUST evaluate policy over exactly the recipient set it will transmit to, and it MUST NOT transmit to any address not present in those members. Without the first, policy can be evaluated over a parse of the recipients that differs from the one used for transmission; without the second, a Runtime-side addition (a bcc, an archive address) escapes the evaluated set. The narrow addr-spec profile exists to make the evaluation mechanical. Display names, address lists inside a single element, quoted local parts, comments, IP literals, and folding whitespace are precisely the forms that make "does this address match the allowlist" a parsing problem with divergent answers — the classic confusion in which one parser sees the allowlisted address and the transmitting parser sees the attacker's. The profile is deliberately narrower than [RFC5322] for that reason, and the cost is disclosed: some legitimate [RFC5322] addresses cannot be expressed under this document. Rapp & Adams Expires 24 April 2027 [Page 84] Internet-Draft AMMP October 2026 Header injection is the second half. Any Agent-authored string that reaches the composed message can attempt to introduce a header boundary with CR, LF, or NUL. The schema patterns are the first line of defense, but a Runtime MUST also reject or strip those characters at composition time, because a document can reach composition by a path where schema validation was bypassed upstream. The two fields most easily overlooked are the ones composed _directly into headers_ rather than into a body or display context: reply_to_message_id, which becomes an In-Reply-To: header, and an attachment's media_type, which becomes a MIME part's Content-Type: [RFC2045]. 14.8. The outbound digest is not a trust boundary sha256 on an outbound attachment Descriptor is computed by the Connector over bytes the Connector wrote (Section 5.1). The Agent controls both, so a matching digest proves only that the request is internally consistent. It catches corruption and interrupted writes; it authenticates nothing. A Runtime MUST NOT treat a digest match as evidence about the content's safety, and MUST apply its own policy — sniffing the bytes, applying type and size rules — to the staged copy regardless. The inbound direction is weaker still and is labelled as such: inbound size_bytes is provider-declared and advisory, not Runtime- verified (Section 7.2). 14.9. Confirmation-oracle avoidance Inbound metadata is gated so that withholding bytes does not become an information channel. sha256 on an inbound Descriptor is emitted only where the Runtime asserts clean (Section 7.2). Publishing a digest for a withheld attachment would let an Agent confirm whether a _guessed_ file was the one quarantined — a confirmation oracle — while providing no integrity value at all, since the Agent has no bytes to check it against. The same principle governs quarantined mail generally: it produces no Notice at all, rather than a Notice announcing that something was withheld. 14.10. Byte-absence and artifact-absence as the access gate The access gate in this protocol is the absence of bytes, not a flag inside a document (Section 4.1). A Runtime MUST NOT place non-clean bytes in an Agent's Namespace; content_ref is the grant, and it exists only where bytes were published. An Agent that ignores every Disposition it is shown still cannot read what was never written. Rapp & Adams Expires 24 April 2027 [Page 85] Internet-Draft AMMP October 2026 The same shape appears for bodies: a withheld or never-delivered message is simply not spooled, and a read against a missing artifact returns "not resolved," with no fallback fetch available to the Connector (Section 7.3). Because retention may also remove an artifact (Section 4.2), "retired" and "never spooled" are indistinguishable from the Connector's side — which is a deliberate property, not an ambiguity to be resolved by a new signal. The content_ref binding rules (Section 7.2) close the corresponding path-confusion risk: a consumer derives the byte path from the enclosing notice_id and the array index rather than from the content_ref string, and refuses any reference that does not match the pinned pattern or points outside its own inbound tree. Otherwise a Descriptor could name another Notice's Sidecar directory — including one belonging to a different delivery whose bytes the Agent was never granted. 14.11. Fail-closed version handling, and the cost of tolerance A consumer reading a higher major than it speaks fails closed, and the version check runs _before_ schema validation so that "wrong version" and "malformed" remain distinguishable (Section 13.1). Folding the two together is how a protocol acquires silent, undiagnosable mail loss. The unknown-member tolerance rule is direction-aware precisely because tolerance has a security cost in one direction and a safety benefit in the other. On Runtime-authored artifacts, strictness defends nothing — the producer is trusted, and an attacker able to author a Notice at all (a writable inbound tree) can author a schema- valid one — while its failure mode is silent mail loss. On the Agent-authored Submit Request, closure is doing real work: it is what makes an outbound content_ref, or any other smuggled member, a hard rejection rather than a member the Runtime might one day interpret. A Runtime therefore MUST keep rejecting unrecognized members inbound from an Agent, and its rejection is legible — a rejected Result — rather than silent. The value-level tolerance is scoped to the two explicitly advisory vocabularies for the same reason: kind, outcome, and disposition gate processing paths and byte publication, and treating an unrecognized value in any of them as opaque would let a producer's new value silently select a consumer's default path. Rapp & Adams Expires 24 April 2027 [Page 86] Internet-Draft AMMP October 2026 14.12. Inbound integrity and the writability of the inbound tree A read-only inbound mount is the mechanism that makes a delivered Notice unforgeable (Section 4.1). Permission bits alone do not achieve it: a same-uid Agent can change a mode it owns, so an inbound tree protected only by bits is writable by the attacker in practice. Deployments that leave inbound writable should understand what they are accepting — an Agent can then author a schema-valid Notice for mail that never arrived, or alter one that did. This is also why the tolerance rule does not weaken the inbound direction: in the writable-inbound posture, the attacker's forgery is schema-valid anyway, so schema strictness was never the control. The control is the mount. Correspondingly, a Connector MUST NOT require write access under inbound/, and consumption tracking is Connector-private state kept elsewhere (Section 4.1). A Connector that crashes on a read-only inbound tree is broken against any Runtime enforcing inbound integrity. 14.13. Retention, deletion, and lost verdicts Deletion rights are named per artifact class (Section 4.2) because an unnamed right becomes either unbounded growth or an accidental data- loss path. Two hazards are worth restating. *A Result deleted before it is read is a lost verdict.* No signal in this protocol tells a Runtime that a Connector has read one, so Runtime-side retention SHOULD be age-based and generous. *Deletion interacts with req_id monotonicity.* A Connector exercising its deletion rights in processed/ or results/ removes exactly the state from which the next req_id is derived, and MUST preserve monotonicity by other durable means. Symmetrically, a Runtime MUST NOT treat processed/ as durable state — for deduplication, for instance — because the Connector may delete there. The Runtime's own record of what it sent belongs outside this protocol entirely. 14.14. What is wire-verifiable, and what is a Runtime obligation A green conformance run proves that documents have the right shape. It proves nothing about a filesystem. The following are Runtime or Connector obligations that no document validator can observe, and a reviewer assessing an implementation must check them another way (Appendix J lists them too): Rapp & Adams Expires 24 April 2027 [Page 87] Internet-Draft AMMP October 2026 * Sidecar file existence, and the binding between an ordinal and a directory entry on disk. * Whether size_bytes and sha256 match the bytes actually on disk. * Display sanitization of filename, and its never-a-path invariant. * The keying of inbound Sidecar directories on the Runtime-minted notice identifier. * The location of Runtime-private Staging, outside the mounted Volume. * Write ordering and commit-sentinel atomicity. * Caps, allowlists, and quotas. * The absence of a spooled body for a quarantined or blocked message. * A Connector's tolerance of a read-only inbound tree. * Ingest by copy. * Write-side path discipline, in all three of its conjunctive legs. * Read-side open discipline. * Single writer and single drainer per Namespace. * Compose-time rejection or stripping of CR, LF, and NUL. Two of these deserve emphasis as trust-model statements rather than as testing notes. First, disposition == "clean" is the _Runtime's assertion_, and this document deliberately does not specify the basis for it; a consumer that treats clean as a proof rather than as an assertion has misread the trust model. Second, this protocol cannot make a Runtime trustworthy — it can only ensure that everything the Agent contributes is inert until a Runtime acts on it, and that whatever the Runtime asserts is legible and attributable to the Runtime. Rapp & Adams Expires 24 April 2027 [Page 88] Internet-Draft AMMP October 2026 14.15. The extension space, and outcomes as claims outbound/ext// (Section 4.1) is Connector-owned and Agent- writable, and this document specifies no shape for anything under it. Its is a stable, opaque Connector id (Section 4.1), so a Runtime's out-of-band agreement with a Connector survives that Connector's repository being renamed. Nothing security-relevant may be built on it. Its first use, a per-Notice outcome file, is written by the receiving Agent's own uid and is therefore forgeable by construction: a receiving Agent can claim a delivery that did not happen, deny one that did, or suppress a refusal. An outcome is a statement by the receiving side about what it did with a Notice; a party MUST NOT treat an outcome as proof of delivery or of non- delivery, and the Runtime's own delivery record is authoritative over it. A Runtime that reads outcomes may let them change what a sender is told or what an operator sees; it MUST NOT let them change what its own record says happened. There is deliberately no inbound extension space, because an inbound tree is a Runtime assertion about origin (Section 7.1) and a Connector cannot know what a vendor-named assertion means. 14.16. Special-use domain names Deployments commonly give Agents addresses under names that are not part of the public DNS: special-use domain names listed in the registry established by [RFC6761] (such as .test, .local, .home.arpa and .onion), and names reserved for private use, such as .internal. A Runtime MUST NOT hand mail addressed to such a name to a mail system outside the deployment, whatever its recipient policy allows: doing so leaks fleet-internal addresses, and the mail they carry, onto the public Internet. A deployment's own mail system that is authoritative for such a name may carry it, and then MUST NOT relay it onward. 14.17. The Peer-Origin Profile The profile adds a tree that a consumer may treat as authenticated origin, so its security rests on who can write that tree and on what a verifying Runtime accepts from another host. *Forgery of the peer tree.* A component that parses mail from an open transport is the most exposed component in a Runtime. If it could write peer/, any sender able to reach it could forge authenticated peer origin. The write-authority partition and the placement of verification (Section 12.3) exist to prevent that, and are checked by a write probe rather than by configuration review. Rapp & Adams Expires 24 April 2027 [Page 89] Internet-Draft AMMP October 2026 *Key compromise.* A Runtime's signing key speaks for every Agent in that Runtime's domains. An attacker holding it can place peer messages, as any of those Agents, at every Runtime that configures the key, until the key is revoked; the key-domain check (Section 12.6.3) confines the damage to the compromised Runtime's own domains. Keeping the key out of every Agent Namespace and out of the admitting component, using it for nothing else, and supporting rotation (Section 12.6.7) are the mitigations. Because replay records are scoped to the Runtime rather than the key, rotation does not reopen a replay window. *Replay and clocks.* A statement is a bearer of authenticated origin for as long as it is fresh. The replay store, the high-water clock H and the recovery floor (Section 12.6.5) bound how long that is, including across a clock that steps backwards and a store that is lost. The profile does not promise exactly-once delivery, and says so; a deployment that needs it beyond the retention period has to forbid re-signing. *Parsing hostile input.* The verifier reads data an attacker chose. It verifies the signature over the exact bytes received before parsing anything, defines no canonicalization (so there is no second parse to disagree with the first), refuses duplicate JSON member names, and receives the message already reduced to a candidate (Section 12.6.2), so that it never parses MIME itself. Oversize and invalid fields are carried as markers so that a malformed second field cannot be silently removed. *Backscatter and loops.* A Control Notice is sent only after the statement has been authenticated, only to the configured Runtime address of the key's owner, never about another Control Notice, and at most once per record (Section 12.6.8). An unauthenticated message therefore cannot direct a Runtime to send mail to a third party, and two Runtimes cannot be made to answer each other indefinitely. *Authenticated is not trustworthy.* A verified peer is a same- principal Agent, which may itself have read hostile external mail and be relaying it. The tree authenticates _who_, never _what_ (Section 12.8). sender_exposure (Section 12.7) lets a Runtime say whether the sender was exposed to external mail, and absence of the stamp means that nothing was assessed. *Exposure in transit.* Between hosts, the statement travels in a header field. Its subject, addresses and attachment names are encoded rather than encrypted, and are visible to every mail system on the path, as the rest of the message is. The profile provides integrity and origin, not confidentiality; a deployment that needs confidentiality between hosts has to obtain it from the transport. Rapp & Adams Expires 24 April 2027 [Page 90] Internet-Draft AMMP October 2026 15. IANA Considerations 15.1. Message Header Fields The peer-origin profile (Section 12) defines two header fields carried in mail between Runtimes. IANA is requested to register them in the "Provisional Message Header Field Names" registry, following the procedure of [RFC3864]: +=====================+==========+=============+==================+ | Header Field Name | Protocol | Status | Reference | +=====================+==========+=============+==================+ | AMAP-Peer-Statement | mail | provisional | Section 12.6.2 | | | | | of this document | +---------------------+----------+-------------+------------------+ | AMAP-Peer-Control | mail | provisional | Section 12.6.8 | | | | | of this document | +---------------------+----------+-------------+------------------+ Table 6: Header field registrations Change controller: IETF. 15.2. Possible Future Registries This document creates no registry. Several of its values are candidates for one, and the authors would welcome the community's view on whether IANA should govern them: the values of contract_version (Section 13.1), whose fail-closed semantics make an unambiguous assignment important; the open reason_code vocabulary of the Result (Section 6.1); and the closed outcome and cause vocabularies of the Control Notice (Section 12.6.8). 16. References 16.1. Normative References [FIPS180-4] National Institute of Standards and Technology, "Secure Hash Standard (SHS)", FIPS PUB 180-4, DOI 10.6028/NIST.FIPS.180-4, August 2015, . Rapp & Adams Expires 24 April 2027 [Page 91] Internet-Draft AMMP October 2026 [JSON-SCHEMA-2020-12] Wright, A., Ed., Andrews, H., Ed., Hutton, B., Ed., and G. Dennis, "JSON Schema: A Media Type for Describing JSON Documents", Work in Progress, Internet-Draft, draft- bhutton-json-schema-01, 10 June 2022, . [RFC2045] Freed, N. and N. Borenstein, "Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies", RFC 2045, DOI 10.17487/RFC2045, November 1996, . [RFC2046] Freed, N. and N. Borenstein, "Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types", RFC 2046, DOI 10.17487/RFC2046, November 1996, . [RFC2047] Moore, K., "MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text", RFC 2047, DOI 10.17487/RFC2047, November 1996, . [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . [RFC2183] Troost, R., Dorner, S., and K. Moore, Ed., "Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field", RFC 2183, DOI 10.17487/RFC2183, August 1997, . [RFC2231] Freed, N. and K. Moore, "MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations", RFC 2231, DOI 10.17487/RFC2231, November 1997, . [RFC3339] Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002, . [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003, . Rapp & Adams Expires 24 April 2027 [Page 92] Internet-Draft AMMP October 2026 [RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006, . [RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322, DOI 10.17487/RFC5322, October 2008, . [RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, DOI 10.17487/RFC8032, January 2017, . [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . [RFC8259] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, December 2017, . [RFC8792] Watsen, K., Auerswald, E., Farrel, A., and Q. Wu, "Handling Long Lines in Content of Internet-Drafts and RFCs", RFC 8792, DOI 10.17487/RFC8792, June 2020, . 16.2. Informative References [A2A] Linux Foundation, "Agent2Agent Protocol, v1.0.1", May 2026, . [ACP-CLIENT] Zed Industries, "Agent Client Protocol", n.d., . [ACP-MERGER] LF AI & Data, "Agent Communication Protocol merges into the Agent2Agent project", August 2025. [MCP] Anthropic, "Model Context Protocol specification, revision 2025-11-25", November 2025, . [RFC2033] Myers, J., "Local Mail Transfer Protocol", RFC 2033, DOI 10.17487/RFC2033, October 1996, . Rapp & Adams Expires 24 April 2027 [Page 93] Internet-Draft AMMP October 2026 [RFC3864] Klyne, G., Nottingham, M., and J. Mogul, "Registration Procedures for Message Header Fields", BCP 90, RFC 3864, DOI 10.17487/RFC3864, September 2004, . [RFC5321] Klensin, J., "Simple Mail Transfer Protocol", RFC 5321, DOI 10.17487/RFC5321, October 2008, . [RFC6761] Cheshire, S. and M. Krochmal, "Special-Use Domain Names", RFC 6761, DOI 10.17487/RFC6761, February 2013, . Appendix A. Submit-Request Schema _(Normative.)_ The schema below is written in the JSON Schema 2020-12 dialect [JSON-SCHEMA-2020-12] and governs the artifact defined in Section 5. This is the Agent-authored artifact, and its envelope is closed at every level: an unrecognized member is a hard rejection, surfaced as a rejected Result rather than as a silent drop (Section 13.1). NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/submit-request.schema.json", "title": "submit-request", "description": "agent -> runtime: an inert outbound request. AMMP\ , section \"Outbound: Submit Request\". Agent-authored: this doc\ ument's envelope stays CLOSED (additionalProperties:false) at ev\ ery level per the asymmetry of section \"Versioning\" — an unrec\ ognized member here is a designed, legible `rejected` refusal, n\ ever a silent drop.", "type": "object", "required": ["contract_version", "req_id", "draft"], "additionalProperties": false, "properties": { "contract_version": { "const": "2" }, "req_id": { "type": "string", "minLength": 1, "description": "monotonic per namespace; filename-safe (path a\ uthority is the filename, not this field — the filename-authorit\ y-for-paths obligation of section \"Obligations\").", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" }, "agent_id": { "type": "string", "minLength": 1, "pattern": "^[^\\ Rapp & Adams Expires 24 April 2027 [Page 94] Internet-Draft AMMP October 2026 u0000-\u001f\u007f]*(?!\\n)$" }, "ts": { "type": "string", "minLength": 1, "pattern": "^[^\u0000-\ \u001f\u007f]*(?!\\n)$" }, "in_reply_to": { "type": "string", "pattern": "^[^\u0000-\u001f\\ u007f]*(?!\\n)$" }, "draft": { "type": "object", "required": ["to", "subject", "body_text"], "additionalProperties": false, "properties": { "to": { "type": "array", "items": { "type": "string", "description": "one bare addr-spec per element (no displ\ ay name, address list, quoted local part, comment, IP literal, o\ r folding whitespace — deliberately narrower than RFC 5322: thos\ e forms are exactly what make allowlist evaluation non-mechanica\ l). EAI/SMTPUTF8 is out of scope at this version.", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9\ .-]+(?!\\n)$" }, "minItems": 1 }, "cc": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9\ .-]+(?!\\n)$" } }, "subject": { "type": "string", "pattern": "^[^\u0000-\u001f\\ u007f]*(?!\\n)$" }, "body_text": { "type": "string" }, "reply_to_message_id": { "type": ["string", "null"], "patter\ n": "^[^\u0000-\u001f\u007f]*(?!\\n)$" }, "attachments": { "type": "array", "items": { "type": "object", "required": ["filename", "media_type", "size_bytes", "sh\ a256"], "additionalProperties": false, "properties": { "filename": { "type": "string", "minLength": 1, "pat\ tern": "^[^\u0000-\u001f\u007f]*(?!\\n)$" }, "media_type": { "type": "string", "minLength": 1, "pat\ Rapp & Adams Expires 24 April 2027 [Page 95] Internet-Draft AMMP October 2026 tern": "^[^\u0000-\u001f\u007f]*(?!\\n)$" }, "size_bytes": { "type": "integer", "minimum": 0 }, "sha256": { "type": "string", "pattern": "^[a-f0-9\ ]{64}(?!\\n)$" } } } } } } } } Figure 10: schemas/submit-request.schema.json Appendix B. Result Schema _(Normative.)_ JSON Schema 2020-12 [JSON-SCHEMA-2020-12]; governs the artifact defined in Section 6. Runtime-authored, and therefore open at every level: a consumer tolerates unrecognized members (Section 13.1). The attachments[i].index == i binding stated in Section 6.1 is not expressible here and is enforced as a named post- check by the conformance gate (Appendix J). NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/result.schema.json", "title": "result", "description": "runtime -> agent: verdict for a submit-request. AM\ MP, section \"Outbound: Result\". Runtime-authored: this documen\ t's envelope is OPEN (no additionalProperties:false) at every le\ vel per the tolerance rule of section \"Versioning\" — a consume\ r MUST ignore members it does not recognize; emitting an unrecog\ nized member remains producer non-conformance.", "type": "object", "required": ["contract_version", "req_id", "outcome"], "properties": { "contract_version": { "const": "2" }, "req_id": { "type": "string", "minLength": 1 }, "outcome": { "type": "string", "enum": ["accepted", "queued_for_human", "rejected"] }, "detail": { "type": ["string", "null"] }, "reason_code": { "type": "string", "minLength": 1 }, "ts": { "type": "string", "minLength": 1 }, "recipients": { "type": "array", "items": { "type": "string" } }, Rapp & Adams Expires 24 April 2027 [Page 96] Internet-Draft AMMP October 2026 "message_id": { "type": ["string", "null"] }, "job_id": { "type": ["string", "null"] }, "attestation": { "type": "object", "required": ["form"], "properties": { "form": { "type": "string", "enum": ["self-stamp", "verifi\ able"] }, "token": { "type": "string", "minLength": 1 }, "claims": { "type": "object" } } }, "attachments": { "type": "array", "items": { "type": "object", "required": ["index", "outcome"], "properties": { "index": { "type": "integer", "minimum": 0 }, "outcome": { "type": "string", "enum": ["accepted", "dlp_held", "rejected_ty\ pe", "too_large"] }, "detail": { "type": ["string", "null"] } } } } } } Figure 11: schemas/result.schema.json Appendix C. Deliver-Notice Schema _(Normative.)_ JSON Schema 2020-12 [JSON-SCHEMA-2020-12]; governs the artifact defined in Section 7. Runtime-authored and open at every level. The two allOf conditionals implement the clean-gating of content_ref and sha256 (Section 7.2); the content_ref index and prefix binding, by contrast, is not expressible here and is enforced as a named post-check (Appendix J). NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/deliver-notice.schema.json", "title": "deliver-notice", "description": "runtime -> connector: one notice per delivered mes\ sage. AMMP, section \"Inbound: Deliver Notice and Inbound Messag\ Rapp & Adams Expires 24 April 2027 [Page 97] Internet-Draft AMMP October 2026 e\". Runtime-authored: this document's envelope is OPEN (no addi\ tionalProperties:false) at every level per the tolerance rule o\ f section \"Versioning\" — a consumer MUST ignore members it doe\ s not recognize; emitting an unrecognized member remains produce\ r non-conformance.", "type": "object", "required": ["contract_version", "notice_id", "ts", "kind", "messa\ ge"], "properties": { "contract_version": { "const": "2" }, "notice_id": { "type": "string", "minLength": 1, "pattern": "^[A\ -Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" }, "ts": { "type": "string", "minLength": 1 }, "kind": { "const": "deliver" }, "message": { "type": "object", "required": ["id", "from", "subject", "preview", "mailbox"], "properties": { "id": { "type": "string", "minLength": 1 }, "from": { "type": "string", "minLength": 1 }, "subject": { "type": "string" }, "preview": { "type": "string" }, "mailbox": { "type": "string", "minLength": 1 }, "task_id": { "type": "string", "minLength": 1 }, "thread_id": { "type": "string" }, "in_reply_to": { "type": "string" }, "references": { "type": "array", "items": { "type": "string\ " } }, "sender_standing": { "type": "string", "description": "advisory, open vocabulary (known, agent-ti\ er, first-contact, read-only, ...). A consumer MUST tolerate a v\ alue outside this list, treating it as opaque — never grounds t\ o drop the notice; a runtime may add a value without a new wire \ major (section \"Versioning\")." }, "provenance": { "type": "string", "description": "advisory, open vocabulary (external, inter\ nal, ...). A consumer MUST tolerate a value outside this list, t\ reating it as opaque — never grounds to drop the notice; a runti\ me may add a value without a new wire major (section \"Versionin\ g\")." }, "verdict": { "type": "object", "properties": { "auth": { "type": "string" }, Rapp & Adams Expires 24 April 2027 [Page 98] Internet-Draft AMMP October 2026 "screen": { "type": "string" }, "entitlement": { "type": "string" } } }, "attachments": { "type": "array", "items": { "type": "object", "required": ["filename", "media_type", "size_bytes", "di\ sposition"], "properties": { "filename": { "type": "string", "minLength": 1 }, "media_type": { "type": "string", "minLength": 1 }, "size_bytes": { "type": "integer", "minimum": 0 }, "sha256": { "type": "string", "pattern": "^[a-f0-\ 9]{64}(?!\\n)$" }, "disposition": { "type": "string", "enum": ["clean", "stripped", "quaran\ tined", "unscanned"] }, "content_ref": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+\\.attach\ ments/(0|[1-9][0-9]*)(?!\\n)$" } }, "allOf": [ { "if": { "required": ["content_ref"] }, "then": { "properties": { "disposition": { "const": \ "clean" } } } }, { "if": { "required": ["sha256"] }, "then": { "properties": { "disposition": { "const": \ "clean" } } } } ] } } } } } } Figure 12: schemas/deliver-notice.schema.json Appendix D. Inbound-Message Schema _(Normative.)_ JSON Schema 2020-12 [JSON-SCHEMA-2020-12]; governs the artifact defined in Section 7.3. Runtime-authored and open at every level. Its Descriptor rules, including the clean-gating conditionals, match the deliver-notice schema's. Rapp & Adams Expires 24 April 2027 [Page 99] Internet-Draft AMMP October 2026 NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/inbound-message.schema.json", "title": "inbound-message", "description": "runtime -> connector: the full delivered message (\ incl. body_text), spooled at inbound/messages/notice-.json, \ keyed on the runtime-minted notice-id (never the provider messag\ e.id). AMMP, section \"The Inbound Message (body spool)\". Runti\ me-authored: this document's envelope is OPEN (no additionalProp\ erties:false) at every level per the tolerance rule of section \\ "Versioning\" — a consumer MUST ignore members it does not recog\ nize; emitting an unrecognized member remains producer non-confo\ rmance.", "type": "object", "required": ["contract_version", "notice_id", "body_text"], "properties": { "contract_version": { "const": "2" }, "notice_id": { "type": "string", "minLength": 1, "pattern": "^[A\ -Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" }, "body_text": { "type": "string" }, "id": { "type": "string" }, "date": { "type": "string" }, "from": { "type": "string" }, "to": { "type": "string" }, "cc": { "type": "string" }, "subject": { "type": "string" }, "attachments": { "type": "array", "items": { "type": "object", "required": ["filename", "media_type", "size_bytes", "dispos\ ition"], "properties": { "filename": { "type": "string", "minLength": 1 }, "media_type": { "type": "string", "minLength": 1 }, "size_bytes": { "type": "integer", "minimum": 0 }, "sha256": { "type": "string", "pattern": "^[a-f0-9]{6\ 4}(?!\\n)$" }, "disposition": { "type": "string", "enum": ["clean", "stripped", "quarantine\ d", "unscanned"] }, "content_ref": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+\\.attachment\ s/(0|[1-9][0-9]*)(?!\\n)$" } }, "allOf": [ Rapp & Adams Expires 24 April 2027 [Page 100] Internet-Draft AMMP October 2026 { "if": { "required": ["content_ref"] }, "then": { "properties": { "disposition": { "const": "cle\ an" } } } }, { "if": { "required": ["sha256"] }, "then": { "properties": { "disposition": { "const": "cle\ an" } } } } ] } } } } Figure 13: schemas/inbound-message.schema.json Appendix E. Binding-Record Schema _(Normative.)_ JSON Schema 2020-12 [JSON-SCHEMA-2020-12]; governs the artifact defined in Section 9. This envelope is closed, for the reason given in Section 9.1 — an anti-secret-leak lint rather than a compatibility surface. NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/binding-record.schema.json", "title": "binding-record", "description": "runtime/provisioner -> volume root: identity.json\ , the mailbox<->volume binding record. AMMP, section \"Binding R\ ecord\". INFORMATIONAL, NOT AUTHORITATIVE: the volume is agent-w\ ritable, so a consumer MUST treat contents as untrusted data an\ d MUST tolerate the file's absence. Unlike the other runtime-aut\ hored documents, this schema is closed (section \"Versioning\") \ — its closure is an anti-secret-leak lint, not a compatibility s\ urface.", "type": "object", "required": ["agent_id", "address", "provisioned_ts"], "additionalProperties": false, "properties": { "contract_version": { "const": "2" }, "agent_id": { "type": "string", "minLength": 1 }, "address": { "type": "string", "minLength": 1 }, "provisioned_ts": { "type": "string", "minLength": 1 }, "label": { "type": "string" } } } Figure 14: schemas/binding-record.schema.json Rapp & Adams Expires 24 April 2027 [Page 101] Internet-Draft AMMP October 2026 Appendix F. Peer-Origin Profile Schemas _(Normative.)_ JSON Schema 2020-12 [JSON-SCHEMA-2020-12]. The first schema governs a Peer Notice, the artifact the peer-origin profile (Section 12) places under /peer/notices/ (Section 12.2). Its envelope is Runtime-authored and therefore open (Section 13.1). A document with kind: "peer" found under inbound/notices/ fails the deliver-notice schema (Appendix C), and a document with kind: "deliver" found under peer/notices/ fails this one; both are dropped under the rule of Section 7.1. NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/peer-notice.schema.json", "title": "peer-notice", "description": "runtime -> connector: one notice per peer message \ placed in /peer/notices/. AMMP, section \"The Peer-Ori\ gin Profile (Optional)\". Same layout as deliver-notice with kin\ d and mailbox pinned to \"peer\", message.from pinned to a bare \ addr-spec (the one runtime-asserted `from` in the protocol), mes\ sage.id / in_reply_to / references pinned to the notice_id chars\ et (the peer message id is bare on the wire; its Message-ID enco\ ding exists only inside a mail header, never in an AMMP document\ ), plus the optional runtime-asserted message.sender_exposure. R\ untime-authored: envelope OPEN at every level per section \"Vers\ ioning\" — a consumer MUST ignore members it does not recognize\ . A document with kind \"peer\" found under inbound/notices/ fai\ ls deliver-notice.schema.json; a document with kind \"deliver\" \ found under peer/notices/ fails this schema: both are dropped un\ der the drop rule of section \"Field semantics and obligations\\ " (the profile's cross-lock, section \"The Peer Tree\").", "type": "object", "required": ["contract_version", "notice_id", "ts", "kind", "messa\ ge"], "properties": { "contract_version": { "const": "2" }, "notice_id": { "type": "string", "minLength": 1, "pattern": "^[A\ -Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" }, "ts": { "type": "string", "minLength": 1 }, "kind": { "const": "peer" }, "message": { "type": "object", "required": ["id", "from", "subject", "preview", "mailbox"], "properties": { "id": { "type": "string", "minLength": 1, Rapp & Adams Expires 24 April 2027 [Page 102] Internet-Draft AMMP October 2026 "description": "the peer message id: minted by the sendin\ g runtime on accepting the submit, returned to the sender as res\ ult.message_id, >=128 bits of entropy, globally unique and ungue\ ssable. Bare on the wire under the notice_id charset; when carri\ ed in a mail header it is encoded as and decoded aga\ in before it enters any AMMP document.", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" }, "from": { "type": "string", "minLength": 1, "description": "bare addr-spec, runtime-asserted from the \ restricted write path or the verified cross-host statement, neve\ r copied from sender text. The one `from` in the protocol a cons\ umer may treat as authenticated origin.", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-\ ]+(?!\\n)$" }, "subject": { "type": "string" }, "preview": { "type": "string" }, "mailbox": { "const": "peer" }, "task_id": { "type": "string", "minLength": 1 }, "thread_id": { "type": "string" }, "in_reply_to": { "type": "string", "minLength": 1, "description": "present iff the placing runtime resolved i\ t from its own ledger (a peer message id it issued or placed). R\ EQUIRED on a result by prose; a fresh task omits it. Bare peer i\ d, never the Message-ID-encoded form.", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" }, "references": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern": "^\ [A-Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" } }, "sender_standing": { "type": "string", "description": "advisory, open vocabulary; SHOULD be `agen\ t-tier` when set on a peer notice." }, "provenance": { "type": "string", "description": "advisory, open vocabulary; redundant with \ the tree on a peer notice and MAY be omitted. Never grounds to d\ erive or override origin class." }, "verdict": { "type": "object", Rapp & Adams Expires 24 April 2027 [Page 103] Internet-Draft AMMP October 2026 "properties": { "auth": { "type": "string" }, "screen": { "type": "string" }, "entitlement": { "type": "string" } } }, "sender_exposure": { "type": "object", "description": "runtime-asserted, never sender-settable (\ a submit-request carrying it is `rejected` by the closed envelop\ e; cross-host it is taken only from the verified statement). Abs\ ence means NOT ASSESSED, never `no exposure`. MUST NOT drive rou\ ting or treatment.", "required": ["asserted_by", "external_mail_delivered", "wi\ ndow_start", "window_end"], "properties": { "asserted_by": { "type": "string", "minLength": 1, "patt\ ern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+(?!\\n)$\ " }, "external_mail_delivered": { "type": "boolean" }, "window_start": { "type": "string", "minLength": 1 }, "window_end": { "type": "string", "minLength": 1 }, "last_external_delivery_ts": { "type": "string", "minLen\ gth": 1 }, "derived_from": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern"\ : "^[A-Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" } } }, "if": { "required": ["external_mail_delivered"], "properti\ es": { "external_mail_delivered": { "const": true } } }, "then": { "required": ["last_external_delivery_ts"] } }, "attachments": { "type": "array", "items": { "type": "object", "required": ["filename", "media_type", "size_bytes", "di\ sposition"], "properties": { "filename": { "type": "string", "minLength": 1 }, "media_type": { "type": "string", "minLength": 1 }, "size_bytes": { "type": "integer", "minimum": 0 }, "sha256": { "type": "string", "pattern": "^[a-f0-\ 9]{64}(?!\\n)$" }, "disposition": { "type": "string", "enum": ["clean", "stripped", "quaran\ Rapp & Adams Expires 24 April 2027 [Page 104] Internet-Draft AMMP October 2026 tined", "unscanned"] }, "content_ref": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+\\.attach\ ments/(0|[1-9][0-9]*)(?!\\n)$" } }, "allOf": [ { "if": { "required": ["content_ref"] }, "then": { "properties": { "disposition": { "const": \ "clean" } } } }, { "if": { "required": ["sha256"] }, "then": { "properties": { "disposition": { "const": \ "clean" } } } } ] } } } } } } Figure 15: schemas/peer-notice.schema.json Between hosts, a peer message travels as mail carrying a signed Peer Statement in one AMAP-Peer-Statement header field (Section 12.6). The receiving Runtime verifies the statement's bytes before parsing them, then validates them against the schema below, which is closed at every level: these are signed bytes another Runtime acts on, not a Runtime-authored document a Connector reads. The statement-* fixtures (Appendix J) exercise the schema, and the signed cases exercise the verification rules. NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/peer-statement.schema.json", "title": "peer-statement", "description": "runtime -> runtime: the signed statement that carr\ ies a peer message between hosts. AMMP, section \"Peer Traffic B\ etween Hosts\". The sending runtime signs the statement's exact \ UTF-8 bytes with Ed25519 and ships both in one AMAP-Peer-Stateme\ nt header field (section \"Carriage, and the Candidate the Verif\ ier Sees\"); the receiving runtime verifies the signature over t\ he received bytes before parsing, then validates against this sc\ hema. CLOSED at every level, unlike the runtime-authored notices\ : these are signed bytes another runtime acts on, a member the r\ eceiver does not know is a member it cannot bind to what it publ\ ishes, and the version member `v` is the only evolution path. A \ Rapp & Adams Expires 24 April 2027 [Page 105] Internet-Draft AMMP October 2026 statement that fails this schema fails verification, and the mes\ sage is demoted to mail (sections \"Verification\" and \"Outcome\ s\"). Numbers are lexical integers (no fraction or exponent) an\ d strings contain no unpaired surrogate (section \"The Statement\ \", checked at check 5(b) of section \"Verification\"); the sche\ ma cannot express either, nor calendar validity, interval order\ , the entropy of `id`, or any binding to the message: those are \ the verification steps of section \"Verification\".", "type": "object", "additionalProperties": false, "required": [ "v", "key_id", "kind", "id", "from", "to", "ts", "subject", "body_sha256", "attachments" ], "properties": { "v": { "type": "integer", "const": 1 }, "key_id": { "type": "string", "description": "names one key in the signing peer's configure\ d key set; opaque, unique within that set, 1-64 characters.", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?!\\n)$" }, "kind": { "const": "peer" }, "id": { "type": "string", "minLength": 1, "description": "the peer message id (section \"Correlation an\ d Replies\"), bare. A sending runtime mints it under ^[A-Za-z0-9\ _][A-Za-z0-9_-]*$ (hex, or base64url re-minted if it would star\ t with '-'): the notice_id charset without '.', so its Message-I\ D encoding is a valid msg-id (section \"The Statemen\ t\"); the receiver publishes it as message.id.", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_-]*(?!\\n)$" }, "in_reply_to": { Rapp & Adams Expires 24 April 2027 [Page 106] Internet-Draft AMMP October 2026 "type": "string", "minLength": 1, "description": "the peer message id this message answers (sect\ ion \"Correlation and Replies\"), bare, minted by the other runt\ ime under the same pattern. Absent on a fresh task. Resolved aga\ inst the receiver's ledger with the participants bound (section \ \"Correlation and Edges Between Hosts\").", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_-]*(?!\\n)$" }, "from": { "type": "string", "minLength": 1, "description": "bare addr-spec of the sending agent; its domai\ n MUST be one of the signing peer's configured domains.", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+(?\ !\\n)$" }, "to": { "type": "string", "minLength": 1, "description": "bare addr-spec of the one receiving agent (sec\ tion \"Addressing\": a peer message is addressed to one agent).", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+(?\ !\\n)$" }, "ts": { "type": "string", "description": "when the sending runtime signed: RFC 3339 UTC\ , whole seconds, upper-case T and Z, no offset. MUST be a real c\ alendar instant (section \"The Statement\").", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9\ ]{2}Z(?!\\n)$" }, "subject": { "type": "string", "description": "the subject the receiver publishes as message.\ subject; no control characters.", "pattern": "^[^\u0000-\u001f]*(?!\\n)$" }, "body_sha256": { "type": "string", "description": "SHA-256 of the UTF-8 encoding of the body_tex\ t the receiver will publish, lower-case hex.", "pattern": "^[a-f0-9]{64}(?!\\n)$" }, "attachments": { "type": "array", "description": "one entry per attachment, in order; empty whe\ Rapp & Adams Expires 24 April 2027 [Page 107] Internet-Draft AMMP October 2026 n there are none.", "items": { "type": "object", "additionalProperties": false, "required": [ "sha256", "size_bytes", "filename", "media_type" ], "properties": { "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}(?!\\n)$" }, "size_bytes": { "type": "integer", "minimum": 0 }, "filename": { "type": "string", "minLength": 1 }, "media_type": { "type": "string", "minLength": 1 } } } }, "sender_exposure": { "type": "object", "additionalProperties": false, "description": "section \"Sender Exposure\", asserted by the s\ ending runtime; omitted when it made no assessment. Timestamps i\ n ts's format; window_start <= window_end; last_external_deliver\ y_ts present iff external_mail_delivered is true, and then withi\ n the window; asserted_by is the signing runtime's configured ru\ ntime address (section \"The Statement\"; checks 5(b) and 8 of s\ ection \"Verification\").", "required": [ "asserted_by", "external_mail_delivered", "window_start", "window_end" ], "properties": { "asserted_by": { Rapp & Adams Expires 24 April 2027 [Page 108] Internet-Draft AMMP October 2026 "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-\ ]+(?!\\n)$" }, "external_mail_delivered": { "type": "boolean" }, "window_start": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:\ [0-9]{2}Z(?!\\n)$" }, "window_end": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:\ [0-9]{2}Z(?!\\n)$" }, "last_external_delivery_ts": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:\ [0-9]{2}Z(?!\\n)$" }, "derived_from": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]*(?!\\n)$" } } }, "if": { "required": [ "external_mail_delivered" ], "properties": { "external_mail_delivered": { "const": true } } }, "then": { "required": [ "last_external_delivery_ts" ] } }, Rapp & Adams Expires 24 April 2027 [Page 109] Internet-Draft AMMP October 2026 "sender_since": { "type": "string", "description": "OPTIONAL (section \"Optional: Sender Age, and \ Deployment Admission Policy\"): when the sending runtime first e\ nrolled the sending agent's identity (as that runtime defines id\ entity), from its durable registry; same format as ts and not la\ ter than ts. A runtime assertion, not a sender claim. Never rese\ t for a continuation of the same identity (restart, rebind, re-c\ reation, key rotation); a new enrolment starts a new value. A re\ ceiver enforcing a sender age keeps the last value per (runtime\ , from) and refuses an earlier one as inconsistent. Absence mean\ s NOT ASSERTED, never 'new'.", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9\ ]{2}Z(?!\\n)$" } } } Figure 16: schemas/peer-statement.schema.json A receiving Runtime reports what it did with a statement it did not place (refused, dropped or demoted) to the configured sending Runtime in a signed Control Notice, carried in one AMAP-Peer-Control header field with the same grammar and key (Section 12.6.8). Its kind is "control", so neither document validates as the other; it is closed and carries no free text. NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/peer-control.schema.json", "title": "peer-control", "description": "runtime -> runtime: the signed control notice a re\ ceiving runtime sends to a configured sending runtime about wha\ t it did with an authenticated peer statement that it did not pl\ ace (refused, dropped, or demoted). AMMP, section \"Control Noti\ ces\". Carried in an AMAP-Peer-Control header field with the sam\ e k/s/b grammar as AMAP-Peer-Statement, signed with the same per\ -runtime key; `kind` is \"control\", so a control notice never v\ alidates as a peer statement and is never placed for an agent. C\ LOSED at every level, and it carries no free text: the receivin\ g runtime composes any agent-facing words itself (the delivery f\ ailure notice of section \"Correlation and Replies\"). The schem\ a cannot check that `about` names a peer message this runtime is\ sued to the notice's signer; that is the receiving runtime's che\ ck (section \"Control Notices\").", "type": "object", Rapp & Adams Expires 24 April 2027 [Page 110] Internet-Draft AMMP October 2026 "additionalProperties": false, "required": [ "v", "key_id", "kind", "id", "from", "to", "ts", "about", "outcome", "cause" ], "properties": { "v": { "type": "integer", "const": 1 }, "key_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?!\\n)$" }, "kind": { "const": "control" }, "id": { "type": "string", "description": "this notice's own id, minted like a peer messa\ ge id; replay-checked in the same store, scoped to the signing r\ untime.", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_-]*(?!\\n)$" }, "from": { "type": "string", "description": "the signing (receiving) runtime's configured r\ untime address.", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+(?\ !\\n)$" }, "to": { "type": "string", "description": "the configured runtime address of the runtime \ that signed the statement this notice is about.", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+(?\ !\\n)$" }, "ts": { "type": "string", Rapp & Adams Expires 24 April 2027 [Page 111] Internet-Draft AMMP October 2026 "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9\ ]{2}Z(?!\\n)$" }, "about": { "type": "string", "description": "the `id` of the peer statement this notice is \ about.", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_-]*(?!\\n)$" }, "outcome": { "enum": [ "refused", "dropped", "demoted" ] }, "cause": { "description": "why, from the outcome table of section \"Outco\ mes\". `policy` is a deployment refusal (for example a first-sig\ ht quarantine), named further by policy_code.", "enum": [ "statement_value", "key_domain", "ts_window", "exposure_signer", "body_charset", "body_digest", "attachment_count", "attachment_digest", "message_id", "duplicate", "misrouted", "unresolved_reply", "no_edge", "policy" ] }, "policy_code": { "type": "string", "description": "deployment-defined token, only with cause `pol\ icy` (e.g. `quarantined`).", "pattern": "^[a-z][a-z0-9_]{0,63}(?!\\n)$" } }, "allOf": [ { "if": { "required": [ Rapp & Adams Expires 24 April 2027 [Page 112] Internet-Draft AMMP October 2026 "policy_code" ] }, "then": { "properties": { "cause": { "const": "policy" } } } }, { "if": { "properties": { "cause": { "const": "policy" } }, "required": [ "cause" ] }, "then": { "required": [ "policy_code" ] } }, { "if": { "properties": { "outcome": { "const": "refused" } }, "required": [ "outcome" ] }, "then": { "properties": { "cause": { "enum": [ "unresolved_reply", "no_edge", "policy" ] } Rapp & Adams Expires 24 April 2027 [Page 113] Internet-Draft AMMP October 2026 } } }, { "if": { "properties": { "outcome": { "const": "dropped" } }, "required": [ "outcome" ] }, "then": { "properties": { "cause": { "enum": [ "duplicate", "misrouted" ] } } } }, { "if": { "properties": { "outcome": { "const": "demoted" } }, "required": [ "outcome" ] }, "then": { "properties": { "cause": { "enum": [ "statement_value", "key_domain", "ts_window", "exposure_signer", "body_charset", "body_digest", "attachment_count", "attachment_digest", Rapp & Adams Expires 24 April 2027 [Page 114] Internet-Draft AMMP October 2026 "message_id" ] } } } } ] } Figure 17: schemas/peer-control.schema.json Appendix G. Peer-Directory Schema _(Normative.)_ JSON Schema 2020-12 [JSON-SCHEMA-2020-12]; governs the optional peer directory defined in Section 10, which a Runtime MAY publish at /directory.json. The document is Runtime- authored and therefore open (Section 13.1), and it is advisory: the schema pins its shape, never what it predicts. NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/directory.schema.json", "title": "directory", "description": "runtime -> connector: the router's most recent wor\ d about which peers this agent may address, published per instan\ ce at /directory.json. AMMP, section \"The Peer Direct\ ory\". ADVISORY, NEVER AUTHORITATIVE: an address's presence is n\ ot a prediction that a submit to it will be accepted, nor its ab\ sence a prediction of refusal — authorisation is evaluated by th\ e runtime at submit time and only the result (section \"Outbound\ : Result\") reports what happened. A runtime MUST NOT read this \ file back as authorisation; enforcement stays the runtime's own \ graph check on every message. The file MAY be absent. Runtime-au\ thored: envelope is OPEN at every level per the tolerance rule o\ f section \"Versioning\" — a consumer MUST ignore members it doe\ s not recognize; emitting an unrecognized member remains produce\ r non-conformance.", "type": "object", "required": [ "router", "written_at", "delegate_to", "mail_to" ], "properties": { "contract_version": { Rapp & Adams Expires 24 April 2027 [Page 115] Internet-Draft AMMP October 2026 "const": "2" }, "router": { "type": "string", "description": "the publishing runtime's own address, one bar\ e addr-spec. Never appears in delegate_to or mail_to.", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+(?\ !\\n)$" }, "written_at": { "type": "string", "minLength": 1, "description": "RFC 3339, the publishing runtime's clock. The \ only staleness signal a consumer gets; an honest marker, not a f\ reshness guarantee.", "pattern": "^[^\\u0000-\\u001f\\u007f]*(?!\\n)$" }, "delegate_to": { "type": "array", "description": "sorted addresses this agent may task — the aut\ horisation graph transposed for this instance. Always present; e\ mpty when the lane is closed. Never this agent's own address, an\ d never who may task THIS agent (an allowlist, withheld by const\ ruction).", "items": { "type": "string", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+\ (?!\\n)$" } }, "mail_to": { "type": "array", "description": "sorted addresses on the mutual mail lane. Alwa\ ys present; empty when the lane is closed, which is the normal c\ ase for a delegation-only deployment.", "items": { "type": "string", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+\ (?!\\n)$" } } } } Figure 18: schemas/directory.schema.json Rapp & Adams Expires 24 April 2027 [Page 116] Internet-Draft AMMP October 2026 Appendix H. Fleet-Roster Schema _(Normative.)_ JSON Schema 2020-12; governs the fleet roster defined in Section 11. The document is Runtime-authored and therefore open (Section 13.1), and it pins shape only: whether a member exists says nothing about whether it may be addressed. NOTE: '\' line wrapping per RFC 8792 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://amap-spec/schemas/roster.schema.json", "title": "roster", "description": "runtime -> every agent: the fleet roster, the runt\ ime's most recent word about which instances it has admitted, wr\ itten once for the whole fleet to a location the deployment desi\ gnates and exposes read-only to each agent it points at the rost\ er. AMMP, section \"The Fleet Roster\". EXISTENCE IS NOT AUTHORI\ SATION: a member's presence says the instance exists, never tha\ t this agent may address it or that a submission to it will be a\ ccepted; authorisation is evaluated by the runtime at submit tim\ e and only the result reports what happened. NOT A SOURCE OF TH\ E READER'S IDENTITY: the reader's own address may appear, and at\ tribution stays the namespace a file arrived in. A runtime MUST \ NOT read this file back. The file MAY be absent; absent or stal\ e means unknown, never an empty fleet. Runtime-authored: envelop\ e is OPEN at every level — a consumer MUST ignore members it doe\ s not recognize; emitting an unrecognized member remains produce\ r non-conformance.", "type": "object", "required": [ "contract_version", "router", "written_at", "members" ], "properties": { "contract_version": { "const": "2" }, "router": { "type": "string", "description": "the publishing runtime's own address, one bar\ e addr-spec. Never appears in members.", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9.-]+(?\ !\\n)$" }, "written_at": { Rapp & Adams Expires 24 April 2027 [Page 117] Internet-Draft AMMP October 2026 "type": "string", "minLength": 1, "description": "RFC 3339, the publishing runtime's clock. The \ only staleness signal a consumer gets; an honest marker, not a f\ reshness guarantee.", "pattern": "^[^\\u0000-\\u001f\\u007f]*(?!\\n)$" }, "interval_s": { "type": "number", "minimum": 0, "description": "OPTIONAL: the runtime's nominal rewrite interv\ al in seconds, so a consumer can judge written_at against it. Om\ itted when the runtime does not know it." }, "members": { "type": "array", "description": "the instances the runtime has admitted, sorte\ d by address. Always present; never written as empty when member\ ship is unknown (the previous file is left to age instead). The \ router is never a member.", "items": { "type": "object", "required": [ "address", "state" ], "properties": { "address": { "type": "string", "description": "the member's address, one bare addr-spec\ .", "pattern": "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9\ .-]+(?!\\n)$" }, "state": { "type": "string", "minLength": 1, "description": "OPEN advisory vocabulary; \"admitted\" i\ s the only value defined at this version. A consumer MUST tolera\ te an unknown value as opaque.", "pattern": "^[^\\u0000-\\u001f\\u007f]*(?!\\n)$" } } } } } } Rapp & Adams Expires 24 April 2027 [Page 118] Internet-Draft AMMP October 2026 Figure 19: schemas/roster.schema.json Appendix I. Golden Examples _(Normative.)_ The documents below are reproduced byte-verbatim from the conformance suite (Appendix J). They are a small selection covering the core artifact types and three rejections; the suite itself is the authority. One further example — a Binding Record — is shown inline at Section 9 instead of being repeated here. I.1. Valid Submit Request with attachments Sidecar blobs are named by array index (req-.attachments/); no content_ref may appear on a request and the schema rejects one (Section 5.1). NOTE: '\' line wrapping per RFC 8792 { "contract_version": "2", "req_id": "00000010", "agent_id": "agent.research", "ts": "2026-07-21T04:10:00Z", "draft": { "to": ["owner@example.com"], "subject": "Report attached", "body_text": "See the attached report and notes.", "attachments": [ { "filename": "report.pdf", "media_type": "application/pdf", "size_bytes": 48213, "sha256": "970e441b6c4ecd37abf2541e1276c680bdec8481cef9368ff\ 1d010fd6b784b07" }, { "filename": "notes.txt", "media_type": "text/plain", "size_bytes": 128, "sha256": "087d07499f2c097ac5396af7a0f1ef5e7a977fa91c631ebd1\ 5eb61bb23ef20aa" } ] } } Figure 20: fixtures/valid/request-attachments.json I.2. Valid Deliver Notice with a clean attachment Descriptor content_ref's prefix equals the enclosing document's own notice_id and its trailing index equals the Descriptor's own array position (Section 7.2); a Connector that relays the Notice without ever dereferencing the reference is fully conformant. Rapp & Adams Expires 24 April 2027 [Page 119] Internet-Draft AMMP October 2026 NOTE: '\' line wrapping per RFC 8792 { "contract_version": "2", "notice_id": "1721457600000000003-4242", "ts": "2026-07-21T04:06:00Z", "kind": "deliver", "message": { "id": "45", "from": "colleague@example.com", "subject": "Clean attachment (deferred-release wire shape)", "preview": "byte-access forward-valid example", "mailbox": "inbox", "attachments": [ { "filename": "photo.jpg", "media_type": "image/jpeg", "size_bytes": 204800, "sha256": "04c22051bc3f76bc44b952d9fdd3e59214662e7b3fd8a84b8\ 9de09352aeb2264", "disposition": "clean", "content_ref": "1721457600000000003-4242.attachments/0" } ] } } Figure 21: fixtures/valid/notice-attachment-clean-ref.json I.3. Valid Inbound Message The body spool for a delivered message is keyed on the Runtime-minted notice_id, never on the provider message.id (Section 7.3). { "contract_version": "2", "notice_id": "1721457600000000000-4242", "id": "42", "date": "Mon, 20 Jul 2026 04:00:00 +0000", "from": "Alex Doe ", "to": "agent@example.org", "subject": "status?", "body_text": "hey, how's the run going", "attachments": [ { "filename": "report.pdf", "media_type": "application/pdf", "size_bytes": 48213, "disposition": "unscanned" } ] } Figure 22: fixtures/valid/message-clean.json Rapp & Adams Expires 24 April 2027 [Page 120] Internet-Draft AMMP October 2026 I.4. Valid Result — a policy hold with per-attachment diagnostics attachments[i].index == i on a Result, contiguous and unique over 0..n-1; per-attachment entries are diagnostic detail under one governing top-level outcome and do not license a partial send (Section 6.1). NOTE: '\' line wrapping per RFC 8792 { "contract_version": "2", "req_id": "00000010", "outcome": "queued_for_human", "detail": "no outbound screener wired; attachments fail-closed to \ human review", "recipients": ["owner@example.com"], "message_id": null, "job_id": null, "attachments": [ { "index": 0, "outcome": "dlp_held", "detail": "no verdict sourc\ e" }, { "index": 1, "outcome": "dlp_held", "detail": "no verdict sourc\ e" } ] } Figure 23: fixtures/valid/result-attachments-held.json I.5. Invalid Deliver Notice — a content_ref that fails the binding The drop rule of Section 7.1 is two independent checks: this document is schema-valid and is caught only by the content_ref post-check (Section 7.2, Section 13.3). content_ref names a different notice's Sidecar directory and a non- matching index — it is neither .attachments/0 for its own array position 0, nor prefixed by this document's own notice_id. Schema validation alone reports zero errors; only the content_ref index/prefix post-check catches it. The conformance gate's own errors for this document: $.attachments[0].content_ref: 'OTHER-NOTICE.attachments/7' != 'n-index-mismatch.attachments/0' Rapp & Adams Expires 24 April 2027 [Page 121] Internet-Draft AMMP October 2026 { "contract_version": "2", "notice_id": "n-index-mismatch", "ts": "2026-07-21T04:06:00Z", "kind": "deliver", "message": { "id": "46", "from": "colleague@example.com", "subject": "wrong content_ref", "preview": "descriptor points at another notice's sidecar", "mailbox": "inbox", "attachments": [ { "filename": "photo.jpg", "media_type": "image/jpeg", "size_bytes": 204800, "disposition": "clean", "content_ref": "OTHER-NOTICE.attachments/7" } ] } } Figure 24: fixtures/invalid/notice-attachment-ref-index-mismatch.json I.6. Valid Deliver Notice carrying an unrecognized member The Runtime-authored envelope is OPEN: a consumer MUST ignore a member it does not recognize, at every level (Section 13.1). NOTE: '\' line wrapping per RFC 8792 { "contract_version": "2", "notice_id": "x", "ts": "t", "kind": "del\ iver", "x_injected": "surprise", "message": { "id": "1", "from": "a@b", "subject": "s", "preview": \ "p", "mailbox": "inbox", "x_message_injected": "also surprise" } } Figure 25: fixtures/valid/notice-unknown-member.json I.7. Invalid Submit Request carrying an unrecognized member The Agent-authored envelope stays CLOSED at every level: an unrecognized member is a designed, legible rejected refusal, never a silent drop (Section 13.1). additionalProperties: false at the top level of the submit-request schema rejects any member this document does not define. The conformance gate's own errors for this document: Rapp & Adams Expires 24 April 2027 [Page 122] Internet-Draft AMMP October 2026 $: additional property 'x_injected' not allowed NOTE: '\' line wrapping per RFC 8792 { "contract_version": "2", "req_id": "00000021", "x_injected": "surp\ rise", "draft": { "to": ["a@b"], "subject": "s", "body_text": "b" } } Figure 26: fixtures/invalid/request-extra-field.json I.8. Invalid Peer Notice — a deliver-kind document under the peer tree Origin class comes from the tree a notice arrived in, never from a self-declared field: a kind: "deliver" document found under peer/ notices/ fails the peer-notice schema, the profile's cross-lock (Section 12.2). kind is const "peer" in the peer-notice schema; a document carrying "deliver" fails that const check. The conformance gate's own errors for this document: $.kind: 'deliver' != const 'peer' NOTE: '\' line wrapping per RFC 8792 { "contract_version": "2", "notice_id": "9f1c2b7e4d3a4f60b8e2c1d5a7f\ 39e04", "ts": "t", "kind": "deliver", "message": { "id": "9f1c2b7e4d3a4f60b8e2c1d5a7f39e04", "from": "pl\ anner@fleet.example", "subject": "s", "preview": "p", "mailbox"\ : "peer" } } Figure 27: fixtures/invalid/peer-kind-deliver.json Appendix J. Conformance Suite _(Normative for the classes of Section 13.3; the honesty statement below is part of the specification.)_ The suite consists of golden artifacts under fixtures/valid/ and fixtures/invalid/, plus a validator, fixtures/validate.py. The suite contains *121 fixtures — 42 valid and 79 invalid*. Rapp & Adams Expires 24 April 2027 [Page 123] Internet-Draft AMMP October 2026 +======================================+============================+ | fixture | schema | +======================================+============================+ | valid/directory-both-lanes.json | directory.schema.json | +--------------------------------------+----------------------------+ | valid/directory-delegation-only.json | directory.schema.json | +--------------------------------------+----------------------------+ | valid/directory-minimal.json | directory.schema.json | +--------------------------------------+----------------------------+ | valid/directory-unknown-member.json | directory.schema.json | +--------------------------------------+----------------------------+ | valid/identity-basic.json | binding- | | | record.schema.json | +--------------------------------------+----------------------------+ | valid/identity-labeled.json | binding- | | | record.schema.json | +--------------------------------------+----------------------------+ | valid/message-clean.json | inbound- | | | message.schema.json | +--------------------------------------+----------------------------+ | valid/message-unknown-member.json | inbound- | | | message.schema.json | +--------------------------------------+----------------------------+ | valid/notice-attachment-clean- | deliver- | | ref.json | notice.schema.json | +--------------------------------------+----------------------------+ | valid/notice-attachments- | deliver- | | descriptors.json | notice.schema.json | +--------------------------------------+----------------------------+ | valid/notice-enriched.json | deliver- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/notice-minimal.json | deliver- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/notice-provenance- | deliver- | | unrecognized-value.json | notice.schema.json | +--------------------------------------+----------------------------+ | valid/notice-unknown-member.json | deliver- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/peer-attachment-clean-ref.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/peer-exposure-false.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/peer-exposure-true.json | peer- | Rapp & Adams Expires 24 April 2027 [Page 124] Internet-Draft AMMP October 2026 | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/peer-minimal.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/peer-result.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/peer-unknown-member.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | valid/request-attachment-empty- | submit- | | file.json | request.schema.json | +--------------------------------------+----------------------------+ | valid/request-attachments-empty.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | valid/request-attachments.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | valid/request-minimal.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | valid/request-reply.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | valid/result-accepted.json | result.schema.json | +--------------------------------------+----------------------------+ | valid/result-attachments- | result.schema.json | | accepted.json | | +--------------------------------------+----------------------------+ | valid/result-attachments-held.json | result.schema.json | +--------------------------------------+----------------------------+ | valid/result-attachments-rejected- | result.schema.json | | malformed.json | | +--------------------------------------+----------------------------+ | valid/result-queued.json | result.schema.json | +--------------------------------------+----------------------------+ | valid/result-unknown-member.json | result.schema.json | +--------------------------------------+----------------------------+ | valid/roster-basic.json | roster.schema.json | +--------------------------------------+----------------------------+ | valid/roster-empty.json | roster.schema.json | +--------------------------------------+----------------------------+ | valid/roster-unknown-member.json | roster.schema.json | +--------------------------------------+----------------------------+ | valid/control-demoted-body- | peer- | | digest.json | control.schema.json | Rapp & Adams Expires 24 April 2027 [Page 125] Internet-Draft AMMP October 2026 +--------------------------------------+----------------------------+ | valid/control-dropped-duplicate.json | peer- | | | control.schema.json | +--------------------------------------+----------------------------+ | valid/control-refused-no-edge.json | peer- | | | control.schema.json | +--------------------------------------+----------------------------+ | valid/control-refused-policy.json | peer- | | | control.schema.json | +--------------------------------------+----------------------------+ | valid/statement-exposure-true.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | valid/statement-minimal.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | valid/statement-result- | peer- | | attachment.json | statement.schema.json | +--------------------------------------+----------------------------+ | valid/statement-sender-since.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/directory-display-name- | directory.schema.json | | address.json | | +--------------------------------------+----------------------------+ | invalid/directory-missing-mail- | directory.schema.json | | to.json | | +--------------------------------------+----------------------------+ | invalid/directory-version-1.json | directory.schema.json | +--------------------------------------+----------------------------+ | invalid/identity-extra-field.json | binding- | | | record.schema.json | +--------------------------------------+----------------------------+ | invalid/identity-missing- | binding- | | address.json | record.schema.json | +--------------------------------------+----------------------------+ | invalid/message-missing-body.json | inbound- | | | message.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-attachment-bad- | deliver- | | disposition.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-attachment-content- | deliver- | | ref-trailing-newline.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-attachment-missing- | deliver- | | disposition.json | notice.schema.json | +--------------------------------------+----------------------------+ Rapp & Adams Expires 24 April 2027 [Page 126] Internet-Draft AMMP October 2026 | invalid/notice-attachment-ref-index- | deliver- | | mismatch.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-attachment-ref-not- | deliver- | | clean.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-attachment-sha-not- | deliver- | | clean.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-attachment-traversal- | deliver- | | ref.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-kind-peer.json | deliver- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-missing-preview.json | deliver- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-notice-id-trailing- | deliver- | | newline.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-traversal- | deliver- | | noticeid.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-version-1.json | deliver- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/notice-wrong-kind.json | deliver- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-attachment-ref-index- | peer- | | mismatch.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-exposure-missing- | peer- | | asserted-by.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-exposure-true-missing- | peer- | | last-ts.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-from-crlf.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-from-display-name.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-in-reply-to- | peer- | | encoded.json | notice.schema.json | +--------------------------------------+----------------------------+ Rapp & Adams Expires 24 April 2027 [Page 127] Internet-Draft AMMP October 2026 | invalid/peer-kind-deliver.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-mailbox-inbox.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-message-id-encoded.json | peer- | | | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/peer-notice-id-trailing- | peer- | | newline.json | notice.schema.json | +--------------------------------------+----------------------------+ | invalid/request-attachment-bad- | submit- | | sha.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-attachment-content- | submit- | | ref.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-attachment-filename- | submit- | | crlf.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-attachment-media- | submit- | | type-trailing-newline.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-attachment-missing- | submit- | | sha.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-attachment-negative- | submit- | | size.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-empty-to.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-extra-field.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-recipient-display- | submit- | | name.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-recipient-trailing- | submit- | | newline.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-reply-id-crlf.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-reqid-trailing- | submit- | | newline.json | request.schema.json | +--------------------------------------+----------------------------+ Rapp & Adams Expires 24 April 2027 [Page 128] Internet-Draft AMMP October 2026 | invalid/request-subject-crlf.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-subject-trailing- | submit- | | newline.json | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-traversal-reqid.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | invalid/request-version-absent.json | submit- | | | request.schema.json | +--------------------------------------+----------------------------+ | invalid/result-attachment-bad- | result.schema.json | | outcome.json | | +--------------------------------------+----------------------------+ | invalid/result-attachments-dup- | result.schema.json | | index.json | | +--------------------------------------+----------------------------+ | invalid/result-attachments-gap- | result.schema.json | | index.json | | +--------------------------------------+----------------------------+ | invalid/result-attestation-missing- | result.schema.json | | form.json | | +--------------------------------------+----------------------------+ | invalid/result-bad-attestation- | result.schema.json | | form.json | | +--------------------------------------+----------------------------+ | invalid/result-bad-outcome.json | result.schema.json | +--------------------------------------+----------------------------+ | invalid/roster-display-name- | roster.schema.json | | address.json | | +--------------------------------------+----------------------------+ | invalid/roster-member-missing- | roster.schema.json | | state.json | | +--------------------------------------+----------------------------+ | invalid/roster-missing-members.json | roster.schema.json | +--------------------------------------+----------------------------+ | invalid/roster-version-1.json | roster.schema.json | +--------------------------------------+----------------------------+ | invalid/control-free-text.json | peer- | | | control.schema.json | +--------------------------------------+----------------------------+ | invalid/control-kind-peer.json | peer- | | | control.schema.json | +--------------------------------------+----------------------------+ | invalid/control-outcome-cause- | peer- | | mismatch.json | control.schema.json | +--------------------------------------+----------------------------+ Rapp & Adams Expires 24 April 2027 [Page 129] Internet-Draft AMMP October 2026 | invalid/control-policy-code-without- | peer- | | policy.json | control.schema.json | +--------------------------------------+----------------------------+ | invalid/control-policy-without- | peer- | | code.json | control.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-attachment- | peer- | | unknown-member.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-digest- | peer- | | uppercase.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-exposure-date- | peer- | | free-text.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-exposure-unknown- | peer- | | member.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-from-display- | peer- | | name.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-id-dotted.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-id-leading- | peer- | | dash.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-in-reply-to- | peer- | | leading-dash.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-id-encoded.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-key-id-empty.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-missing- | peer- | | attachments.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-sender-since-date- | peer- | | only.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-subject-crlf.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-to-array.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ Rapp & Adams Expires 24 April 2027 [Page 130] Internet-Draft AMMP October 2026 | invalid/statement-ts-fractional.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-ts-offset.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-unknown- | peer- | | member.json | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-v-boolean.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ | invalid/statement-v2.json | peer- | | | statement.schema.json | +--------------------------------------+----------------------------+ Table 7 *Running it.* The validator is stdlib-only and takes no arguments: python3 fixtures/validate.py It exits 0 if and only if every fixture under valid/ passes and every fixture under invalid/ fails. It requires no counterpart, no network, and no dependency: *the fixtures are the other side.* *Signed cases for the peer-origin profile.* The validator then runs fixtures/peer-signed/check_cases.py, which checks three further sets, all of which a Runtime + peer-origin implementation that verifies peer traffic from other hosts MUST pass (Section 13.3): * rfc8032-vectors.json: the test vectors of Section 7.1 of [RFC8032] (TEST 1 to 3), and negative vectors for the verification policy of Section 12.6.7, each marked for rejection either at verification or at configuration, which are two separate gates; * mime/: 10 raw messages, and the candidate the selection rule of Section 12.6.2 makes of each, reduced by a byte-exact splitter; * cases/: 114 sequences of candidates and events (a crash at each point of Section 12.6.5, recovery, loss of the store with and without H, an operator bound, clock and window changes, and edge and key changes), each step with the outcome, cause, published members and Control Notice that the receiver configured in receiver.json or receiver-host-a.json MUST reach. Single-step cases break exactly one rule; multi- cases pin the precedence of Section 12.6.3; seq- cases pin state across steps; config- cases are configurations a receiver MUST refuse to load. The receiver Rapp & Adams Expires 24 April 2027 [Page 131] Internet-Draft AMMP October 2026 files set retention to its minimum, P, and the cases that probe the retention boundary assume it; a receiver that retains records longer, as Section 12.6.5 permits, maps those steps by its own retention. check_cases.py models only the state that Section 12.6.5 makes normative. It is a self-check of the fixtures, not a Runtime, and its Ed25519 implementation, which is slow and not constant-time, exists for the gate alone. *How a fixture is mapped to a schema.* By filename prefix — request- to the submit-request schema, notice- to deliver-notice, result- to result, message- to inbound-message, identity- to binding-record, and peer- to peer-notice, statement- to peer-statement, control- to peer- control (Appendix F), directory- to peer-directory (Appendix G) and roster- to roster (Appendix H). *What the validator checks.* Schema validation in the JSON Schema 2020-12 dialect [JSON-SCHEMA-2020-12], over the subset of keywords these schemas use (type, const, enum, minLength, pattern, minItems, minimum, items, required, properties, additionalProperties, allOf, and a single if/then pair) — plus two *named post-checks*, neither expressible in standard JSON Schema: * the Result-side binding attachments[i].index == i, contiguous and unique over 0 through n-1 (Section 6.1); * the content_ref index and prefix binding on Notice, Inbound Message, and peer-notice documents: every Descriptor carrying content_ref must equal .attachments/ for its own array position i, binding both the trailing index and the leading prefix (Section 7.2). A Connector conformance claim requires running *both* the schema validation and the second post-check, because exactly one invalid notice-* fixture — and its peer-* counterpart — is schema-valid on its own and is caught only by that post-check (Appendix I, Section 13.3). Rapp & Adams Expires 24 April 2027 [Page 132] Internet-Draft AMMP October 2026 *A note on pattern anchoring, and a portability cost.* Every schema pattern in this suite that ends in $ is written as (?!\n)$. The reason is that a bare $ matches before a single trailing newline in several widely used regex engines — Python's re, Java, and .NET among them — unlike the ECMA-262 dialect JSON Schema specifies. Since every one of these patterns exists to forbid exactly the control characters a permissive $ would let through at the end of a string, the lookahead closes that gap in the schemas themselves rather than in any one validator. Seven fixtures pin the trailing-newline case specifically, distinct from the CRLF fixtures, which embed \r\n and were already rejected even under a permissive engine. The cost is disclosed rather than discovered: *lookahead-less engines cannot compile these patterns at all.* RE2 (Go's regexp), Rust's regex crate, and POSIX ERE reject (?! outright, so the eight schemas carrying the guard (every schema except result and binding-record) fail to load rather than validating loosely. Those engines already treat $ as strict end-of-string and never had the gap, so an implementer on one of them should strip the lookahead — not add a check outside the pattern, which would be both unnecessary and not what breaks. The failure is loud (a compile error, never silent acceptance). *What a green run proves.* That every document shape this protocol defines is accepted or rejected as specified, by an implementation's own parser, with no counterpart present. *What a green run does NOT prove.* The following are filesystem and policy invariants that no document validator can observe. They are obligations of the conformance classes regardless (Section 13.3), and are checked operationally: * Sidecar file existence — an extra file, a missing file, or a directory present when the Descriptor array is empty or absent. * size_bytes and sha256 actually matching the bytes on disk. * filename display-sanitization, and the "never a path" invariant. * The binding between an ordinal and a directory entry on disk. The schema checks only the _Descriptor_ array's own index member, not that directory entry N exists. * Inbound Sidecar directories keyed on the Runtime-minted notice identifier rather than the provider message.id. * The location of Runtime-private Staging, outside the mounted Volume. Rapp & Adams Expires 24 April 2027 [Page 133] Internet-Draft AMMP October 2026 * Write ordering and commit-sentinel atomicity — Sidecars before req-.json; .tmp plus rename. * Caps and allowlists — type allowlists, size caps, Volume quotas. * The absence of a spooled body for a quarantined or blocked message (enforcement by absence, Section 7.3). * A Connector's tolerance of a read-only inbound/ tree. The prescribed check is operational, not a fixture: run the Connector against an inbound tree it cannot write and confirm that it starts, relays every pending Notice, resolves bodies, and submits outbound requests, without requiring any inbound write to succeed (Section 13.3). * Ingest by copy — validation, policy evaluation, and composition running over a Runtime-private snapshot taken before any of those steps, rather than re-reading an Agent-writable path (Section 4.1). * Write-side path discipline, all three conjunctive legs: full parent-chain resolution through pinned directory descriptors from the Namespace root; each directory component opened O_NOFOLLOW|O_DIRECTORY at the moment it is added to that chain; and the final component created O_EXCL and never followed, failing closed on EEXIST and ELOOP. Any one leg alone is a known- insufficient partial mitigation (Section 4.1). * Read-side open discipline — per-component no-symlink-follow opens, non-blocking opens, fstat-based regular-file verification through the open descriptor, and bounded reads and caps on every Agent- influenced path (Section 4.1). * Single writer and single drainer per Namespace — an operational and deployment invariant, such as an OS-level lock held for the process lifetime (Section 4.1). * Compose-time header-boundary stripping: even where the schemas' control-character patterns are bypassed upstream, the Runtime rejects or strips CR, LF, and NUL before composing the outbound message (Section 5.1). * The peer-origin profile's operational obligations (Section 12), none of them visible to a document validator: the write-authority partition — no component that admits messages from a transport not restricted to authorised peer senders holds write authority over peer/, and the prescribed check is a write attempt from inside the admitting component that must fail; intake isolation for shared- Rapp & Adams Expires 24 April 2027 [Page 134] Internet-Draft AMMP October 2026 transport peer candidates, in a location that is neither inbound/ nor peer/ and unreachable from any Agent Namespace; verification placement in a component holding peer write authority that hashes body and attachment bytes itself; sender_exposure never sender- settable; a Peer Notice carrying in_reply_to only when the placing Runtime resolved it from its own record, an unresolvable key being refused rather than stripped; and at-most-once action by the Connector, with an ambiguous target held rather than fanned out. Also invisible to the gate: the durability of the replay store, of H, of each record's disposition and of pending Control Notices across restarts; the closing of admission when H is lost; and the protection of the signing key (Section 12.6). Beyond these, the behavioral obligations of Sections 7 and 8 — the untrusted-content posture, the treatment of task_id as correlation rather than authorization, and the requirement that verified sender identity never be taken from a mail message.from — sit outside the fixture gate entirely. They constrain what an implementation _does_ with a valid document, which no document-level gate can see. Appendix K. A Deferred Seam-Visible "Handled" Signal (Non-Normative) *This appendix is non-normative and is not in force at this document version.* It records the reasoning behind a gap deliberately left open, not an addition to the protocol. Nothing below is a directory, a member, or an artifact that a conforming implementation can rely on, and no requirement keyword appears anywhere in it. *The gap.* Making inbound consumption-tracking Connector-private (Section 4.1) closes the writability conflict but leaves a knowledge and capability split unbridged: only the Connector knows that a Notice has been relayed to its Agent, while on a read-only inbound tree only the Runtime can delete anything. Neither side alone can turn "consumed" into "reclaimed." *A sketch, not a specification.* One shape that would bridge it is a Connector-written, zero-byte marker per acknowledged Notice, written into outbound/, where the Connector already has write access in every posture: outbound/acks/notice- # sketch only - not part of the protocol Figure 28: Sketch only — not part of this protocol A Runtime could then delete the acknowledged Notice and its spooled message, and finally the marker itself. This is presented purely as a sketch: no directory and no wire member is introduced by this appendix. Rapp & Adams Expires 24 April 2027 [Page 135] Internet-Draft AMMP October 2026 *Why a Connector-forgeable signal would be acceptable here.* A garbage-collection signal is a resource decision rather than a security decision. An Agent that falsely claims consumption loses only its own unread mail; one that never claims consumption only bloats its own inbox. Neither crosses a trust boundary that matters, and neither touches any record of the exchange the Runtime keeps on its own side. That is what distinguishes this case from every other "the Agent writes it, so it cannot be trusted" case in this document — but it is a distinction a future version would need to state explicitly rather than assume. *Why a high-water mark cannot substitute.* Section 8 requires task_id to be unguessable and never sequential, and nothing in this document orders notice identifiers, so no single "processed through N" cursor is expressible. Any consumption signal has to be a _set_ of identifiers, or time-ordered — and time-ordering would require the consumer to process strictly in delivery order, which nothing today requires. *The lifecycle this would enable, and its boundaries.* A reliable "is this one done?" answer is the watermark a real retention lifecycle promotes against: cycling a working set, rolling handled traffic into period-compacted archives, then expiring or tiering to cold storage. Three boundaries would need to hold if this is ever specified. * *The seam is not the record.* A Runtime that needs an exchange of record already keeps one outside this protocol, tamper-evident. Retention here is about an Agent's access to its own history, not about the durability of evidence; conflating the two produces an archive that satisfies neither. * *An Agent-writable archive is never evidence.* A compacted archive living under outbound/, which is Agent-writable in every posture, can be rewritten by the Agent it belongs to. Anything meant to be relied on has to be Runtime-written or kept outside this protocol entirely. * *It must not grow the read surface.* The read available today is a point lookup from a notice identifier to a body. "Search my archive" is a materially larger capability — decompression, indexing, query — for a deliberately dumb, credential-free relay to take on. If archives are ever exposed to an Agent, that likely belongs behind a Runtime-served surface rather than the file-drop seam. *Why this is deferred rather than specified now.* A capability starts with a change to this document once a second implementation independently wants it, with members and fixtures proposed together. Rapp & Adams Expires 24 April 2027 [Page 136] Internet-Draft AMMP October 2026 Today exactly one reference Connector carries a private workaround for the underlying problem — a writability-probing fallback with three modes: move, ledger, and in-memory. There is no second data point yet to generalize from, and specifying now would freeze one side's convention as the protocol's answer. *One honesty note for a future implementer.* The sketch's own garbage-collection step is not atomic: if a Runtime crashes between deleting the Notice artifacts and deleting the acknowledgment marker, the marker is orphaned, pointing at a Notice that no longer exists. Any real design needs idempotent collection, in which a missing target is not an error — recorded here so that it is not rediscovered from scratch. Appendix L. Historical Background This section is to be removed before publishing as an RFC. _(Informative.)_ AMMP was specified and implemented before it was brought to the IETF. Its contract was versioned on its own scale, from 1.0.0 to 3.1.0, separately from the wire version that documents carry. This appendix records that history, newest first, because implementations were built against each version, and because the reasons for several rules in this document are clearest as the story of what went wrong without them. An entry that turned out to be wrong is left standing, with a correction attached, rather than quietly rewritten. L.1. draft-rapp-ammp-00 — brought to the IETF (contract 3.1.0) * *One document.* The peer-origin profile's prose, previously a companion document in the repository, is now Section 12. The contract's own version numbers no longer appear in the protocol sections: revisions of this document are draft revisions, and the wire version is contract_version (Section 13.1), which stays "2". The reasoning that explained earlier version changes moved into this appendix (Appendix L.2). Rapp & Adams Expires 24 April 2027 [Page 137] Internet-Draft AMMP October 2026 * *The protocol is AMMP.* It was AMAP, the Agent Mailbox Access Protocol, and is renamed the Agent Mediated Messaging Protocol before its first submission: "mailbox access" named only its first channel, and the contract is a mediation seam for an Agent's correspondence, mail and peer traffic alike. The abstract and the definitions of Agent and Runtime describe it that way, as a store- and-forward messaging runtime; email remains the only transport this document defines. The acronym AMAP was also already in use by an expired 2003 individual draft. Nothing on the wire changes: no document carries the protocol's name, and the header fields of Section 15.1 keep their names. * *Editorial review for submission.* Terminology was alphabetized and Drop and Fail Closed became defined terms; requirements previously phrased as "never" are, where they are requirements, stated with [RFC2119] keywords; references to earlier revisions were removed from the protocol sections and the security considerations; the open questions the authors know of are listed in Section 1.6; and IANA considerations were added for the two header fields the profile defines. * *The signed statement between hosts*, after reports from the first implementations of revision s5-r4, which now stands as the profile's text, without a revision label (an implementation names the draft revision it implements): the id grammar of the Peer Statement and Control Notice schemas is narrowed to ^[A-Za- z0-9_][A-Za-z0-9_-]*$, the notice_id character set without "." (Section 12.6.1; see the correction to the 3.1.0 entry below); a point is decoded canonically before the small-order check (Section 12.6.7), and the gate's reference Ed25519 decoder was corrected to match, with two "negative zero" vectors added; the 12288-octet limit is stated to apply to a field body as transmitted (Section 12.6.2); Fmax after the loss of H is taken from the retained history of F if provable, and is 900 otherwise (Section 12.6.5); and how a Runtime divides its intake is stated to be an implementation choice (Section 12.6.10). Fixtures: two invalid statement fixtures (121 in all: 42 valid, 79 invalid); signed cases for a leading "-" and for the loss of the F history (114); and a MIME case for the size limit (10). L.2. How the versioning rules came about *The move from wire major "1" to "2".* When attachment support was added, every top-level schema and every nested object except verdict and attestation.claims was additionalProperties: false. Under that regime, adding a member was not a safe additive change: a strict major-1 validator rejected the *whole document*, not just the new member. Inbound, that was *silent total message loss* — an Rapp & Adams Expires 24 April 2027 [Page 138] Internet-Draft AMMP October 2026 attachment-bearing email vanished instead of degrading. Outbound, it was an accidental schema error rather than a designed refusal. And there was no signal to gate on while contract_version stayed "1". That is why attachments required a major change: a major-1 consumer receiving a "2" document failed closed _by version_, a legible and designed refusal, and both sides upgraded deliberately. *Two version axes.* From contract 3.0.0 the contract's own version and the wire major were allowed to diverge. The wire major tracks whole-document acceptance compatibility; the contract version tracked the fuller set of obligations, including narrower, disclosed producer-side tightenings. Contract 3.0.0 was a major contract change — it moved fixtures between the valid and invalid sets, and added Runtime obligations that a strictly compliant earlier implementation did not meet — while the wire major stayed "2". Neither of its two producer-side narrowings was the failure a wire major exists to prevent: the Submit Request tightening yields a per- request rejected Result, and the notice_id character-set tightening yields a per-Notice drop under check 1 of Section 7.1, a real cost, disclosed, but narrow and legible. Changing the wire major to "3" would instead have forced every major-2 counterpart on both sides to refuse all traffic, including all the traffic the new tolerance rule protects. Contract 3.1.0 was an additive minor change over 3.0.0, and the wire major stayed "2". *Corrections.* Two claims that shipped in earlier revisions of the contract's normative text were false, and were named rather than quietly replaced, because an implementer who read them may have built against them. 1. *The write-rule "strengthening" that was a regression.* An earlier revision described the exclusive-create and no-symlink- follow requirement on the final path component as having been "strengthened to" the pinned directory-descriptor walk — that is, replaced by it — and deleted the earlier clause outright. That was a *weakening*: a parent chain pinned by descriptor is silent about the final .tmp filename itself, so dropping exclusive- create there let an Agent that pre-plants a symlink at a predictable .tmp name have the Runtime write through it. Section 4.1 states the rule conjunctively, with all three legs required together. 2. *The drop rule described as a closure over schema validation.* An earlier revision described the inbound drop rule as a pure closure over schema validation, and claimed that formulation was exact against the fixture set. Both statements were false: the content_ref index and prefix binding is not expressible in the schema dialect used here, and a fixture exists that is schema- Rapp & Adams Expires 24 April 2027 [Page 139] Internet-Draft AMMP October 2026 valid on its own and must nonetheless be dropped. Section 7.1 states two independent checks, and so does the Connector conformance class. A third correction concerns the change history itself: the contract version that introduced message.provenance was labelled an additive minor change, which was unsound against the strictly validating consumers of the time. The 2.1.0 entry below records what actually happened. *Lessons from implementation.* Several rules in this document exist because an early implementation got them wrong: it trusted a req_id member inside a request body to name the path its Result was written to; it used the provider's message identifier as a path component; it minted guessable, sequential task_id values; and its schema coverage of control characters stopped at subject and filename, missing the two members composed directly into headers. Each was a confirmed exploit path, and each is now a requirement (Section 4.1, Section 5.1, Section 8). *The peer-origin profile's presentation rule.* A prior revision of the profile said only that a Connector "MAY present peer bodies under the same untrusted-content framing it uses for mail". That was a permission with no floor: it allowed the mail framing without saying what any framing had to preserve. A Connector following it chose the cautious option, which on this lane is the wrong one, in the specific direction of telling an Agent not to act on work it was authorized to do. Section 12.8 states what a presentation must and must not convey. L.3. 3.1.0 — the protocol is AMAP; the outbound extension space; the peer-origin profile An additive *minor* bump; the wire major stays "2". The entries below are as recorded at the time: the companion document they cite is now Section 12, and the items marked proposed were adopted in draft-rapp-ammp-00 (Appendix L.1). * *The signed cross-host peer statement and control notice* (2026-10-09, proposed, not yet reviewed; companion-document section 5 revision s5-r4). Two new schemas (Appendix F): the statement a sending Runtime signs with Ed25519 and carries in one AMAP-Peer-Statement header field, and the control notice a receiving Runtime returns about a statement it did not place, both CLOSED at every level; statement-* and control-* fixtures; and, in fixtures/peer-signed/, RFC 8032 vectors with negative cases, MIME selection cases, and signed sequences of candidates and events with the outcome a receiver MUST reach at each step. The rules Rapp & Adams Expires 24 April 2027 [Page 140] Internet-Draft AMMP October 2026 are the companion document's (spec/peer-origin.md, section 5). Additive: nothing existing changes shape, and a deployment without cross-host peer traffic is unaffected. The 30 new fixtures (8 valid, 22 invalid) bring version 3.1.0 to 119 (42 valid, 77 invalid). *Correction (draft-rapp-ammp-00).* The statement schema accepted an id or in_reply_to beginning with "-", which the notice_id character set forbids, so a receiver could verify a statement it could not then publish; one signed case minted such an id. The grammar is narrowed (Appendix L.1), and the case was minted again. * *The fleet roster* (2026-09-24). A new, optional artifact (Section 11, schema in Appendix H): one Runtime-written file naming the instances the Runtime has admitted, so that an Agent on a deployment whose addresses carry an underivable suffix can learn which peers exist. It answers existence, never authorisation, and states that separately from the Peer Directory. Its readability obligation is split between the Runtime, which publishes only into a designated location and never creates it, and the deployment, which exposes that location. Additive: a new schema and seven fixtures, and nothing existing changes shape. * *The protocol is AMAP; the schema $id authority is amap-spec* (2026-09-15). The protocol formerly called AMP ("Agent Mailbox Protocol") is renamed AMAP ("Agent Mailbox Access Protocol") because AMP collides with the IETF DTN working group's adopted draft-ietf-dtn-amp; the specification repository moves from agent- mailbox-protocol to amap-spec with it. The six schema $id values move from https://agent-mailbox-protocol/schemas/… to https://amap-spec/schemas/…. Nothing on the wire changes shape: no document carries the protocol's name as a field, and $id is an identifier the gate never dereferences. Implementations that mirror the Runtime's address spell its local part amap.router from this version. This draft's filename is a separate decision and is unchanged here. This version is marked DRAFT: its profile prose is a companion document (spec/peer-origin.md, itself DRAFT), and its schema and fixtures may still change before it is settled. * *Outbound extension space.* outbound/ext// is defined as a Connector-owned side channel a Runtime MAY read by out-of-band agreement, with no shape specified and nothing security-relevant permitted to depend on it (Section 4.1). is pinned as a stable Connector id — chosen once, unchanged by any rename of the Connector's repository, package, or deployment, and opaque to the Runtime that reads it; the wording it replaced ("a Connector or deployment name") permitted a repository name to leak into a wire- visible path. The reference Claude Code Connector chose claude- Rapp & Adams Expires 24 April 2027 [Page 141] Internet-Draft AMMP October 2026 code as its id; ids are chosen by each Connector, and this document neither lists nor registers them. One normative clarification sits beside it: an outcome written there is a claim by the receiving side, never proof of delivery or of non-delivery, and the Runtime's own delivery record is authoritative over it (Section 14.15). There is deliberately no inbound extension space. * *Peer-notice schema.* The peer-origin profile's artifact is pinned by peer-notice.schema.json (Appendix F): the deliver-notice layout with kind and mailbox fixed to peer, message.from a bare addr- spec, message identifiers in the notice-identifier character set, and the optional Runtime-asserted sender_exposure stamp with its required members. The profile's prose is the companion document spec/peer-origin.md (v3.1.0 DRAFT). * *The Peer Directory.* A Runtime may publish a per-instance projection of the authorisation graph, directory.json, at the Agent's Namespace root (Section 10, schema in Appendix G), so that a Connector can learn which peers it may address without the host rendering a snapshot onto an Agent-writable mount that goes stale whenever the fleet changes. It is advisory and never authoritative, and a Runtime never reads it back as authorisation, which is what stops it decaying into an allowlist the Agent can see. The Namespace root gains a Runtime-owned posture for Runtime-written files: the file sits beside the lanes rather than inside one, because it spans both. Additive: a new optional artifact, a new schema and seven fixtures (4 valid, 3 invalid); no existing schema or fixture changes, and the wire contract_version stays "2". _Restored 2026-10-05._ This entry was in the versioning record this document was reconciled against and was lost in the reconciliation, along with its fixtures from the count below. Rapp & Adams Expires 24 April 2027 [Page 142] Internet-Draft AMMP October 2026 * *Clarifications from implementation* (proposed, 2026-10-07). Questions from Runtime implementers reconciling against this document, each answered from the existing text and then stated: a Runtime knows the of every Namespace it serves and rejects a request from a Namespace it cannot name (Section 4.1); outbound/ext//'s id is a lowercase single path segment (Section 4.1); a Runtime may be built from several trusted components, and its obligations bind the composite (Section 3.5, informative); the Deletion rights table gains rows for outbound/ ext// and directory.json (Section 4.2); message.id is the mail store's identifier, not the Message-ID (Section 7); and a Runtime does not hand mail for special-use or private-use domain names to a mail system outside the deployment (Section 14.16). The companion peer-origin profile clarifies what a routable peer domain is. No schema or fixture changes. * *Fixtures: 75 total (27 valid, 48 invalid), up from 58.* 16 peer-* fixtures were added (6 valid, 10 invalid), covering a minimal notice, a reply, the exposure stamp asserted both ways, a clean attachment reference, an unknown-member case, and rejections for a deliver kind under the peer tree, a peer kind under the inbound tree, a display-name or CRLF from, encoded message identifiers, a missing asserted_by, a missing last-delivery timestamp when exposure is asserted true, an inbox mailbox, a content_ref index mismatch, and a trailing-newline identifier. The validator maps the peer- prefix to the new schema and applies the content_ref post-check to it. Three existing fixtures had example strings normalised to example.* identities (one free-text body, two addresses in injection examples); each stays valid or invalid for the same reason as before, and no schema changed. *Correction (2026-10-05).* The count above omits the seven directory-* fixtures (4 valid, 3 invalid) added with the Peer Directory. With them and the fleet roster's seven, version 3.1.0 has 89 fixtures (34 valid, 55 invalid). *Correction (draft-rapp-ammp-00).* The breakdown above does not sum: 58 + 16 is 74, and 37 + 10 is 47. The record does not identify the seventeenth addition. The totals from 89 onward are the ones verified against the suite as it ships (Appendix J). L.4. 3.0.0 — direction-aware tolerance, and six closed safety holes A *major document bump*; the wire major stays "2". Appendix L.2 states why the two numbers diverge here, and why bumping the wire would have caused the exact harm this version exists to repair. Rapp & Adams Expires 24 April 2027 [Page 143] Internet-Draft AMMP October 2026 * *Unknown-member tolerance, direction-aware.* The three Runtime- authored artifacts — deliver-notice, inbound-message, and result — became *open* at every level: additionalProperties: false was removed from the top level, from message, from Descriptor items, and from attestation, and a consumer must tolerate an unrecognized member. Emitting one remains producer non-conformance. The Agent-authored submit-request envelope stays *closed* at every level. Consequence: adding an optional member to a Runtime- authored artifact is now genuinely additive; adding any member to the submit-request is a major change. * *...and for values.* The enum constraint was removed from message.sender_standing and message.provenance, the two explicitly advisory vocabularies, and a consumer must tolerate an unrecognized value in either. kind, outcome (both levels), and disposition remain closed, enum-locked vocabularies. * *Ingest by copy.* A Runtime must copy an Agent-authored request and its Sidecars into private staging before validating, must act only on that copy, and must not re-read an Agent-writable path after a decision depending on its content (Section 4.1). * *Address syntax and header-injection discipline.* A new obligation in Section 5.1 plus schema patterns on draft.to/cc, draft.subject, attachment filename and media_type, draft.reply_to_message_id, in_reply_to, agent_id, and ts — including the two members composed directly into headers, which earlier schema coverage missed. * *Path discipline, write- and read-side both.* The write-side rule became *conjunctive* (Section 4.1), and a new read-side rule applies the equivalent discipline to every Agent-influenceable read. A notice_id character-set pattern was added to the deliver- notice and inbound-message schemas so that every legal notice_id is also a legal content_ref prefix. *Disclosed cost:* notice_id was previously unconstrained, so a Runtime minting identifiers outside the new character set — an ISO-8601 timestamp, standard base64, a URN, or anything else carrying punctuation outside it — now emits Notices and Inbound Messages that fail schema validation on a _pattern violation on a required member_, and a conforming Connector drops them. *Migration:* such a Runtime must re-mint identifiers under the new character set (unpadded base64url or lowercase hex, for instance) before any Connector it talks to upgrades, because the failure is otherwise silent on both sides. * *Single writer, single drainer.* Section 4.1 now names the concurrency assumption that every atomicity, commit-sentinel, and req_id rule already depended on. Rapp & Adams Expires 24 April 2027 [Page 144] Internet-Draft AMMP October 2026 * *message.from named untrusted.* The untrusted-content sentence now covers explicitly the member most likely to be rendered to a human or used for a trust judgement (Section 7.1). * *Document authority.* The contract, its schemas, and its fixtures were made normative; the rendering then generated from them was non-authoritative where they disagreed. (This document is now the prose of record.) The conformance-class text was moved into the contract itself so that the demotion deletes nothing, and a stray claim of a second, message-specific Sidecar directory was corrected to match the layout tree. * *Corrections shipped with this version*, rather than a tidied history: (i) the write rule described in an earlier revision as "strengthened" had in fact been weakened — the exclusive-create requirement on the final component had been deleted rather than supplemented (Appendix L.2); (ii) the inbound drop rule described as a pure closure over schema validation was refuted by this version's own content_ref-binding fixture, and is now stated as two independent checks (Section 7.1, Section 13.3); (iii) the claim that this version's producer bytes are unchanged is explicitly _not_ made — two producer-side narrowings are disclosed above, each travelling a legible per-artifact refusal path; and (iv) the 2.1.0 entry below is corrected in place. * *Fixtures: 58 total (21 valid, 37 invalid), up from 44.* The unknown-member notice fixture moved from the invalid set to the valid set and was extended with an unrecognized member inside message; valid unknown-member fixtures were added for result and inbound-message; the withheld-body invalid fixture was deleted, having become schema-valid under the relaxed envelope (its name always over-claimed what additionalProperties alone tested); the bad-provenance fixture moved to the valid set as an unrecognized- value case; an invalid extra-member submit-request fixture was added to pin the closed side of the asymmetry, previously untested at the top level; four invalid fixtures were added for the address-syntax and control-character rules; a traversal notice_id fixture was added; the content_ref index-mismatch fixture was added, and is the one invalid notice-* case that is schema-valid on its own; and six fixtures pin the trailing-newline anchoring case. Rapp & Adams Expires 24 April 2027 [Page 145] Internet-Draft AMMP October 2026 L.5. 2.3.0 — inbound byte access in force A *minor bump*, prose only: no schema rule and no fixture changed, and the wire major stayed "2". It resolved an inconsistency carried since 2.0.0. A fixture showing a clean Descriptor with content_ref and sha256 sat in the valid set as a deliberately forward-valid shape, while the prose forbade any Runtime from producing it — the gate blessed what the prose forbade. This version sided with the fixtures: the release-scoped byte embargo was dropped, and the standing rule became the one stated in Section 7.2 — a Runtime may publish bytes and emit content_ref/sha256 only for a Descriptor it asserts clean, and a Runtime that cannot make that determination must not assert clean. The substantive clarification is that disposition is the Runtime's _assertion_, its basis deliberately unspecified; the older prose presumed a scanner, which is what made byte access read as a deferred capability rather than as a policy decision whose wire shape major 2 already carried. Nothing was tightened, and there is still no outbound content_ref. L.6. 2.2.0 — deletion rights, read-only inbound A *permissive minor bump*: no wire member, no schema change, no fixture changed or added. It (a) declared a read-only inbound tree a valid Runtime posture and made the inbound processed/ archive directory optional, (b) obliged a Connector not to require inbound write access, making consumption-recording Connector-private, and (c) named deletion rights per artifact class, along with the two constraints attached to them: a Connector must not delete or rewrite a published request, and a Connector exercising its new deletion rights must preserve req_id monotonicity by other durable means. It also pinned three previously implicit reason_code rules: setting one on every Result became a keyworded SHOULD, and a consumer must tolerate an unrecognized code and must not infer outcome from it. L.7. 2.1.0 — the body spool, and three optional Result members Recorded at the time as an *additive minor bump*: one new optional spooled artifact (the Inbound Message) and its schema, one new optional Notice member (provenance), and three new optional Result members (reason_code, attestation, ts). No existing schema gained a required member, no existing fixture changed shape, and the wire major stayed "2". *Correction (recorded at 3.0.0).* That "additive minor" label was unsound as written. Against a strictly validating 2.0.0 consumer — every schema at that version was closed at every level — a Notice carrying the new provenance member failed schema validation as a *whole document* and was silently dropped: precisely the failure mode Rapp & Adams Expires 24 April 2027 [Page 146] Internet-Draft AMMP October 2026 cited as the reason attachments had required a major bump, reintroduced as a minor. Version 3.0.0 repairs the versioning model rather than relabelling this history, which is why this entry still records what shipped rather than what would have been correct. L.8. 2.0.0 — attachments, and contract_version required A *major bump*, moving the wire major from "1" to "2". It added attachment support in both directions — outbound Descriptors with required size_bytes and sha256 and their Sidecar bytes, inbound Descriptors with a disposition — and made contract_version REQUIRED on the submit-request, deliver-notice, and result artifacts, with no compatibility mode. The reason the bump had to be major rather than minor is the reasoning in Section 13.1: under the then-universal closed envelopes, a new member caused a major-1 consumer to reject the _whole document_, which inbound is silent total message loss. A version signal, rather than an unknown-member rejection, is what makes that refusal legible. L.9. Fixture identifier normalization Across these versions the fixture suite's identities were normalized to example.com and example.org placeholders, and deployment-specific identifiers were removed from fixture content. This changed no shape and no expectation: every affected fixture validates exactly as before, and no identifier removed in that pass is reintroduced by this document. Authors' Addresses Daniel Rapp Proofpoint, Inc. Email: drapp@proofpoint.com J. Trent Adams Proofpoint, Inc. Email: tadams@proofpoint.com Rapp & Adams Expires 24 April 2027 [Page 147]