Email as Tool Transport

Status: design only. Nothing here is implemented. Prerequisites are listed at the end and at least one of them (reply correlation) is a hard blocker.

The idea: an email address is a tool. Envoy composes a request email to a dedicated address, the endpoint behind that address processes it and replies, and Envoy folds the reply into its context. This is the email-native equivalent of a tool call — async, decoupled, composable. It complements rather than replaces the in-process mechanisms in API Call Types.

Two Kinds of Endpoint

These are frequently conflated. They have different costs, different failure modes, and different security postures. Keep them separate.

Deterministic responderSpecialist sub-agent
Exampleloc@ — returns current OwnTracks positionsummarise@, websearch@
ImplementationSmall script; parse request, call API, replyA full orchestrator instance with its own mailbox and start note
LLM involvedNoYes
DeterminismSame request → same replyNon-deterministic
CostNegligibleOne or more LLM calls per request
IdempotentYesNo
TestableDirectly, by injecting a request emailOnly end-to-end
Attack surfaceFixed schema, no interpretationReads free text; injectable

The deterministic responder is the more useful and more overlooked of the two. Most things Envoy needs from the outside world are lookups, not judgements. Build responders first; sub-agents only where judgement is genuinely required.

The sub-agent form is already sketched under TODO (External Specialist Agent Endpoints). This note supersedes that sketch for design purposes and adds the responder case, the correlation requirement, and the security constraints.

When to Use Email Transport

Email transport is not free. It costs a cron interval per hop (currently up to 10 minutes), plus mailbox round-trip, plus correlation bookkeeping. It earns that cost only in specific circumstances.

Use python_calls when the capability is local, fast, and owned by us. Use http_calls when it is a remote service with an OpenAPI spec and a synchronous response. Use email transport when at least one of the following holds:

• The work is long-running and a synchronous call would block or time out.• The endpoint is itself an agent, and the request/reply pair is worth keeping as an audit artefact.• The endpoint is on the other side of a network boundary that only mail crosses.• The pipeline is composable: endpoint A may forward to endpoint B without Envoy in the loop.

Counter-example, and an instructive one: loc@. The OwnTracks position is already available synchronously from the gdata-server /location JSON API and through its MCP wrapper. Routing that through email adds ten minutes of latency to answer a question whose answer changes every few seconds. Do it as an http_call. Use loc@ as the reference implementation of the pattern precisely because it is trivial — not because email is the right transport for it.

Message Format

A request is an email with a machine-readable payload and a human-readable body. Both matter: the payload is what the endpoint parses, the body is what makes the mailbox an audit trail a human can read.

Payload placement, in order of preference:

1. A typed MIME part (application/json) — correct, but blocked on attachment parsing, which is currently on the Essential list in TODO. Each part is a self-contained object; there is no delimiter to escape.2. A fenced block in the body — works today, but it is a delimiter-based interface, and therefore the thing PROGRAMMING_RULES/security-interfaces exists to forbid. The endpoint must re-parse a flat string that an attacker may have influenced. It is system(), not execve().3. Structured headers — fine for metadata (call ID, schema version), not for payloads.

This ordering is not a matter of taste. The house rule is explicit: prefer interfaces where each element is a separate, self-contained object over flat strings the receiver must re-parse, and sanitising input is defence-in-depth rather than a substitute for a protocol in which injection is structurally impossible. A fenced body block fails that test. Option (2) is therefore an interim measure adopted knowingly, not a design — and it means attachment parsing is a prerequisite for email transport, not merely a convenience. Do not build endpoints on fenced bodies and then leave them there.

The reply mirrors the request: same payload format, plus an explicit status. An endpoint that cannot answer replies with a structured error, never with prose. Prose errors force an LLM call to interpret a failure, which is exactly when you least want one.

Correlation — The Blocking Prerequisite

Envoy cannot currently do any of this, and the reason was recorded as a known limitation in TODO (promoted to Essential on 2026-07-09): the orchestrator does not return the Message-ID of sent emails to the LLM, so Envoy cannot track its own outgoing threads or correlate replies to them.

For an ordinary outbound reply this is a nuisance with a workaround (search Sent by subject and date). For email-as-transport it is fatal. Request/response pairing is the transport. Without it there is no way to know which reply answers which call, and a pipeline of two endpoints becomes unresolvable.

What is needed:

send_emails returns the generated Message-ID for each sent message, surfaced to the LLM in the next iteration.• Each outbound tool-call email starts a fresh Message-ID chain, not a reply to the originating user thread.• The endpoint replies with In-Reply-To set to the request Message-ID — standard, free, and sufficient.• Outstanding calls are recorded in the continuation JSON (call ID → Message-ID → phase to resume in), so a pending call survives the iteration limit and the cron boundary. This is the same mechanism as idle_count: per-instance, no shared note, no locking.• A pending call that never returns must time out. An endpoint that is down should not strand a task forever; after N cron cycles, the call is marked failed and the LLM told so.

The waiting phase already exists and is the natural place for a task with calls outstanding. See waiting.

Security

The Perimeter Is Necessary and Insufficient

The intended deployment is a walled garden: endpoint mailboxes are reachable only from inside, never from public SMTP. This is right, and it should be enforced at the transport layer (LMTP/Maildir, no public MX), not by obscurity of the address.

It does not, however, address the actual threat. Envoy reads public inbound mail — anyone may send to the primary mailbox. Envoy can also send mail, and send_emails dispatches on every non-terminal phase. A prompt injection in an ordinary inbound message therefore does not need to reach code@ directly. It only needs to persuade Envoy to send there. Envoy is inside the garden; Envoy is the confused deputy.

Note what this means for the threat model. The attacker is not impersonating John, and does not need to. Impersonation is an authentication problem, and the walled garden already solves it — no outsider can deliver mail to an endpoint mailbox. The attacker instead sends an ordinary email to the ordinary inbox, as anyone may, and its content persuades a genuine, authenticated Envoy to make the call on their behalf. Every credential checks out. Every hop is legitimate. This is an authorisation problem wearing an authentication problem's clothes.

This is also why cryptographic signing of request emails, though sound in principle, buys nothing here. A compromised-by-injection Envoy signs the malicious request with the same key it signs legitimate ones. Signing authenticates the sender, and the sender is genuinely Envoy. The confused deputy problem is not an authentication problem.

What Actually Contains It

Constrain the capability, not the caller.

No endpoint accepts code as a payload. Not Python, not Perl, not C, not shell. An address that receives a program and runs it is a remote code execution endpoint whose only control is that Envoy is well-behaved — and Envoy reads attacker-supplied text every ten minutes. This constraint is the single most important line in this note.• Fixed schema per endpoint. Each responder declares its parameters and validates them; anything unrecognised is rejected, not interpreted. This is structured over delimiter-based applied at the mail layer: a task named in a schema field cannot escape into a task the schema does not name.• Least capability per endpoint. loc@ reads a position and can do nothing else. Capability is bounded by what the responder implements, not by what the request asks for.• Sub-agents inherit the injection surface of their input. A sub-agent that reads free text and then acts is as exposed as Envoy. Give it read-only capabilities, or none.• Reply payloads are untrusted input. An endpoint reply enters Envoy's context and is data, not instruction. This is true even of our own endpoints, and doubly so of any that fetch from the web.

If a code@ endpoint is ever wanted, it is a sandboxed execution service with a resource budget and no network or filesystem access, reached through a schema that names a task rather than supplying a program. That is a separate project, not an email address. See Host Safety and Remote Execution for the adjacent lesson, and the noodle incident for what happens when an agent's reach exceeds its judgement.

Write the Constraint Before the Address Exists

The guard rule adopted after the HOT note applies directly: do not implement a convention before its preconditions are met. The preconditions for any endpoint mailbox are a walled-garden transport, a declared schema, and a bounded capability. An endpoint created before those exist is an endpoint that will be used before those exist.

Prerequisites

In dependency order. None are done.

1. send_emails returns Message-IDs to the LLM. Blocking; nothing works without it.2. Outstanding-call tracking in the continuation JSON, with timeout.3. Walled-garden LMTP/Maildir transport for endpoint mailboxes.4. Attachment parsing, for typed MIME payloads — a prerequisite, not a convenience, per Message Format above.5. First responder: loc@, wrapping the existing /location API. Reference implementation, not a production need.6. Schema declaration convention for endpoints — probably a stub note per endpoint, mirroring the pattern in Registration.

Navigation

API Calls — Design — parentAPI Call Types — the in-process alternatives (python_calls, http_calls)Security Interfaces: Structured vs Delimiter-Based — the house rule governing payload formatArchitecture Summary — the Agents/ folder is reserved for inter-agent trafficOwnTracks — the /location API behind the loc@ exampleTODO — External Specialist Agent Endpoints; Message-ID prerequisite

created 2026-07-09  ·  description Design for using email as the transport for tool calls: deterministic responders and specialist sub-agents. Not yet implemented.  ·  tags envoy, design, transport, security  ·  updated 2026-07-09  ·  version 3