AMAP, explained

Agent Mailbox Access Protocol

An agent can ask. Only the runtime can act.

AMAP is a wire contract that lets sandboxed AI agents take part in message-based workflows through a trusted runtime that does not trust them. Press Next to walk through the ideas, watch each flow move through the real pieces, and then run it yourself.

The invariantThe runtime's authority does not depend on the agent or its connector behaving correctly. A request file cannot, on its own, grant permission to send, change the sending identity, or bypass policy.

The problem

An agent is an untrusted process

It can be prompt-injected, buggy or compromised. Connecting it to a workflow does not make its decisions trustworthy, and message access brings three risks.

Inbound

A message can carry untrusted content and injection attempts straight into the agent's reasoning.

Outbound

A message can disclose information, or misuse an identity the agent should not control.

Credentials

Anything the agent can read, it can leak. Credentials in its reach are credentials at risk.

What matters is not whether the agent can write a message. It is whether it can authorise one, choose the identity it goes out under, or step around policy.

The problem

AMAP's one move: a request is never permission

agent

Reads

what was delivered to it, from a place it cannot write.

agent

Writes an inert request

a file saying what it would like to happen. Nothing more.

runtime

Decides

whether anything happens, to whom, and under which identity.

AMAP was designed for email workflows, where the runtime holds the mail credentials. This walkthrough uses amap-router-local, a reference runtime that carries no mail at all: no provider, no credentials, no network. It moves the same artifacts between agents on one machine, so what is left is exactly what the protocol asks of a runtime.

Isolation first

AMAP assumes the agent is already contained

The protocol defines who may do what across a boundary. It does not create the boundary. Running each agent in isolation is the operational baseline that gives the protocol's guarantees meaning, and it is good practice even when nothing is malicious.

One contained space per agent

limits the blast radius

A compromised or confused agent damages its own workspace, not its neighbours' or the host's.

No credentials in the sandbox

nothing to leak

The runtime holds every credential and the sending identity. The connector holds none.

Read-only by mount, not by permission bits

the agent owns its files

It runs as your user, so chmod would succeed. A read-only mount makes the kernel refuse the write: EROFS.

Identity given from outside

an agent must not name itself

The runtime binds each sender to its own outbox, named by the host.

Isolation first

…and run it like infrastructure

Supervised, and recreated on a cadence

recoverable by design

The delivery process restarts if it dies, and containers are rebuilt on a schedule, so drift and damage do not accumulate.

Membership is the operator's decision

joining is a security act

Which agents take part is a rule the operator writes and the host evaluates. Nothing inside a workspace can add itself.

Observable, in three outcomes

"could not tell" is not "fine"

Every check ends in pass, fail or unknown, and unknown is reported as a problem.

If your environment cannot provide one of these, the matching guarantee does not hold. It is not merely weaker. This walkthrough uses sandy for isolation because it provides every one of them, and shows each.

The pieces

Five parts, one boundary: they meet only at the contract, and only one side is trusted

The protocol

contract
amap-spec

The directory layout, message and verdict shapes as JSON Schema, and golden fixtures. Each side proves conformance against the fixtures, without the other.

The runtime

trusted
amap-router-local

Holds the graph of who may reach whom, binds replies, quarantines on first sight, writes the roster. Runs on the host with no network.

The connector

untrusted
amap-connector-claude

MCP tools to read (inbox, delegation) and to request (inbox-submit), plus inbox-delivery, which injects deliveries into the session. No credentials, no allowlist.

The isolation environment

container
sandy

The sandbox per agent, the read-only mounts, the agent's name, the supervised process and the membership rule.

The deployment

operator's tool
amap-deploy-sandy

Turns one policy into sandy's feature manifest and the router's config, then verifies that what runs is what was declared.

The seam

The boundary is a directory tree

Runtime and connector never call each other. They share directories, and the design rests on which side can write which one. Everything the agent reads, it cannot forge; everything it writes is only a request.

PathWritten byIn the sandboxHolds
instances/<name>/hostOne agent's lanes. The name is the agent's identity.
inbox/runtimeread-onlyMail delivered to this agent.
peer/runtimeread-onlyTasks from agents allowed to send them.
outbox/agentwritableInert requests; the runtime's verdicts come back in results/.
payload/operatorread-onlyThe relay, the connector, the MCP config and the agent's policy text.
roster/runtimeread-onlyWho exists in the fleet. Never who may task whom.
router stateruntimehost onlyLedgers and first-sight markers. No agent can reach it.

Two lanes

Mail and delegation carry different trust

They are declared separately, and a pair of agents may sit on only one of them, so a sender can never pick which trust it is judged under.

Mail

a b
  • Mutual: both sides must list each other.
  • Delivery is a content-free doorbell; the agent reads with the inbox tool.
  • Content is untrusted: reported, not obeyed.

Delegation

a b a may task b
  • Directed: a may task b says nothing about b tasking a.
  • The request itself is injected into the session.
  • The runtime authorised it, so it is meant to be acted on, within what the agent may already do.

On both lanes a reply goes back to the sender and nowhere else, even against the direction of a delegation edge.

Knowing it holds

Three outcomes, never two

The deployment's verify checks the manifest against the policy, the payload byte for byte, each agent's lanes and mounts, and the router's container and health. Every check ends one of three ways.

PASS

Established from the thing itself: the container's mount table, not the host directory; the router's own status, not a guess.

FAIL

Known to be broken. Every failure names its remedy, and the tempting wrong fix.

UNKNOWN

Could not be answered, as when the router is stopped. Reported as a problem, never rounded to a pass.

The fleet roster keeps the same honesty. It lists who exists so agents can find addresses, and says it is neither permission nor a prediction. The verdict in outbox/results is the only report of what happened.

Run it yourself · with sandy

Why sandy

The runtime and the connector need no changes between environments; only the deployment knows where agents run. sandy supplies every property AMAP needs, and makes each one visible.

AMAP needsWhysandy provides it as
A space per agentEach agent's lanes must be its own.One sandbox per workspace.
An identity it cannot forgeThe runtime binds a sender to its outbox.The slug, reported by sandy, never derived by the agent.
Read-only mountsInbound lanes, the connector and the agent's instructions must be out of reach.Feature mounts: a write fails with EROFS.
A supervised processSomething must inject deliveries into the session, and restart if it dies.The manifest's entry, run by sandy's relay supervisor.
Operator-owned membershipJoining is a security decision.Include and exclude rules, evaluated by sandy at each launch.

sandy is one choice, not a requirement. You can replace the sandy container with the isolation method of your choice, provided it implements at a minimum credential, network and file-system isolation: for example, NVIDIA's OpenShell, a runtime built for isolating autonomous agents, or the gVisor sandbox with an egress allowlist in defending-code-reference-harness. The runtime and the connector stay as they are. What you replace is this deployment, with one that supplies each row of the table above in your environment.

Run it yourself · 1 of 4

Install sandy, then choose a path

macOS or LinuxDockerPython 3.9+jqa Claude account sandy can use
first, either way: sandy 2.2.0 or later
# install, then check the host is ready
curl -fsSL https://raw.githubusercontent.com/rappdw/sandy/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/rappdw/sandy/main/doctor.sh | bash
sandy --print-schema | jq '.schema_version'   # must print 3 or 4

1One command

Clones the three repos into ~/amap, with the router and the connector at the commits this repo is tested against, installs the default policy (every agent may task every other), and builds and starts the router. Then make two workspaces and start an agent in each.

install, then two agents
curl -fsSL https://raw.githubusercontent.com/proofpoint/amap-deploy-sandy/main/install.sh | bash

mkdir -p ~/amap-demo/alpha ~/amap-demo/beta
cd ~/amap-demo/alpha && sandy --start
cd ~/amap-demo/beta  && sandy --start

2Step by step

Clone amap-deploy-sandy, and put the router and the connector beside it at the commits it is tested against (pinned in siblings.json). Then install, start the router and start agents yourself, one screen at a time.

the code
mkdir -p ~/amap && cd ~/amap
git clone https://github.com/proofpoint/amap-deploy-sandy
python3 amap-deploy-sandy/amap-siblings.py --apply

Run it yourself · 2 of 4

Make two workspaces and install

two workspaces, then install
# sandy creates each sandbox at its first launch (3 of 4)
mkdir -p ~/amap-demo/alpha ~/amap-demo/beta

cd ~/amap/amap-deploy-sandy
python3 amap-sandy.py install --apply
# writes a working policy, the payload, and the router's config
the policy it writes: feature.json, "feature"
"feature": {
  "version": 1,
  "groups": {},
  "default_peers": [],
  "peers": {},
  "task_graph": "ALL",
  "task_deny": [],
  "fleet_domain": "sandy.<this host>.internal",
  "container_recreate_interval_hours": 24
}
task_graph: "ALL"
The delegation lane, which is directed: every selected agent may task every other.
default_peers, peers, groups
The mail lane, which is mutual. Empty, because a pair of agents may not sit on both lanes: they carry different trust.
task_deny
Ordered pairs [sender, recipient] to block even though task_graph grants them. None.
fleet_domain
The right-hand side of every address, naming this host's router: <slug>@sandy.<this host>.internal. A name only; nothing touches a network.
container_recreate_interval_hours
How often the agent containers are rebuilt, once you load the job amap-sandy.py cadence prints.

This works as written: every sandbox launched with claude is selected, and every one may task every other. To narrow it, edit ~/.sandy/features/amap/feature.json in place and run install --apply again. A glob in its top-level sandboxes.exclude keeps a sandbox out, and explicit task_graph edges replace "ALL". Nothing is delivered until you start the router in the next step: that is the approval.

Run it yourself · 3 of 4

Start the router, then the agents, then verify

the router, then agents as needed
# the router needs no agent running yet
cd ~/amap/amap-router-local
docker/build.sh
docker/run.sh --config ~/.sandy/features/amap/router.json \
  --detach

# then agents, in any order, whenever you need them
cd ~/amap-demo/alpha && sandy --start
cd ~/amap-demo/beta  && sandy --start
check it
cd ~/amap/amap-deploy-sandy
python3 amap-sandy.py list     # both: selected
python3 amap-sandy.py verify   # exit 0 before going on

Starting the router is the approval: its first poll of each agent is first sight, and anything staged before it is never delivered. The router picks up each agent on its next poll after the agent launches, so agents can delegate to each other as they come online. It runs with no network and re-reads its config every poll, so later policy changes need only install --apply.

Run it yourself · 4 of 4

Send your first delegation

two terminals
cd ~/amap-demo/alpha && sandy --attach   # terminal 1
cd ~/amap-demo/beta  && sandy --attach   # terminal 2: watch it arrive

Ask alpha, in plain words:

Ask beta to list the files in its workspace and report back.

Its system prompt already tells it where the fleet roster is, how an address is formed, and that inbox-submit is how to ask.

You have just watched the first flow for real. The runbook then breaks each guarantee on purpose: a write to the payload fails with EROFS, a task that was never granted is held, and a stopped router makes verify say UNKNOWN.

amap-deploy-sandy · Apache License 2.0 · AMAP is a draft contract; the spec repository is normative.