<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE rfc [
  <!ENTITY nbsp    "&#160;">
  <!ENTITY zwsp   "&#8203;">
  <!ENTITY nbhy   "&#8209;">
  <!ENTITY wj     "&#8288;">
]>
<rfc 
	xmlns:xi="http://www.w3.org/2001/XInclude" 
	ipr="trust200902" 
	docName="draft-rapp-ammp-00" 
	category="std" 
	submissionType="IETF" 
	tocInclude="true" 
	sortRefs="true" 
	symRefs="true" 
	version="3">
  <front>
    <title abbrev="AMMP">The Agent Mediated Messaging Protocol (AMMP)</title>
    <seriesInfo name="Internet-Draft" value="draft-rapp-ammp-00"/>
    <author initials="D." surname="Rapp" fullname="Daniel Rapp">
      <organization>Proofpoint, Inc.</organization>
      <address>
        <email>drapp@proofpoint.com</email>
      </address>
    </author>
    <author initials="T." surname="Adams" fullname="J. Trent Adams">
      <organization>Proofpoint, Inc.</organization>
      <address>
        <email>tadams@proofpoint.com</email>
      </address>
    </author>
    <date year="2026" month="October" day="21"/>
    <abstract>

<t>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. </t>

<t>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.</t>

<t>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.</t>

    </abstract>
    <note>
      <name>About This Draft</name>
      <t>This document and the accompanying schemas and conformance fixtures are
published under the Apache License 2.0.</t>
    </note>
    <note removeInRFC="true">
      <name>Discussion Venues</name>
      <t>Source for this draft, its schemas, its conformance fixtures and an
issue tracker can be found at <eref target="https://github.com/proofpoint/amap-spec"/>.</t>
    </note>

  </front>
  <middle>

<section anchor="sec-1">
      <name>Introduction — Motivation and Problem Statement</name>
      <t>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.</t>
      <t><strong>Inbound.</strong> 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.</t>
      <t><strong>Outbound.</strong> 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.</t>
      <t><strong>Credentials and identity.</strong> 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.</t>
      <section anchor="sec-why-the-existing-options-are-insufficient">
        <name>Why the existing options are insufficient</name>
        <t>Handing the agent mailbox credentials, or a mail-send API, makes the agent a
<em>trusted</em> 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.</t>
        <t>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.</t>
        <t>Adjacent agent protocols do not fill the gap, because they are solving a
different problem: integration and interoperation <em>among credentialed
participants</em>, rather than isolation of an <em>untrusted</em> one. <xref target="sec-3-4"/>
positions AMMP against that landscape in detail.</t>
      </section>
      <section anchor="sec-the-approach">
        <name>The approach</name>
        <t>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.</t>
        <t>AMMP interposes a <strong>trusted runtime</strong> 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 <strong>no capability</strong>: 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.</t>
        <t>The payoff, which the rest of this document formalizes, is the load-bearing
invariant:</t>
        <ul empty="true">
          <li>
            <t>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.</t>
          </li>
        </ul>
        <t>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.</t>
      </section>
      <section anchor="sec-why-a-filesystem-namespace-rather-than-an-api-rpc-or-queue">
        <name>Why a filesystem namespace rather than an API, RPC, or queue</name>
        <t>The medium <em>is</em> 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. <xref target="sec-3-3"/> develops this point, along with the obligations it puts
on the runtime in exchange.</t>
      </section>
      <section anchor="sec-design-goals">
        <name>Design goals</name>
        <ul spacing="normal">
          <li>
            <t><strong>Capability-free isolation.</strong> The channel available to an untrusted agent
grants no send capability, no credential, and no reach outside its own
namespace.</t>
          </li>
          <li>
            <t><strong>Vendor-neutral interoperability.</strong> A runtime and a connector built
independently interoperate because both are built against this contract
rather than against each other.</t>
          </li>
          <li>
            <t><strong>Versioned co-evolution with offline conformance.</strong> Either side proves
conformance against a fixture suite with no counterpart present, and
version changes are deliberate, legible, and fail closed rather than
silent.</t>
          </li>
          <li>
            <t><strong>Composition with real email.</strong> Interactions remain federated, of-record,
and human-inclusive, rather than confined to a closed agent-only channel.</t>
          </li>
          <li>
            <t><strong>Portability across agent runtimes.</strong> Nothing in the contract presumes a
particular agent harness, language, or tool interface.</t>
          </li>
        </ul>
      </section>
      <section anchor="sec-non-goals">
        <name>Non-goals</name>
        <ul spacing="normal">
          <li>
            <t><strong>AMMP is not a mail transport.</strong> It neither defines nor replaces SMTP,
IMAP, or any provider-side mail infrastructure. It describes what a runtime
and a connector exchange <em>locally</em>; whatever the runtime does with a mail
provider is out of scope.</t>
          </li>
          <li>
            <t><strong>AMMP is not agent-to-agent RPC.</strong> 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
(<xref target="sec-peer"/>) 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.</t>
          </li>
          <li>
            <t><strong>AMMP is not a policy language.</strong> 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.</t>
          </li>
        </ul>
        <t>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 (<xref target="app-i"/>), and where a property is a runtime obligation rather than a
wire-verifiable fact, this document says so (<xref target="sec-11-14"/>).</t>
      </section>
      <section anchor="sec-why-ietf" removeInRFC="true">
        <name>Why This Work Is Brought to the IETF</name>
        <t>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:</t>
        <ul spacing="normal">
          <li>
            <t><strong>Review.</strong> The design rests on a security argument
(<xref target="sec-3-2"/>), and that argument needs review by people who did not
make it, particularly from the mail, security and identity communities.</t>
          </li>
          <li>
            <t><strong>Interoperation.</strong> 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.</t>
          </li>
          <li>
            <t><strong>Collaborators.</strong> The work should be shaped by more than one
organization, and by the people who operate mail at scale.</t>
          </li>
          <li>
            <t><strong>Alignment with related work.</strong> 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.</t>
          </li>
        </ul>
        <t>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.</t>
        <ul spacing="normal">
          <li>
            <t><strong>The wire version.</strong> The wire version starts at
<tt>"2"</tt> rather than <tt>"1"</tt>, because implementations already emit and
check <tt>"2"</tt> (<xref target="sec-10-1"/>). Whether to reset it is a fair
question.</t>
          </li>
          <li>
            <t><strong>Registries.</strong> Whether the wire version, the Result
<tt>reason_code</tt> vocabulary, or the Control Notice's outcome and cause vocabularies should be
governed by IANA (<xref target="sec-12"/>).</t>
          </li>
          <li>
            <t><strong>The name of the <tt>attestation</tt> member.</strong> In this
document it is the identity stamp a Runtime applied to an outbound message
(<xref target="sec-6-1"/>). In IETF usage, attestation usually means remote
attestation of platform state, and the member may need another name.</t>
          </li>
          <li>
            <t><strong>Identity between Runtimes.</strong> The peer-origin profile signs
traffic between hosts with Ed25519 keys configured per Runtime
(<xref target="sec-peer-keys"/>). 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.</t>
          </li>
          <li>
            <t><strong>Document structure.</strong> Whether the peer-origin profile belongs
in this document or in a companion document.</t>
          </li>
          <li>
            <t><strong>Internationalized addresses.</strong> Addresses are restricted to a
narrow ASCII addr-spec (<xref target="sec-5-1"/>), and internationalized
addresses are out of scope for now.</t>
          </li>
          <li>
            <t><strong>A consumption signal.</strong> Whether a Connector should be able to
tell its Runtime that a Notice has been handled
(<xref target="app-i"/>).</t>
          </li>
        </ul>
      </section>
    </section>
    <section anchor="sec-2">
      <name>Conventions and Terminology</name>
      <t>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 <xref target="RFC2119"/> <xref target="RFC8174"/>
when, and only when, they appear in all capitals, as shown here.</t>

	      <t>Every message defined by this document is a JSON text as defined by
	<xref target="RFC8259"/>, encoded in UTF-8 <xref target="RFC3629"/>. The schemas in <xref target="app-a"/> through
	<xref target="app-roster"/> are written in the JSON Schema 2020-12 dialect
	<xref target="JSON-SCHEMA-2020-12"/>.</t>
      <t>Long lines in the JSON examples and schemas in this document are folded
using the single-backslash strategy of <xref target="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.</t>
	      <t>Sections and subsections explicitly marked <em>(informative)</em> or
	<em>(non-normative)</em> carry no requirements; <xref target="sec-3-4"/>, <xref target="sec-3-5"/>, <xref target="app-i"/>,
	and <xref target="app-history"/> are the only such parts of this document.</t>

    <section anchor="terminology">
      <name>Terminology</name>

      <ul spacing="normal">
        <li>
          <t><strong>Admitting Component</strong> — 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 (<xref target="sec-peer-origin"/>).</t>
        </li>

        <li>
          <t><strong>Agent</strong> — 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.</t>
        </li>

        <li>
          <t><strong>Binding Record</strong> — the optional, informational artifact at the Volume
root naming which mailbox the Volume is bound to (<xref target="sec-9"/>).</t>
        </li>

        <li>
          <t><strong>Connector</strong> — 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.</t>
        </li>

        <li>
          <t><strong>Control Notice</strong> — 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 (<xref target="sec-peer-control"/>).</t>
        </li>

        <li>
          <t><strong>Descriptor</strong> — one entry in an <tt>attachments[]</tt> 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.</t>
        </li>

        <li>
          <t><strong>Disposition</strong> — the Runtime's assertion about an inbound attachment's
handling: <tt>clean</tt>, <tt>stripped</tt>, <tt>quarantined</tt>, or <tt>unscanned</tt> (<xref target="sec-7-2"/>).</t>
        </li>

        <li>
          <t><strong>Drop</strong> — 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.</t>
        </li>

        <li>
          <t><strong>Fail Closed</strong> — to refuse to process an artifact rather than proceed on a
partial or guessed interpretation of it.</t>
        </li>

        <li>
          <t><strong>Inbound Message</strong> — the Runtime-authored artifact carrying a delivered
message's body text and headers, spooled under a Notice's identifier and
fetched on demand (<xref target="sec-7-3"/>).</t>
        </li>

        <li>
          <t><strong>Lane</strong> — one class of traffic between an Agent and its
Runtime, or between two Agents: the mail lane (<tt>inbound/</tt> and outbound
mail) and, under the peer-origin profile, the peer lane (<tt>peer/</tt>). The
Peer Directory states, per peer, which lanes are open.</t>
        </li>

        <li>
          <t><strong>Namespace</strong> — the per-Agent subtree <tt>&lt;volume-root&gt;/&lt;agent-id&gt;/</tt>, mounted
into that Agent's sandbox. It is the unit of isolation and the sole
authority for attributing an artifact to an Agent.</t>
        </li>

        <li>
          <t><strong>Notice</strong> (Deliver Notice) — the Runtime-authored artifact announcing that
a message was delivered to the Agent's mailbox (<xref target="sec-7"/>). It is a
pointer and summary, not the message.</t>
        </li>

        <li>
          <t><strong>Ordinal</strong> — 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.</t>
        </li>

        <li>
          <t><strong>Origin Class</strong> — the class of origin a consumer assigns to an inbound
document, derived only from the Tree it was read from: mail for
<tt>inbound/</tt>, and same-principal peer origin for <tt>peer/</tt>
(<xref target="sec-4-1"/>, <xref target="sec-peer-origin"/>).</t>
        </li>

        <li>
          <t><strong>Peer Notice</strong> — the Runtime-authored artifact, in the peer-origin
profile, announcing a message from a same-principal Agent; the Deliver Notice
shape with <tt>kind</tt> <tt>"peer"</tt>, placed under <tt>peer/notices/</tt>
(<xref target="sec-peer-tree"/>).</t>
        </li>

        <li>
          <t><strong>Peer Statement</strong> — the signed JSON object a sending Runtime attaches, in an
<tt>AMAP-Peer-Statement</tt> header field, to peer traffic it sends to another
host (<xref target="sec-peer-stmt"/>).</t>
        </li>

        <li>
          <t><strong>Result</strong> — the Runtime-authored verdict for one consumed Submit Request
(<xref target="sec-6"/>).</t>
        </li>

        <li>
          <t><strong>Runtime</strong> — 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.</t>
        </li>

        <li>
          <t><strong>Sandbox</strong> — 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.</t>
        </li>

        <li>
          <t><strong>Sidecar</strong> — 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.</t>
        </li>

        <li>
          <t><strong>Staging</strong> — Runtime-private storage outside the Volume, unreachable from
any Agent's mount, into which the Runtime copies Agent-authored bytes
before acting on them (<xref target="sec-4-1"/>).</t>
        </li>

        <li>
          <t><strong>Submit Request</strong> — the Connector-authored artifact requesting that a
message be sent (<xref target="sec-5"/>). It is inert: it carries no capability and
effects nothing by existing.</t>
        </li>

        <li>
          <t><strong>Tree</strong> — one of the Runtime-owned inbound-class subtrees of a
Namespace: <tt>inbound/</tt>, and, under the peer-origin profile,
<tt>peer/</tt>. Each Tree is an Origin Class.</t>
        </li>

        <li>
          <t><strong>Volume</strong> — 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
<tt>&lt;volume-root&gt;/</tt> for it and nothing in the protocol derives from its name.
No implementation may depend on that name or any other.</t>
        </li>

      </ul>


</section>

    </section>
    <section anchor="sec-3">
      <name>Architecture and Trust Model</name>
      <section anchor="sec-3-1">
        <name>Actors</name>
        <table>
          <thead>
            <tr>
              <th align="left">Actor</th>
              <th align="left">Trust</th>
              <th align="left">Holds</th>
              <th align="left">Reaches</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td align="left">
                <strong>Runtime</strong> (host-side)</td>
              <td align="left">trusted</td>
              <td align="left">messaging credentials, Agent identity, policy, the record of what was sent</td>
              <td align="left">the mail provider, the Volume</td>
            </tr>
            <tr>
              <td align="left">
                <strong>Connector</strong> (per Agent)</td>
              <td align="left">semi-trusted, Agent-side</td>
              <td align="left">nothing credentialed</td>
              <td align="left">its own Namespace on the Volume; its Agent's input and output</td>
            </tr>
            <tr>
              <td align="left">
                <strong>Agent</strong></td>
              <td align="left">
                <strong>untrusted</strong></td>
              <td align="left">only what the Connector surfaces</td>
              <td align="left">the Connector; its sandbox</td>
            </tr>
          </tbody>
        </table>
        <t>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:</t>
        <ul spacing="normal">
          <li>
            <t><strong>Well-formedness and liveness obligations</strong> — 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.</t>
          </li>
          <li>
            <t><strong>Obligations that protect an Agent from its own inbox</strong> — the
untrusted-content posture of <xref target="sec-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.</t>
          </li>
        </ul>
        <t>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;
<xref target="sec-4-1"/>'s ingest-by-copy rule removes that dependency, leaving
the obligation as pure well-formedness.</t>
      </section>
      <section anchor="sec-3-2">
        <name>The load-bearing invariant</name>
        <ul empty="true">
          <li>
            <t><strong>Invariant.</strong> 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.</t>
          </li>
        </ul>
        <t>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 (<xref target="sec-4-1"/>); why the Runtime re-derives every path from data it
controls rather than from a file's contents (<xref target="sec-4-1"/>); why recipient
policy is evaluated Runtime-side over the exact recipient set that will be
transmitted to (<xref target="sec-5-1"/>); why the digest an Agent supplies for its own
attachment bytes is an integrity aid and not a trust boundary
(<xref target="sec-5-1"/>); and why the absence of bytes, rather than a flag inside a
document, is the access gate on inbound attachments (<xref target="sec-7-2"/>).</t>
        <t>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. <xref target="sec-11-14"/> states which properties are
wire-verifiable and which are Runtime obligations that a green conformance
run does not prove.</t>
      </section>
      <section anchor="sec-3-3">
        <name>Why a filesystem namespace</name>
        <t>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.</t>
        <t><strong>Writing a file carries no capability.</strong> 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.</t>
        <t><strong>Isolation is structural rather than enforced by policy.</strong> A Connector
scoped to <tt>&lt;volume-root&gt;/&lt;agent-id&gt;/</tt> cannot name another Agent's Namespace
at all. Attribution follows from <em>where a file appeared</em>, not from what the
file says about itself, so spoofing attribution requires defeating the mount
rather than lying in a JSON member.</t>
        <t><strong>The medium's costs are real and are paid explicitly.</strong> 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. <xref target="sec-4-1"/>
states the path, open, copy, and concurrency discipline a Runtime observes
in exchange for the medium's advantages, and <xref target="sec-11"/> revisits each as a
threat.</t>
      </section>
      <section anchor="sec-3-4">
        <name>Relationship to Other Agent Protocols (Informative)</name>
        <t><em>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.</em></t>
        <t>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.</t>
        <t><strong>Model Context Protocol (MCP)</strong> <xref target="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
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.</t>
        <t><strong>Agent2Agent (A2A)</strong> <xref target="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.</t>
        <t><strong>The two protocols abbreviated "ACP" are not the same protocol</strong>, and the
collision is worth disambiguating in a document a reviewer may arrive at
with one of them in mind.</t>
        <ul spacing="normal">
          <li>
            <t>The <strong>Agent Communication Protocol</strong> (IBM/BeeAI) is of historical interest
here: it merged into A2A under the Linux Foundation, announced in August
2025, with its repository archived <xref target="ACP-MERGER"/>.</t>
          </li>
          <li>
            <t>The <strong>Agent Client Protocol</strong> <xref target="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.</t>
          </li>
        </ul>
      </section>
      <section anchor="sec-3-5">
        <name>Runtimes Built from Several Components (Informative)</name>
        <t><em>This subsection is informative. It contains no requirements.</em></t>
        <t>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
<xref target="RFC5321"/> between hosts or LMTP <xref target="RFC2033"/> within one; this document
specifies only the Agent boundary.</t>
        <t>Some existing obligations are easy to lose when the work is divided:</t>
        <ul spacing="normal">
          <li>
            <t>exactly one Result per consumed request, including when an inner
component fails or never answers (<xref target="sec-6-1"/>);</t>
          </li>
          <li>
            <t><tt>accepted</tt> only once the provider has accepted the message for relay
(<xref target="sec-6-1"/>);</t>
          </li>
          <li>
            <t>the recipient set handed to the provider is exactly the set on which
recipient policy was evaluated (<xref target="sec-5"/>);</t>
          </li>
          <li>
            <t>nothing quarantined or unverified becomes a Notice (<xref target="sec-7"/>), and
<tt>clean</tt> is asserted only for bytes the publishing component has verified,
with every <tt>content_ref</tt> bound to the Notice it sits in (<xref target="sec-7-2"/>);</t>
          </li>
          <li>
            <t>attribution is carried between components by Runtime configuration,
never by anything the Agent wrote (<xref target="sec-4-1"/>); and</t>
          </li>
          <li>
            <t>only the component facing the Agent's Namespace writes the Agent's
lanes.</t>
          </li>
        </ul>
      </section>
    </section>
    <section anchor="sec-4">
      <name>Volume Layout</name>
      <t>The Runtime owns the Volume and mounts each Agent's Namespace into that
Agent's sandbox. A Connector has access <strong>only</strong> within its own
<tt>&lt;agent-id&gt;/</tt>, and only to the paths below. Within that Namespace, write
access may be narrower still: the <tt>inbound/</tt> subtree is Runtime-owned and MAY
be mounted read-only.</t>
      <t>The Volume root's own name is a <strong>deployment choice and is not part of this
protocol</strong> — nothing derives from it, and no implementation may depend on
it. <tt>&lt;volume-root&gt;/</tt> below stands for whatever a deployment calls it.</t>
      <figure anchor="ex-volume-layout">
        <name>Volume layout (illustrative — not a fixture)</name>
        <sourcecode type="text"><![CDATA[
<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
]]></sourcecode>
      </figure>
      <t>There is exactly one inbound Sidecar directory —
<tt>inbound/notices/&lt;notice-id&gt;.attachments/</tt> — and both a Deliver Notice's and
an Inbound Message's <tt>content_ref</tt> resolve there. No second, message-specific
Sidecar directory exists.</t>
      <section anchor="sec-4-1">
        <name>Obligations</name>
        <t><strong>Attribution by Namespace.</strong> <tt>&lt;agent-id&gt;</tt> is assigned by the Runtime, not
declared by the Agent. Attribution is <em>the Namespace a file arrived in</em>; an
<tt>agent_id</tt> member inside the file is a cross-check only. A Runtime MUST reject a Submit Request whose
<tt>agent_id</tt>, 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
<tt>&lt;agent-id&gt;</tt> 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 <tt>agent_id</tt> unchecked.</t>
        <t><strong>Isolation is structural.</strong> A Connector scoped to <tt>&lt;agent-id&gt;/</tt> cannot read
another Agent's drafts or Notices.</t>
        <t><strong>All writes are atomic.</strong> Every artifact in the tree above is published by
writing a <tt>.tmp</tt> sibling and then renaming it into place (<tt>os.replace</tt> or the
equivalent). Readers MUST ignore <tt>*.tmp</tt> entries.</t>
        <t><strong>Single writer, single drainer.</strong> At any moment a Namespace has exactly one
Connector instance writing <tt>outbound/</tt> and exactly one Runtime instance
draining it. Every concurrency-sensitive rule in this document — the atomic
<tt>.tmp</tt>-then-rename discipline above, the commit sentinel below, and the
derivation of <tt>req_id</tt> from <tt>max(pending, processed, results) + 1</tt>
(<xref target="sec-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.</t>
        <t><strong>Filename identifiers are the authority for paths.</strong> An identifier taken
from a file's <em>contents</em> — a <tt>req_id</tt> 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.</t>
        <t><strong>Write-side path discipline.</strong> Every write, rename, directory creation, or
archive move into or within a Namespace MUST satisfy all three of the
following <strong>conjunctively</strong>. Each closes a distinct gap the other two leave
open, and none is sufficient alone.</t>
        <ul spacing="normal">
          <li>
            <t><strong>(a) Full parent-chain pinning.</strong> The write resolves its full parent
chain through pinned directory descriptors from the Namespace root —
<tt>openat</tt>, <tt>mkdirat</tt>, <tt>renameat</tt> 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 <em>final</em> 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.</t>
          </li>
          <li>
            <t><strong>(b) No-follow while building the chain.</strong> Each directory component that
goes into that parent chain MUST itself have been opened with
<tt>O_NOFOLLOW|O_DIRECTORY</tt> 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 <em>acquiring</em> 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.</t>
          </li>
          <li>
            <t><strong>(c) Non-following final component.</strong> The final path component MUST NOT
be followed if a symlink exists at that name: the open MUST carry
<tt>O_NOFOLLOW</tt> and MUST fail closed on <tt>ELOOP</tt> rather than writing through
whatever the name currently resolves to. Two cases differ in what else
they require, and both are covered:
            </t>
            <ul spacing="normal">
              <li>
                <t><strong>A newly created file</strong> — the <tt>.tmp</tt> sibling the atomicity rule above
already requires, and any Sidecar blob — MUST additionally be created
with <tt>O_EXCL</tt> (<tt>O_WRONLY|O_CREAT|O_EXCL</tt>, never <tt>O_TRUNC</tt> in place of
<tt>O_EXCL</tt>), failing closed on <tt>EEXIST</tt>.</t>
              </li>
              <li>
                <t><strong>An append-mode or directory-creating write that has no <tt>.tmp</tt>
sibling</strong> — <tt>audit/log.jsonl</tt> is the case this document names — cannot
use <tt>O_EXCL</tt>, because the target legitimately already exists. It MUST
instead open with <tt>O_NOFOLLOW|O_APPEND|O_NONBLOCK</tt> (or use <tt>mkdirat</tt>
with the parent pinned per (b)), MUST NOT block on a special file, and
MUST verify through the resulting descriptor (<tt>fstat</tt>, never a pre-open
<tt>stat</tt>) 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: <tt>O_NOFOLLOW</tt> does not
apply to a FIFO — a FIFO is not a symlink — and opening one for writing
blocks until a reader appears, so without <tt>O_NONBLOCK</tt> the <tt>fstat</tt> is
never reached and an Agent that plants a FIFO at this name stalls the
drain indefinitely. The first branch's <tt>O_EXCL</tt> refuses that case on its
own; the second branch has no <tt>O_EXCL</tt> to lean on and states it
explicitly. Requiring <tt>O_EXCL</tt> 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 <tt>.tmp</tt> sibling
unprotected.</t>
              </li>
            </ul>
            <t>
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. <tt>req_id</tt>
is Agent-chosen, and every Runtime-written filename in the layout tree
above — <tt>results/&lt;id&gt;.json</tt>, the <tt>.tmp</tt> sibling of <tt>processed/req-&lt;id&gt;.json</tt>,
<tt>inbound/notices/notice-&lt;id&gt;.json</tt>, <tt>inbound/messages/notice-&lt;id&gt;.json</tt>,
<tt>audit/log.jsonl</tt> — 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).  </t>
            <t>
This discipline governs every Runtime-side write named in the layout tree,
not only the initial consumption of an Agent-authored artifact.</t>
          </li>
        </ul>
        <t><strong>Read-side discipline.</strong> The same rule applied to reads. When the Runtime
opens any path the Agent can influence — every <tt>req-&lt;id&gt;.json</tt>, every
Sidecar ordinal, and, in a deployment that leaves <tt>inbound/</tt> writable,
anything it later reads back from there — it MUST open without following
symbolic links at any component (per-component <tt>O_NOFOLLOW</tt>, 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 (<tt>fstat</tt> and an <tt>S_ISREG</tt> test — never a
pre-open <tt>stat</tt>, 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 <tt>too_large</tt> value in
the Result vocabulary (<xref target="sec-6-1"/>) presumes that cap: enforcing one is an
obligation, not an option.</t>
        <t>A Connector's one Agent-influenceable read path — resolving a <tt>notice_id</tt> to
<tt>inbound/messages/notice-&lt;id&gt;.json</tt> — MUST be built only from a
pattern-validated <tt>notice_id</tt> (the schemas pin the pattern) and MUST confine
resolution to the Connector's own <tt>inbound/</tt> tree (<xref target="sec-7-3"/>).</t>
        <t><strong>Sidecar ordinal binding.</strong> 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 <tt>filename</tt> is
display-only, and MUST NOT be used as a path.</t>
        <t><strong>Commit sentinel.</strong> All Sidecar bytes are written — <tt>.tmp</tt> sibling, then
rename — <strong>before</strong> <tt>req-&lt;id&gt;.json</tt>; 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.</t>
        <t><strong>Inbound Sidecar directories are keyed on the Runtime-minted notice
identifier.</strong> A Runtime MUST NOT name one after the provider <tt>message.id</tt>. 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: <tt>inbound/messages/notice-&lt;id&gt;.json</tt> MUST be keyed on the
notice identifier, and MUST NOT be keyed on <tt>message.id</tt>. Inbound bytes are published only
at <tt>inbound/notices/&lt;notice-id&gt;.attachments/&lt;index&gt;</tt>, the single Sidecar
directory named in the layout tree.</t>
        <t><strong>Runtime-private Staging lives outside the mounted Volume</strong> — at a path the
Agent's mount cannot reach at all.</t>
        <t><strong>Ingest by copy.</strong> Before validating, evaluating policy on, or composing
from any Agent-authored artifact — <tt>req-&lt;id&gt;.json</tt> 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 <tt>sha256</tt> 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.</t>
        <t>This obligation is what makes the Connector's "MUST NOT delete or rewrite a
published request" (<xref target="sec-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 <xref target="sec-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.)</t>
        <t><strong>Byte-absence is the gate.</strong> The Runtime MUST NOT place non-clean bytes in
an Agent's Namespace. <tt>content_ref</tt> is the Runtime's access grant, present
only for bytes it has published as clean (<xref target="sec-7-2"/>).</t>
        <t><strong>Tree rules.</strong> Every Runtime-owned inbound-class tree is an <strong>origin
class</strong>: 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. <tt>notice_id</tt> 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.
<tt>content_ref</tt> resolves only within the <tt>notices/</tt> 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. <tt>inbound/</tt> is the one tree the core protocol defines; a
profile, such as the peer-origin profile of <xref target="sec-peer"/>, MAY
define others.</t>
        <t><strong>Inbound is Runtime-owned, and read-only is a valid posture.</strong> The
<tt>inbound/</tt> 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 <tt>inbound/</tt>:
it MUST start, relay Notices, and resolve bodies when every write, rename,
and directory creation under <tt>inbound/</tt> fails. <tt>inbound/notices/processed/</tt>
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.</t>
        <t><strong>Consumption tracking is Connector-private.</strong> 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 <tt>outbound/</tt> not
enumerated in the layout tree — which MUST NOT use the <tt>req-</tt> prefix,
reserved for Submit Requests and their Sidecars, so that a Runtime's scan of
<tt>outbound/</tt> 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; <xref target="app-i"/> records the
reasoning.</t>
        <t><strong>Outbound extension space.</strong> <tt>outbound/ext/&lt;name&gt;/</tt> is a
Connector-owned side channel, one directory per <tt>&lt;name&gt;</tt>, that a Runtime MAY
read by out-of-band agreement. <strong><tt>&lt;name&gt;</tt> 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 <tt>&lt;name&gt;</tt> as opaque — it is an identifier, never a
name to be parsed, resolved, or matched against a repository or path.</strong>
Because it is also a path segment in an Agent-writable tree, <tt>&lt;name&gt;</tt> MUST
be a single path segment of 1 to 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
<tt>req-</tt> prefix the Runtime scans, and nothing security-relevant may be built
on it. Its first use is a Connector's per-Notice <strong>outcome</strong> 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
(<xref target="sec-11-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.</t>
      </section>
      <section anchor="sec-4-2">
        <name>Deletion rights</name>
        <t>This table names who may delete each artifact. It is <strong>permissive</strong>: 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.</t>
        <table>
          <thead>
            <tr>
              <th align="left">Artifact</th>
              <th align="left">Written by</th>
              <th align="left">May be deleted by</th>
              <th align="left">Basis</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td align="left">
                <tt>outbound/req-&lt;id&gt;.json</tt> (pending) and <tt>req-&lt;id&gt;.attachments/</tt></td>
              <td align="left">Connector</td>
              <td align="left">Runtime</td>
              <td align="left">Consumption — the move to <tt>processed/</tt>, or 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.</td>
            </tr>
            <tr>
              <td align="left">
                <tt>outbound/processed/req-&lt;id&gt;.json</tt></td>
              <td align="left">Runtime</td>
              <td align="left">Runtime at will; Connector after reading the matching Result</td>
              <td align="left">The Runtime's own archive. <strong>Caveat:</strong> <xref target="sec-5-1"/> derives <tt>req_id</tt> from <tt>max(pending, processed, results) + 1</tt>, so a Connector that deletes here (or in <tt>results/</tt>) MUST preserve <tt>req_id</tt> monotonicity by other durable means, such as its own counter. Correspondingly, a Runtime MUST NOT rely on <tt>processed/</tt> contents as durable state (for deduplication, say) — the Connector may delete there, and the Runtime's own record lives outside this protocol.</td>
            </tr>
            <tr>
              <td align="left">
                <tt>outbound/results/&lt;id&gt;.json</tt></td>
              <td align="left">Runtime</td>
              <td align="left">Connector, once read; Runtime by retention</td>
              <td align="left">No signal in this protocol tells the Runtime that a 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.</td>
            </tr>
            <tr>
              <td align="left">
                <tt>inbound/notices/notice-&lt;id&gt;.json</tt>, <tt>inbound/messages/notice-&lt;id&gt;.json</tt>, and their <tt>.attachments/</tt> Sidecar directories</td>
              <td align="left">Runtime</td>
              <td align="left">Runtime only. Where a deployment leaves inbound writable, the Connector MAY move or delete per the optional <tt>processed/</tt> convention, but MUST NOT depend on the ability.</td>
              <td align="left">Runtime retention policy. Absent a seam-visible consumption signal (<xref target="app-i"/>), retention MUST be conservative and age-based. A Connector already tolerates the disappearance of these artifacts: a read against a missing one returns "not resolved" (<xref target="sec-7-3"/>).</td>
            </tr>
            <tr>
              <td align="left">
                <tt>peer/</tt> notices, messages and their <tt>.attachments/</tt> Sidecar directories (peer-origin profile)</td>
              <td align="left">Runtime</td>
              <td align="left">Runtime only, as for the inbound row above.</td>
              <td align="left">The same retention rule as the inbound row (<xref target="sec-peer-presentation"/>).</td>
            </tr>
            <tr>
              <td align="left">
                <tt>inbound/cursor.json</tt></td>
              <td align="left">Runtime</td>
              <td align="left">Runtime</td>
              <td align="left">Internal state.</td>
            </tr>
            <tr>
              <td align="left">
                <tt>audit/log.jsonl</tt></td>
              <td align="left">Runtime</td>
              <td align="left">Runtime (rotation)</td>
              <td align="left">The Connector MUST NOT delete it.</td>
            </tr>
            <tr>
              <td align="left">
                <tt>identity.json</tt></td>
              <td align="left">Runtime or provisioner</td>
              <td align="left">Runtime or provisioner</td>
              <td align="left">Consumers already tolerate its absence (<xref target="sec-9"/>).</td>
            </tr>
            <tr>
              <td align="left">
                <tt>outbound/ext/&lt;name&gt;/</tt></td>
              <td align="left">Connector</td>
              <td align="left">Connector, at will; Runtime, only what its out-of-band agreement says it consumes (an outcome it has read, for example)</td>
              <td align="left">A Connector-owned side channel (<xref target="sec-4-1"/>). A Runtime MUST NOT delete the directory itself or anything else under it, and an operator reset is not consumption.</td>
            </tr>
            <tr>
              <td align="left">
                <tt>directory.json</tt></td>
              <td align="left">Runtime</td>
              <td align="left">Runtime</td>
              <td align="left">Advisory and Runtime-published (<xref target="sec-directory"/>). It is not read back as authorisation, so deleting or replacing it carries no state.</td>
            </tr>
          </tbody>
        </table>
      </section>
    </section>
    <section anchor="sec-5">
      <name>Outbound: Submit Request</name>
      <t>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.</t>
      <t>A Submit Request is written to <tt>&lt;volume-root&gt;/&lt;agent-id&gt;/outbound/req-&lt;id&gt;.json</tt>
as a JSON text <xref target="RFC8259"/>. Its Sidecar bytes, if any, are written first,
into <tt>req-&lt;id&gt;.attachments/</tt>, one blob per Descriptor, named by that
Descriptor's 0-based array index (<xref target="sec-4-1"/>, commit sentinel).</t>
      <figure anchor="ex-submit-request">
        <name>draft/examples/submit-request.json (illustrative — not a fixture)</name>
        <sourcecode type="json" name="submit-request.json"><![CDATA[
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"
      }
    ]
  }
}
]]></sourcecode>
      </figure>
      <t>The normative shape is <xref target="app-a"/>. The Submit Request envelope is
<strong>closed</strong>: <tt>additionalProperties</tt> is <tt>false</tt> at every level, and a Runtime
MUST reject a request carrying any unrecognized member. <xref target="sec-10-1"/>
explains why this direction is closed while the Runtime-authored artifacts
are open.</t>
      <section anchor="sec-5-1">
        <name>Field semantics and obligations</name>
        <ul spacing="normal">
          <li>
            <t><strong><tt>contract_version</tt></strong> (REQUIRED) is the wire major version, the string
<tt>"2"</tt>. A Runtime MUST check it before schema
validation and MUST fail closed on an absent, lower, or higher major
(<xref target="sec-10-1"/>).</t>
          </li>
          <li>
            <t><strong><tt>req_id</tt></strong> (REQUIRED) is <strong>monotonic per Namespace</strong> and MUST NOT be
reused for the life of the Namespace's <tt>outbound/</tt> tree; derive it from
<tt>max(pending, processed, results) + 1</tt>. Reuse collides message identifiers
and triggers provider-side idempotency deduplication. <tt>req_id</tt> 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 (<xref target="sec-4-1"/>).</t>
          </li>
          <li>
            <t><strong><tt>agent_id</tt></strong> (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 (<xref target="sec-4-1"/>).</t>
          </li>
          <li>
            <t><strong><tt>ts</tt></strong> (OPTIONAL) is the Connector's timestamp for the request. It is
Agent-authored and therefore advisory.</t>
          </li>
          <li>
            <t><strong><tt>in_reply_to</tt></strong> (OPTIONAL) correlates this request with prior inbound
work. It carries either a Runtime-minted <tt>notice_id</tt> or a mail message
identifier <xref target="RFC5322"/>. It is a <strong>hint from an untrusted party</strong> and MUST
NOT by itself widen what the request is permitted to do — the same
posture <xref target="sec-8"/> fixes for <tt>task_id</tt>.</t>
          </li>
          <li>
            <t>The Runtime derives the message identity and the <tt>From</tt>/<tt>Reply-To</tt> of the
composed message itself; identity is Runtime-owned, and the Agent does
<strong>not</strong> choose its sending identity.</t>
          </li>
          <li>
            <t><strong><tt>draft.to</tt></strong> is REQUIRED and carries at least one
recipient; <strong><tt>draft.cc</tt></strong> is OPTIONAL and MAY be empty.
<strong><tt>draft.subject</tt></strong> and <strong><tt>draft.body_text</tt></strong> are REQUIRED and MAY be
empty strings. <tt>body_text</tt> is <strong>plain text</strong>; this document defines no
alternative body representation.</t>
          </li>
          <li>
            <t><strong><tt>draft.reply_to_message_id</tt></strong> (OPTIONAL, nullable) names the message
this draft replies to, as an <xref target="RFC5322"/> message identifier. It is
composed directly into an <tt>In-Reply-To:</tt> header, which is why it carries a
control-character pattern in the schema (below).</t>
          </li>
          <li>
            <t><strong>Recipient policy is a Runtime decision.</strong> The Agent's chosen <tt>draft.to</tt>
and <tt>draft.cc</tt> are <strong>not</strong> 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.</t>
          </li>
          <li>
            <t><strong>Recipient syntax and header-injection discipline.</strong> Each <tt>draft.to</tt> and
<tt>draft.cc</tt> 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 <xref target="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 <tt>rejected</tt> 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 <tt>draft.to</tt> or <tt>draft.cc</tt>.  </t>
            <t>
Separately, a Runtime MUST NOT allow any Agent-authored string —
<tt>subject</tt>, a recipient entry, an attachment <tt>filename</tt>, an attachment
<tt>media_type</tt>, <tt>draft.reply_to_message_id</tt>, <tt>in_reply_to</tt>, <tt>agent_id</tt>,
<tt>ts</tt>, or <tt>body_text</tt> where it abuts headers — to introduce a header
boundary in the composed message. CR, LF, and NUL MUST be rejected or
stripped before composition <strong>even where schema validation was bypassed
upstream</strong>: the schema-level control-character patterns on <tt>subject</tt>,
<tt>filename</tt>, <tt>media_type</tt>, <tt>reply_to_message_id</tt>, <tt>in_reply_to</tt>,
<tt>agent_id</tt>, and <tt>ts</tt> are the first line of defense, not the only one. Two
of those fields are easily overlooked: <tt>reply_to_message_id</tt> and <tt>media_type</tt> are composed
<em>directly into headers</em> — <tt>In-Reply-To:</tt> and a MIME part's <tt>Content-Type:</tt>
              <xref target="RFC2045"/> respectively — rather than into a body or a display field.</t>
          </li>
          <li>
            <t><strong><tt>draft.attachments[]</tt></strong> (OPTIONAL) is an array of Descriptors, each
carrying <tt>filename</tt>, <tt>media_type</tt>, <tt>size_bytes</tt>, and <tt>sha256</tt>.
<strong><tt>size_bytes</tt> and <tt>sha256</tt> are REQUIRED on every Descriptor</strong>, computed
by the Connector over the <strong>decoded</strong> bytes it wrote, with <tt>sha256</tt> in
lowercase hexadecimal.  </t>
            <t>
There is <strong>no outbound <tt>content_ref</tt></strong>. The Runtime re-derives each
Sidecar's path from <tt>req-&lt;id&gt;</tt> 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 <tt>additionalProperties: false</tt> rejects it. <tt>attachments: []</tt> is valid
and is equivalent to the member being absent; the Sidecar directory MUST
then be absent.  </t>
            <t><tt>sha256</tt> here is an integrity and deduplication aid, <strong>not an outbound
trust boundary</strong>: 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.</t>
          </li>
        </ul>
      </section>
    </section>
    <section anchor="sec-6">
      <name>Outbound: Result</name>
      <t>A Result is the Runtime's verdict for one consumed Submit Request, written
to <tt>&lt;volume-root&gt;/&lt;agent-id&gt;/outbound/results/&lt;id&gt;.json</tt>.</t>
      <figure anchor="ex-result">
        <name>draft/examples/result.json (illustrative — not a fixture)</name>
        <sourcecode type="json" name="result.json"><![CDATA[
{
  "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 }
  ]
}
]]></sourcecode>
      </figure>
      <t>The normative shape is <xref target="app-b"/>. The Result envelope is <strong>open</strong>: a
consumer MUST tolerate members it does not recognize, at every level
(<xref target="sec-10-1"/>).</t>
      <section anchor="sec-6-1">
        <name>Field semantics and obligations</name>
        <ul spacing="normal">
          <li>
            <t><strong>The Runtime writes exactly one Result per Submit Request it consumes.</strong>
A request the Runtime never consumes gets no Result, and no signal in this
protocol distinguishes "not yet" from "never."</t>
          </li>
          <li>
            <t><strong><tt>outcome</tt></strong> (REQUIRED) is one of <tt>accepted</tt>, <tt>queued_for_human</tt>, or
<tt>rejected</tt>. This is a <strong>closed vocabulary</strong>: 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
(<xref target="sec-10-1"/>).</t>
          </li>
          <li>
            <t><strong>Any policy miss — allowlist, rate limit, DLP hold, halt — MUST produce
<tt>queued_for_human</tt>.</strong> A Runtime MUST NOT send such a request, and MUST NOT
drop it without a Result.</t>
          </li>
          <li>
            <t><strong><tt>accepted</tt></strong> 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.</t>
          </li>
          <li>
            <t><strong><tt>detail</tt></strong> (OPTIONAL, nullable) is human-readable text. It carries no
machine semantics.</t>
          </li>
          <li>
            <t><strong><tt>reason_code</tt></strong> (OPTIONAL) records <em>which policy path</em> authorized or
refused the request; human text stays in <tt>detail</tt>. A Runtime <strong>SHOULD</strong>
set a reason on every Result, successes included. The suggested
vocabulary — <tt>recipient_bound</tt>, <tt>allowlisted</tt>, <tt>no_verified_counterparty</tt>,
<tt>recipient_not_allowlisted</tt>, <tt>dlp_hold</tt>, <tt>no_screener</tt>, <tt>halted</tt>,
<tt>rate_limited</tt>, <tt>unsupported_contract_version</tt> — is <strong>extensible and not
enum-locked in the schema</strong>. Two consumer obligations follow: a consumer
MUST tolerate a <tt>reason_code</tt> it does not recognize, treating it as opaque
and never as an error, and a consumer MUST NOT infer the <tt>outcome</tt> from
it. <tt>outcome</tt> is the sole authoritative verdict; <tt>reason_code</tt> only
explains which path produced it, and a Runtime may add codes at any time
without a protocol change. <tt>unsupported_contract_version</tt> is the code a
Runtime sets on the <tt>rejected</tt> Result it writes for a version-refused
Submit Request (<xref target="sec-10-1"/>).</t>
          </li>
          <li>
            <t><strong><tt>ts</tt></strong> (OPTIONAL) is the Runtime-set, of-record time of the verdict, in
ISO-8601 form.</t>
          </li>
          <li>
            <t><strong><tt>recipients</tt></strong> (OPTIONAL) records the recipient set the Runtime
actually transmitted to.</t>
          </li>
          <li>
            <t><strong><tt>message_id</tt></strong> (OPTIONAL, nullable) is the <xref target="RFC5322"/> message
identifier the Runtime assigned to the composed message.</t>
          </li>
          <li>
            <t><strong><tt>job_id</tt></strong> (OPTIONAL, nullable) is a provider-side job identifier,
opaque to this protocol.</t>
          </li>
          <li>
            <t><strong><tt>attestation</tt></strong> (OPTIONAL) is the identity stamp the Runtime applied to
the outbound message. Its <tt>form</tt> is <tt>self-stamp</tt> or <tt>verifiable</tt>,
distinguishing a stamp a stranger cannot verify from one that can be
verified independently. A Connector <em>surfaces</em> an attestation, and does not
verify it; verification is a Runtime concern.</t>
          </li>
          <li>
            <t><strong><tt>attachments[]</tt></strong> (OPTIONAL) carries one diagnostic entry per submitted
attachment: <tt>index</tt>, an <tt>outcome</tt> of <tt>accepted</tt>, <tt>dlp_held</tt>,
<tt>rejected_type</tt>, or <tt>too_large</tt>, and an optional <tt>detail</tt>. <strong><tt>index</tt> MUST
equal the entry's own position in the array</strong> — contiguous, unique, and
covering <tt>0</tt> through <tt>n-1</tt> exactly once. Array position is the binding
authority for attachments in both directions (<xref target="sec-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
(<xref target="app-h"/>).  </t>
            <t><strong>Two paths, kept apart.</strong> A <em>malformed</em> 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 <tt>rejected</tt>.
A <em>policy hold</em> — DLP, a disallowed type, a size cap, including a
fail-closed hold when no screener is available — is <tt>queued_for_human</tt>.
Per-attachment outcomes are diagnostic detail under <strong>one</strong> governing
top-level outcome, and a Runtime MUST NOT send part of a request on their
strength.</t>
          </li>
        </ul>
      </section>
    </section>
    <section anchor="sec-7">
      <name>Inbound: Deliver Notice and Inbound Message</name>
      <t>The Runtime writes one Deliver Notice per <strong>delivered</strong> message. Quarantined
or unverified mail produces <strong>no Notice</strong>: a Runtime MUST NOT write a Notice
for it, and logs it on its own side instead. The Notice is a <em>pointer and summary</em>; the body is
fetched on demand from a separate spooled artifact (<xref target="sec-7-3"/>) and is
<strong>untrusted content</strong>.</t>
      <figure anchor="ex-deliver-notice">
        <name>draft/examples/deliver-notice.json (illustrative — not a fixture)</name>
        <sourcecode type="json" name="deliver-notice.json"><![CDATA[
{
  "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"
      }
    ]
  }
}
]]></sourcecode>
      </figure>
      <t>The normative shape is <xref target="app-c"/>. The Deliver Notice envelope is <strong>open</strong>:
a consumer MUST tolerate members it does not recognize, at every level
(<xref target="sec-10-1"/>).</t>
      <section anchor="sec-7-1">
        <name>Field semantics and obligations</name>
        <t><strong>The drop rule.</strong> A Connector relays only <tt>deliver</tt> Notices. It MUST drop —
not surface — a Notice that fails <strong>either</strong> of two independent checks,
treating a failure of either one alone as sufficient grounds to drop:</t>
        <ol spacing="normal" type="1"><li>
            <t><strong>Schema failure.</strong> The Notice fails validation against the
deliver-notice schema (<xref target="app-c"/>) for <strong>any reason other than an
unrecognized member</strong>. That single formulation covers a <tt>kind</tt> other than
<tt>deliver</tt>, a missing required member, an enum violation on a closed
vocabulary, a pattern violation (including a traversal or character-set
violation in <tt>notice_id</tt>, or a shape violation in <tt>content_ref</tt>'s own
pattern), a conditional (<tt>allOf</tt> / <tt>if</tt>-<tt>then</tt>) violation, and a type
violation alike, with no need to enumerate schema-failure categories
separately. <strong>A Connector MUST NOT drop a Notice because it carries an unrecognized member</strong>
(<xref target="sec-10-1"/>); that is the one carve-out from "fails schema
validation."</t>
          </li>
          <li>
            <t><strong>The <tt>content_ref</tt> index and prefix binding.</strong> A Notice can pass check 1
— every member individually well-shaped — and still carry an attachment
Descriptor whose <tt>content_ref</tt> does not equal
<tt>&lt;notice_id&gt;.attachments/&lt;i&gt;</tt> for its own 0-based array position <tt>i</tt>
(<xref target="sec-7-2"/>): a mismatched trailing index, or a leading <tt>notice_id</tt>
prefix pointing into a <em>different</em> 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.</t>
          </li>
        </ol>
        <t><strong>This is not a closure over schema validation.</strong> Check 2 binds two members
of the same document — a Descriptor's <tt>content_ref</tt> against the enclosing
document's own <tt>notice_id</tt> and against the Descriptor's own array position —
against each other, which is not expressible in the JSON Schema vocabulary
these schemas use <xref target="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
(<xref target="app-h"/>).</t>
        <t>A <strong>version refusal</strong> — an absent or non-matching <tt>contract_version</tt> major —
is explicitly <strong>not</strong> part of this drop path. It is handled first and
separately (<xref target="sec-10-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.</t>
        <t>A document with <tt>kind: "peer"</tt> found under <tt>inbound/notices/</tt> fails check 1
against this schema, and is dropped the same way (<xref target="app-f"/>, the
peer-origin profile's cross-lock).</t>
        <t><strong>Field obligations.</strong></t>
        <ul spacing="normal">
          <li>
            <t><strong><tt>contract_version</tt></strong>, <strong><tt>notice_id</tt></strong>, <strong><tt>ts</tt></strong>, <strong><tt>kind</tt></strong>, and
<strong><tt>message</tt></strong> are REQUIRED. <tt>notice_id</tt> is Runtime-minted and is pinned by
the schema to a character set that is safe as a path component and as a
<tt>content_ref</tt> prefix.</t>
          </li>
          <li>
            <t>Required members of <tt>message</tt> are <tt>id</tt>, <tt>from</tt>, <tt>subject</tt>, <tt>preview</tt>, and
<tt>mailbox</tt>. The rest is enrichment (<xref target="sec-10-2"/>).</t>
          </li>
          <li>
            <t><strong><tt>message.id</tt></strong> is the provider's message identifier. It is <strong>external
and not character-set-safe</strong>, and MUST NOT be used as a path component
anywhere (<xref target="sec-4-1"/>). It is the mail store's identifier for the
message (an IMAP UID, for example), not the <xref target="RFC5322"/> Message-ID; an
Agent replies to a delivered message by naming its notice identifier in
<tt>in_reply_to</tt> (<xref target="sec-5"/>).</t>
          </li>
          <li>
            <t><strong><tt>message.from</tt> is attacker-controlled display data.</strong> Its display-name
half is chosen freely by the sender — <tt>"security@yourbank.example
&lt;attacker@evil.example&gt;"</tt> 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 <tt>sender_standing</tt> and <tt>verdict</tt>; it is not the <tt>from</tt>
string itself. The one exception in this protocol is the <tt>message.from</tt> of a
Peer Notice, which the Runtime asserts (<xref target="sec-peer-addr"/>).</t>
          </li>
          <li>
            <t><strong>Presentation obligations.</strong> 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 <tt>disposition</tt> 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
<tt>filename</tt> and <tt>media_type</tt> 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 <tt>sender_standing</tt> and <tt>verdict</tt>, named here as the locus and
not as vocabulary to repeat to a reader — and that <tt>message.from</tt> 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.</t>
          </li>
          <li>
            <t><strong>The untrusted-content posture covers every sender-originated string</strong> in
the Notice and in the spooled Inbound Message: <tt>message.from</tt>, <tt>subject</tt>,
<tt>preview</tt>, <tt>body_text</tt>, <tt>references</tt>, attachment <tt>filename</tt> and
<tt>media_type</tt>, and the Inbound Message's <tt>from</tt>, <tt>to</tt>, <tt>cc</tt>, and <tt>subject</tt>.
This is not limited to the members called out individually here.</t>
          </li>
          <li>
            <t><strong><tt>message.sender_standing</tt></strong> (OPTIONAL) is an <strong>open, advisory
vocabulary</strong> — <tt>known</tt>, <tt>agent-tier</tt>, <tt>first-contact</tt>, <tt>read-only</tt>, 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 (<xref target="sec-10-1"/>).</t>
          </li>
          <li>
            <t><strong><tt>message.provenance</tt></strong> (OPTIONAL) classifies the message source —
<tt>external</tt>, <tt>internal</tt>, and others — so that a Runtime can route
provenance-labelled screening. It is advisory metadata, like <tt>verdict</tt>, and a
Connector MUST NOT present it as license for an Agent to obey body content. It is likewise an
<strong>open, advisory vocabulary</strong>: 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.</t>
          </li>
          <li>
            <t><strong><tt>message.verdict</tt></strong> (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.</t>
          </li>
          <li>
            <t><strong><tt>message.thread_id</tt></strong>, <strong><tt>message.in_reply_to</tt></strong>, and
<strong><tt>message.references</tt></strong> (all OPTIONAL) carry provider and <xref target="RFC5322"/>
threading identity. They grant correlation, and a consumer MUST NOT treat them as
grounds for trust (<xref target="sec-8"/>).</t>
          </li>
          <li>
            <t><strong><tt>message.task_id</tt></strong> (OPTIONAL) is Runtime-minted; <xref target="sec-8"/> states the
obligations on it.</t>
          </li>
        </ul>
        <t><strong>Direction and openness.</strong> This artifact is Runtime-authored, and its
envelope is open at every level: a Connector MUST tolerate a member it does
not recognize, in <tt>message</tt> or at the top level, and MUST NOT drop the
Notice on that account. Emitting an unrecognized member remains Runtime
non-conformance (<xref target="sec-10-1"/>).</t>
      </section>
      <section anchor="sec-7-2">
        <name>Inbound attachment Descriptors</name>
        <t><strong><tt>message.attachments[]</tt></strong> (OPTIONAL) is an array of Descriptors carrying
<tt>filename</tt>, <tt>media_type</tt>, and <tt>size_bytes</tt> — all REQUIRED — plus a REQUIRED
<tt>disposition</tt>, and OPTIONAL <tt>sha256</tt> and <tt>content_ref</tt> members gated on it.</t>
        <t><strong><tt>disposition</tt> is the Runtime's assertion, not a scanner verdict.</strong> Its
values are <tt>clean</tt>, <tt>stripped</tt>, <tt>quarantined</tt>, and <tt>unscanned</tt>, and the
vocabulary is <strong>closed</strong>: 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 (<xref target="sec-10-1"/>). On what basis a Runtime asserts <tt>clean</tt> 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 <tt>sender_standing</tt> takes: the
wire fixes the vocabulary, never the computation. The Runtime is trusted by
construction (<xref target="sec-3-1"/>); the load-bearing invariant is that the <em>Agent</em>
is not.</t>
        <t>The counterpart obligation is equally plain: <strong>a Runtime that cannot make
the determination MUST NOT assert <tt>clean</tt>.</strong> It emits <tt>unscanned</tt> — or
<tt>stripped</tt> or <tt>quarantined</tt> where it made that decision — and publishes no
bytes.</t>
        <t><strong>Byte publication and the access grant.</strong> A Runtime MAY publish an
attachment's bytes at <tt>inbound/notices/&lt;notice-id&gt;.attachments/&lt;index&gt;</tt>
          <strong>only</strong> for a Descriptor it asserts <tt>disposition == "clean"</tt>, and only then
MAY it emit <tt>content_ref</tt> (a path of the form
<tt>&lt;notice-id&gt;.attachments/&lt;index&gt;</tt>) and <tt>sha256</tt>. Byte-absence remains the
gate (<xref target="sec-4-1"/>) for anything not asserted clean. Exposing <tt>sha256</tt> 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.</t>
        <t><strong>The <tt>content_ref</tt> binding.</strong> <tt>content_ref</tt>'s leading <tt>notice_id</tt> prefix
MUST equal the enclosing document's own <tt>notice_id</tt>, and its trailing
<tt>&lt;index&gt;</tt> MUST equal the Descriptor's own 0-based array index. The array
index remains the sole binding authority (<xref target="sec-4-1"/>), and a consumer that
resolves bytes MUST derive the path from the enclosing <tt>notice_id</tt> and the
index, and MUST NOT use the <tt>content_ref</tt> 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 (<xref target="app-h"/>) in the same way the
Result-side <tt>attachments[i].index == i</tt> binding is.</t>
        <t><strong>A grant, not a directive.</strong> A consumer MUST tolerate a Descriptor whose
bytes it does not resolve — relaying the Notice as usual — and MUST NOT
treat the presence of <tt>content_ref</tt> as a requirement to fetch. A Connector
that never dereferences <tt>content_ref</tt> remains fully conformant. A Connector
MUST resolve published bytes only within its own Namespace's
<tt>inbound/notices/</tt> tree and MUST reject any <tt>content_ref</tt> that does not
match the pinned pattern — the same confinement <xref target="sec-4-1"/>'s read-side
discipline requires generally.</t>
        <t><strong><tt>filename</tt> and <tt>media_type</tt> here are attacker-controlled, untrusted
display strings.</strong> 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 <tt>clean</tt> assertion
covers the bytes' screening disposition, never the truthfulness of these
strings.</t>
        <t><strong>Inbound <tt>size_bytes</tt> is provider-declared and therefore advisory and
untrusted</strong> — it comes from the provider's structure report, and is not
Runtime-verified the way outbound <tt>size_bytes</tt> is.</t>
        <t><strong>MIME edges.</strong> <tt>filename</tt> is the <xref target="RFC2047"/> / <xref target="RFC2231"/>-<strong>decoded</strong>,
Runtime-sanitized display string. An "attachment" is any part carrying
<tt>Content-Disposition: attachment</tt> <xref target="RFC2183"/>, or any named or non-inline
text part — inline images and <tt>multipart/related</tt> parts count, which is the
conservative reading of <xref target="RFC2045"/> and <xref target="RFC2046"/>. The declared
<tt>media_type</tt> is display-only: the Runtime sniffs the bytes, and the sniffed
type governs policy on the outbound gate.</t>
      </section>
      <section anchor="sec-7-3">
        <name>The Inbound Message (body spool)</name>
        <t>The Runtime spools the delivered body at
<tt>&lt;volume-root&gt;/&lt;agent-id&gt;/inbound/messages/notice-&lt;id&gt;.json</tt>, keyed on the
Runtime-minted notice identifier — never on the provider <tt>message.id</tt>, for
the reason given in <xref target="sec-4-1"/>.</t>
        <figure anchor="ex-inbound-message">
          <name>draft/examples/inbound-message.json (illustrative — not a fixture)</name>
          <sourcecode type="json" name="inbound-message.json"><![CDATA[
{
  "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"
    }
  ]
}
]]></sourcecode>
        </figure>
        <t>The normative shape is <xref target="app-d"/>. Required members are <tt>contract_version</tt>,
<tt>notice_id</tt>, and <tt>body_text</tt>; the rest — <tt>id</tt>, <tt>date</tt>, <tt>from</tt>, <tt>to</tt>, <tt>cc</tt>,
<tt>subject</tt>, and <tt>attachments[]</tt> — is enrichment (<xref target="sec-10-2"/>). Descriptors
here follow exactly the rules of <xref target="sec-7-2"/>, including the <tt>content_ref</tt>
binding and the clean gating. This artifact's envelope is <strong>open</strong>, like the
other Runtime-authored artifacts.</t>
        <t><strong>Resolution.</strong> 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
<strong>untrusted content</strong>, the same posture as the rest of <tt>message</tt>.</t>
        <t><strong>Enforcement by absence.</strong> 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 (<xref target="sec-4-2"/>): from the Connector's side, retired
and never-spooled are indistinguishable, and both are already-specified
behavior.</t>
      </section>
    </section>
    <section anchor="sec-8">
      <name>Task Correlation</name>
      <t>The Runtime mints and owns <tt>task_id</tt>; a Connector only reads it, to
correlate a Notice with prior work. The minting and thread-resolution
<em>algorithm</em> is Runtime policy and out of scope. This protocol fixes three
security obligations on it.</t>
      <ul spacing="normal">
        <li>
          <t><strong><tt>task_id</tt> MUST be unguessable and Runtime-minted</strong> — 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 <tt>task_id</tt> lets a sender who has seen one predict
others.</t>
        </li>
        <li>
          <t><strong>Matching a <tt>task_id</tt>, by itself, MUST NOT grant a message entry into
that task.</strong> Authoritative correlation is header threading <strong>plus</strong>
per-message verified sender identity. A subaddress or header token
supplied by an inbound sender is a <em>hint</em> only, and a Runtime MUST NOT treat
it as authoritative: it is attacker-controllable, and it is stripped by many gateways.</t>
        </li>
        <li>
          <t><strong>Threading grants correlation, not trust.</strong> A message that threads onto
a known task is still identity-verified per message; it does not inherit
the task's standing.</t>
        </li>
      </ul>
    </section>
    <section anchor="sec-9">
      <name>Binding Record</name>
      <t>The Binding Record is written by the Runtime or provisioner at the
<strong>Volume root</strong> — <tt>&lt;volume-root&gt;/identity.json</tt>, a sibling of
<tt>&lt;agent-id&gt;/</tt>, not inside it — and names which mailbox this Volume is bound
to. Its normative shape is <xref target="app-e"/>.</t>
      <t>The Binding Record names which mailbox a Volume is bound to; it is informational and not consulted for enforcement.</t>
      <figure anchor="fx-identity-basic-src">
        <name>fixtures/valid/identity-basic.json</name>
        <sourcecode type="json" name="identity-basic.json"><![CDATA[
{
  "contract_version": "2",
  "agent_id": "agent-research",
  "address": "agent@example.org",
  "provisioned_ts": "2026-07-20T04:10:56Z"
}
]]></sourcecode>
      </figure>
      <section anchor="sec-9-1">
        <name>Field semantics and obligations</name>
        <ul spacing="normal">
          <li>
            <t><strong>Informational, not authoritative.</strong> 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
(<xref target="sec-4-1"/>). This record grants nothing, and a Runtime MUST NOT consult it for
enforcement.</t>
          </li>
          <li>
            <t><strong>Consumers MUST tolerate its absence</strong>, and <strong>MUST treat its contents as
untrusted data</strong>: a reader MUST NOT crash, or change its protocol behavior,
because of a malformed, oversized, or hostile file.</t>
          </li>
          <li>
            <t>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.</t>
          </li>
          <li>
            <t><strong>The file is OPTIONAL</strong> (<xref target="sec-10-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.</t>
          </li>
          <li>
            <t>Required members, if the file is present, are <tt>agent_id</tt>, <tt>address</tt>, and
<tt>provisioned_ts</tt>. <tt>label</tt> is OPTIONAL. <tt>contract_version</tt> is OPTIONAL
here, as it is on the Peer Directory (<xref target="sec-directory"/>), because
the file is not a protocol artifact a consumer must parse to function; if
present it is pinned to <tt>"2"</tt>.</t>
          </li>
          <li>
            <t>This artifact's envelope is <strong>closed</strong> (<tt>additionalProperties: false</tt>),
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.</t>
          </li>
        </ul>
      </section>
    </section>
    <section anchor="sec-directory">
      <name>The Peer Directory</name>
      <t>A Runtime MAY publish, at the Agent's Namespace root
(<tt>&lt;agent-id&gt;/directory.json</tt>, beside the lanes rather than inside one), a
per-instance projection of the authorisation graph naming which peers that
Agent may address.</t>
      <figure anchor="ex-directory">
        <name>draft/examples/directory.json (illustrative — not a fixture)</name>
        <sourcecode type="json" name="directory.json"><![CDATA[
{
  "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": []
}
]]></sourcecode>
      </figure>
      <ul spacing="normal">
        <li>
          <t><strong>Advisory, not authoritative.</strong> 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 (<xref target="sec-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.</t>
        </li>
        <li>
          <t><strong>A Runtime MUST NOT read <tt>directory.json</tt> back, for any purpose.</strong> 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.</t>
        </li>
        <li>
          <t><strong>Per-instance projection.</strong> 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
(<xref target="sec-4"/>), so an address an Agent reads is a claim rather than an identity.</t>
        </li>
        <li>
          <t><tt>router</tt> is the publishing Runtime's own address and MUST NOT appear in
either list. Every address is one bare addr-spec (<xref target="sec-5"/>).</t>
        </li>
        <li>
          <t><strong>Both lists are always present</strong>, and empty when that lane is closed for this
Agent; <tt>mail_to</tt> is empty in a delegation-only deployment. Absent and empty
MUST NOT be conflated.</t>
        </li>
        <li>
          <t><strong>Written whole and atomically.</strong> A Runtime MUST NOT publish a newly adopted
instance's address to others before it has adopted that instance itself.</t>
        </li>
        <li>
          <t><strong>A Runtime that publishes the directory MUST ensure the Agent can read it</strong>,
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.</t>
        </li>
        <li>
          <t><strong><tt>written_at</tt></strong> is the only staleness signal a consumer gets: an honest
marker, not a freshness guarantee.</t>
        </li>
        <li>
          <t><strong>Where the consumer is an Agent rather than a parser, these become
obligations to convey.</strong> 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 <xref target="sec-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 <tt>written_at</tt> is the only staleness signal
there is. A Connector with no directory parser does not thereby satisfy them
vacuously.</t>
        </li>
        <li>
          <t><strong>The file MAY be absent</strong> — 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
(<xref target="sec-9"/>).</t>
        </li>
        <li>
          <t><strong>A Runtime that cannot write it keeps routing.</strong> 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 <tt>written_at</tt> going stale.</t>
        </li>
      </ul>
    </section>
    <section anchor="sec-roster">
      <name>The Fleet Roster</name>
      <t>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
(<xref target="sec-directory"/>) answers which peers an Agent may address; the
roster answers only which instances exist. It is a separate artifact, and no
obligation of <xref target="sec-directory"/> applies to it except where this
section states one.</t>
      <ul spacing="normal">
        <li>
          <t><strong>Existence is not authorisation.</strong> 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 (<xref target="sec-6"/>) is
the only thing that reports what happened. A Connector MUST NOT treat membership
as authorisation or as a prediction of acceptance.</t>
        </li>
        <li>
          <t><strong>Not a source of identity.</strong> 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 (<xref target="sec-4"/>), and an
address read from a file is a claim rather than an identity.</t>
        </li>
        <li>
          <t><strong>Write-only from the Runtime's side.</strong> A Runtime MUST NOT read
the roster back, for any purpose, for the reasons given for the directory in
<xref target="sec-directory"/>.</t>
        </li>
        <li>
          <t><strong>Written whole and atomically.</strong> A Runtime MUST NOT list a newly
admitted instance before it has adopted that instance itself.
<tt>router</tt> is the publishing Runtime's own address and MUST NOT appear in
<tt>members</tt>. Every address is one bare addr-spec.</t>
        </li>
        <li>
          <t><strong>Unknown is not empty.</strong> <tt>members</tt> is always present.
When the Runtime cannot determine membership, it MUST NOT write an empty
<tt>members</tt> list; it leaves the previous file in place to age.
<tt>written_at</tt> is the only staleness signal a consumer gets, and
<tt>interval_s</tt>, 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 <tt>interval_s</tt> where it is present.</t>
        </li>
        <li>
          <t><strong>Readability is split between the parties that can observe it.</strong>
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.</t>
        </li>
        <li>
          <t><strong>Conveyed, where the consumer is an Agent.</strong> 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.</t>
        </li>
      </ul>
      <t>The roster's shape is pinned by its schema (<xref target="app-roster"/>)
and by the <tt>roster-*</tt> fixtures (<xref target="app-h"/>). The envelope is
Runtime-authored and therefore open (<xref target="sec-10-1"/>): a consumer
ignores members it does not recognize, and emitting one remains producer
non-conformance.</t>
    </section>
    <section anchor="sec-peer">
      <name>The Peer-Origin Profile (Optional)</name>
      <t>This section is an optional profile. It gives agent-to-agent traffic between
Agents of the same principal its own inbound tree, <tt>peer/</tt>, 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
<tt>contract_version</tt> is the same for both.</t>
      <section anchor="sec-peer-scope">
        <name>Scope</name>
        <t>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.</t>
        <t>The tree is named for what it asserts, not for what it is used for:
<tt>peer/</tt> asserts authenticated, same-principal peer origin, and the
document <tt>kind</tt> is <tt>peer</tt>. 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
<tt>kind</tt> equals the name of the tree it was read from is mechanical.</t>
        <t>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 <xref target="sec-non-goals"/>.</t>
        <t>The profile relies on two generic rules in <xref target="sec-4-1"/>: the
<em>tree rules</em> (every Runtime-owned inbound-class tree is an origin class,
derived from the tree and never from a member, with <tt>notice_id</tt>
uniqueness, <tt>content_ref</tt> confinement and write authority all per tree),
and the <em>outbound extension space</em> <tt>outbound/ext/&lt;name&gt;/</tt>,
where delivery outcomes are kept (<xref target="sec-peer-presentation"/>). There
is deliberately no inbound extension space.</t>
      </section>
      <section anchor="sec-peer-tree">
        <name>The Peer Tree</name>
        <figure anchor="ex-peer-layout">
          <name>Namespace layout with the peer tree (illustrative — not a fixture)</name>
          <sourcecode type="text"><![CDATA[
<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/
]]></sourcecode>
        </figure>
        <t><strong>A sibling of <tt>inbound/</tt>, never a child.</strong> 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
(<xref target="sec-peer-origin"/>) 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 <tt>inbound/</tt> in <xref target="sec-4"/>
applies to <tt>peer/</tt> 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.</t>
        <t><strong>Its own schema.</strong> A Peer Notice is pinned by the peer-notice
schema (<xref target="app-f"/>): the Deliver Notice shape with <tt>kind</tt>
fixed to <tt>"peer"</tt> and <tt>message.mailbox</tt> fixed to
<tt>"peer"</tt>, <tt>message.from</tt> pinned to a bare addr-spec
(<xref target="sec-peer-addr"/>), <tt>message.id</tt>,
<tt>message.in_reply_to</tt> and <tt>message.references[]</tt> pinned to the
<tt>notice_id</tt> character set (<xref target="sec-peer-corr"/>), and the
optional Runtime-asserted <tt>message.sender_exposure</tt>
(<xref target="sec-peer-exposure"/>). Attachment Descriptors are exactly those
of <xref target="sec-7-2"/>. The body spool uses the Inbound Message schema
(<xref target="app-d"/>) unchanged. Because a Peer Notice appears only in a
tree that a mail-only consumer never reads, the closed <tt>kind</tt>
vocabulary of the Deliver Notice is untouched.</t>
        <t><strong>The cross-lock.</strong> A <tt>deliver</tt> document under
<tt>peer/notices/</tt> fails the peer-notice schema, and a <tt>peer</tt>
document under <tt>inbound/notices/</tt> fails the Deliver Notice schema. Both
are dropped under the rule of <xref target="sec-7-1"/>.</t>
        <t><strong>The placer mints <tt>notice_id</tt>.</strong> The component that
places a notice into a tree mints its <tt>notice_id</tt>. 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 <tt>notice_id</tt> 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
<tt>notice_id</tt> equal to the peer message id (<xref target="sec-peer-corr"/>).
Between hosts they differ, because the receiving Runtime mints the
<tt>notice_id</tt> and the message id is the sender's. A consumer MUST NOT
assume that they coincide.</t>
        <t><strong><tt>content_ref</tt> is confined to its tree.</strong> Bytes resolve
only within the <tt>notices/</tt> directory of the tree the enclosing document
was read from, recomputed from that document's <tt>notice_id</tt> and the
Descriptor's index. A Peer Notice cannot point into the mail Sidecar
directory, or the reverse.</t>
        <t><strong>Advisory members.</strong> <tt>provenance</tt> 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.
<tt>sender_standing</tt>, if present, SHOULD be the existing
<tt>agent-tier</tt> value.</t>
      </section>
      <section anchor="sec-peer-origin">
        <name>Origin Class Follows the Tree</name>
        <t>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 (<xref target="sec-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 it (user identities, mount sets, a host boundary) is
out of scope; the property is not, and this section prescribes a check for
it.</t>
        <t><strong>Connector obligations.</strong> The tree a notice is read from is the
Runtime's sole assertion of its origin class: <tt>inbound/</tt> is mail, and
<tt>peer/</tt> 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 <tt>kind</tt> does not equal the name of the tree it
was read from is dropped under the rule of <xref target="sec-7-1"/>. A Connector
that cannot establish which tree a document came from MUST treat it as
mail.</t>
        <t>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
(<xref target="sec-peer-exposure"/>). 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 <xref target="sec-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
<xref target="sec-3-2"/> is unaffected.</t>
        <t><strong>Write-authority partition (Runtime).</strong> Write authority over
<tt>peer/</tt> MUST be distinct from write authority over <tt>inbound/</tt>.
A component that admits messages from a transport not restricted to authorized
peer senders (the <em>admitting component</em>, of which a mail deliverer is
the usual example) MUST NOT hold write authority over <tt>peer/</tt>. The
restriction MUST be structural, and MUST NOT be configuration that the
component reads. The prescribed operational check is a write attempt against
<tt>peer/</tt> from inside the admitting component, which MUST fail. The rule
is one-directional: a component holding peer write authority MAY write
<tt>inbound/</tt>, as the delivery failure notice of
<xref target="sec-peer-corr"/> requires.</t>
        <t>A Runtime MUST NOT write a notice into <tt>peer/</tt> 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 (<xref target="sec-peer-xhost"/>).</t>
        <t><strong>Verification placement (Runtime).</strong> Verification MUST be
performed by a component holding peer write authority, which receives
candidates through a location that is neither tree
(<xref target="sec-peer-intake"/>). 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.</t>
      </section>
      <section anchor="sec-peer-addr">
        <name>Addressing</name>
        <t><strong>A peer address is a bare addr-spec</strong>, under the same pattern and
exclusions that <xref target="sec-5-1"/> fixes for <tt>draft.to</tt>, 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 (<xref target="sec-peer-xhost"/>).
"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 (<xref target="sec-peer-keys"/>). 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 <tt>address</tt>,
<xref target="sec-9"/>), the deployment SHOULD use it, so that the Agent has one
identity on both lanes.</t>
        <t>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.</t>
        <t><strong>Sending needs no new wire shape.</strong> A sending Agent writes an
ordinary Submit Request (<xref target="sec-5"/>) whose <tt>draft.to</tt> 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.</t>
        <t><strong><tt>message.from</tt> on a Peer Notice is authenticated.</strong> It is
the one <tt>from</tt> 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
(<xref target="sec-peer-xhost"/>). A Runtime MUST NOT copy it from text the
sender supplied. This is the opposite of the rule for mail <tt>from</tt>
(<xref target="sec-7-1"/>). A Connector's sender check may key on it, because
the Runtime asserted it.</t>
        <t><strong>A configured peer key vouches only for its own Runtime's
domains</strong> (<xref target="sec-peer-keys"/>). Without this, one Runtime's key could
sign a message claiming an address in another Runtime's domain.</t>
        <t><strong>One address, one Namespace.</strong> 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.</t>
        <t>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
(<xref target="sec-peer-presentation"/>).</t>
        <t>"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.</t>
      </section>
      <section anchor="sec-peer-corr">
        <name>Correlation and Replies</name>
        <t><strong>The peer message id.</strong> 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
<tt>notice_id</tt> character set (<tt>^[A-Za-z0-9_][A-Za-z0-9_.-]*$</tt>).
It has exactly one spelling inside every AMMP document:
<tt>message.id</tt>, <tt>message.in_reply_to</tt> and
<tt>message.references[]</tt> on a Peer Notice, <tt>message_id</tt> 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
<tt>&lt;id@domain&gt;</tt>, where <tt>domain</tt> is the domain of the sender's
address (<xref target="sec-peer-verify"/>). The encoding is applied on the way into the header and removed on
the way out, and MUST NOT appear in an AMMP document.</t>
        <t>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 <xref target="sec-peer-stmt"/>). A mail notice's
<tt>in_reply_to</tt> remains an <xref target="RFC5322"/> identifier; the Deliver
Notice schema is unchanged.</t>
        <t><strong>A reply is a Peer Notice in the reverse direction</strong>, 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 <xref target="RFC5322"/> threading.</t>
        <table anchor="tab-peer-corr">
          <name>Correlation members on the peer lane</name>
          <thead>
            <tr>
              <th align="left">Member</th>
              <th align="left">On the peer lane</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td align="left"><tt>message.id</tt> (Peer Notice)</td>
              <td align="left">The peer message id.</td>
            </tr>
            <tr>
              <td align="left"><tt>message_id</tt> (the sender's Result)</td>
              <td align="left">MUST equal the peer message id, so that the sender learns it from its own Result. <tt>reason_code</tt> SHOULD be <tt>peer_routed</tt>, so that the sender can tell the lanes apart (an open vocabulary, <xref target="sec-6-1"/>).</td>
            </tr>
            <tr>
              <td align="left"><tt>message.in_reply_to</tt> (Peer Notice)</td>
              <td align="left">REQUIRED on a reply, and equal to the id being answered. A new task omits it.</td>
            </tr>
            <tr>
              <td align="left"><tt>message.references[]</tt> (Peer Notice)</td>
              <td align="left">Multi-step threads, as in mail.</td>
            </tr>
            <tr>
              <td align="left"><tt>in_reply_to</tt> or <tt>draft.reply_to_message_id</tt> (Submit Request)</td>
              <td align="left">How a responder names the message it answers.</td>
            </tr>
          </tbody>
        </table>
        <t><tt>task_id</tt> remains an optional, Runtime-minted thread handle
(<xref target="sec-8"/>).</t>
        <t><strong>Reverse edges.</strong> 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
<xref target="sec-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.</t>
        <t><strong>An unresolved reply key is refused, not stripped.</strong> A reply key
(<tt>in_reply_to</tt>, or else <tt>draft.reply_to_message_id</tt>) that the
Runtime cannot resolve from its own ledger MUST produce a <tt>rejected</tt>
Result with <tt>reason_code</tt> <tt>unresolved_reply</tt>. A Runtime MUST NOT
strip the key and route the message on <tt>draft.to</tt> 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 <tt>rejected</tt> Result
(<tt>reason_code</tt> <tt>peer_reply_window_expired</tt>, or the Runtime's own
code), and MUST NOT be re-routed. A Peer Notice MUST carry
<tt>in_reply_to</tt> if and only if the placing Runtime resolved it from its own
ledger to a peer message id that it issued or placed.</t>
        <t>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.</t>
        <t>Between hosts, the sending Runtime has already rejected an unresolvable
key at submission. A verified statement whose <tt>in_reply_to</tt> the
<em>receiving</em> Runtime cannot resolve therefore indicates a stale or forged
capability from a configured peer. The receiver MUST NOT place it in
<tt>peer/</tt> 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
(<xref target="sec-peer-control"/>). The ledger binds the participants as well as
the id (<xref target="sec-peer-edges"/>). 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.</t>
        <t><strong>Two things called a result.</strong> A <em>work result</em> is the
responder's content for the requester, described above. A <em>delivery
outcome</em> is mechanical feedback from a Connector to its Runtime
(<tt>delivered</tt>, <tt>held</tt>, and so on). A delivery outcome is not a peer
message; it is kept in <tt>outbound/ext/&lt;name&gt;/</tt>
(<xref target="sec-peer-presentation"/>).</t>
        <t><strong>Delivery failure notices.</strong> A Runtime that reports a terminal
delivery failure to the sending Agent SHOULD do so as a <tt>deliver</tt> Notice
in the sender's <tt>inbound/</tt> tree, with <tt>message.from</tt> set to the
Runtime's own address (<tt>amap.router@&lt;domain&gt;</tt> or equivalent),
<tt>in_reply_to</tt> set to the peer message id, and a body authored by the
Runtime alone. Such a notice MUST NOT be placed in <tt>peer/</tt>, and MUST
NOT quote or forward any text from the receiving Connector's outcome file.</t>
        <t>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
<tt>inbound/</tt> is the one direction <xref target="sec-peer-origin"/> 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.</t>
      </section>
      <section anchor="sec-peer-xhost">
        <name>Peer Traffic Between Hosts</name>
        <t>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 <tt>peer/</tt> only
when every rule in this section holds. Otherwise it hands the message to mail
admission, drops it, or refuses it, as <xref target="sec-peer-outcomes"/>
specifies.</t>
        <section anchor="sec-peer-stmt">
          <name>The Statement</name>
          <t>The sending Runtime builds a <em>statement</em>, a small JSON object,
signs its exact bytes with Ed25519 (<xref target="RFC8032" section="5.1"/>), and
carries the bytes and the signature in one header field
(<xref target="sec-peer-carriage"/>). The receiver verifies the signature over
the bytes it received <em>before</em> parsing them. Nothing is re-serialized:
no canonicalization is defined, and whatever serialization the sender chose is
the one that is signed.</t>
          <t>The peer-statement schema (<xref target="app-f"/>) pins the statement:
version <tt>v</tt> 1, closed at every level, with the members <tt>v</tt>,
<tt>key_id</tt>, <tt>kind</tt> (<tt>"peer"</tt>), <tt>id</tt>,
<tt>in_reply_to</tt> (optional), <tt>from</tt>, <tt>to</tt>, <tt>ts</tt>,
<tt>subject</tt>, <tt>body_sha256</tt> (the digest of the body the receiver
will publish), <tt>attachments</tt> (each with <tt>sha256</tt>,
<tt>size_bytes</tt>, <tt>filename</tt> and <tt>media_type</tt>, in leaf order),
<tt>sender_exposure</tt> (optional, <xref target="sec-peer-exposure"/>) and
<tt>sender_since</tt> (optional, <xref target="sec-peer-policy"/>).</t>
          <t>Unlike the Runtime-authored notices (<xref target="sec-10-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 <tt>v</tt> is the path for evolution. The schema states the
shapes. The following rules, which JSON Schema cannot express, are also part of
the statement:</t>
          <ul spacing="normal">
            <li>
              <t><strong>Formats.</strong> <tt>ts</tt>, <tt>sender_since</tt> and the
timestamps in <tt>sender_exposure</tt> are <xref target="RFC3339"/> date-times in
UTC, in whole seconds, with the <tt>Z</tt> designator, and each MUST denote a
real instant. Digests are SHA-256 <xref target="FIPS180-4"/>, in lowercase
hexadecimal. Every number is a lexical integer, with no fraction and no
exponent, so <tt>1.0</tt> is not <tt>1</tt>. No string contains an unpaired
surrogate (<xref target="RFC8259" section="8.2"/>).</t>
            </li>
            <li>
              <t><strong>Ids.</strong> A sending Runtime MUST mint the ids it sends between
hosts under <tt>^[A-Za-z0-9_][A-Za-z0-9_-]*$</tt>, which is the
<tt>notice_id</tt> character set (<xref target="sec-peer-corr"/>) without
"<tt>.</tt>", with at least 128 bits of entropy, so that
<tt>&lt;id@domain&gt;</tt> is a valid msg-id
(<xref target="RFC5322" section="3.6.4"/>). Hexadecimal satisfies this; a
base64url id whose first character is "<tt>-</tt>" MUST be minted again. The
statement schema enforces the pattern for <tt>id</tt> and
<tt>in_reply_to</tt>. An id used only on one host MAY also contain
"<tt>.</tt>", as the <tt>notice_id</tt> character set allows; an id sent
between hosts MUST NOT.</t>
            </li>
            <li>
              <t><strong><tt>to</tt></strong> is one address: a peer message is addressed to one
Agent (<xref target="sec-peer-addr"/>).</t>
            </li>
            <li>
              <t><strong><tt>sender_exposure</tt></strong> obeys
<xref target="sec-peer-exposure"/>: <tt>window_start</tt> is not later than
<tt>window_end</tt>; <tt>last_external_delivery_ts</tt> is present if and only
if <tt>external_mail_delivered</tt> is <tt>true</tt>, and then lies within the
window; and <tt>asserted_by</tt> is the signing Runtime's configured Runtime
address (<xref target="sec-peer-keys"/>).</t>
            </li>
            <li>
              <t><strong><tt>sender_since</tt></strong>, when present, is not later than
<tt>ts</tt>.</t>
            </li>
          </ul>
        </section>
        <section anchor="sec-peer-carriage">
          <name>Carriage, and the Candidate the Verifier Sees</name>
          <t><strong>The header field.</strong></t>
          <sourcecode type="text"><![CDATA[
AMAP-Peer-Statement: k=<key_id>; s=<statement>; b=<signature>
]]></sourcecode>
          <ul spacing="normal">
            <li>
              <t><tt>s</tt> is the statement's bytes, and <tt>b</tt> is the 64-byte
signature over exactly those bytes. Each is base64url without padding
(<xref target="RFC4648" section="5"/>).</t>
            </li>
            <li>
              <t><tt>k</tt> is the statement's <tt>key_id</tt>, in the clear: the receiver
verifies before it parses, so it must choose the key before it can read the
statement. The signed <tt>key_id</tt> MUST equal <tt>k</tt>.</t>
            </li>
            <li>
              <t>The field name is matched case-insensitively. The sending Runtime MUST
emit exactly one such field.</t>
            </li>
            <li>
              <t>The sender MUST fold the field so that no line exceeds 998 octets
(<xref target="RFC5322" section="2.1.1"/>), and SHOULD fold at 78. It may insert
folding whitespace anywhere, because the receiver removes all of it.</t>
            </li>
            <li>
              <t>A statement MUST NOT exceed 8192 bytes. A sending Runtime that would
otherwise emit a larger one SHOULD refuse the submit instead, with a
<tt>rejected</tt> Result whose <tt>reason_code</tt> is
<tt>peer_statement_too_large</tt>.</t>
            </li>
            <li>
              <t>The sending Runtime MUST also set <tt>Message-ID</tt> to
<tt>&lt;id@domain&gt;</tt>, where <tt>domain</tt> is the domain of
<tt>from</tt>, and SHOULD set <tt>In-Reply-To</tt> and <tt>References</tt> to
the encoded forms (<xref target="sec-peer-corr"/>), so that mail threads
coherently and a demoted message still threads as mail.</t>
            </li>
          </ul>
          <t><strong>The message shape.</strong> A sending Runtime SHOULD send either a
single <tt>text/plain</tt> part, or <tt>multipart/mixed</tt> holding one
<tt>text/plain</tt> part followed by the attachments. The text part SHOULD be
<tt>charset=utf-8</tt> 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.</t>
          <t><strong>The selection rule.</strong> The admitting component reduces a message
to a candidate, and the rule is the same at every receiver:</t>
          <ul spacing="normal">
            <li>
              <t><strong>Leaves.</strong> Walk the MIME tree <xref target="RFC2046"/> depth-first,
descending only into <tt>multipart/*</tt>. Every other part is a leaf, including
<tt>message/rfc822</tt>, whose octets are its part body as transmitted.</t>
            </li>
            <li>
              <t><strong>Body.</strong> The first leaf of type <tt>text/plain</tt> whose
<tt>Content-Disposition</tt> <xref target="RFC2183"/> is not
<tt>attachment</tt> is the body.</t>
            </li>
            <li>
              <t><strong>Attachments.</strong> Every other leaf, in walk order, is an
attachment: a <tt>text/html</tt> alternative, an inline image, or a second
<tt>text/plain</tt> part.</t>
            </li>
            <li>
              <t><strong>No body leaf</strong> means the empty body.</t>
            </li>
          </ul>
          <t>A different structure is not forbidden; it is simply likely to fail the
attachment count or the body digest, and so to be demoted.</t>
          <t><strong>The candidate.</strong> The intake location
(<xref target="sec-peer-intake"/>) MUST hold the following, in a layout this
document does not prescribe:</t>
          <ul spacing="normal">
            <li>
              <t><strong>Field bodies.</strong> The body of each
<tt>AMAP-Peer-Statement</tt>, <tt>AMAP-Peer-Control</tt> and
<tt>Message-ID</tt> 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
<em>oversize</em> 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 <em>invalid</em> 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.</t>
            </li>
            <li>
              <t><strong>The body leaf</strong>, if there is one: its octets after transfer
decoding, and the value of its <tt>charset</tt> parameter, if any.</t>
            </li>
            <li>
              <t><strong>Each attachment leaf:</strong> its octets after transfer decoding, in
order.</t>
            </li>
          </ul>
          <t>The verifier, not the admitting component, decodes the body octets
strictly. The charset label, compared case-insensitively, MUST be
<tt>utf-8</tt> or <tt>us-ascii</tt>; an absent label means <tt>us-ascii</tt>.
Any other label, or octets that are invalid in the labelled charset, fail check
9 (<xref target="sec-peer-verify"/>). The verifier then replaces CRLF pairs
with LF and hashes the result, and that same text is what it publishes as
<tt>body_text</tt>.</t>
          <t><strong>What is published.</strong> On placement, the receiver takes all of the
following from the verified statement, and MUST NOT take them from headers:
<tt>message.id</tt>, <tt>in_reply_to</tt> and <tt>from</tt>; <tt>subject</tt>;
<tt>sender_exposure</tt>; and the attachment Descriptors' <tt>filename</tt>,
<tt>media_type</tt>, <tt>size_bytes</tt> and <tt>sha256</tt>. <tt>to</tt> selects
the Namespace. The receiver MUST NOT publish <tt>references</tt> from headers;
it MAY derive them from its own ledger (<xref target="sec-peer-edges"/>). The
byte sniffing, caps and <tt>disposition</tt> rules of <xref target="sec-7-2"/>
still apply to the attachment bytes.</t>
          <t><strong>Comparisons.</strong> Addresses (<tt>from</tt>, <tt>to</tt>, 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, <tt>Message-ID</tt> MUST be exactly
"<tt>&lt;</tt>", the id, "<tt>@</tt>", the domain and "<tt>&gt;</tt>", with
the id equal to the statement's <tt>id</tt> and the domain equal to the domain
of <tt>from</tt>.</t>
          <t><strong>Logging.</strong> A receiver MUST NOT log a raw header field value
unescaped. Fields are supplied by the attacker, and <tt>k</tt> is validated
against the <tt>key_id</tt> pattern before it is used for anything.</t>
        </section>
        <section anchor="sec-peer-verify">
          <name>Verification</name>
          <t>The receiver applies these checks in this order. The first one that fails
decides the outcome (<xref target="sec-peer-outcomes"/>). Only a candidate that
passes every check is eligible for <tt>peer/</tt>.</t>
          <ol spacing="normal" type="1">
            <li>
              <t><strong>The field.</strong> There is exactly one
<tt>AMAP-Peer-Statement</tt> field; with none, the message is not a peer
candidate, and is ordinary mail. A message that also carries an
<tt>AMAP-Peer-Control</tt> field is dropped
(<xref target="sec-peer-control"/>). The field body is ASCII and is not
oversize. With every SP, HTAB, CR and LF removed, it consists of exactly the
tags <tt>k</tt>, <tt>s</tt> and <tt>b</tt>, each once, in any order, separated
by "<tt>;</tt>". <tt>k</tt> matches the <tt>key_id</tt> pattern. <tt>s</tt> and
<tt>b</tt> are unpadded base64url; <tt>b</tt> decodes to 64 bytes, and
<tt>s</tt> to at most 8192.</t>
            </li>
            <li>
              <t><strong>Known statement.</strong> If the SHA-256 digest of the statement
bytes equals that of a statement the receiver has recorded and still retains
(<xref target="sec-peer-replay"/>), 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.</t>
            </li>
            <li>
              <t><strong>Key.</strong> <tt>k</tt> names exactly one configured key, valid at
the receiver's clock (not-before ≤ now ≤ not-after, inclusive).</t>
            </li>
            <li>
              <t><strong>Signature.</strong> The signature is valid for that key over the
received bytes, under the verification policy of
<xref target="sec-peer-keys"/>.</t>
            </li>
            <li>
              <t><strong>Statement.</strong> (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 <tt>key_id</tt> equals <tt>k</tt>. (b) Every
rule of <xref target="sec-peer-stmt"/> that the schema cannot express holds.</t>
            </li>
            <li>
              <t><strong>Key domain.</strong> The domain of <tt>from</tt> is one of the domains
configured for the Runtime that owns the key. A key vouches only for its own
Runtime's domains (<xref target="sec-peer-addr"/>).</t>
            </li>
            <li>
              <t><strong>Freshness.</strong> <tt>ts</tt> lies within the window of
<xref target="sec-peer-replay"/>. While peer admission is closed (no trustworthy
bound, <xref target="sec-peer-replay"/>), the window is undefined: the candidate
is <em>held</em> in intake, undisposed, until admission reopens.</t>
            </li>
            <li>
              <t><strong>Exposure asserter.</strong> If <tt>sender_exposure</tt> is present,
<tt>asserted_by</tt> is the configured Runtime address of the Runtime that owns
the key.</t>
            </li>
            <li>
              <t><strong>Body.</strong> The body decodes under the charset rule of
<xref target="sec-peer-carriage"/>, and its digest equals
<tt>body_sha256</tt>. The verifier MUST hash bytes it holds itself, and MUST
NOT use a digest that the admitting component computed.</t>
            </li>
            <li>
              <t><strong>Attachments.</strong> The count matches; then, for each attachment in
order, <tt>sha256</tt> and <tt>size_bytes</tt> match the octets.</t>
            </li>
            <li>
              <t><strong><tt>Message-ID</tt>.</strong> There is exactly one such field, matching
<xref target="sec-peer-carriage"/>.</t>
            </li>
            <li>
              <t><strong>Reservation.</strong> No record exists for the pair (the configured
Runtime that owns the key, <tt>id</tt>) (<xref target="sec-peer-replay"/>).
The candidate is reserved, atomically.</t>
            </li>
          </ol>
          <t>A reserved candidate is then <em>decided</em>, and the following are not
verification failures:</t>
          <ul spacing="normal">
            <li>
              <t>if <tt>to</tt> is not hosted here, the candidate is misrouted;</t>
            </li>
            <li>
              <t>if <tt>in_reply_to</tt> is present, it must resolve and be permitted
(<xref target="sec-peer-edges"/>);</t>
            </li>
            <li>
              <t>otherwise, the receiver's own graph must have the edge from
<tt>from</tt> to <tt>to</tt> (<xref target="sec-peer-edges"/>);</t>
            </li>
            <li>
              <t>any deployment admission policy is applied
(<xref target="sec-peer-policy"/>);</t>
            </li>
            <li>
              <t>otherwise, the decision is placement.</t>
            </li>
          </ul>
          <t>The decision MUST be persisted on the record before it has any effect
(<xref target="sec-peer-replay"/>). Only a persisted placement decision is ever
published; a persisted refusal or drop MUST NOT be published, whatever happens
afterwards.</t>
        </section>
        <section anchor="sec-peer-outcomes">
          <name>Outcomes</name>
          <table anchor="tab-peer-outcomes">
            <name>Outcomes for a candidate from another host</name>
            <thead>
              <tr>
                <th align="left">Outcome</th>
                <th align="left">When</th>
                <th align="left">Control Notice</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td align="left"><tt>peer</tt></td>
                <td align="left">Every check passes, and the message is routed and permitted.</td>
                <td align="left">none</td>
              </tr>
              <tr>
                <td align="left">held</td>
                <td align="left">Check 7 while peer admission is closed (<xref target="sec-peer-replay"/>); not yet disposed of.</td>
                <td align="left">none</td>
              </tr>
              <tr>
                <td align="left">mail, no statement</td>
                <td align="left">No <tt>AMAP-Peer-Statement</tt> field.</td>
                <td align="left">none</td>
              </tr>
              <tr>
                <td align="left">mail, demoted</td>
                <td align="left">A failure of check 1 (other than the both-fields drop below), 3, 4, 5, 6, 7, 8, 9, 10 or 11.</td>
                <td align="left">SHOULD, for 5(b) and 6 to 11</td>
              </tr>
              <tr>
                <td align="left">drop: both fields</td>
                <td align="left">An <tt>AMAP-Peer-Statement</tt> and an <tt>AMAP-Peer-Control</tt> field in one message (<xref target="sec-peer-control"/>).</td>
                <td align="left">none</td>
              </tr>
              <tr>
                <td align="left">drop: duplicate</td>
                <td align="left">Check 2 (a known statement) or check 12 (a known <tt>id</tt>).</td>
                <td align="left">SHOULD, for check 12 only</td>
              </tr>
              <tr>
                <td align="left">drop: misrouted</td>
                <td align="left"><tt>to</tt> is not hosted here.</td>
                <td align="left">SHOULD</td>
              </tr>
              <tr>
                <td align="left">refuse: unresolved reply</td>
                <td align="left"><xref target="sec-peer-edges"/>.</td>
                <td align="left">SHOULD</td>
              </tr>
              <tr>
                <td align="left">refuse: no edge</td>
                <td align="left"><xref target="sec-peer-edges"/>.</td>
                <td align="left">SHOULD</td>
              </tr>
              <tr>
                <td align="left">refuse: policy</td>
                <td align="left">A deployment admission policy (<xref target="sec-peer-policy"/>).</td>
                <td align="left">SHOULD, with <tt>policy_code</tt></td>
              </tr>
            </tbody>
          </table>
          <t><strong>Mail, demoted.</strong> 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 delivered. The demoted
notice MUST NOT carry <tt>sender_exposure</tt>, 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.</t>
          <t><strong>Drop and refuse.</strong> A dropped or refused candidate is neither
placed nor handed to mail admission. Every outcome other than <tt>peer</tt> 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
(<xref target="sec-peer-corr"/>).</t>
          <t><strong>Who is told.</strong> A Control Notice
(<xref target="sec-peer-control"/>) MUST be sent only when every one of these
holds; a configured <tt>k</tt> alone is never enough:</t>
          <ul spacing="normal">
            <li>
              <t>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;</t>
            </li>
            <li>
              <t>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);</t>
            </li>
            <li>
              <t>the candidate is a statement, not a Control Notice: nothing is ever sent
about a Control Notice (<xref target="sec-peer-control"/>);</t>
            </li>
            <li>
              <t>the destination is the Runtime address configured for the key's owner,
and never an address taken from the message.</t>
            </li>
          </ul>
          <t>A "dropped, duplicate" notice (check 12) tells the sender that a record
with that <tt>id</tt> 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
<xref target="sec-peer-control"/>.</t>
        </section>
        <section anchor="sec-peer-replay">
          <name>The Replay Store, Windows and Recovery</name>
          <t><strong>Windows.</strong> The receiver accepts <tt>ts</tt> when
<tt>lower ≤ ts ≤ now + F</tt>, both bounds inclusive, where
<tt>lower = max(H − P, floor)</tt>:</t>
          <ul spacing="normal">
            <li>
              <t><strong>F</strong>, the allowance for clock skew, defaults to 300 seconds
and MUST NOT exceed 900.</t>
            </li>
            <li>
              <t><strong>P</strong>, 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 (<xref target="sec-peer-keys"/>).</t>
            </li>
            <li>
              <t><strong>H</strong> 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.</t>
            </li>
            <li>
              <t><strong>floor</strong> is set only by recovery and by window changes, both
below.</t>
            </li>
          </ul>
          <t><strong>Records.</strong> Replay identity is scoped to the pair (configured
Runtime, <tt>id</tt>), 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 <tt>id</tt> and <tt>ts</tt>; the SHA-256 digest of the statement
bytes; the <tt>notice_id</tt> minted at reservation; a state, which is
<tt>reserved</tt>, <tt>decided</tt> or <tt>done</tt>; and, once decided, the
<em>disposition</em>: placement, or a refusal or drop with its cause (and
<tt>policy_code</tt>), and whether a Control Notice is still pending.</t>
          <t><strong>Recording.</strong> 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
(<xref target="sec-peer-control"/>) use the same store.</t>
          <t><strong>Reservation, decision and effect.</strong> These are three durable
steps, in order, each atomic:</t>
          <ol spacing="normal" type="1">
            <li>
              <t><strong>Reserve.</strong> Check 12 creates the <tt>reserved</tt> record and
mints a fresh <tt>notice_id</tt>. The candidate stays in the intake location
until the record is <tt>done</tt>. A reservation is undecided, and authorizes
nothing.</t>
            </li>
            <li>
              <t><strong>Decide.</strong> The receiver routes, correlates, permits and applies
policy (<xref target="sec-peer-verify"/>), and persists the disposition on the
record, which becomes <tt>decided</tt>. A refusal or drop also marks its control
notice as pending.</t>
            </li>
            <li>
              <t><strong>Effect.</strong> For a placement, the notice is written under the
minted <tt>notice_id</tt>; for a refusal or drop, the Control Notice is durably
queued. Only then is the record <tt>done</tt>. Placement is idempotent: a notice
that is already present is not written again.</t>
            </li>
          </ol>
          <t>A copy that arrives while another copy is <tt>reserved</tt> or
<tt>decided</tt> matches it at check 2, and is dropped. On restart, the receiver
MUST finish every record that is not <tt>done</tt>, exactly once, by resuming
its <em>recorded</em> state, and MUST NOT infer a disposition from the absence
of a notice:</t>
          <ul spacing="normal">
            <li>
              <t>a <tt>reserved</tt> record is decided then, under the configuration in
force at recovery (the receiver has no older one it can trust), and then takes
effect;</t>
            </li>
            <li>
              <t>a <tt>decided</tt> placement is placed if the notice is absent, and is
marked <tt>done</tt> without placing it again if the notice is present;</t>
            </li>
            <li>
              <t>a <tt>decided</tt> refusal or drop is not published: its pending control
notice is queued, once, and the record is marked <tt>done</tt>.</t>
            </li>
          </ul>
          <t>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.</t>
          <t><strong>Retention.</strong> The receiver MUST retain each <tt>done</tt> record
while <tt>H ≤ ts + P</tt>, and MAY retain it longer. It MUST NOT prune a
record that is not <tt>done</tt>. 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.</t>
          <t><strong>Resending.</strong> A sending Runtime whose hand-off was ambiguous MAY
resend the same <tt>id</tt>, signed again with a new <tt>ts</tt>. 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 <tt>id</tt>, including
across a resend that a person approved.</t>
          <t>This permission is idempotent only within retention. Once the first
copy's record has been pruned, a resend of the same <tt>id</tt> under a fresh
<tt>ts</tt> 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.</t>
          <t><strong>Recovery.</strong> 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.</t>
          <ul spacing="normal">
            <li>
              <t><strong>With H intact</strong>, the receiver sets <tt>floor</tt> to one second
after <tt>max(C, H) + Fmax</tt>, 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
<tt>ts ≤ H + F ≤ max(C, H) + Fmax</tt>, so none reaches the floor. The floor
is strictly above that bound: an inclusive floor would re-admit a statement
accepted at exactly <tt>now + F</tt>, and C alone would re-admit one accepted
before the clock stepped back.</t>
            </li>
            <li>
              <t><strong>With H lost as well</strong>, 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
<tt>B + Fmax</tt>, 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.</t>
            </li>
          </ul>
          <t><strong>Window changes.</strong> 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.</t>
        </section>
        <section anchor="sec-peer-edges">
          <name>Correlation and Edges Between Hosts</name>
          <t><strong>The ledger.</strong> 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 (<tt>issued</tt> or <tt>placed</tt>); and an
expiry, the configured reply window (<xref target="sec-peer-corr"/>). The ledger
is scoped to peer messages: a mail notice id never resolves.</t>
          <t><strong>Resolution.</strong> A verified statement carrying <tt>in_reply_to</tt>
resolves only if a record exists whose id equals <tt>in_reply_to</tt>, whose
local Agent equals <tt>to</tt>, whose remote Agent equals <tt>from</tt>, 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.</t>
          <t><strong>Permission.</strong> A resolved reply to a record this Runtime
<tt>issued</tt> is a reverse message, and is permitted without an edge
(<xref target="sec-peer-corr"/>). A resolved message against a record this
Runtime <tt>placed</tt> is a follow-up in the same direction, and needs the
edge from <tt>from</tt> to <tt>to</tt>. A statement without
<tt>in_reply_to</tt> needs the edge.</t>
          <t><strong>The receiver's own graph.</strong> The edge MUST be checked against the
receiver's own graph. The sending Runtime checked its edge at submission
(<xref target="sec-peer-addr"/>), 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.</t>
          <t><strong>At submission.</strong> 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.</t>
        </section>
        <section anchor="sec-peer-keys">
          <name>Keys</name>
          <t><strong>Configuration.</strong> The operator configures, at every receiver, each
peer Runtime's domain set, its Runtime address
(<tt>amap.router@&lt;domain&gt;</tt> 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:</t>
          <ul spacing="normal">
            <li>
              <t>a <tt>key_id</tt> occurs twice, across all peers (the receiver
selects a key by <tt>k</tt> alone);</t>
            </li>
            <li>
              <t>a not-before time is later than its not-after time;</t>
            </li>
            <li>
              <t>a public key is not a canonical encoding of a curve point; or</t>
            </li>
            <li>
              <t>a public key is of small order.</t>
            </li>
          </ul>
          <t>A sending Runtime SHOULD make its key ids globally unique, for example
by deriving them from a fingerprint of the public key.</t>
          <t>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.</t>
          <t><strong>Rotation.</strong> A receiver MUST support at least two concurrent keys
per peer. A rotation proceeds as follows:</t>
          <ol spacing="normal" type="1">
            <li>
              <t>Add the new key at every receiver.</t>
            </li>
            <li>
              <t>Switch the sender to it.</t>
            </li>
            <li>
              <t>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 <tt>ts</tt> up to T + F, and arrive up to P after that.</t>
            </li>
          </ol>
          <t>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
(<xref target="sec-peer-replay"/>).</t>
          <t><strong>The signing key</strong> is a Runtime credential
(<xref target="sec-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 <tt>kind</tt> members
keep apart.</t>
          <t><strong>Verification policy.</strong> Verification is Ed25519 as specified in
<xref target="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.</t>
          <t>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
<xref target="RFC8032" section="7.1"/> (TEST 1 to 3), and negative vectors marked
for rejection either at verification or at configuration
(<xref target="app-h"/>). An implementation MUST pass both kinds through the
implementation it ships.</t>
        </section>
        <section anchor="sec-peer-control">
          <name>Control Notices</name>
          <t>A receiving Runtime tells a configured sending Runtime what it did with a
statement it did not place, using a signed <em>Control Notice</em>. The
peer-control schema (<xref target="app-f"/>) pins it: version <tt>v</tt> 1,
closed, with the members <tt>v</tt>, <tt>key_id</tt>, <tt>kind</tt>
(<tt>"control"</tt>), <tt>id</tt>, <tt>from</tt>, <tt>to</tt>, <tt>ts</tt>,
<tt>about</tt>, <tt>outcome</tt>, <tt>cause</tt>, and <tt>policy_code</tt>
(with cause <tt>policy</tt>).</t>
          <t><strong>Form.</strong> A Control Notice is carried in an
<tt>AMAP-Peer-Control</tt> header field with the <tt>k</tt>, <tt>s</tt> and
<tt>b</tt> grammar of <xref target="sec-peer-carriage"/>, signed with the same
per-Runtime key. Its <tt>kind</tt> 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.</t>
          <t><strong>When.</strong> A receiver SHOULD send one for every outcome
<xref target="sec-peer-outcomes"/> marks, and MUST send one only when the
eligibility conditions of <xref target="sec-peer-outcomes"/> 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
(<xref target="sec-peer-replay"/>), MUST be queued durably before the record is
<tt>done</tt>, and MUST be sent at most once, including across a crash. A
receiver MAY rate-limit Control Notices; any suppression MUST be logged.</t>
          <t><strong><tt>policy_code</tt></strong> 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.</t>
          <t><strong>Addressing.</strong> <tt>from</tt> is the receiver's own Runtime address.
<tt>to</tt> 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.</t>
          <t><strong>Receiving one.</strong> A Runtime applies checks 1 to 5 of
<xref target="sec-peer-verify"/>, with <tt>AMAP-Peer-Control</tt> and the
control schema in place of the statement's. It then requires all of these:
<tt>from</tt> is the Runtime address of the peer that owns the key; <tt>to</tt>
is its own Runtime address; <tt>ts</tt> is fresh
(<xref target="sec-peer-replay"/>); the notice's pair (Runtime, <tt>id</tt>) is
new; and <tt>about</tt> names a ledger record that this Runtime
<tt>issued</tt> to an Agent in the signer's domains.</t>
          <t><strong>What it does.</strong> It SHOULD give the issuing Agent a failure
notice authored by the Runtime in <tt>inbound/</tt> (the delivery failure notice
of <xref target="sec-peer-corr"/>), 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 <tt>done</tt> only once the
failure notice is durable; a crash in between is finished by recovery, once.</t>
          <t><strong>One per message.</strong> An issued record receives at most one failure
notice. A later Control Notice about the same <tt>about</tt>, 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.</t>
          <t><strong>Failures.</strong> 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.</t>
        </section>
        <section anchor="sec-peer-policy">
          <name>Optional: Sender Age, and Deployment Admission Policy</name>
          <t>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 <tt>sender_exposure</tt>. The refusal is
"refuse: policy", with a <tt>policy_code</tt> defined by the deployment. The
policies themselves are the deployment's business, not this profile's.</t>
          <t>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
<tt>sender_since</tt> exists to avoid that:</t>
          <ul spacing="normal">
            <li>
              <t>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.</t>
            </li>
            <li>
              <t>It is a Runtime assertion, covered by the signature, and MUST NOT be taken
from text the sender supplied.</t>
            </li>
            <li>
              <t>It has the format of <tt>ts</tt>, and is not later than <tt>ts</tt>.
Skew is absorbed by the age itself: a receiver measures age as its own clock
minus <tt>sender_since</tt>, so a few minutes of skew change nothing under a
policy measured in hours.</t>
            </li>
            <li>
              <t><strong>Continuity.</strong> 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.</t>
            </li>
            <li>
              <t><strong>Consistency.</strong> A receiver that enforces a sender-age policy MUST
keep the last value it accepted for each pair (Runtime, <tt>from</tt>). A later
statement that asserts an <em>earlier</em> value claims an older enrollment
than the Runtime previously asserted; it is inconsistent, and the receiver
refuses it with its own <tt>policy_code</tt>. A later value is a new
enrollment, and the age starts again.</t>
            </li>
          </ul>
          <t>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 <tt>policy_code</tt>.</t>
        </section>
        <section anchor="sec-peer-intake">
          <name>The Intake Location</name>
          <t>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 <tt>inbound/</tt> nor <tt>peer/</tt>, 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
<xref target="sec-peer-carriage"/>: the verifier MUST NOT parse mail. The verifier
MUST apply the read-side discipline of <xref target="sec-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 <tt>peer/</tt>,
minting the <tt>notice_id</tt> at reservation
(<xref target="sec-peer-replay"/>); or it hands it to mail admission, which, if
it delivers, places it in <tt>inbound/</tt> under a <tt>notice_id</tt> minted
there; or it drops or refuses it, logging the cause. A candidate held while peer
admission is closed (<xref target="sec-peer-replay"/>) is not yet disposed of,
and stays in the intake location until admission reopens.</t>
          <t>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.</t>
        </section>
      </section>
      <section anchor="sec-peer-exposure">
        <name>Sender Exposure</name>
        <t>The split between Runtime assertions and sender claims
(<xref target="sec-7-1"/>) applies: <tt>disposition</tt>, <tt>verdict</tt> and
<tt>sender_standing</tt> are Runtime assertions, while <tt>subject</tt> and
<tt>body_text</tt> (and, on the mail tree, <tt>from</tt>) are sender claims. An exposure record
belongs on the assertion side. It records <em>delivery</em>, not
<em>reading</em>: a Runtime can know that it delivered external mail to the
sender's Namespace, but cannot know what the sender did with it.</t>
<t>A Peer Notice asserting exposure, with the stamp in <tt>message.sender_exposure</tt>:</t>
        <figure anchor="fx-peer-exposure-true-src">
          <name>fixtures/valid/peer-exposure-true.json</name>
          <sourcecode type="json" name="peer-exposure-true.json"><![CDATA[
{
  "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"]
    }
  }
}
]]></sourcecode>
        </figure>
        <ul spacing="normal">
          <li>
            <t><strong>Placement.</strong> It is a member of <tt>message</tt>, beside
<tt>sender_standing</tt>, <tt>provenance</tt> and <tt>verdict</tt>, so that the
Runtime's assessments of the message are kept together. The schema requires
<tt>asserted_by</tt> (a bare addr-spec naming the asserting Runtime; between
hosts, the signer), <tt>external_mail_delivered</tt>, <tt>window_start</tt> and
<tt>window_end</tt>, and requires <tt>last_external_delivery_ts</tt> when the
boolean is <tt>true</tt>. Its absence when the boolean is <tt>false</tt> is
stated here, not in the schema.</t>
          </li>
          <li>
            <t><strong>Shape.</strong> 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. <tt>derived_from</tt> is optional, and present
only where the Runtime observed the derivation itself (the sender's reply key
named a notice in its mail tree).</t>
          </li>
          <li>
            <t><strong>Absence means "not assessed".</strong> A consumer MUST NOT read the
member's absence as "no exposure".
<tt>false</tt>, 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 <tt>false</tt>. Notices the Runtime authors in the mail tree (the
delivery failure notices of <xref target="sec-peer-corr"/>) are not external
mail, and MUST NOT make the boolean <tt>true</tt>. Moving <tt>window_start</tt> 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.</t>
          </li>
          <li>
            <t><strong>Not settable by the sender.</strong> On one host, the Submit Request
envelope is closed (<xref target="sec-10-1"/>), so a request carrying the
member is <tt>rejected</tt>, which is stronger than stripping it. Between hosts,
it is taken only from the verified statement, and is removed on demotion
(<xref target="sec-peer-outcomes"/>). A Runtime MUST NOT accept it from any
sender.</t>
          </li>
          <li>
            <t><strong>Use.</strong> It MUST NOT drive routing or treatment
(<xref target="sec-peer-origin"/>). It is advisory, for audit, and covered by
the signature.</t>
          </li>
        </ul>
        <t><strong>Lineage declared by the sender is not included.</strong> 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 <em>mail</em> tree, the Runtime knows which external
message the peer message derives from, and records that as its own observation
(<tt>derived_from</tt>, and the audit log).</t>
      </section>
      <section anchor="sec-peer-presentation">
        <name>Delivery Outcomes, Retention, Deletion and Presentation</name>
        <ul spacing="normal">
          <li>
            <t><strong>Delivery outcomes are not a profile artifact.</strong> A Connector that
reports what it did with a notice (<tt>delivered</tt>, <tt>held</tt>,
<tt>refused</tt>, and so on) writes the report under
<tt>outbound/ext/&lt;name&gt;/</tt>, in a shape agreed out of band with its
Runtime, under the stable Connector id that <xref target="sec-4-1"/> requires.
<xref target="sec-4-1"/> carries the one normative rule: an outcome 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
(<xref target="sec-11-15"/>). If a second Connector needs outcomes, that is the
point at which to define a shape for them in this profile.</t>
          </li>
          <li>
            <t><strong>Retention.</strong> The record is the audit log, not the spool. The
Runtime logs every candidate it disposes of (id, <tt>from</tt>, <tt>to</tt>, tree,
exposure, signature verdict, and any demotion or refusal cause) to the per-Agent
<tt>audit/log.jsonl</tt> it owns, and to its own store of record. Spool
retention for <tt>peer/</tt> follows the mail row of the deletion table
(<xref target="sec-4-2"/>) unchanged.</t>
          </li>
          <li>
            <t><strong>Deletion rights.</strong> The deletion table
(<xref target="sec-4-2"/>) applies to <tt>peer/</tt> artifacts exactly as to
inbound ones: written by the Runtime, and deleted by the Runtime only.</t>
          </li>
          <li>
            <t><strong>Presentation.</strong> A Connector presenting a peer body MUST meet the
presentation obligations of <xref target="sec-7-1"/>, with one prohibition and
two additions. It MUST NOT convey that the sender is unauthenticated: on this
tree, <tt>message.from</tt> is asserted by the Runtime from the restricted write
path (<xref target="sec-peer-addr"/>), 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 (<xref target="sec-peer-exposure"/>).
Authenticated <em>who</em>; unverified <em>what</em>.</t>
            <t>None of this is an enforcement mechanism. Enforcement on this tree is the
write-authority partition and the origin-follows-the-tree rule
(<xref target="sec-peer-origin"/>); 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.</t>
          </li>
        </ul>
      </section>
    </section>
    <section anchor="sec-10">
      <name>Versioning and Conformance</name>
      <section anchor="sec-10-1">
        <name>Versioning</name>
        <t><strong>The fail-closed rule.</strong> <tt>contract_version</tt> is a major-version string. A
consumer that reads a <strong>higher major</strong> than it speaks MUST <strong>fail closed</strong>
— refuse to process the artifact — and MUST NOT silently mis-parse it.</t>
        <t><strong>Version-check ordering.</strong> A consumer MUST implement the version check by
reading <tt>contract_version</tt> <strong>before</strong> any schema validation, comparing
majors as non-negative decimal integers. An absent major, a lower major, or
a higher major is a <strong>version refusal</strong>: fail closed, as a distinct, logged
outcome, never folded into the malformed-document drop path of
<xref target="sec-7-1"/>.</t>
        <t>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.</t>
        <t>This ordering rule is about <em>inbound</em> 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 <em>outbound</em> direction: a Runtime
version-refusing a Submit Request MAY carry that refusal on the ordinary
request-rejection path — a top-level <tt>rejected</tt> Result with
<tt>reason_code: "unsupported_contract_version"</tt> (<xref target="sec-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: <tt>reason_code</tt> is exactly this protocol's
mechanism for saying <em>why</em> a <tt>rejected</tt> fired, and a consumer MUST already
tolerate a <tt>reason_code</tt> it does not otherwise interpret. A Runtime has no
equivalent document to write for a version-refused <em>Notice</em> it never emits
— hence the asymmetry: logged-only inbound, <tt>rejected</tt> outbound.</t>
        <t><strong>A version signal, not member rejection, is the boundary.</strong>
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 <em>whole document</em>: 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 <em>by version</em>, 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.</t>
        <t><strong>Unknown-member tolerance is direction-aware.</strong> The rule is stated
per direction because the two directions have opposite threat models.</t>
        <ul spacing="normal">
          <li>
            <t><strong>Runtime-authored artifacts — Deliver Notice, Inbound Message, Result,
Peer Notice — are open.</strong> 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 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
<strong>producer</strong> non-conformance: a Runtime MUST NOT emit members this
document does not define. In the schemas, <tt>additionalProperties:
false</tt> is absent from the deliver-notice, inbound-message, result and
peer-notice schemas at every level — top level, <tt>message</tt>, Descriptor
items, and <tt>attestation</tt> — while the clean-gating conditionals are
orthogonal and remain.</t>
          </li>
          <li>
            <t><strong>The Submit Request envelope is closed</strong>, <tt>additionalProperties:
false</tt> at every level. Closure does real security work there: it is
what bars an outbound <tt>content_ref</tt> and any other smuggled member; the
validator is the trusted Runtime; and a refusal is a <em>legible,
per-request</em> <tt>rejected</tt> Result, never a silent loss. A Runtime MUST reject
a request carrying unrecognized members.</t>
          </li>
          <li>
            <t><strong>Consequently:</strong> 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.</t>
          </li>
          <li>
            <t>The Binding Record is closed, for the separate reason given in
<xref target="sec-9-1"/>. The signed Peer Statement and Control Notice are closed
because they are signed bytes another Runtime acts on
(<xref target="sec-peer-stmt"/>); they carry their own version member,
<tt>v</tt>.</t>
          </li>
        </ul>
        <t><strong>Tolerance extends to values, not only members.</strong> An optional, explicitly
advisory enum on a Runtime-authored artifact — <tt>message.sender_standing</tt> and
<tt>message.provenance</tt> (<xref target="sec-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 <tt>sender_standing</tt> or
<tt>provenance</tt> 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 <tt>enum</tt> constraint is absent from these two
members.</t>
        <t>This is <strong>not</strong> a universal loosening of every enum. <tt>kind</tt>, <tt>outcome</tt> —
both the top-level Result outcome and the per-attachment outcome — and the
inbound Descriptor's <tt>disposition</tt> remain <strong>closed, enum-locked
vocabularies</strong>. <tt>kind</tt> and <tt>outcome</tt> gate which processing path a document
takes at all, and <tt>disposition</tt> gates byte publication (<xref target="sec-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.</t>
        <t><strong>When the wire major changes, and when it does not.</strong> The wire
major exists to force a legible, fail-closed refusal where a <em>whole
document</em> 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 <tt>rejected</tt>
Result for one request, or the drop of one Notice under
<xref target="sec-7-1"/>) does not require one. Changing the major for such a change
would make every counterpart on the old major refuse <em>all</em> 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 <tt>contract_version</tt> unchanged.</t>
        <t><strong>Why the wire version is <tt>"2"</tt>.</strong> The wire version starts at
<tt>"2"</tt>, not <tt>"1"</tt>, because the value <tt>"1"</tt> is already taken.
Before this document, implementations exchanged an earlier, incompatible shape
under <tt>"1"</tt>: it had no attachments, and its envelopes were closed. Reusing <tt>"1"</tt> 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. <tt>"2"</tt> is also what
current implementations emit and check. <xref target="app-history"/> records the
history. The authors would be glad of the community's view on whether a reset is
nonetheless worth its cost.</t>
        <t><strong>Change control.</strong> A capability change starts with a change to this
document, its schemas, and its fixtures, <em>before</em> 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.</t>
      </section>
      <section anchor="sec-10-2">
        <name>Required versus optional</name>
        <t><tt>contract_version</tt> 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 (<xref target="sec-9-1"/>) and the Peer Directory, and the signed
Peer Statement and Control Notice carry their own version member, <tt>v</tt>,
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.</t>
        <table>
          <thead>
            <tr>
              <th align="left">Artifact</th>
              <th align="left">Required</th>
              <th align="left">Optional (specified; may be absent)</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td align="left">
                <strong>deliver-notice</strong></td>
              <td align="left">
                <tt>contract_version, notice_id, ts, kind, message{id, from, subject, preview, mailbox}</tt></td>
              <td align="left">
                <tt>message.{task_id, thread_id, in_reply_to, references, sender_standing, verdict, provenance}</tt>, <tt>message.attachments[]</tt> (items require <tt>filename, media_type, size_bytes, disposition</tt>; <tt>sha256</tt> and <tt>content_ref</tt> optional, gated to <tt>disposition == "clean"</tt>)</td>
            </tr>
            <tr>
              <td align="left">
                <strong>submit-request</strong></td>
              <td align="left">
                <tt>contract_version, req_id, draft{to, subject, body_text}</tt></td>
              <td align="left">
                <tt>agent_id, ts, in_reply_to, draft.{cc, reply_to_message_id}</tt>, <tt>draft.attachments[]</tt> (items require <tt>filename, media_type, size_bytes, sha256</tt>)</td>
            </tr>
            <tr>
              <td align="left">
                <strong>result</strong></td>
              <td align="left">
                <tt>contract_version, req_id, outcome</tt></td>
              <td align="left">
                <tt>detail, reason_code, ts, recipients, message_id, job_id, attestation</tt> (requires <tt>form</tt>; <tt>token</tt> and <tt>claims</tt> optional), <tt>attachments[]</tt> (items require <tt>index, outcome</tt>)</td>
            </tr>
            <tr>
              <td align="left">
                <strong>inbound-message</strong></td>
              <td align="left">
                <em>(the artifact itself is OPTIONAL — <xref target="sec-7-3"/>)</em>; if present: <tt>contract_version, notice_id, body_text</tt></td>
              <td align="left">
                <tt>id, date, from, to, cc, subject</tt>, <tt>attachments[]</tt> (same Descriptor rules as deliver-notice)</td>
            </tr>
            <tr>
              <td align="left">
                <strong>binding-record</strong></td>
              <td align="left">
                <em>(the file itself is OPTIONAL — <xref target="sec-9"/>)</em>; if present: <tt>agent_id, address, provisioned_ts</tt></td>
              <td align="left">
                <tt>label</tt>; <tt>contract_version</tt> (optional — if present, pinned to <tt>"2"</tt>)</td>
            </tr>
            <tr>
              <td align="left">
                <strong>directory</strong></td>
              <td align="left">
                <em>(the artifact itself is OPTIONAL — <xref target="sec-directory"/>)</em>; if present: <tt>router, written_at, delegate_to, mail_to</tt></td>
              <td align="left">
                <tt>contract_version</tt> (if present, pinned to <tt>"2"</tt>)</td>
            </tr>
            <tr>
              <td align="left">
                <strong>peer-notice</strong></td>
              <td align="left">
                <em>(peer-origin profile — <xref target="sec-peer-tree"/>)</em> <tt>contract_version, notice_id, ts, kind: "peer", message{id, from, subject, preview, mailbox: "peer"}</tt></td>
              <td align="left">
                <tt>message.{in_reply_to, references, task_id, thread_id, sender_standing, provenance, verdict, sender_exposure, attachments}</tt>; <tt>in_reply_to</tt> is REQUIRED on a reply (<xref target="sec-peer-corr"/>). The body spool and Sidecars are as for deliver-notice.</td>
            </tr>
            <tr>
              <td align="left">
                <strong>peer-statement</strong></td>
              <td align="left">
                <em>(between hosts — <xref target="sec-peer-stmt"/>)</em> <tt>v, key_id, kind, id, from, to, ts, subject, body_sha256, attachments</tt>; carried in exactly one <tt>AMAP-Peer-Statement</tt> field with the tags <tt>k</tt>, <tt>s</tt> and <tt>b</tt></td>
              <td align="left">
                <tt>in_reply_to, sender_exposure, sender_since</tt></td>
            </tr>
            <tr>
              <td align="left">
                <strong>peer-control</strong></td>
              <td align="left">
                <em>(between hosts — <xref target="sec-peer-control"/>)</em> <tt>v, key_id, kind, id, from, to, ts, about, outcome, cause</tt>; carried in exactly one <tt>AMAP-Peer-Control</tt> field with the tags <tt>k</tt>, <tt>s</tt> and <tt>b</tt></td>
              <td align="left">
                <tt>policy_code</tt> (REQUIRED with cause <tt>policy</tt>)</td>
            </tr>
            <tr>
              <td align="left">
                <strong>roster</strong></td>
              <td align="left">
                <em>(the artifact itself is OPTIONAL — <xref target="sec-roster"/>)</em>; if present: <tt>contract_version, router, written_at, members[]</tt> (items require <tt>address, state</tt>)</td>
              <td align="left">
                <tt>interval_s</tt></td>
            </tr>
          </tbody>
        </table>
        <t><tt>inbound/notices/processed/</tt> is an OPTIONAL <strong>directory convention</strong>
(<xref target="sec-4-1"/>), not a protocol artifact: it carries no schema, and no
fixture exists or is needed for it.</t>
        <t>Emission of <tt>contract_version</tt> 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.</t>
      </section>
      <section anchor="sec-10-3">
        <name>Conformance</name>
        <t><strong>The fixtures are the other side.</strong> <tt>fixtures/</tt> holds golden artifacts,
and a producer or consumer proves conformance by passing them with <strong>no
counterpart present</strong> (<xref target="app-h"/>). That is what makes independent
implementation practical: neither side needs the other to exist in order to
demonstrate conformance.</t>
        <t>Not every obligation in this document is wire-shaped. A <em>behavioral</em>
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. <xref target="app-h"/> lists
every obligation of this kind that a green run does not prove.</t>
        <t>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.</t>
        <t><strong>Runtime conformance class.</strong> An implementation claiming Runtime
conformance:</t>
        <ul spacing="normal">
          <li>
            <t>MUST produce Deliver Notice documents valid against the deliver-notice
schema (<xref target="app-c"/>) and Result documents valid against the result schema
(<xref target="app-b"/>); MAY produce Inbound Message documents, which when produced
MUST be valid against the inbound-message schema (<xref target="app-d"/>); and MAY
produce a Binding Record valid against the binding-record schema
(<xref target="app-e"/>).</t>
          </li>
          <li>
            <t>MUST consume Submit Request documents: it MUST accept every valid
<tt>request-*</tt> fixture and MUST reject every invalid <tt>request-*</tt> fixture.</t>
          </li>
          <li>
            <t>MUST demonstrate its producing side by confirming that its own emitted
Notices, Results, and Inbound Messages satisfy the same schemas the
suite's valid <tt>notice-*</tt>, <tt>result-*</tt>, and <tt>message-*</tt> fixtures satisfy —
<strong>including the three fixtures carrying unrecognized members</strong>. A Runtime
MUST NOT itself emit unrecognized members, but its <em>parser</em>, 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 <tt>identity-*</tt> shape.</t>
          </li>
          <li>
            <t>MUST uphold the filesystem and policy obligations of Sections <xref format="counter" target="sec-4"/>
through <xref format="counter" target="sec-8"/> that no document validator can observe. <xref target="app-h"/>
lists them explicitly as what a green fixture run does <strong>not</strong> 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.</t>
          </li>
        </ul>
        <t><strong>Connector conformance class.</strong> An implementation claiming Connector
conformance:</t>
        <ul spacing="normal">
          <li>
            <t>MUST produce Submit Request documents valid against the submit-request
schema (<xref target="app-a"/>), and MUST NOT emit an outbound <tt>content_ref</tt>
(<xref target="sec-5-1"/>).</t>
          </li>
          <li>
            <t>MUST consume Deliver Notice, Inbound Message, and Result documents: it
MUST accept every valid <tt>notice-*</tt>, <tt>message-*</tt>, and <tt>result-*</tt> fixture —
<strong>including those carrying unrecognized members or unrecognized
advisory-enum values</strong> — and MUST reject, meaning drop rather than
surface, every invalid <tt>notice-*</tt> fixture <strong>except the version-refusal
one</strong>, which it MUST instead refuse via the distinct version-refusal path
(<xref target="sec-10-1"/>): never folded into the drop path, and never surfaced to
the Agent either.  </t>
            <t><xref target="sec-7-1"/>'s drop rule is <strong>two independent conditions, not a closure
over schema validation</strong>. "Fails schema validation for any reason other
than an unrecognized member" (check 1) covers eleven of the twelve other
invalid <tt>notice-*</tt> fixtures — missing-required-member, wrong-<tt>kind</tt>,
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 <tt>content_ref</tt> index and prefix
binding (check 2, <xref target="sec-7-2"/>), a named post-check the schema cannot
express. A Connector conformance claim requires running <strong>both</strong> checks;
schema validation alone does not close this set.  </t>
            <t>
Accepting the clean-Descriptor fixture — a Descriptor asserted <tt>clean</tt>
and carrying <tt>content_ref</tt> and <tt>sha256</tt> — is part of this obligation, and
acceptance means relaying the Notice, not fetching the bytes: a Connector
that never dereferences <tt>content_ref</tt> satisfies it (<xref target="sec-7-2"/>).</t>
          </li>
          <li>
            <t>MUST return "not resolved" for a body whose Inbound Message is absent,
and MUST NOT attempt any fallback fetch (<xref target="sec-7-3"/>).</t>
          </li>
          <li>
            <t>MUST tolerate a Binding Record that is absent, and MUST NOT crash or
otherwise misbehave on one that is hostile or malformed (<xref target="sec-9-1"/>).</t>
          </li>
          <li>
            <t>MUST NOT require write access anywhere under <tt>inbound/</tt>, demonstrated by
the operational check below rather than by a fixture (<xref target="sec-4-1"/>).</t>
          </li>
          <li>
            <t>MUST demonstrate its producing side by confirming that its own emitted
requests satisfy the same schema the suite's valid <tt>request-*</tt> fixtures
satisfy, and MUST uphold the write-ordering, atomicity, and
read-discipline obligations of <xref target="sec-4-1"/>, which the fixtures cannot
check.</t>
          </li>
        </ul>
        <t><strong>Peer-origin sub-classes.</strong> The peer-origin profile
(<xref target="sec-peer"/>) 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.</t>
        <t>An implementation claiming <strong>Runtime + peer-origin</strong>
conformance, in addition to the Runtime class:</t>
        <ul spacing="normal">
          <li>
            <t>MUST produce Peer Notices valid against the peer-notice schema
(<xref target="app-f"/>), and MUST confirm that its own emitted Peer Notices
satisfy the same schema the valid <tt>peer-*</tt> fixtures satisfy;</t>
          </li>
          <li>
            <t>MUST refuse to emit into <tt>peer/</tt> any document that would fail that
schema, and MUST NOT emit a <tt>peer</tt> document into <tt>inbound/</tt>
(<xref target="sec-peer-tree"/>);</t>
          </li>
          <li>
            <t>MUST uphold the operational obligations of the profile: the
write-authority partition, proven by the write probe, and verification placement
(<xref target="sec-peer-origin"/>); a Runtime-asserted peer <tt>from</tt>
(<xref target="sec-peer-addr"/>); <tt>in_reply_to</tt> only when resolved, and
refusal rather than stripping (<xref target="sec-peer-corr"/>); the intake
location, and hashing of bytes by the verifier itself
(<xref target="sec-peer-xhost"/>); and <tt>sender_exposure</tt> never settable by a
sender (<xref target="sec-peer-exposure"/>);</t>
          </li>
          <li>
            <t>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 (<xref target="app-h"/>).</t>
          </li>
        </ul>
        <t>An implementation claiming <strong>Connector + peer-origin</strong>
conformance, in addition to the Connector class:</t>
        <ul spacing="normal">
          <li>
            <t>MUST accept every valid <tt>peer-*</tt> fixture and MUST reject every
invalid <tt>peer-*</tt> fixture when read from <tt>peer/notices/</tt>, and MUST
reject <tt>invalid/notice-kind-peer.json</tt> when read from
<tt>inbound/notices/</tt>;</t>
          </li>
          <li>
            <t>MUST derive the origin class from the tree alone
(<xref target="sec-peer-origin"/>), and MUST act on a peer message at most once,
holding rather than fanning out when it cannot identify a single target
(<xref target="sec-peer-addr"/>);</t>
          </li>
          <li>
            <t>MUST tolerate unknown members on a Peer Notice, whose envelope is open
(<xref target="sec-10-1"/>).</t>
          </li>
        </ul>
        <t><strong>Read-only inbound tolerance — the prescribed operational check.</strong> 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 <tt>inbound/</tt> to succeed.</t>
      </section>
    </section>
    <section anchor="sec-11">
      <name>Security Considerations</name>
      <t>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 (<xref target="sec-11-16"/>), which is stated only here, every
requirement referenced here is stated normatively in Sections <xref format="counter" target="sec-4"/> through <xref format="counter" target="sec-10"/>. The
organizing principle throughout is the invariant of <xref target="sec-3-2"/> —
enforcement is Runtime-side, and no property depends on the Agent or its
Connector behaving.</t>
      <t>The threat actor assumed throughout is a <strong>fully compromised Agent</strong> 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.</t>
      <section anchor="sec-11-1">
        <name>Path authority and traversal</name>
        <t>An identifier drawn from a file's <em>contents</em> MUST NOT name a write path
(<xref target="sec-4-1"/>). A Runtime that trusted a <tt>req_id</tt> 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 <em>validated
filename</em>, over a restricted character set, and re-derives every path from
data it controls.</t>
        <t>The same reasoning is why inbound Sidecar directories and the body spool
are keyed on the Runtime-minted notice identifier rather than the provider
<tt>message.id</tt>: 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 <tt>notice_id</tt> and <tt>req_id</tt> to
character sets that are safe as path components; <tt>message.id</tt> is
deliberately unconstrained, because it is never used as one.</t>
      </section>
      <section anchor="sec-11-2">
        <name>Symlinks, descriptors, and open discipline</name>
        <t>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.</t>
        <t><xref target="sec-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.</t>
        <ul spacing="normal">
          <li>
            <t>Pinning only the final component leaves the <em>parent</em> 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.</t>
          </li>
          <li>
            <t>Pinning the parent chain without refusing symlinks <em>while acquiring</em> 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.</t>
          </li>
          <li>
            <t>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 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 (<tt>O_EXCL</tt>).</t>
          </li>
        </ul>
        <t>The append-only audit log is the one write with no <tt>.tmp</tt> sibling to lean
on, and it is called out separately for two reasons. <tt>O_EXCL</tt> cannot apply
to a file that legitimately already exists, so the final-component
protection there is a no-follow, append-mode, <strong>non-blocking</strong> open plus an
<tt>fstat</tt> 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.</t>
        <t>The read side mirrors all of it (<xref target="sec-4-1"/>): per-component no-follow
opens or an equivalently pinned walk, a non-blocking open, an <tt>fstat</tt>
through the open descriptor rather than a pre-open <tt>stat</tt> (which is itself
a race), refusal of a multiply-linked file, and enforced size and count
caps. The caps are not optional hardening: the <tt>too_large</tt> Result value
presumes one, and without them an Agent can present an unbounded Sidecar or
an unbounded request and exhaust the Runtime.</t>
      </section>
      <section anchor="sec-11-3">
        <name>Time-of-check to time-of-use, and ingest by copy</name>
        <t>Even with every open performed correctly, validating a file <em>in place</em> 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 (<xref target="sec-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.</t>
        <t>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
<xref target="sec-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.</t>
      </section>
      <section anchor="sec-11-4">
        <name>Concurrency</name>
        <t>Every atomicity guarantee in this protocol — the <tt>.tmp</tt>-then-rename
discipline, the commit sentinel, the derivation of <tt>req_id</tt> 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 (<xref target="sec-4-1"/>). Two concurrent drains can deliver the same
request twice; two concurrent writers can collide on a <tt>req_id</tt> 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.</t>
      </section>
      <section anchor="sec-11-5">
        <name>Attribution spoofing</name>
        <t>Attribution is the Namespace a file arrived in, never an identifier inside
the file (<xref target="sec-4-1"/>). A compromised Agent can write any <tt>agent_id</tt> 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.</t>
        <t>The Binding Record deserves the same skepticism from the other direction
(<xref target="sec-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.</t>
      </section>
      <section anchor="sec-11-6">
        <name>Untrusted content</name>
        <t>Inbound mail is an injection vector aimed at the Agent's reasoning, and
this protocol's posture is that <em>every sender-originated string</em> is
untrusted content: bodies, subjects, previews, threading references, sender
display names, and attachment Descriptors alike (<xref target="sec-7-1"/>).</t>
        <t>Two cases are worth restating because they are the ones most often assumed
safe.</t>
        <t><strong><tt>message.from</tt> is attacker-controlled display data.</strong> Its display-name
half is chosen freely by the sender, so a value such as
<tt>"security@yourbank.example &lt;attacker@evil.example&gt;"</tt> 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 <tt>sender_standing</tt> and <tt>verdict</tt>, or,
on a Peer Notice, the <tt>message.from</tt> the Runtime asserts
(<xref target="sec-peer-addr"/>).</t>
        <t><strong>An attachment <tt>filename</tt> is an injection payload.</strong> A Descriptor is
metadata about a file the Agent may never receive the bytes of, and a
<tt>clean</tt> Disposition asserts something about <em>bytes</em>, 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. <tt>media_type</tt> 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.</t>
        <t><tt>verdict</tt>, <tt>sender_standing</tt>, and <tt>provenance</tt> 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.</t>
      </section>
      <section anchor="sec-11-7">
        <name>Recipient policy and header injection</name>
        <t>Recipient policy is the exfiltration boundary, and it is a Runtime decision
(<xref target="sec-5-1"/>). The Agent's <tt>draft.to</tt> and <tt>draft.cc</tt> 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.</t>
        <t>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 <xref target="RFC5322"/> for that reason, and the
cost is disclosed: some legitimate <xref target="RFC5322"/> addresses cannot be
expressed under this document.</t>
        <t>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 <em>directly into headers</em> rather than into a body or display
context: <tt>reply_to_message_id</tt>, which becomes an <tt>In-Reply-To:</tt> header, and
an attachment's <tt>media_type</tt>, which becomes a MIME part's <tt>Content-Type:</tt>
          <xref target="RFC2045"/>.</t>
      </section>
      <section anchor="sec-11-8">
        <name>The outbound digest is not a trust boundary</name>
        <t><tt>sha256</tt> on an outbound attachment Descriptor is computed by the Connector
over bytes the Connector wrote (<xref target="sec-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.</t>
        <t>The inbound direction is weaker still and is labelled as such: inbound
<tt>size_bytes</tt> is provider-declared and advisory, not Runtime-verified
(<xref target="sec-7-2"/>).</t>
      </section>
      <section anchor="sec-11-9">
        <name>Confirmation-oracle avoidance</name>
        <t>Inbound metadata is gated so that withholding bytes does not become an
information channel. <tt>sha256</tt> on an inbound Descriptor is emitted only
where the Runtime asserts <tt>clean</tt> (<xref target="sec-7-2"/>). Publishing a digest for a
withheld attachment would let an Agent confirm whether a <em>guessed</em> 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.</t>
        <t>The same principle governs quarantined mail generally: it produces no
Notice at all, rather than a Notice announcing that something was
withheld.</t>
      </section>
      <section anchor="sec-11-10">
        <name>Byte-absence and artifact-absence as the access gate</name>
        <t>The access gate in this protocol is the absence of bytes, not a flag
inside a document (<xref target="sec-4-1"/>). A Runtime MUST NOT place non-clean bytes
in an Agent's Namespace; <tt>content_ref</tt> 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.</t>
        <t>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
(<xref target="sec-7-3"/>). Because retention may also remove an artifact
(<xref target="sec-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.</t>
        <t>The <tt>content_ref</tt> binding rules (<xref target="sec-7-2"/>) close the corresponding
path-confusion risk: a consumer derives the byte path from the enclosing
<tt>notice_id</tt> and the array index rather than from the <tt>content_ref</tt> 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.</t>
      </section>
      <section anchor="sec-11-11">
        <name>Fail-closed version handling, and the cost of tolerance</name>
        <t>A consumer reading a higher major than it speaks fails closed, and the
version check runs <em>before</em> schema validation so that "wrong version" and
"malformed" remain distinguishable (<xref target="sec-10-1"/>). Folding the two
together is how a protocol acquires silent, undiagnosable mail loss.</t>
        <t>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 <tt>content_ref</tt>, 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 <tt>rejected</tt>
Result — rather than silent.</t>
        <t>The value-level tolerance is scoped to the two explicitly advisory
vocabularies for the same reason: <tt>kind</tt>, <tt>outcome</tt>, and <tt>disposition</tt> 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.</t>
      </section>
      <section anchor="sec-11-12">
        <name>Inbound integrity and the writability of the inbound tree</name>
        <t>A read-only inbound mount is the mechanism that makes a delivered Notice
unforgeable (<xref target="sec-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.</t>
        <t>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.</t>
        <t>Correspondingly, a Connector MUST NOT require write access under
<tt>inbound/</tt>, and consumption tracking is Connector-private state kept
elsewhere (<xref target="sec-4-1"/>). A Connector that crashes on a read-only inbound
tree is broken against any Runtime enforcing inbound integrity.</t>
      </section>
      <section anchor="sec-11-13">
        <name>Retention, deletion, and lost verdicts</name>
        <t>Deletion rights are named per artifact class (<xref target="sec-4-2"/>) because an
unnamed right becomes either unbounded growth or an accidental data-loss
path. Two hazards are worth restating.</t>
        <t><strong>A Result deleted before it is read is a lost verdict.</strong> No signal in
this protocol tells a Runtime that a Connector has read one, so
Runtime-side retention SHOULD be age-based and generous.</t>
        <t><strong>Deletion interacts with <tt>req_id</tt> monotonicity.</strong> A Connector exercising
its deletion rights in <tt>processed/</tt> or <tt>results/</tt> removes exactly the
state from which the next <tt>req_id</tt> is derived, and MUST preserve
monotonicity by other durable means. Symmetrically, a Runtime MUST NOT
treat <tt>processed/</tt> 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.</t>
      </section>
      <section anchor="sec-11-14">
        <name>What is wire-verifiable, and what is a Runtime obligation</name>
        <t>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 (<xref target="app-h"/> lists
them too):</t>
        <ul spacing="normal">
          <li>
            <t>Sidecar file existence, and the binding between an ordinal and a
directory entry on disk.</t>
          </li>
          <li>
            <t>Whether <tt>size_bytes</tt> and <tt>sha256</tt> match the bytes actually on disk.</t>
          </li>
          <li>
            <t>Display sanitization of <tt>filename</tt>, and its never-a-path invariant.</t>
          </li>
          <li>
            <t>The keying of inbound Sidecar directories on the Runtime-minted notice
identifier.</t>
          </li>
          <li>
            <t>The location of Runtime-private Staging, outside the mounted Volume.</t>
          </li>
          <li>
            <t>Write ordering and commit-sentinel atomicity.</t>
          </li>
          <li>
            <t>Caps, allowlists, and quotas.</t>
          </li>
          <li>
            <t>The absence of a spooled body for a quarantined or blocked message.</t>
          </li>
          <li>
            <t>A Connector's tolerance of a read-only inbound tree.</t>
          </li>
          <li>
            <t>Ingest by copy.</t>
          </li>
          <li>
            <t>Write-side path discipline, in all three of its conjunctive legs.</t>
          </li>
          <li>
            <t>Read-side open discipline.</t>
          </li>
          <li>
            <t>Single writer and single drainer per Namespace.</t>
          </li>
          <li>
            <t>Compose-time rejection or stripping of CR, LF, and NUL.</t>
          </li>
        </ul>
        <t>Two of these deserve emphasis as trust-model statements rather than as
testing notes. First, <tt>disposition == "clean"</tt> is the <em>Runtime's
assertion</em>, and this document deliberately does not specify the basis for
it; a consumer that treats <tt>clean</tt> 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.</t>
      </section>
      <section anchor="sec-11-15">
        <name>The extension space, and outcomes as claims</name>
        <t><tt>outbound/ext/&lt;name&gt;/</tt> (<xref target="sec-4-1"/>) is Connector-owned and Agent-writable,
and this document specifies no shape for anything under it. Its <tt>&lt;name&gt;</tt>
is a stable, opaque Connector id (<xref target="sec-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 (<xref target="sec-7-1"/>) and a Connector cannot know what a vendor-named
assertion means.</t>
      </section>
      <section anchor="sec-11-16">
        <name>Special-use domain names</name>
        <t>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 <xref target="RFC6761"/> (such as <tt>.test</tt>, <tt>.local</tt>, <tt>.home.arpa</tt> and <tt>.onion</tt>),
and names reserved for private use, such as <tt>.internal</tt>. 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.</t>
      </section>
      <section anchor="sec-11-17">
        <name>The Peer-Origin Profile</name>
        <t>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.</t>
        <t><strong>Forgery of the peer tree.</strong> A component that parses mail
from an open transport is the most exposed component in a Runtime. If it could
write <tt>peer/</tt>, any sender able to reach it could forge authenticated peer
origin. The write-authority partition and the placement of verification
(<xref target="sec-peer-origin"/>) exist to prevent that, and are checked by a
write probe rather than by configuration review.</t>
        <t><strong>Key compromise.</strong> 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 (<xref target="sec-peer-verify"/>) 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 (<xref target="sec-peer-keys"/>) are the mitigations.
Because replay records are scoped to the Runtime rather than the key, rotation
does not reopen a replay window.</t>
        <t><strong>Replay and clocks.</strong> 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 (<xref target="sec-peer-replay"/>) 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.</t>
        <t><strong>Parsing hostile input.</strong> 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 (<xref target="sec-peer-carriage"/>), 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.</t>
        <t><strong>Backscatter and loops.</strong> 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
(<xref target="sec-peer-control"/>). 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.</t>
        <t><strong>Authenticated is not trustworthy.</strong> A verified peer is a
same-principal Agent, which may itself have read hostile external mail and be
relaying it. The tree authenticates <em>who</em>, never <em>what</em>
(<xref target="sec-peer-presentation"/>). <tt>sender_exposure</tt>
(<xref target="sec-peer-exposure"/>) lets a Runtime say whether the sender
was exposed to external mail, and absence of the stamp means that nothing was
assessed.</t>
        <t><strong>Exposure in transit.</strong> 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.</t>
      </section>
    </section>
    <section anchor="sec-12">
      <name>IANA Considerations</name>
      <section anchor="sec-iana-headers">
        <name>Message Header Fields</name>
        <t>The peer-origin profile (<xref target="sec-peer"/>) 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 <xref target="RFC3864"/>:</t>
        <table anchor="tab-iana-headers">
          <name>Header field registrations</name>
          <thead>
            <tr>
              <th align="left">Header Field Name</th>
              <th align="left">Protocol</th>
              <th align="left">Status</th>
              <th align="left">Reference</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td align="left">AMAP-Peer-Statement</td>
              <td align="left">mail</td>
              <td align="left">provisional</td>
              <td align="left"><xref target="sec-peer-carriage"/> of this document</td>
            </tr>
            <tr>
              <td align="left">AMAP-Peer-Control</td>
              <td align="left">mail</td>
              <td align="left">provisional</td>
              <td align="left"><xref target="sec-peer-control"/> of this document</td>
            </tr>
          </tbody>
        </table>
        <t>Change controller: IETF.</t>
      </section>
      <section anchor="sec-iana-open">
        <name>Possible Future Registries</name>
        <t>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 <tt>contract_version</tt>
(<xref target="sec-10-1"/>), whose fail-closed semantics make an unambiguous
assignment important; the open <tt>reason_code</tt> vocabulary of the Result
(<xref target="sec-6-1"/>); and the closed outcome and cause vocabularies of the
Control Notice (<xref target="sec-peer-control"/>).</t>
      </section>
    </section>
  </middle>
  <back>
    <references anchor="sec-combined-references">
      <name>References</name>
      <references anchor="sec-normative-references">
        <name>Normative References</name>
        <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
          <front>
            <title>Key words for use in RFCs to Indicate Requirement Levels</title>
            <author fullname="S. Bradner" initials="S." surname="Bradner"/>
            <date month="March" year="1997"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="2119"/>
          <seriesInfo name="DOI" value="10.17487/RFC2119"/>
        </reference>
        <reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
          <front>
            <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
            <author fullname="B. Leiba" initials="B." surname="Leiba"/>
            <date month="May" year="2017"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="8174"/>
          <seriesInfo name="DOI" value="10.17487/RFC8174"/>
        </reference>
        <reference anchor="RFC8259" target="https://www.rfc-editor.org/info/rfc8259">
          <front>
            <title>The JavaScript Object Notation (JSON) Data Interchange Format</title>
            <author fullname="T. Bray" initials="T." surname="Bray" role="editor"/>
            <date month="December" year="2017"/>
          </front>
          <seriesInfo name="STD" value="90"/>
          <seriesInfo name="RFC" value="8259"/>
          <seriesInfo name="DOI" value="10.17487/RFC8259"/>
        </reference>
        <reference anchor="RFC3629" target="https://www.rfc-editor.org/info/rfc3629">
          <front>
            <title>UTF-8, a transformation format of ISO 10646</title>
            <author fullname="F. Yergeau" initials="F." surname="Yergeau"/>
            <date month="November" year="2003"/>
          </front>
          <seriesInfo name="STD" value="63"/>
          <seriesInfo name="RFC" value="3629"/>
          <seriesInfo name="DOI" value="10.17487/RFC3629"/>
        </reference>
        <reference anchor="RFC5322" target="https://www.rfc-editor.org/info/rfc5322">
          <front>
            <title>Internet Message Format</title>
            <author fullname="P. Resnick" initials="P." surname="Resnick" role="editor"/>
            <date month="October" year="2008"/>
          </front>
          <seriesInfo name="RFC" value="5322"/>
          <seriesInfo name="DOI" value="10.17487/RFC5322"/>
        </reference>
        <reference anchor="RFC2045" target="https://www.rfc-editor.org/info/rfc2045">
          <front>
            <title>Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</title>
            <author fullname="N. Freed" initials="N." surname="Freed"/>
            <author fullname="N. Borenstein" initials="N." surname="Borenstein"/>
            <date month="November" year="1996"/>
          </front>
          <seriesInfo name="RFC" value="2045"/>
          <seriesInfo name="DOI" value="10.17487/RFC2045"/>
        </reference>
        <reference anchor="RFC2046" target="https://www.rfc-editor.org/info/rfc2046">
          <front>
            <title>Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types</title>
            <author fullname="N. Freed" initials="N." surname="Freed"/>
            <author fullname="N. Borenstein" initials="N." surname="Borenstein"/>
            <date month="November" year="1996"/>
          </front>
          <seriesInfo name="RFC" value="2046"/>
          <seriesInfo name="DOI" value="10.17487/RFC2046"/>
        </reference>
        <reference anchor="RFC2047" target="https://www.rfc-editor.org/info/rfc2047">
          <front>
            <title>MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text</title>
            <author fullname="K. Moore" initials="K." surname="Moore"/>
            <date month="November" year="1996"/>
          </front>
          <seriesInfo name="RFC" value="2047"/>
          <seriesInfo name="DOI" value="10.17487/RFC2047"/>
        </reference>
        <reference anchor="RFC2183" target="https://www.rfc-editor.org/info/rfc2183">
          <front>
            <title>Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field</title>
            <author fullname="R. Troost" initials="R." surname="Troost"/>
            <author fullname="S. Dorner" initials="S." surname="Dorner"/>
            <author fullname="K. Moore" initials="K." surname="Moore" role="editor"/>
            <date month="August" year="1997"/>
          </front>
          <seriesInfo name="RFC" value="2183"/>
          <seriesInfo name="DOI" value="10.17487/RFC2183"/>
        </reference>
        <reference anchor="RFC2231" target="https://www.rfc-editor.org/info/rfc2231">
          <front>
            <title>MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations</title>
            <author fullname="N. Freed" initials="N." surname="Freed"/>
            <author fullname="K. Moore" initials="K." surname="Moore"/>
            <date month="November" year="1997"/>
          </front>
          <seriesInfo name="RFC" value="2231"/>
          <seriesInfo name="DOI" value="10.17487/RFC2231"/>
        </reference>
        <reference anchor="RFC8792" target="https://www.rfc-editor.org/info/rfc8792">
          <front>
            <title>Handling Long Lines in Content of Internet-Drafts and RFCs</title>
            <author fullname="K. Watsen" initials="K." surname="Watsen"/>
            <author fullname="E. Auerswald" initials="E." surname="Auerswald"/>
            <author fullname="A. Farrel" initials="A." surname="Farrel"/>
            <author fullname="Q. Wu" initials="Q." surname="Wu"/>
            <date month="June" year="2020"/>
          </front>
          <seriesInfo name="RFC" value="8792"/>
          <seriesInfo name="DOI" value="10.17487/RFC8792"/>
        </reference>
        <reference anchor="RFC8032" target="https://www.rfc-editor.org/info/rfc8032">
          <front>
            <title>Edwards-Curve Digital Signature Algorithm (EdDSA)</title>
            <author fullname="S. Josefsson" initials="S." surname="Josefsson"/>
            <author fullname="I. Liusvaara" initials="I." surname="Liusvaara"/>
            <date month="January" year="2017"/>
          </front>
          <seriesInfo name="RFC" value="8032"/>
          <seriesInfo name="DOI" value="10.17487/RFC8032"/>
        </reference>
        <reference anchor="RFC3339" target="https://www.rfc-editor.org/info/rfc3339">
          <front>
            <title>Date and Time on the Internet: Timestamps</title>
            <author fullname="G. Klyne" initials="G." surname="Klyne"/>
            <author fullname="C. Newman" initials="C." surname="Newman"/>
            <date month="July" year="2002"/>
          </front>
          <seriesInfo name="RFC" value="3339"/>
          <seriesInfo name="DOI" value="10.17487/RFC3339"/>
        </reference>
        <reference anchor="RFC4648" target="https://www.rfc-editor.org/info/rfc4648">
          <front>
            <title>The Base16, Base32, and Base64 Data Encodings</title>
            <author fullname="S. Josefsson" initials="S." surname="Josefsson"/>
            <date month="October" year="2006"/>
          </front>
          <seriesInfo name="RFC" value="4648"/>
          <seriesInfo name="DOI" value="10.17487/RFC4648"/>
        </reference>
        <reference anchor="JSON-SCHEMA-2020-12" target="https://json-schema.org/draft/2020-12/json-schema-core">
          <front>
            <title>JSON Schema: A Media Type for Describing JSON Documents</title>
            <author fullname="Austin Wright" initials="A." surname="Wright" role="editor"/>
            <author fullname="Henry Andrews" initials="H." surname="Andrews" role="editor"/>
            <author fullname="Ben Hutton" initials="B." surname="Hutton" role="editor"/>
            <author fullname="Greg Dennis" initials="G." surname="Dennis"/>
            <date month="June" day="10" year="2022"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-bhutton-json-schema-01"/>
        </reference>
        <reference anchor="FIPS180-4" target="https://doi.org/10.6028/NIST.FIPS.180-4">
          <front>
            <title>Secure Hash Standard (SHS)</title>
            <author>
              <organization>National Institute of Standards and Technology</organization>
            </author>
            <date month="August" year="2015"/>
          </front>
          <seriesInfo name="FIPS PUB" value="180-4"/>
          <seriesInfo name="DOI" value="10.6028/NIST.FIPS.180-4"/>
        </reference>
      </references>
      <references anchor="sec-informative-references">
        <name>Informative References</name>
        <reference anchor="RFC2033" target="https://www.rfc-editor.org/info/rfc2033">
          <front>
            <title>Local Mail Transfer Protocol</title>
            <author fullname="J. Myers" initials="J." surname="Myers"/>
            <date month="October" year="1996"/>
          </front>
          <seriesInfo name="RFC" value="2033"/>
          <seriesInfo name="DOI" value="10.17487/RFC2033"/>
        </reference>
        <reference anchor="RFC5321" target="https://www.rfc-editor.org/info/rfc5321">
          <front>
            <title>Simple Mail Transfer Protocol</title>
            <author fullname="J. Klensin" initials="J." surname="Klensin"/>
            <date month="October" year="2008"/>
          </front>
          <seriesInfo name="RFC" value="5321"/>
          <seriesInfo name="DOI" value="10.17487/RFC5321"/>
        </reference>
        <reference anchor="RFC6761" target="https://www.rfc-editor.org/info/rfc6761">
          <front>
            <title>Special-Use Domain Names</title>
            <author fullname="S. Cheshire" initials="S." surname="Cheshire"/>
            <author fullname="M. Krochmal" initials="M." surname="Krochmal"/>
            <date month="February" year="2013"/>
          </front>
          <seriesInfo name="RFC" value="6761"/>
          <seriesInfo name="DOI" value="10.17487/RFC6761"/>
        </reference>
        <reference anchor="RFC3864" target="https://www.rfc-editor.org/info/rfc3864">
          <front>
            <title>Registration Procedures for Message Header Fields</title>
            <author fullname="G. Klyne" initials="G." surname="Klyne"/>
            <author fullname="M. Nottingham" initials="M." surname="Nottingham"/>
            <author fullname="J. Mogul" initials="J." surname="Mogul"/>
            <date month="September" year="2004"/>
          </front>
          <seriesInfo name="BCP" value="90"/>
          <seriesInfo name="RFC" value="3864"/>
          <seriesInfo name="DOI" value="10.17487/RFC3864"/>
        </reference>
        <reference anchor="MCP" target="https://modelcontextprotocol.io/specification/2025-11-25">
          <front>
            <title>Model Context Protocol specification, revision 2025-11-25</title>
            <author>
              <organization>Anthropic</organization>
            </author>
            <date year="2025" month="November"/>
          </front>
        </reference>
        <reference anchor="A2A" target="https://a2a-protocol.org">
          <front>
            <title>Agent2Agent Protocol, v1.0.1</title>
            <author>
              <organization>Linux Foundation</organization>
            </author>
            <date year="2026" month="May"/>
          </front>
        </reference>
        <reference anchor="ACP-MERGER">
          <front>
            <title>Agent Communication Protocol merges into the Agent2Agent project</title>
            <author>
              <organization>LF AI &amp; Data</organization>
            </author>
            <date year="2025" month="August"/>
          </front>
        </reference>
        <reference anchor="ACP-CLIENT" target="https://agentclientprotocol.com">
          <front>
            <title>Agent Client Protocol</title>
            <author>
              <organization>Zed Industries</organization>
            </author>
            <date>n.d.</date>
          </front>
        </reference>
      </references>
    </references>

<section anchor="app-a">
      <name>Submit-Request Schema</name>
      <t><em>(Normative.)</em> The schema below is written in the JSON Schema 2020-12
dialect <xref target="JSON-SCHEMA-2020-12"/> and governs the artifact defined in
<xref target="sec-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
<tt>rejected</tt> Result rather than as a silent drop (<xref target="sec-10-1"/>).</t>
      <figure anchor="schema-submit-request">
        <name>schemas/submit-request.schema.json</name>
        <sourcecode type="json" name="submit-request.schema.json"><![CDATA[
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": "^[^\\
    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\
    tern": "^[^\u0000-\u001f\u007f]*(?!\\n)$" },
              "size_bytes": { "type": "integer", "minimum": 0 },
              "sha256":     { "type": "string", "pattern": "^[a-f0-9\
    ]{64}(?!\\n)$" }
            }
          }
        }
      }
    }
  }
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-b">
      <name>Result Schema</name>
      <t><em>(Normative.)</em> JSON Schema 2020-12 <xref target="JSON-SCHEMA-2020-12"/>; governs the
artifact defined in <xref target="sec-6"/>. Runtime-authored, and therefore open at
every level: a consumer tolerates unrecognized members (<xref target="sec-10-1"/>). The
<tt>attachments[i].index == i</tt> binding stated in <xref target="sec-6-1"/> is not
expressible here and is enforced as a named post-check by the conformance
gate (<xref target="app-h"/>).</t>
      <figure anchor="schema-result">
        <name>schemas/result.schema.json</name>
        <sourcecode type="json" name="result.schema.json"><![CDATA[
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" } },
    "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"] }
        }
      }
    }
  }
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-c">
      <name>Deliver-Notice Schema</name>
      <t><em>(Normative.)</em> JSON Schema 2020-12 <xref target="JSON-SCHEMA-2020-12"/>; governs the
artifact defined in <xref target="sec-7"/>. Runtime-authored and open at every level.
The two <tt>allOf</tt> conditionals implement the clean-gating of <tt>content_ref</tt>
and <tt>sha256</tt> (<xref target="sec-7-2"/>); the <tt>content_ref</tt> index and prefix binding, by
contrast, is not expressible here and is enforced as a named post-check
(<xref target="app-h"/>).</t>
      <figure anchor="schema-deliver-notice">
        <name>schemas/deliver-notice.schema.json</name>
        <sourcecode type="json" name="deliver-notice.schema.json"><![CDATA[
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\
    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" },
            "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" } } } }
            ]
          }
        }
      }
    }
  }
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-d">
      <name>Inbound-Message Schema</name>
      <t><em>(Normative.)</em> JSON Schema 2020-12 <xref target="JSON-SCHEMA-2020-12"/>; governs the
artifact defined in <xref target="sec-7-3"/>. Runtime-authored and open at every level.
Its Descriptor rules, including the clean-gating conditionals, match the
deliver-notice schema's.</t>
      <figure anchor="schema-inbound-message">
        <name>schemas/inbound-message.schema.json</name>
        <sourcecode type="json" name="inbound-message.schema.json"><![CDATA[
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": [
          { "if": { "required": ["content_ref"] },
            "then": { "properties": { "disposition": { "const": "cle\
    an" } } } },
          { "if": { "required": ["sha256"] },
            "then": { "properties": { "disposition": { "const": "cle\
    an" } } } }
        ]
      }
    }
  }
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-e">
      <name>Binding-Record Schema</name>
      <t><em>(Normative.)</em> JSON Schema 2020-12 <xref target="JSON-SCHEMA-2020-12"/>; governs the
artifact defined in <xref target="sec-9"/>. This envelope is closed, for the reason
given in <xref target="sec-9-1"/> — an anti-secret-leak lint rather than a
compatibility surface.</t>
      <figure anchor="schema-binding-record">
        <name>schemas/binding-record.schema.json</name>
        <sourcecode type="json" name="binding-record.schema.json"><![CDATA[
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" }
  }
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-f">
      <name>Peer-Origin Profile Schemas</name>
      <t><em>(Normative.)</em> JSON Schema 2020-12 <xref target="JSON-SCHEMA-2020-12"/>.
The first schema governs a Peer Notice, the artifact the peer-origin profile
(<xref target="sec-peer"/>) places under <tt>&lt;agent-id&gt;/peer/notices/</tt>
(<xref target="sec-peer-tree"/>). Its envelope is Runtime-authored and therefore open
(<xref target="sec-10-1"/>). A document
with <tt>kind: "peer"</tt> found under <tt>inbound/notices/</tt> fails the deliver-notice
schema (<xref target="app-c"/>), and a document with <tt>kind: "deliver"</tt> found under
<tt>peer/notices/</tt> fails this one; both are dropped under the rule of
<xref target="sec-7-1"/>.</t>
      <figure anchor="schema-peer-notice">
        <name>schemas/peer-notice.schema.json</name>
        <sourcecode type="json" name="peer-notice.schema.json"><![CDATA[
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,
          "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",
          "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\
    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" } } } }
            ]
          }
        }
      }
    }
  }
}
]]></sourcecode>
      </figure>
      <t>Between hosts, a peer message travels as mail carrying a signed Peer
Statement in one <tt>AMAP-Peer-Statement</tt> header field
(<xref target="sec-peer-xhost"/>). 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 <tt>statement-*</tt> fixtures
(<xref target="app-h"/>) exercise the schema, and the signed cases exercise the
verification rules.</t>
      <figure anchor="schema-peer-statement">
        <name>schemas/peer-statement.schema.json</name>
        <sourcecode type="json" name="peer-statement.schema.json"><![CDATA[
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 \
    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": {
      "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\
    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": {
          "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"
        ]
      }
    },
    "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)$"
    }
  }
}
]]></sourcecode>
      </figure>
      <t>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 <tt>AMAP-Peer-Control</tt> header field with the same grammar and
key (<xref target="sec-peer-control"/>). Its <tt>kind</tt> is <tt>"control"</tt>, so
neither document validates as the other; it is closed and carries no free text.</t>
      <figure anchor="schema-peer-control">
        <name>schemas/peer-control.schema.json</name>
        <sourcecode type="json" name="peer-control.schema.json"><![CDATA[
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",
  "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",
      "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": [
          "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"
            ]
          }
        }
      }
    },
    {
      "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",
              "message_id"
            ]
          }
        }
      }
    }
  ]
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-directory">
      <name>Peer-Directory Schema</name>
      <t><em>(Normative.)</em> JSON Schema 2020-12 <xref target="JSON-SCHEMA-2020-12"/>; governs the optional peer
directory defined in <xref target="sec-directory"/>, which a Runtime MAY publish at
<tt>&lt;agent-id&gt;/directory.json</tt>. The document is Runtime-authored and therefore
open (<xref target="sec-10-1"/>), and it is advisory: the schema pins its shape, never
what it predicts.</t>
      <figure anchor="schema-directory">
        <name>schemas/directory.schema.json</name>
        <sourcecode type="json" name="directory.schema.json"><![CDATA[
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": {
      "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)$"
      }
    }
  }
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-roster">
      <name>Fleet-Roster Schema</name>
      <t><em>(Normative.)</em> JSON Schema 2020-12; governs the fleet roster defined in
<xref target="sec-roster"/>. The document is Runtime-authored and therefore open
(<xref target="sec-10-1"/>), and it pins shape only: whether a member exists
says nothing about whether it may be addressed.</t>
      <figure anchor="schema-roster">
        <name>schemas/roster.schema.json</name>
        <sourcecode type="json" name="roster.schema.json"><![CDATA[
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": {
      "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)$"
          }
        }
      }
    }
  }
}
]]></sourcecode>
      </figure>
    </section>
    <section anchor="app-g">
      <name>Golden Examples</name>
      <t><em>(Normative.)</em> The documents below are reproduced byte-verbatim from the
conformance suite (<xref target="app-h"/>). 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
<xref target="sec-9"/> instead of being repeated here.</t>
      <section anchor="fx-request-attachments">
        <name>Valid Submit Request with attachments</name>
        <t>Sidecar blobs are named by array index (<tt>req-&lt;id&gt;.attachments/&lt;i&gt;</tt>); no content_ref may appear on a request and the schema rejects one (<xref target="sec-5-1"/>).</t>
        <figure anchor="fx-request-attachments-src">
          <name>fixtures/valid/request-attachments.json</name>
          <sourcecode type="json" name="request-attachments.json"><![CDATA[
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" }
    ]
  }
}
]]></sourcecode>
        </figure>
      </section>
      <section anchor="fx-notice-clean-ref">
        <name>Valid Deliver Notice with a clean attachment Descriptor</name>
        <t>content_ref's prefix equals the enclosing document's own notice_id and its trailing index equals the Descriptor's own array position (<xref target="sec-7-2"/>); a Connector that relays the Notice without ever dereferencing the reference is fully conformant.</t>
        <figure anchor="fx-notice-clean-ref-src">
          <name>fixtures/valid/notice-attachment-clean-ref.json</name>
          <sourcecode type="json" name="notice-attachment-clean-ref.json"><![CDATA[
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" }
    ]
  }
}
]]></sourcecode>
        </figure>
      </section>
      <section anchor="fx-message-clean">
        <name>Valid Inbound Message</name>
        <t>The body spool for a delivered message is keyed on the Runtime-minted notice_id, never on the provider message.id (<xref target="sec-7-3"/>).</t>
        <figure anchor="fx-message-clean-src">
          <name>fixtures/valid/message-clean.json</name>
          <sourcecode type="json" name="message-clean.json"><![CDATA[
{
  "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" }
  ]
}
]]></sourcecode>
        </figure>
      </section>
      <section anchor="fx-result-attachments-held">
        <name>Valid Result — a policy hold with per-attachment diagnostics</name>
        <t><tt>attachments[i].index == i</tt> 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 (<xref target="sec-6-1"/>).</t>
        <figure anchor="fx-result-attachments-held-src">
          <name>fixtures/valid/result-attachments-held.json</name>
          <sourcecode type="json" name="result-attachments-held.json"><![CDATA[
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" }
  ]
}
]]></sourcecode>
        </figure>
      </section>
      <section anchor="fx-notice-ref-mismatch">
        <name>Invalid Deliver Notice — a content_ref that fails the binding</name>
        <t>The drop rule of <xref target="sec-7-1"/> is two independent checks: this document is schema-valid and is caught only by the content_ref post-check (<xref target="sec-7-2"/>, <xref target="sec-10-3"/>).</t>
        <t>content_ref names a different notice's Sidecar directory and a non-matching index — it is neither <tt>&lt;notice_id&gt;.attachments/0</tt> 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.</t>
        <t>The conformance gate's own errors for this document:</t>
        <sourcecode type="text"><![CDATA[
$.attachments[0].content_ref: 'OTHER-NOTICE.attachments/7' !=
  'n-index-mismatch.attachments/0'
]]></sourcecode>
        <figure anchor="fx-notice-ref-mismatch-src">
          <name>fixtures/invalid/notice-attachment-ref-index-mismatch.json</name>
          <sourcecode type="json" name="notice-attachment-ref-index-mismatch.json"><![CDATA[
{
  "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" }
    ]
  }
}
]]></sourcecode>
        </figure>
      </section>
      <section anchor="fx-notice-unknown-member">
        <name>Valid Deliver Notice carrying an unrecognized member</name>
        <t>The Runtime-authored envelope is OPEN: a consumer MUST ignore a member it does not recognize, at every level (<xref target="sec-10-1"/>).</t>
        <figure anchor="fx-notice-unknown-member-src">
          <name>fixtures/valid/notice-unknown-member.json</name>
          <sourcecode type="json" name="notice-unknown-member.json"><![CDATA[
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" } }
]]></sourcecode>
        </figure>
      </section>
      <section anchor="fx-request-extra-field">
        <name>Invalid Submit Request carrying an unrecognized member</name>
        <t>The Agent-authored envelope stays CLOSED at every level: an unrecognized member is a designed, legible rejected refusal, never a silent drop (<xref target="sec-10-1"/>).</t>
        <t>additionalProperties: false at the top level of the submit-request schema rejects any member this document does not define.</t>
        <t>The conformance gate's own errors for this document:</t>
        <sourcecode type="text"><![CDATA[
$: additional property 'x_injected' not allowed
]]></sourcecode>
        <figure anchor="fx-request-extra-field-src">
          <name>fixtures/invalid/request-extra-field.json</name>
          <sourcecode type="json" name="request-extra-field.json"><![CDATA[
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" } }
]]></sourcecode>
        </figure>
      </section>
      <section anchor="fx-peer-kind-deliver">
        <name>Invalid Peer Notice — a deliver-kind document under the peer tree</name>
        <t>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 (<xref target="sec-peer-tree"/>).</t>
        <t>kind is const "peer" in the peer-notice schema; a document carrying "deliver" fails that const check.</t>
        <t>The conformance gate's own errors for this document:</t>
        <sourcecode type="text"><![CDATA[
$.kind: 'deliver' != const 'peer'
]]></sourcecode>
        <figure anchor="fx-peer-kind-deliver-src">
          <name>fixtures/invalid/peer-kind-deliver.json</name>
          <sourcecode type="json" name="peer-kind-deliver.json"><![CDATA[
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" } }
]]></sourcecode>
        </figure>
      </section>
    </section>
    <section anchor="app-h">
      <name>Conformance Suite</name>
      <t><em>(Normative for the classes of <xref target="sec-10-3"/>; the honesty statement below
is part of the specification.)</em></t>
      <t>The suite consists of golden artifacts under <tt>fixtures/valid/</tt> and
<tt>fixtures/invalid/</tt>, plus a validator, <tt>fixtures/validate.py</tt>.</t>
      <t>The suite contains <strong>121 fixtures — 42 valid and 79 invalid</strong>.</t>
      <table>
        <thead>
          <tr>
            <th align="left">fixture</th>
            <th align="left">schema</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td align="left">
              <tt>valid/directory-both-lanes.json</tt></td>
            <td align="left">
              <tt>directory.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/directory-delegation-only.json</tt></td>
            <td align="left">
              <tt>directory.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/directory-minimal.json</tt></td>
            <td align="left">
              <tt>directory.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/directory-unknown-member.json</tt></td>
            <td align="left">
              <tt>directory.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/identity-basic.json</tt></td>
            <td align="left">
              <tt>binding-record.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/identity-labeled.json</tt></td>
            <td align="left">
              <tt>binding-record.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/message-clean.json</tt></td>
            <td align="left">
              <tt>inbound-message.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/message-unknown-member.json</tt></td>
            <td align="left">
              <tt>inbound-message.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/notice-attachment-clean-ref.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/notice-attachments-descriptors.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/notice-enriched.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/notice-minimal.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/notice-provenance-unrecognized-value.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/notice-unknown-member.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/peer-attachment-clean-ref.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/peer-exposure-false.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/peer-exposure-true.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/peer-minimal.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/peer-result.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/peer-unknown-member.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/request-attachment-empty-file.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/request-attachments-empty.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/request-attachments.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/request-minimal.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/request-reply.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/result-accepted.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/result-attachments-accepted.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/result-attachments-held.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/result-attachments-rejected-malformed.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/result-queued.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/result-unknown-member.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/roster-basic.json</tt></td>
            <td align="left">
              <tt>roster.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/roster-empty.json</tt></td>
            <td align="left">
              <tt>roster.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/roster-unknown-member.json</tt></td>
            <td align="left">
              <tt>roster.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/control-demoted-body-digest.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/control-dropped-duplicate.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/control-refused-no-edge.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/control-refused-policy.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/statement-exposure-true.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/statement-minimal.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/statement-result-attachment.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>valid/statement-sender-since.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/directory-display-name-address.json</tt></td>
            <td align="left">
              <tt>directory.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/directory-missing-mail-to.json</tt></td>
            <td align="left">
              <tt>directory.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/directory-version-1.json</tt></td>
            <td align="left">
              <tt>directory.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/identity-extra-field.json</tt></td>
            <td align="left">
              <tt>binding-record.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/identity-missing-address.json</tt></td>
            <td align="left">
              <tt>binding-record.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/message-missing-body.json</tt></td>
            <td align="left">
              <tt>inbound-message.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-attachment-bad-disposition.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-attachment-content-ref-trailing-newline.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-attachment-missing-disposition.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-attachment-ref-index-mismatch.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-attachment-ref-not-clean.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-attachment-sha-not-clean.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-attachment-traversal-ref.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-kind-peer.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-missing-preview.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-notice-id-trailing-newline.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-traversal-noticeid.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-version-1.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/notice-wrong-kind.json</tt></td>
            <td align="left">
              <tt>deliver-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-attachment-ref-index-mismatch.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-exposure-missing-asserted-by.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-exposure-true-missing-last-ts.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-from-crlf.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-from-display-name.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-in-reply-to-encoded.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-kind-deliver.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-mailbox-inbox.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-message-id-encoded.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/peer-notice-id-trailing-newline.json</tt></td>
            <td align="left">
              <tt>peer-notice.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-attachment-bad-sha.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-attachment-content-ref.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-attachment-filename-crlf.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-attachment-media-type-trailing-newline.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-attachment-missing-sha.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-attachment-negative-size.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-empty-to.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-extra-field.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-recipient-display-name.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-recipient-trailing-newline.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-reply-id-crlf.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-reqid-trailing-newline.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-subject-crlf.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-subject-trailing-newline.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-traversal-reqid.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/request-version-absent.json</tt></td>
            <td align="left">
              <tt>submit-request.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/result-attachment-bad-outcome.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/result-attachments-dup-index.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/result-attachments-gap-index.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/result-attestation-missing-form.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/result-bad-attestation-form.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/result-bad-outcome.json</tt></td>
            <td align="left">
              <tt>result.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/roster-display-name-address.json</tt></td>
            <td align="left">
              <tt>roster.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/roster-member-missing-state.json</tt></td>
            <td align="left">
              <tt>roster.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/roster-missing-members.json</tt></td>
            <td align="left">
              <tt>roster.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/roster-version-1.json</tt></td>
            <td align="left">
              <tt>roster.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/control-free-text.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/control-kind-peer.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/control-outcome-cause-mismatch.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/control-policy-code-without-policy.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/control-policy-without-code.json</tt></td>
            <td align="left">
              <tt>peer-control.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-attachment-unknown-member.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-digest-uppercase.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-exposure-date-free-text.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-exposure-unknown-member.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-from-display-name.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-id-dotted.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-id-leading-dash.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-in-reply-to-leading-dash.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-id-encoded.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-key-id-empty.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-missing-attachments.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-sender-since-date-only.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-subject-crlf.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-to-array.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-ts-fractional.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-ts-offset.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-unknown-member.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-v-boolean.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
          <tr>
            <td align="left">
              <tt>invalid/statement-v2.json</tt></td>
            <td align="left">
              <tt>peer-statement.schema.json</tt></td>
          </tr>
        </tbody>
      </table>
      <t><strong>Running it.</strong> The validator is stdlib-only and takes no arguments:</t>
      <sourcecode type="text"><![CDATA[
python3 fixtures/validate.py
]]></sourcecode>
      <t>It exits 0 if and only if every fixture under <tt>valid/</tt> passes and every
fixture under <tt>invalid/</tt> fails. It requires no counterpart, no network, and
no dependency: <strong>the fixtures are the other side.</strong></t>
      <t><strong>Signed cases for the peer-origin profile.</strong> The validator then
runs <tt>fixtures/peer-signed/check_cases.py</tt>, which checks three further sets,
all of which a Runtime + peer-origin implementation that verifies peer traffic
from other hosts MUST pass (<xref target="sec-10-3"/>):</t>
      <ul spacing="normal">
        <li>
          <t><tt>rfc8032-vectors.json</tt>: the test vectors of
<xref target="RFC8032" section="7.1"/> (TEST 1 to 3), and negative vectors for the
verification policy of <xref target="sec-peer-keys"/>, each marked for rejection
either at verification or at configuration, which are two separate gates;</t>
        </li>
        <li>
          <t><tt>mime/</tt>: 10 raw messages, and the candidate the selection rule of
<xref target="sec-peer-carriage"/> makes of each, reduced by a byte-exact
splitter;</t>
        </li>
        <li>
          <t><tt>cases/</tt>: 114 sequences of candidates and events (a crash at each
point of <xref target="sec-peer-replay"/>, 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 <tt>receiver.json</tt> or
<tt>receiver-host-a.json</tt> MUST reach. Single-step cases break exactly one
rule; <tt>multi-</tt> cases pin the precedence of
<xref target="sec-peer-verify"/>; <tt>seq-</tt> cases pin state across steps;
<tt>config-</tt> cases are configurations a receiver MUST refuse to load. The
receiver files set retention to its minimum, P, and the cases that probe the
retention boundary assume it; a receiver that retains records longer, as
<xref target="sec-peer-replay"/> permits, maps those steps by its own
retention.</t>
        </li>
      </ul>
      <t><tt>check_cases.py</tt> models only the state that
<xref target="sec-peer-replay"/> 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.</t>
      <t><strong>How a fixture is mapped to a schema.</strong> By filename prefix — <tt>request-</tt> to
the submit-request schema, <tt>notice-</tt> to deliver-notice, <tt>result-</tt> to
result, <tt>message-</tt> to inbound-message, <tt>identity-</tt> to binding-record, and
<tt>peer-</tt> to peer-notice, <tt>statement-</tt> to peer-statement,
<tt>control-</tt> to peer-control (<xref target="app-f"/>), <tt>directory-</tt> to
peer-directory (<xref target="app-directory"/>) and <tt>roster-</tt> to roster
(<xref target="app-roster"/>).</t>
      <t><strong>What the validator checks.</strong> Schema validation in the JSON Schema
2020-12 dialect <xref target="JSON-SCHEMA-2020-12"/>, over the subset of keywords these
schemas use (<tt>type</tt>, <tt>const</tt>, <tt>enum</tt>, <tt>minLength</tt>, <tt>pattern</tt>, <tt>minItems</tt>,
<tt>minimum</tt>, <tt>items</tt>, <tt>required</tt>, <tt>properties</tt>, <tt>additionalProperties</tt>,
<tt>allOf</tt>, and a single <tt>if</tt>/<tt>then</tt> pair) — plus two <strong>named post-checks</strong>,
neither expressible in standard JSON Schema:</t>
      <ul spacing="normal">
        <li>
          <t>the Result-side binding <tt>attachments[i].index == i</tt>, contiguous and
unique over <tt>0</tt> through <tt>n-1</tt> (<xref target="sec-6-1"/>);</t>
        </li>
        <li>
          <t>the <tt>content_ref</tt> index and prefix binding on Notice, Inbound Message,
and peer-notice documents: every Descriptor carrying <tt>content_ref</tt> must
equal <tt>&lt;notice_id&gt;.attachments/&lt;i&gt;</tt> for its own array position <tt>i</tt>,
binding both the trailing index and the leading prefix (<xref target="sec-7-2"/>).</t>
        </li>
      </ul>
      <t>A Connector conformance claim requires running <strong>both</strong> the schema
validation and the second post-check, because exactly one invalid
<tt>notice-*</tt> fixture — and its <tt>peer-*</tt> counterpart — is schema-valid on its
own and is caught only by that post-check (<xref target="app-g"/>, <xref target="sec-10-3"/>).</t>
      <t><strong>A note on pattern anchoring, and a portability cost.</strong> Every schema
pattern in this suite that ends in <tt>$</tt> is written as <tt>(?!\n)$</tt>. The reason
is that a bare <tt>$</tt> matches before a single trailing newline in several
widely used regex engines — Python's <tt>re</tt>, 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 <tt>$</tt>
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 <tt>\r\n</tt> and were already rejected even under a permissive
engine.</t>
      <t>The cost is disclosed rather than discovered: <strong>lookahead-less engines
cannot compile these patterns at all.</strong> RE2 (Go's <tt>regexp</tt>), Rust's
<tt>regex</tt> crate, and POSIX ERE reject <tt>(?!</tt> 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 <tt>$</tt> 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).</t>
      <t><strong>What a green run proves.</strong> That every document shape this protocol
defines is accepted or rejected as specified, by an implementation's own
parser, with no counterpart present.</t>
      <t><strong>What a green run does NOT prove.</strong> The following are filesystem and
policy invariants that no document validator can observe. They are
obligations of the conformance classes regardless (<xref target="sec-10-3"/>), and are
checked operationally:</t>
      <ul spacing="normal">
        <li>
          <t>Sidecar file existence — an extra file, a missing file, or a directory
present when the Descriptor array is empty or absent.</t>
        </li>
        <li>
          <t><tt>size_bytes</tt> and <tt>sha256</tt> actually matching the bytes on disk.</t>
        </li>
        <li>
          <t><tt>filename</tt> display-sanitization, and the "never a path" invariant.</t>
        </li>
        <li>
          <t>The binding between an ordinal and a directory entry on disk. The schema
checks only the <em>Descriptor</em> array's own index member, not that
directory entry <tt>N</tt> exists.</t>
        </li>
        <li>
          <t>Inbound Sidecar directories keyed on the Runtime-minted notice
identifier rather than the provider <tt>message.id</tt>.</t>
        </li>
        <li>
          <t>The location of Runtime-private Staging, outside the mounted Volume.</t>
        </li>
        <li>
          <t>Write ordering and commit-sentinel atomicity — Sidecars before
<tt>req-&lt;id&gt;.json</tt>; <tt>.tmp</tt> plus rename.</t>
        </li>
        <li>
          <t>Caps and allowlists — type allowlists, size caps, Volume quotas.</t>
        </li>
        <li>
          <t>The absence of a spooled body for a quarantined or blocked message
(enforcement by absence, <xref target="sec-7-3"/>).</t>
        </li>
        <li>
          <t>A Connector's tolerance of a read-only <tt>inbound/</tt> 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 (<xref target="sec-10-3"/>).</t>
        </li>
        <li>
          <t>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 (<xref target="sec-4-1"/>).</t>
        </li>
        <li>
          <t>Write-side path discipline, all three conjunctive legs: full
parent-chain resolution through pinned directory descriptors from the
Namespace root; each directory component opened <tt>O_NOFOLLOW|O_DIRECTORY</tt>
at the moment it is added to that chain; and the final component created
<tt>O_EXCL</tt> and never followed, failing closed on <tt>EEXIST</tt> and <tt>ELOOP</tt>. Any
one leg alone is a known-insufficient partial mitigation (<xref target="sec-4-1"/>).</t>
        </li>
        <li>
          <t>Read-side open discipline — per-component no-symlink-follow opens,
non-blocking opens, <tt>fstat</tt>-based regular-file verification through the
open descriptor, and bounded reads and caps on every Agent-influenced
path (<xref target="sec-4-1"/>).</t>
        </li>
        <li>
          <t>Single writer and single drainer per Namespace — an operational and
deployment invariant, such as an OS-level lock held for the process
lifetime (<xref target="sec-4-1"/>).</t>
        </li>
        <li>
          <t>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
(<xref target="sec-5-1"/>).</t>
        </li>
        <li>
          <t>The peer-origin profile's operational obligations (<xref target="sec-peer"/>),
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 <tt>peer/</tt>, and the
prescribed check is a write attempt from inside the admitting component
that must fail; intake isolation for shared-transport peer candidates, in
a location that is neither <tt>inbound/</tt> nor <tt>peer/</tt> and unreachable from
any Agent Namespace; verification placement in a component holding peer
write authority that hashes body and attachment bytes itself;
<tt>sender_exposure</tt> never sender-settable; a Peer Notice carrying
<tt>in_reply_to</tt> 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 (<xref target="sec-peer-xhost"/>).</t>
        </li>
      </ul>
      <t>Beyond these, the behavioral obligations of Sections <xref format="counter" target="sec-7"/> and
<xref format="counter" target="sec-8"/> — the untrusted-content posture, the treatment of <tt>task_id</tt> as
correlation rather than authorization, and the requirement that verified
sender identity never be taken from a mail <tt>message.from</tt> — sit outside the
fixture gate entirely. They constrain what an implementation <em>does</em> with a
valid document, which no document-level gate can see.</t>
    </section>
    <section anchor="app-i">
      <name>A Deferred Seam-Visible "Handled" Signal (Non-Normative)</name>
      <t><strong>This appendix is non-normative and is not in force at this document
version.</strong> 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.</t>
      <t><strong>The gap.</strong> Making inbound consumption-tracking Connector-private
(<xref target="sec-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."</t>
      <t><strong>A sketch, not a specification.</strong> One shape that would bridge it is a
Connector-written, zero-byte marker per acknowledged Notice, written into
<tt>outbound/</tt>, where the Connector already has write access in every
posture:</t>
      <figure anchor="ex-ack-sketch">
        <name>Sketch only — not part of this protocol</name>
        <sourcecode type="text"><![CDATA[
outbound/acks/notice-<id>   # sketch only - not part of the protocol
]]></sourcecode>
      </figure>
      <t>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.</t>
      <t><strong>Why a Connector-forgeable signal would be acceptable here.</strong> 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.</t>
      <t><strong>Why a high-water mark cannot substitute.</strong> <xref target="sec-8"/> requires <tt>task_id</tt>
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 <em>set</em> of identifiers, or
time-ordered — and time-ordering would require the consumer to process
strictly in delivery order, which nothing today requires.</t>
      <t><strong>The lifecycle this would enable, and its boundaries.</strong> 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.</t>
      <ul spacing="normal">
        <li>
          <t><strong>The seam is not the record.</strong> 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.</t>
        </li>
        <li>
          <t><strong>An Agent-writable archive is never evidence.</strong> A compacted archive
living under <tt>outbound/</tt>, 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.</t>
        </li>
        <li>
          <t><strong>It must not grow the read surface.</strong> 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.</t>
        </li>
      </ul>
      <t><strong>Why this is deferred rather than specified now.</strong> A capability starts
with a change to this document once a second implementation independently
wants it, with members and fixtures proposed together. 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.</t>
      <t><strong>One honesty note for a future implementer.</strong> 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.</t>
    </section>
    <section anchor="app-history" removeInRFC="true">
      <name>Historical Background</name>
      <t><em>(Informative.)</em></t>
      <t>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.</t>
      <section anchor="sec-hist-00">
        <name>draft-rapp-ammp-00 — brought to the IETF (contract 3.1.0)</name>
        <ul spacing="normal">
          <li>
            <t><strong>One document.</strong> The peer-origin profile's prose, previously a
companion document in the repository, is now <xref target="sec-peer"/>. 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
<tt>contract_version</tt> (<xref target="sec-10-1"/>), which stays
<tt>"2"</tt>. The reasoning that explained earlier version changes moved into
this appendix (<xref target="sec-hist-versioning"/>).</t>
          </li>
          <li>
            <t><strong>The protocol is AMMP.</strong> 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 <xref target="sec-iana-headers"/>
keep their names.</t>
          </li>
          <li>
            <t><strong>Editorial review for submission.</strong> Terminology was
alphabetized and Drop and Fail Closed became defined terms; requirements
previously phrased as "never" are, where they are requirements, stated with
<xref target="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
<xref target="sec-why-ietf"/>; and IANA considerations were added for the two
header fields the profile defines.</t>
          </li>
          <li>
            <t><strong>The signed statement between hosts</strong>, 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 <tt>^[A-Za-z0-9_][A-Za-z0-9_-]*$</tt>, the
<tt>notice_id</tt> character set without "<tt>.</tt>"
(<xref target="sec-peer-stmt"/>; see the correction to the 3.1.0 entry below);
a point is decoded canonically before the small-order check
(<xref target="sec-peer-keys"/>), 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
(<xref target="sec-peer-carriage"/>); Fmax after the loss of H is taken
from the retained history of F if provable, and is 900 otherwise
(<xref target="sec-peer-replay"/>); and how a Runtime divides its intake is
stated to be an implementation choice (<xref target="sec-peer-intake"/>).
Fixtures: two invalid statement fixtures (121 in all: 42 valid, 79
invalid); signed cases for a leading "<tt>-</tt>" and for the loss of the F
history (114); and a MIME case for the size limit (10).</t>
          </li>
        </ul>
      </section>
      <section anchor="sec-hist-versioning">
        <name>How the versioning rules came about</name>
        <t><strong>The move from wire major <tt>"1"</tt> to <tt>"2"</tt>.</strong> When
attachment support was added, every top-level schema and every nested object
except <tt>verdict</tt> and <tt>attestation.claims</tt> was
<tt>additionalProperties: false</tt>. Under that regime, adding a member was
not a safe additive change: a strict major-1 validator rejected the
<strong>whole document</strong>, not just the new member. Inbound, that was
<strong>silent total message loss</strong> — an 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
<tt>contract_version</tt> stayed <tt>"1"</tt>. That is why attachments
required a major change: a major-1 consumer receiving a <tt>"2"</tt> document
failed closed <em>by version</em>, a legible and designed refusal, and both
sides upgraded deliberately.</t>
        <t><strong>Two version axes.</strong> 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 <tt>"2"</tt>. Neither of its two producer-side narrowings was the
failure a wire major exists to prevent: the Submit Request tightening yields
a per-request <tt>rejected</tt> Result, and the <tt>notice_id</tt>
character-set tightening yields a per-Notice drop under check 1 of
<xref target="sec-7-1"/>, a real cost, disclosed, but narrow and legible.
Changing the wire major to <tt>"3"</tt> 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 <tt>"2"</tt>.</t>
        <t><strong>Corrections.</strong> 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.</t>
        <ol spacing="normal" type="1"><li>
            <t><strong>The write-rule "strengthening" that was a regression.</strong> 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 <strong>weakening</strong>: a parent chain
pinned by descriptor is silent about the final <tt>.tmp</tt> filename itself,
so dropping exclusive-create there let an Agent that pre-plants a symlink at a
predictable <tt>.tmp</tt> name have the Runtime write through it.
<xref target="sec-4-1"/> states the rule conjunctively, with all three legs
required together.</t>
          </li>
          <li>
            <t><strong>The drop rule described as a closure over schema validation.</strong>
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 <tt>content_ref</tt> index and prefix
binding is not expressible in the schema dialect used here, and a fixture
exists that is schema-valid on its own and must nonetheless be dropped.
<xref target="sec-7-1"/> states two independent checks, and so does the
Connector conformance class.</t>
          </li>
        </ol>
        <t>A third correction concerns the change history itself: the contract
version that introduced <tt>message.provenance</tt> 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.</t>
        <t><strong>Lessons from implementation.</strong> Several rules in this document
exist because an early implementation got them wrong: it trusted a
<tt>req_id</tt> 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 <tt>task_id</tt> values; and its schema coverage
of control characters stopped at <tt>subject</tt> and <tt>filename</tt>, missing
the two members composed directly into headers. Each was a confirmed exploit
path, and each is now a requirement (<xref target="sec-4-1"/>,
<xref target="sec-5-1"/>, <xref target="sec-8"/>).</t>
        <t><strong>The peer-origin profile's presentation rule.</strong> 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. <xref target="sec-peer-presentation"/> states
what a presentation must and must not convey.</t>
      </section>
      <section anchor="sec-draft-the-protocol-is-amap-the-outbound-extension-space-the-peer-origin-profiles-schema">
        <name>3.1.0 — the protocol is AMAP; the outbound extension space; the peer-origin profile</name>
        <t>An additive <strong>minor</strong> bump; the wire major stays <tt>"2"</tt>.
The entries below are as recorded at the time: the companion document they
cite is now <xref target="sec-peer"/>, and the items marked proposed were
adopted in draft-rapp-ammp-00 (<xref target="sec-hist-00"/>).</t>
        <ul spacing="normal">
          <li>
            <t><strong>The signed cross-host peer statement and control notice</strong>
(2026-10-09, proposed, not yet reviewed; companion-document section 5 revision s5-r4).
Two new schemas (<xref target="app-f"/>): the statement a sending Runtime signs with
Ed25519 and carries in one <tt>AMAP-Peer-Statement</tt> header field, and the control
notice a receiving Runtime returns about a statement it did not place, both CLOSED at
every level; <tt>statement-*</tt> and <tt>control-*</tt> fixtures; and, in
<tt>fixtures/peer-signed/</tt>, 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 are the companion document's (<tt>spec/peer-origin.md</tt>,
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).</t>
            <t><strong>Correction (draft-rapp-ammp-00).</strong> The statement schema
accepted an <tt>id</tt> or <tt>in_reply_to</tt> beginning with "<tt>-</tt>",
which the <tt>notice_id</tt> 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 (<xref target="sec-hist-00"/>), and the case was minted
again.</t>
          </li>
          <li>
            <t><strong>The fleet roster</strong> (2026-09-24). A new, optional artifact
(<xref target="sec-roster"/>, schema in <xref target="app-roster"/>): 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.</t>
          </li>
          <li>
            <t><strong>The protocol is AMAP; the schema <tt>$id</tt> authority is <tt>amap-spec</tt></strong>
(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 <tt>draft-ietf-dtn-amp</tt>; the
specification repository moves from <tt>agent-mailbox-protocol</tt> to
<tt>amap-spec</tt> with it. The six schema <tt>$id</tt> values move from
<tt>https://agent-mailbox-protocol/schemas/…</tt> to <tt>https://amap-spec/schemas/…</tt>.
Nothing on the wire changes shape: no document carries the protocol's name
as a field, and <tt>$id</tt> is an identifier the gate never dereferences.
Implementations that mirror the Runtime's address spell its local part
<tt>amap.router</tt> 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 (<tt>spec/peer-origin.md</tt>, itself DRAFT), and
its schema and fixtures may still change before it is settled.</t>
          </li>
          <li>
            <t><strong>Outbound extension space.</strong> <tt>outbound/ext/&lt;name&gt;/</tt> 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 (<xref target="sec-4-1"/>). <tt>&lt;name&gt;</tt> 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 <tt>claude-code</tt> 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 (<xref target="sec-11-15"/>). There is deliberately no inbound
extension space.</t>
          </li>
          <li>
            <t><strong>Peer-notice schema.</strong> The peer-origin profile's artifact is pinned by
<tt>peer-notice.schema.json</tt> (<xref target="app-f"/>): the deliver-notice layout with
<tt>kind</tt> and <tt>mailbox</tt> fixed to <tt>peer</tt>, <tt>message.from</tt> a bare addr-spec,
message identifiers in the notice-identifier character set, and the
optional Runtime-asserted <tt>sender_exposure</tt> stamp with its required
members. The profile's prose is the companion document
<tt>spec/peer-origin.md</tt> (v3.1.0 DRAFT).</t>
          </li>
          <li>
            <t><strong>The Peer Directory.</strong> A Runtime may publish a per-instance
projection of the authorisation graph, <tt>directory.json</tt>, at the Agent's
Namespace root (<xref target="sec-directory"/>, schema in <xref target="app-directory"/>), 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 <tt>contract_version</tt> stays <tt>"2"</tt>.</t>
            <t><em>Restored 2026-10-05.</em> 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.</t>
          </li>
          <li>
            <t><strong>Clarifications from implementation</strong> (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 <tt>&lt;agent-id&gt;</tt>
of every Namespace it serves and rejects a request from a Namespace it cannot
name (<xref target="sec-4-1"/>); <tt>outbound/ext/&lt;name&gt;/</tt>'s id is a lowercase
single path segment (<xref target="sec-4-1"/>); a Runtime may be built from several
trusted components, and its obligations bind the composite (<xref target="sec-3-5"/>,
informative); the Deletion rights table gains rows for <tt>outbound/ext/&lt;name&gt;/</tt>
and <tt>directory.json</tt> (<xref target="sec-4-2"/>); <tt>message.id</tt> is the mail store's
identifier, not the Message-ID (<xref target="sec-7"/>); and a Runtime does not hand mail
for special-use or private-use domain names to a mail system outside the
deployment (<xref target="sec-11-16"/>). The companion peer-origin profile clarifies
what a routable peer domain is. No schema or fixture changes.</t>
          </li>
          <li>
            <t><strong>Fixtures: 75 total (27 valid, 48 invalid), up from 58.</strong> 16 <tt>peer-*</tt>
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 <tt>deliver</tt> kind
under the peer tree, a <tt>peer</tt> kind under the inbound tree, a display-name
or CRLF <tt>from</tt>, encoded message identifiers, a missing <tt>asserted_by</tt>, a
missing last-delivery timestamp when exposure is asserted true, an inbox
<tt>mailbox</tt>, a <tt>content_ref</tt> index mismatch, and a trailing-newline
identifier. The validator maps the <tt>peer-</tt> prefix to the new schema and
applies the <tt>content_ref</tt> post-check to it. Three existing fixtures had
example strings normalised to <tt>example.*</tt> 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.</t>
            <t><strong>Correction (2026-10-05).</strong> The count above omits the seven
<tt>directory-*</tt> 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).</t>
            <t><strong>Correction (draft-rapp-ammp-00).</strong> 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 (<xref target="app-h"/>).</t>
          </li>
        </ul>
      </section>
      <section anchor="sec-direction-aware-tolerance-and-six-closed-safety-holes">
        <name>3.0.0 — direction-aware tolerance, and six closed safety holes</name>
        <t>A <strong>major document bump</strong>; the wire major stays <tt>"2"</tt>. <xref target="sec-hist-versioning"/> states
why the two numbers diverge here, and why bumping the wire would have
caused the exact harm this version exists to repair.</t>
        <ul spacing="normal">
          <li>
            <t><strong>Unknown-member tolerance, direction-aware.</strong> The three Runtime-authored
artifacts — deliver-notice, inbound-message, and result — became <strong>open</strong>
at every level: <tt>additionalProperties: false</tt> was removed from the top
level, from <tt>message</tt>, from Descriptor items, and from <tt>attestation</tt>, and
a consumer must tolerate an unrecognized member. Emitting one remains
producer non-conformance. The Agent-authored submit-request envelope
stays <strong>closed</strong> 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.</t>
          </li>
          <li>
            <t><strong>...and for values.</strong> The <tt>enum</tt> constraint was removed from
<tt>message.sender_standing</tt> and <tt>message.provenance</tt>, the two explicitly
advisory vocabularies, and a consumer must tolerate an unrecognized value
in either. <tt>kind</tt>, <tt>outcome</tt> (both levels), and <tt>disposition</tt> remain
closed, enum-locked vocabularies.</t>
          </li>
          <li>
            <t><strong>Ingest by copy.</strong> 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 (<xref target="sec-4-1"/>).</t>
          </li>
          <li>
            <t><strong>Address syntax and header-injection discipline.</strong> A new obligation in
<xref target="sec-5-1"/> plus schema patterns on <tt>draft.to</tt>/<tt>cc</tt>, <tt>draft.subject</tt>,
attachment <tt>filename</tt> and <tt>media_type</tt>, <tt>draft.reply_to_message_id</tt>,
<tt>in_reply_to</tt>, <tt>agent_id</tt>, and <tt>ts</tt> — including the two members composed
directly into headers, which earlier schema coverage missed.</t>
          </li>
          <li>
            <t><strong>Path discipline, write- and read-side both.</strong> The write-side rule
became <strong>conjunctive</strong> (<xref target="sec-4-1"/>), and a new read-side rule applies
the equivalent discipline to every Agent-influenceable read. A
<tt>notice_id</tt> character-set pattern was added to the deliver-notice and
inbound-message schemas so that every legal <tt>notice_id</tt> is also a legal
<tt>content_ref</tt> prefix. <strong>Disclosed cost:</strong> <tt>notice_id</tt> 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 <em>pattern violation on a required
member</em>, and a conforming Connector drops them. <strong>Migration:</strong> 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.</t>
          </li>
          <li>
            <t><strong>Single writer, single drainer.</strong> <xref target="sec-4-1"/> now names the
concurrency assumption that every atomicity, commit-sentinel, and
<tt>req_id</tt> rule already depended on.</t>
          </li>
          <li>
            <t><strong><tt>message.from</tt> named untrusted.</strong> The untrusted-content sentence now
covers explicitly the member most likely to be rendered to a human or
used for a trust judgement (<xref target="sec-7-1"/>).</t>
          </li>
          <li>
            <t><strong>Document authority.</strong> 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.</t>
          </li>
          <li>
            <t><strong>Corrections shipped with this version</strong>, 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
(<xref target="sec-hist-versioning"/>); (ii) the inbound drop rule described as a pure closure
over schema validation was refuted by this version's own
<tt>content_ref</tt>-binding fixture, and is now stated as two independent
checks (<xref target="sec-7-1"/>, <xref target="sec-10-3"/>); (iii) the claim that this version's
producer bytes are unchanged is explicitly <em>not</em> 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.</t>
          </li>
          <li>
            <t><strong>Fixtures: 58 total (21 valid, 37 invalid), up from 44.</strong> The
unknown-member notice fixture moved from the invalid set to the valid set
and was extended with an unrecognized member inside <tt>message</tt>; 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
<tt>additionalProperties</tt> 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
<tt>notice_id</tt> fixture was added; the <tt>content_ref</tt> index-mismatch fixture
was added, and is the one invalid <tt>notice-*</tt> case that is schema-valid on
its own; and six fixtures pin the trailing-newline anchoring case.</t>
          </li>
        </ul>
      </section>
      <section anchor="sec-inbound-byte-access-in-force">
        <name>2.3.0 — inbound byte access in force</name>
        <t>A <strong>minor bump</strong>, prose only: no schema rule and no fixture changed, and
the wire major stayed <tt>"2"</tt>. It resolved an inconsistency carried since
2.0.0. A fixture showing a <tt>clean</tt> Descriptor with <tt>content_ref</tt> and
<tt>sha256</tt> 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 <xref target="sec-7-2"/> — a Runtime may publish bytes and emit
<tt>content_ref</tt>/<tt>sha256</tt> only for a Descriptor it asserts <tt>clean</tt>, and a
Runtime that cannot make that determination must not assert <tt>clean</tt>. The
substantive clarification is that <tt>disposition</tt> is the Runtime's
<em>assertion</em>, 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 <tt>content_ref</tt>.</t>
      </section>
      <section anchor="sec-deletion-rights-read-only-inbound">
        <name>2.2.0 — deletion rights, read-only inbound</name>
        <t>A <strong>permissive minor bump</strong>: 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 <tt>processed/</tt> 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 <tt>req_id</tt> monotonicity by
other durable means. It also pinned three previously implicit
<tt>reason_code</tt> rules: setting one on every Result became a keyworded
SHOULD, and a consumer must tolerate an unrecognized code and must not
infer <tt>outcome</tt> from it.</t>
      </section>
      <section anchor="sec-the-body-spool-and-three-optional-result-members">
        <name>2.1.0 — the body spool, and three optional Result members</name>
        <t>Recorded at the time as an <strong>additive minor bump</strong>: one new optional
spooled artifact (the Inbound Message) and its schema, one new optional
Notice member (<tt>provenance</tt>), and three new optional Result members
(<tt>reason_code</tt>, <tt>attestation</tt>, <tt>ts</tt>). No existing schema gained a required
member, no existing fixture changed shape, and the wire major stayed <tt>"2"</tt>.</t>
        <t><strong>Correction (recorded at 3.0.0).</strong> 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 <tt>provenance</tt> member failed schema validation as a <strong>whole document</strong>
and was silently dropped: precisely the failure mode 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.</t>
      </section>
      <section anchor="sec-attachments-and-contractversion-required">
        <name>2.0.0 — attachments, and <tt>contract_version</tt> required</name>
        <t>A <strong>major bump</strong>, moving the wire major from <tt>"1"</tt> to <tt>"2"</tt>. It added
attachment support in both directions — outbound Descriptors with required
<tt>size_bytes</tt> and <tt>sha256</tt> and their Sidecar bytes, inbound Descriptors with
a <tt>disposition</tt> — and made <tt>contract_version</tt> 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 <xref target="sec-10-1"/>: under the then-universal closed
envelopes, a new member caused a major-1 consumer to reject the <em>whole
document</em>, which inbound is silent total message loss. A version signal,
rather than an unknown-member rejection, is what makes that refusal
legible.</t>
      </section>
      <section anchor="sec-fixture-identifier-normalization">
        <name>Fixture identifier normalization</name>
        <t>Across these versions the fixture suite's identities were normalized to
<tt>example.com</tt> and <tt>example.org</tt> 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.</t>
      </section>
    </section>
  </back>
</rfc>
