The Agent Mediated Messaging Protocol (AMMP)
draft-rapp-ammp-00
This document is an Internet-Draft (I-D).
Anyone may submit an I-D to the IETF.
This I-D is not endorsed by the IETF and has no formal standing in the
IETF standards process.
| Document | Type | Active Internet-Draft (individual) | |
|---|---|---|---|
| Authors | Daniel Rapp , J. Trent Adams | ||
| Last updated | 2026-10-22 | ||
| RFC stream | (None) | ||
| Intended RFC status | (None) | ||
| Formats | |||
| Stream | Stream state | (No stream defined) | |
| Consensus boilerplate | Unknown | ||
| RFC Editor Note | (None) | ||
| IESG | IESG state | I-D Exists | |
| Telechat date | (None) | ||
| Responsible AD | (None) | ||
| Send notices to | (None) |
draft-rapp-ammp-00
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 <volume-root>/<agent-id>/,
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 <volume-root>/ 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 <volume-root>/<agent-id>/ 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
<agent-id>/, 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. <volume-root>/ below stands for whatever a deployment
calls it.
Rapp & Adams Expires 24 April 2027 [Page 17]
Internet-Draft AMMP October 2026
<volume-root>/
identity.json # runtime/provisioner: Binding Record (S9)
<agent-id>/
outbound/
req-<id>.json # agent -> runtime: Submit Request
req-<id>.attachments/ # agent -> runtime: Sidecar bytes,
# ordinals 0 1 2 ...
results/<id>.json # runtime -> agent: verdict for req <id>
processed/req-<id>.json # runtime: consumed requests
ext/<name>/ # connector-owned side channel a
# runtime MAY read by out-of-band
# agreement; <name> is a STABLE,
# opaque connector id
inbound/
notices/notice-<id>.json # runtime -> agent: delivered event
notices/<notice-id>.attachments/ # bytes asserted clean,
# ordinals 0 1 2 ... (S7.2)
notices/processed/ # OPTIONAL - connector archive,
# writable-inbound deployments only
messages/notice-<id>.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/<notice-id>.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.* <agent-id> 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
<agent-id> 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 <agent-id>/ 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/<id>.json, the .tmp sibling of
processed/req-<id>.json, inbound/notices/notice-<id>.json,
inbound/messages/notice-<id>.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-<id>.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-<id>.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-<id>.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-<id>.json MUST be keyed on
the notice identifier, and MUST NOT be keyed on message.id. Inbound
bytes are published only at inbound/notices/<notice-
id>.attachments/<index>, 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-<id>.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/<name>/ is a Connector-owned
side channel, one directory per <name>, that a Runtime MAY read by
out-of-band agreement. *<name> 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 <name> 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, <name> 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-<id>.json | | |move to |
| (pending) and | | |processed/, or |
| req-<id>.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-<id>.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/<id>.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-<id>.json, | |only. |policy. Absent a |
| inbound/messages/ | |Where a |seam-visible |
| notice-<id>.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/<name>/ |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 <volume-root>/<agent-id>/outbound/
req-<id>.json as a JSON text [RFC8259]. Its Sidecar bytes, if any,
are written first, into req-<id>.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-<id> 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 <volume-root>/<agent-id>/outbound/results/<id>.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 <owner@example.com>",
"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
<notice_id>.attachments/<i> 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 <attacker@evil.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/<notice-id>.attachments/<index>
*only* for a Descriptor it asserts disposition == "clean", and only
then MAY it emit content_ref (a path of the form <notice-
id>.attachments/<index>) 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
<index> 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 <volume-root>/<agent-
id>/inbound/messages/notice-<id>.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 <owner@example.com>",
"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* — <volume-root>/identity.json, a sibling of <agent-
id>/, 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 (<agent-
id>/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/<name>/, where
delivery outcomes are kept (Section 12.8). There is deliberately no
inbound extension space.
12.2. The Peer Tree
<agent-id>/
outbound/ # as in the Volume Layout
inbound/ # mail
notices/ messages/
peer/ # PROFILE: same layout, conventions
notices/notice-<id>.json # and Sidecar rules as inbound/
notices/<notice-id>.attachments/<i>
messages/notice-<id>.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 <id@domain>, 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/<name>/ (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@<domain> 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 <id@domain> 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=<key_id>; s=<statement>; b=<signature>
* 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 <id@domain>, 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@<domain>
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/<name>/, 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 <attacker@evil.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/<name>/ (Section 4.1) is Connector-owned and Agent-
writable, and this document specifies no shape for anything under it.
Its <name> 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,
<https://doi.org/10.6028/NIST.FIPS.180-4>.
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,
<https://json-schema.org/draft/2020-12/json-schema-core>.
[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,
<https://www.rfc-editor.org/info/rfc2045>.
[RFC2046] Freed, N. and N. Borenstein, "Multipurpose Internet Mail
Extensions (MIME) Part Two: Media Types", RFC 2046,
DOI 10.17487/RFC2046, November 1996,
<https://www.rfc-editor.org/info/rfc2046>.
[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,
<https://www.rfc-editor.org/info/rfc2047>.
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119,
DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/info/rfc2119>.
[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,
<https://www.rfc-editor.org/info/rfc2183>.
[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, <https://www.rfc-editor.org/info/rfc2231>.
[RFC3339] Klyne, G. and C. Newman, "Date and Time on the Internet:
Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002,
<https://www.rfc-editor.org/info/rfc3339>.
[RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO
10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November
2003, <https://www.rfc-editor.org/info/rfc3629>.
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,
<https://www.rfc-editor.org/info/rfc4648>.
[RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322,
DOI 10.17487/RFC5322, October 2008,
<https://www.rfc-editor.org/info/rfc5322>.
[RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital
Signature Algorithm (EdDSA)", RFC 8032,
DOI 10.17487/RFC8032, January 2017,
<https://www.rfc-editor.org/info/rfc8032>.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
[RFC8259] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data
Interchange Format", STD 90, RFC 8259,
DOI 10.17487/RFC8259, December 2017,
<https://www.rfc-editor.org/info/rfc8259>.
[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,
<https://www.rfc-editor.org/info/rfc8792>.
16.2. Informative References
[A2A] Linux Foundation, "Agent2Agent Protocol, v1.0.1", May
2026, <https://a2a-protocol.org>.
[ACP-CLIENT]
Zed Industries, "Agent Client Protocol", n.d.,
<https://agentclientprotocol.com>.
[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,
<https://modelcontextprotocol.io/
specification/2025-11-25>.
[RFC2033] Myers, J., "Local Mail Transfer Protocol", RFC 2033,
DOI 10.17487/RFC2033, October 1996,
<https://www.rfc-editor.org/info/rfc2033>.
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,
<https://www.rfc-editor.org/info/rfc3864>.
[RFC5321] Klensin, J., "Simple Mail Transfer Protocol", RFC 5321,
DOI 10.17487/RFC5321, October 2008,
<https://www.rfc-editor.org/info/rfc5321>.
[RFC6761] Cheshire, S. and M. Krochmal, "Special-Use Domain Names",
RFC 6761, DOI 10.17487/RFC6761, February 2013,
<https://www.rfc-editor.org/info/rfc6761>.
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-<id>.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 <agent-id>/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 <agent-id>/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 <id@domain> 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 <id@domain> 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 <agent-id>/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 <agent-id>/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-<id>.attachments/<i>); 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 <owner@example.com>",
"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 <notice_id>.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 <notice_id>.attachments/<i> 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-<id>.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-<id> # 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/<name>/ 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). <name> 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 <agent-id> of every Namespace it serves and
rejects a request from a Namespace it cannot name (Section 4.1);
outbound/ext/<name>/'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/<name>/ 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]