Openagentemail agent

Open communication and task handoffs for AI agents — self-hosted email, task threads, approvals, and webhooks over MCP and REST.

by openagentemail·Apache-2.0 license·★ 49 Stars on the repo·GitHub ↗

Files of Openagentemail

openagentemail/main1 file
README.md
Show the full text508 lines

OpenAgentEmail logo

OpenAgentEmail

Open communication and task handoffs for AI agents.

Self-hosted email, task threads, approvals, and webhooks over MCP and REST
—for the agents you already use.

Bring your own agents. Keep control of the work.

Get started · Try a task handoff · Documentation · Architecture · 简体中文

Apache-2.0 license Main branch CI status MCP package version, not a whole-stack version

See it work

Give an agent an address. Send it a task. Read its progress and result in the same thread. OAE keeps the handoff inspectable; your agent's existing harness does the work.

Task handoff demo cover: requester creates a task, recipient reports working then completed

Watch the handoff recording (MP4) — about 47 seconds. The video is linked, not embedded inline. Both sides drive real REST calls from a recording scaffold (not a live human clicking the console); frames were captured with headless Chrome + CDP; timestamps use UTC+8 12-hour clock; the pointer and subtitle strip are post-production decoration. Activation is explicit in the recording—do not treat it as proof of automatic wakeup. Reproduce the protocol yourself with the two-identity read-only code-review recipe.

sequenceDiagram
    participant A as Requester agent
    participant O as OpenAgentEmail
    participant B as Recipient agent
    A->>O: Create a task for B
    B->>O: Read the task when activated
    B->>O: Report working
    Note over B: Review in its own environment
    B->>O: Report completed with a result
    A->>O: Read the result and history
See the existing email and OTP interface

Existing web dashboard screenshot: an email with its extracted verification code

This repository image demonstrates the email capability, not an Agent orchestration console. Task-board product images appear under Human visibility below.

Why OpenAgentEmail?

Agents running in different terminals or on different devices need a shared way to communicate: who asked for the work, who received it, what is happening, and what came back. They should not have to share an admin credential or switch to one execution environment just to exchange work.

OAE combines ordinary email with structured, authenticated task threads. Use it as an agent mailbox, an inspectable task handoff layer, or both. Email and OTP workflows remain first-class; the project is not limited to being an email-service alternative.

What you can do today

Capability What it gives you Boundary
Agent identities Addresses on your managed domain(s), with individual identity tokens Logical identities over a catch-all mailbox, not unlimited physical mailboxes
Email and messaging Send/read mail, extract OTP codes and links, wait for a match SMTP acceptance is not recipient delivery
Task handoffs Named recipient, state history, structured results and direct parent-child links No automatic worker selection or descendant scheduling
Approvals Record a specified reviewer's approval or rejection The decision does not execute the action
Notifications and webhooks Human alerts and outbound event delivery to your integration Webhooks are opt-in; delivery is not proof an agent consumed the task
Human visibility Inspect mail, tasks, identities and notifications in the dashboard Access depends on the session's identity and permissions

Tasks dashboard: "Waiting for you" view — no tasks in input-required for the selected period

Completed task view with state history and structured result

These are real product screenshots from a controlled capture (anonymized). They show inspectability, not that a notification was consumed or an approved action executed.

REST, HTTP MCP and the stdio MCP wrapper expose these operations. Optional task leases provide recipient claim/renew/release; they do not make external side effects exactly-once. See the tool reference.

Quickstart

Already have an instance?

Ask its operator for your identity address and appropriate identity token, then connect your agent. You do not need to deploy another mailserver. Complete a first task handoff, or send a test email to your identity and confirm you can read it.

Deploy a new instance

Start the guided setup:

npx -y @openagentemail/setup

The setup CLI guides deployment and client configuration. Choose the mail backend that fits your environment:

Deployment Use it when Prerequisites
Bundled mailserver You want to operate your own mail stack Docker Compose, a domain, DNS configuration and reachable inbound SMTP; outbound port 25 or a relay
API-only A provider already hosts your domain Docker Compose, a catch-all mailbox, IMAP/SMTP credentials and permission to send as the identity addresses

Keep the API's localhost binding unless you deliberately configure an SSH tunnel or HTTPS reverse proxy. Mail certificates and HTTPS for the API are separate concerns. Do not publish a bare HTTP API carrying tokens.

After deployment, verify the path you actually chose:

Bundled mailserver. This stack owns mail.$DOMAIN, the mail DKIM selector, and TLS on ports 465/993. Run the doctor against that layout:

sudo ./deploy/doctor.sh

It checks DNS, port access, certificates and notification prerequisites for the bundled stack. It still does not log in over IMAP/SMTP or send a round-trip email.

API-only (your own mail provider). Skip deploy/doctor.sh — it assumes the bundled mail host and will report false failures for a different MX/DKIM setup. Instead: confirm /healthz returns healthy, confirm your IMAP/SMTP credentials work for the catch-all mailbox, then send a real message to an identity and read it back. Only then try the task recipe. A healthy /healthz alone proves the HTTP process is alive, not that mail delivery works.

Existing api-data volume: one-time non-root migration (#93)

The API runtime image runs as the bun user (uid/gid 1000), not root. New named volumes inherit ownership from the image's /app/data directory and need no host-side fix. Existing production volumes that were written while the API ran as root stay root-owned; the new non-root process cannot write them until you migrate once.

Order is fail-fast by design: migrate the volume before rolling the new image. Shipping the new image first will refuse to start (or fail on first write) until ownership is fixed — that is intentional, not a silent fallback.

  1. Take a backup of the project-scoped volume (example project name openagentemail → volume openagentemail_api-data; API-only stacks use their -p / COMPOSE_PROJECT_NAME prefix, e.g. oae-alpha_api-data).
  2. Stop writers that mount the volume (at minimum the api service; full stack: also stop anything else writing api-data during the window) — and keep them down until they are recreated in step 6. Stopping is not enough on its own: the migration only counts once every old container is gone for good. restart ≠ recreate — a root container that comes back up (crashed-and-restarted, or brought up by habit) keeps writing and silently recreates volume files as root:root, rolling your chown back without any error. If a writer came back up for any reason, stop it and redo step 3 before proceeding.
  3. One-shot chown to the runtime user:
# Replace <project>_api-data with your real volume name (docker volume ls).
docker run --rm -v <project>_api-data:/data alpine \
  sh -c 'chown -R 1000:1000 /data'
  1. Spot-check ownership before bringing services back:
docker run --rm -v <project>_api-data:/data alpine \
  sh -c 'ls -ln /data | head'
# Expect uid/gid columns to show 1000 / 1000 for migrated paths.
  1. Read back the migration on the volume — must pass:
docker run --rm -v <project>_api-data:/data alpine \
  sh -c 'find /data ! -user 1000 | head'
# Expect no output: zero files still owned by a non-1000 user.
  1. Pull/build the new images, then verify the API image declares the runtime user before starting anything. Build the whole project (docker compose build with no service name) — or at minimum api and ntfy-provision together: they share one Dockerfile, and the --force-recreate below re-runs the one-shot ntfy-provision container too, so it must come from the same non-root image batch. A stale root-based provision image re-running here would write the volume as root and roll your migration back — the same failure this runbook exists to prevent.
docker inspect <new-api-image> --format '{{.Config.User}}'
# Expect bun (uid 1000) — never empty/root.

Only then start everything (docker compose up -d --force-recreate or your usual deploy path).

Do not reverse this order. Production chown is a deploy-window operation; it is not performed by the image entrypoint.

Deploy-window recreate gotchas (measured 2026-09-21)
  1. --force-recreate <service> follows the dependency chain. Measured on the reference host: recreating api also recreated mailserver along api→mailserver→provision (and ntfy pulls ntfy-provision) — healthy, ~11s, no loss, but unintended. To recreate only the named services, add --no-deps; full-project builds keep the plain form.
  2. Measure override attribution — do not assume it. Before editing or removing any override, run docker compose config against three configurations: base alone, base + each override, and compare the resolved output field by field. On the reference host (2026-09-21) this distinguished two siblings: docker-compose.override.yml was a zero-contribution no-op (its patches had long been absorbed into the base file) and was removed with a timestamped backup, while compose.override.yaml is live and must be kept (adds the loopback binding and widens allowed ports). A "Found multiple override files" warning at recreate time is the cue to run this check. Scope note: override auto-merging happens only on default-file invocations — deployments selecting files explicitly (e.g. API-only docker compose -f compose.api-only.yaml ...) auto-merge no override at all and must pass every override explicitly.
ntfy non-root upgrade (#278)

ntfy now runs as UID 1000 and listens on container port 2587. If an existing deploy previously ran ntfy as root, chown its data before up -d — otherwise ntfy fails to start and the API stays down (depends_on healthy):

# Same volume naming as above; only the ntfy subtree is required here.
docker run --rm -v <project>_api-data:/data alpine \
  sh -c 'chown -R 1000:1000 /data/ntfy'

Then docker compose up -d (recreate ntfy and api). A prior full-volume #93 migration already covers /data/ntfy; re-run only if that subtree is still root-owned.

Connect your agent

Choose the right credential
Identity setup Intended use
scopes: ["read:messages"] Read/wait for permitted mail; not send or task operations
Omit scopes Current full identity permissions, subject to address/participant rules; suitable for task participants
scopes: [] No API operation permissions

These are current API semantics, not a proposal for new scopes. Full identity permissions are not admin permissions. Only an operator should create/list/manage identities. Keep admin keys out of agent client configurations. See credential setup.

Local stdio MCP

The published MCP client requires Node.js 20+, matching its package contract. The API server runs on Bun inside its container. A generic local-client configuration is:

{
  "mcpServers": {
    "openagentemail": {
      "command": "npx",
      "args": ["-y", "@openagentemail/mcp"],
      "env": {
        "OPENAGENTEMAIL_API_URL": "http://localhost:3100",
        "OPENAGENTEMAIL_API_KEY": "oa_replace_with_your_identity_token"
      }
    }
  }
}

Use localhost only for an API on the same host or behind a local SSH tunnel. For a remote instance use its HTTPS base URL. Protect the client configuration; do not commit tokens. The stdio client never needs the mailserver's IMAP/SMTP credentials.

Remote HTTP MCP

Clients supporting remote MCP can connect directly to the instance's /mcp endpoint without installing the stdio package. Configure a public HTTPS origin and follow the client's supported Bearer/OAuth flow. OAuth grants do not have the same mutation permissions as admin or direct identity credentials. Use the client guide; protocol support is not a claim of automatic wakeup or a vendor partnership.

The MCP reference covers all registered tools, permissions and wait semantics. Each server wait is capped by MCP_MAX_WAIT_SECONDS (default 60 seconds, configurable from 1 to 600). The MCP mail client can re-arm shorter segments within its requested total deadline; task waits are one capped turn. A wait timeout is not a failed job. After an uncertain create outcome, check the returned task ID/history rather than blindly creating another task.

How it works

flowchart TB
    Agents["Existing agents / harnesses"] -->|"REST · HTTP MCP · OAuth"| API["OAE API: Bun + Hono"]
    Agents --> Stdio["Node stdio MCP wrapper"]
    Stdio -->|"REST"| API
    Human["Humans via /ui"] --> API
    API -->|"SMTP / IMAP"| Mail["Bundled mailserver or external catch-all"]
    API --> State["DATA_DIR local state<br/>identities · auth · sessions · webhooks<br/>optional lease journal"]
    Mail -->|"IMAP"| Watcher["Watcher in the API process"]
    Watcher --> Delivery["ntfy and/or webhook delivery"]
    Delivery --> Adapter["External adapter / receiver"]
    Adapter -. "Activation is integration-specific" .-> Agents

Tasks are reconstructed from authenticated mail records. Local files also hold operational state, including an optional pending lease journal. Execution stays in the agent's own runtime—OAE does not run the model. This is a single-process service with background loops, not a distributed worker runtime.

The core does not require Orca. The checked-in webhook-wake example currently targets Orca; it is not a universal replacement adapter. Already-recorded pending webhook deliveries can be recovered, but the watcher does not replay all mail that arrived while the API was stopped. Consumers should reconcile their task/mail state on startup or reconnect rather than treating a notification as the only record of work.

Read the current architecture and data ownership for the SMTP/IMAP visibility overlay, task state and notification boundaries.

Deployment and boundaries

Own the data path, not just the server. A self-hosted instance avoids a required OAE SaaS control plane. An external mailbox provider, SMTP relay, archive recipient or notification destination can still receive data according to your configuration. Mail returned to a remote model/client also leaves the server. Review that path before exposing credentials or message content.

Capacity and limits are operational choices. There is no per-identity software licensing charge. Mailbox capacity, provider policy, disk, memory and configurable rate limits still apply (sending defaults to 20 messages/hour per identity). Ordinary mail defaults to 30-day retention; task-marked mail is excluded from that sweeper. Back up the mail store, DATA_DIR and stable signing secrets together.

Seen is shared state. Marking a message read affects other mailbox consumers; it is not a private agent acknowledgement. Use a consumer-specific cursor and periodic reconciliation for independent processing. Reading alone does not mark mail seen.

Optional leases: defaults and rollout boundaries

TASK_LEASES_ENABLED defaults to false. When enabled, use exactly one API process per mailbox. Claim/renew/release require the managed recipient's identity, not an admin impersonation. Do not enable multiple API writers against the same state.

  • Optional TASK_LEASES_EXPIRY_AUDIT_M3 (default false) decouples reclaim from expiry-audit SMTP; late matching expiry receipt tolerance is always on.
  • Optional TASK_LEASES_OVERLAY_BOUND (default false) stops public list/detail replay of unindexed lease overlay events after 15 minutes.
  • Optional TASK_LEASES_PENDING_JOURNAL (default false, requires TASK_LEASES_ENABLED) preserves pending generation fences across restart and records or defers expiry-audit work.
  • Even with journal off (production default), claim returns 409 lease_overlay_pending_index while a fresh (≤15 min) release/renew has been SMTP-accepted but not yet absorbed into the durable rebuild. Callers should retry after indexing. If the receipt is permanently lost, the fence ages out after 15 minutes (mirrors TASK_LEASES_OVERLAY_BOUND) and claim proceeds; divergence is contained by the #305 read-side degrade (#308).

Production expiry-audit emission remains hard-disabled; these flags do not enable it. Read the journal operating guide before provisioning or enabling journal writes. Provisioning is not a wipe/recovery procedure. After a claim_lost tombstone exists, rollback to an old reader is unsafe. Leases do not undo external file changes, commits or other side effects.

Examples and documentation

I want to… Start here
Hand a task to another identity First task handoff
Understand task state and persistence Architecture
Deploy, configure TLS or inspect the UI Operator guide
Connect a client / inspect tool permissions Client guide · MCP tool reference
Integrate HTTP APIs or outbound events REST reference · Webhook specification
Explore external wakeup and framework integration Orca wake example · Adapter examples
Webhook → headless agent reply (recipe + templates) Agent responder recipe · Templates
Understand exposure and privacy Security guide · Report a vulnerability

Framework examples and local fixtures are not evidence of a production end-to-end run. Their own READMEs identify which paths use fake services and which require an explicit live invocation.

Direction and contribution

Bring your own agents. Keep work inspectable. Own your infrastructure. Build the handoff, not another runtime.

The next direction is simpler connection to existing working environments and clearer recovery/operating guidance—not a promise of a general scheduler, shared workspace manager, global federation or automatic code execution. Current capabilities are listed above; release history belongs in CHANGELOG.md. The npm badge tracks the MCP package, not a unified server/deployment version.

See #251 for the README refresh and remaining live-demo/website follow-ups. The website repository owns public-site presentation and documentation.

Issues and PRs are welcome. Read CONTRIBUTING.md: substantial work starts with an issue; independent review and green CI are required before merge. Documentation changes should track actual code, not anticipated capabilities.

Operator shortcuts and advanced deployment

Using your own mail server

Already have a mail provider for your domain? Run the API by itself with compose.api-only.yaml, connected to that provider's catch-all mailbox. The external mail server guide covers the required catch-all setup, Portainer deployment, SMTP sender limits, and TLS certificate verification.

The standalone default project name is openagentemail. If the full compose.yaml stack also runs on the same host, the API-only stack must not share that default project: give it an explicitly different -p value or COMPOSE_PROJECT_NAME so the two stacks cannot adopt each other's resources.

To run multiple API-only instances on one host, give every instance its own environment file, unique Compose project, and host API_PORT. The API always listens on port 3100 inside its container; API_PORT changes only the host-side mapping. For example:

mkdir -p ../oae-api-only-env
cp .env.api-only.example ../oae-api-only-env/alpha.env
cp .env.api-only.example ../oae-api-only-env/beta.env
chmod 600 ../oae-api-only-env/*.env
# Set API_PORT=3100 in alpha.env and API_PORT=3101 in beta.env.
# Generate separate API_KEYS and TASK_SIGNING_SECRET values in each file.

docker compose -p oae-alpha --env-file ../oae-api-only-env/alpha.env -f compose.api-only.yaml up -d
docker compose -p oae-beta --env-file ../oae-api-only-env/beta.env -f compose.api-only.yaml up -d

You may set a unique COMPOSE_PROJECT_NAME for each command instead of using -p. The project names make Compose generate distinct container names and project-scoped named volumes. Each instance must have independently generated API_KEYS and TASK_SIGNING_SECRET values. Use distinct API_PORT values, separate data volumes and the intended independent mailbox/provider boundary; this is not a multi-writer recipe. Keep populated files outside the repository.

Read mail in a browser

Open /ui through localhost, an SSH tunnel or HTTPS and log in with an appropriate token. Ordinary sessions and persisted Trust this device sessions have different lifetimes; restarting the API does not discard every trusted session. See UI access and sessions. The dashboard UI supports en / es / ja / ko / zh-CN via the Settings language selector (oa_lang cookie); terminology follows docs/i18n-glossary.md.

Overview counts are a bounded window, not lifetime totals. Cache timing, unknown counts and scan limits are described in the operator guide.

Multi-domain support

DOMAIN plus EXTRA_DOMAINS defines domains managed by one instance. Explicit identities with the same localpart can coexist on different configured domains; full-address duplicates cannot. Additional domains still require provider/MTA routing and DNS/DKIM setup. This is not independent-instance federation. See multi-domain operations.

Public mail TLS and renewal are opt-in; HTTPS for the API needs its own trusted reverse proxy or tunnel.

Optional compliance archive: ALWAYS_BCC is off by default and creates an additional off-domain data recipient.

Self-host for control of deployment, data paths and policies—not a promise of zero cost, unlimited hardware capacity or immunity from upstream provider policies.

Resource planning depends on the workload; this README does not publish an undated benchmark or a VPS-price guarantee.

OAE is an open-source option for agent email and task handoffs. Detailed vendor comparisons need dated primary sources; the old unchecked feature matrix is retired.

Documentation index.

License

Apache-2.0.

1<p align="center">
2 <a href="https://openagent.email"><img src="docs/images/logo-400.png" width="112" alt="OpenAgentEmail logo"></a>
3</p>
4 
5<h1 align="center">OpenAgentEmail</h1>
6 
7<p align="center"><strong>Open communication and task handoffs for AI agents.</strong></p>
8<p align="center">Self-hosted email, task threads, approvals, and webhooks over MCP and REST<br>—for the agents you already use.</p>
9<p align="center"><strong>Bring your own agents. Keep control of the work.</strong></p>
10 
11<p align="center">
12 <a href="#quickstart">Get started</a> ·
13 <a href="docs/first-task-handoff.md">Try a task handoff</a> ·
14 <a href="https://openagent.email/docs/">Documentation</a> ·
15 <a href="#how-it-works">Architecture</a> ·
16 <a href="README.zh-CN.md">简体中文</a>
17</p>
18 
19<p align="center">
20 <a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="Apache-2.0 license"></a>
21 <a href="https://github.com/openagentemail/openagentemail/actions/workflows/ci.yml"><img src="https://github.com/openagentemail/openagentemail/actions/workflows/ci.yml/badge.svg?branch=main" alt="Main branch CI status"></a>
22 <a href="https://www.npmjs.com/package/@openagentemail/mcp"><img src="https://img.shields.io/npm/v/@openagentemail/mcp.svg?label=MCP%20package" alt="MCP package version, not a whole-stack version"></a>
23</p>
24 
25## See it work
26 
27Give an agent an address. Send it a task. Read its progress and result in the same thread.
28OAE keeps the handoff inspectable; your agent's existing harness does the work.
29 
30[![Task handoff demo cover: requester creates a task, recipient reports working then completed](docs/assets/251/cover.png)](docs/assets/251/demo-handoff.mp4)
31 
32[Watch the handoff recording (MP4)](docs/assets/251/demo-handoff.mp4) — about 47 seconds.
33The video is linked, not embedded inline. Both sides drive **real REST calls** from a
34recording scaffold (not a live human clicking the console); frames were captured with
35headless Chrome + CDP; timestamps use UTC+8 12-hour clock; the pointer and subtitle
36strip are post-production decoration. Activation is explicit in the recording—do not
37treat it as proof of automatic wakeup. Reproduce the protocol yourself with the
38[two-identity read-only code-review recipe](docs/first-task-handoff.md).
39 
40```mermaid
41sequenceDiagram
42 participant A as Requester agent
43 participant O as OpenAgentEmail
44 participant B as Recipient agent
45 A->>O: Create a task for B
46 B->>O: Read the task when activated
47 B->>O: Report working
48 Note over B: Review in its own environment
49 B->>O: Report completed with a result
50 A->>O: Read the result and history
51```
52 
53<details>
54<summary>See the existing email and OTP interface</summary>
55 
56![Existing web dashboard screenshot: an email with its extracted verification code](docs/images/message-detail.png)
57 
58This repository image demonstrates the email capability, not an Agent orchestration
59console. Task-board product images appear under **Human visibility** below.
60 
61</details>
62 
63## Why OpenAgentEmail?
64 
65Agents running in different terminals or on different devices need a shared way to
66communicate: who asked for the work, who received it, what is happening, and what
67came back. They should not have to share an admin credential or switch to one
68execution environment just to exchange work.
69 
70OAE combines ordinary email with structured, authenticated task threads. Use it as
71an agent mailbox, an inspectable task handoff layer, or both. Email and OTP workflows
72remain first-class; the project is not limited to being an email-service alternative.
73 
74<a id="features"></a>
75## What you can do today
76 
77| Capability | What it gives you | Boundary |
78| --- | --- | --- |
79| **Agent identities** | Addresses on your managed domain(s), with individual identity tokens | Logical identities over a catch-all mailbox, not unlimited physical mailboxes |
80| **Email and messaging** | Send/read mail, extract OTP codes and links, wait for a match | SMTP acceptance is not recipient delivery |
81| **Task handoffs** | Named recipient, state history, structured results and direct parent-child links | No automatic worker selection or descendant scheduling |
82| **Approvals** | Record a specified reviewer's approval or rejection | The decision does not execute the action |
83| **Notifications and webhooks** | Human alerts and outbound event delivery to your integration | Webhooks are opt-in; delivery is not proof an agent consumed the task |
84| **Human visibility** | Inspect mail, tasks, identities and notifications in the dashboard | Access depends on the session's identity and permissions |
85 
86![Tasks dashboard: "Waiting for you" view — no tasks in input-required for the selected period](docs/assets/251/tasks-board.png)
87 
88![Completed task view with state history and structured result](docs/assets/251/tasks-completed.png)
89 
90These are real product screenshots from a controlled capture (anonymized). They show
91inspectability, not that a notification was consumed or an approved action executed.
92 
93REST, HTTP MCP and the stdio MCP wrapper expose these operations. Optional task
94leases provide recipient claim/renew/release; they do not make external side effects
95exactly-once. See the [tool reference](packages/mcp/README.md#tools).
96 
97## Quickstart
98 
99### Already have an instance?
100 
101Ask its operator for **your identity address and appropriate identity token**, then
102[connect your agent](#connect-your-agent). You do not need to deploy another mailserver.
103Complete a [first task handoff](docs/first-task-handoff.md), or send a test email to
104your identity and confirm you can read it.
105 
106### Deploy a new instance
107 
108Start the guided setup:
109 
110```bash
111npx -y @openagentemail/setup
112```
113 
114The [setup CLI](packages/setup/README.md) guides deployment and client configuration.
115Choose the mail backend that fits your environment:
116 
117| Deployment | Use it when | Prerequisites |
118| --- | --- | --- |
119| [Bundled mailserver](docs/operator-guide.md#bundled-deployment) | You want to operate your own mail stack | Docker Compose, a domain, DNS configuration and reachable inbound SMTP; outbound port 25 or a relay |
120| [API-only](#using-your-own-mail-server) | A provider already hosts your domain | Docker Compose, a catch-all mailbox, IMAP/SMTP credentials and permission to send as the identity addresses |
121 
122Keep the API's localhost binding unless you deliberately configure an SSH tunnel or
123HTTPS reverse proxy. Mail certificates and HTTPS for the API are separate concerns.
124Do not publish a bare HTTP API carrying tokens.
125 
126After deployment, verify the path you actually chose:
127 
128**Bundled mailserver.** This stack owns `mail.$DOMAIN`, the `mail` DKIM selector, and
129TLS on ports 465/993. Run the doctor against that layout:
130 
131```bash
132sudo ./deploy/doctor.sh
133```
134 
135It checks DNS, port access, certificates and notification prerequisites for the
136bundled stack. It still does **not** log in over IMAP/SMTP or send a round-trip email.
137 
138**API-only (your own mail provider).** Skip `deploy/doctor.sh` — it assumes the
139bundled mail host and will report false failures for a different MX/DKIM setup.
140Instead: confirm `/healthz` returns healthy, confirm your IMAP/SMTP credentials work
141for the catch-all mailbox, then send a real message to an identity and read it back.
142Only then try the task recipe. A healthy `/healthz` alone proves the HTTP process is
143alive, not that mail delivery works.
144 
145## Existing `api-data` volume: one-time non-root migration (#93)
146 
147The API runtime image runs as the `bun` user (`uid`/`gid` **1000**), not root.
148**New** named volumes inherit ownership from the image's `/app/data` directory
149and need no host-side fix. **Existing** production volumes that were written
150while the API ran as root stay root-owned; the new non-root process cannot
151write them until you migrate once.
152 
153**Order is fail-fast by design: migrate the volume before rolling the new
154image.** Shipping the new image first will refuse to start (or fail on first
155write) until ownership is fixed — that is intentional, not a silent fallback.
156 
1571. Take a backup of the project-scoped volume (example project name
158 `openagentemail` → volume `openagentemail_api-data`; API-only stacks use
159 their `-p` / `COMPOSE_PROJECT_NAME` prefix, e.g. `oae-alpha_api-data`).
1602. Stop writers that mount the volume (at minimum the `api` service; full
161 stack: also stop anything else writing `api-data` during the window) —
162 and keep them down until they are **recreated** in step 6. Stopping is
163 not enough on its own: the migration only counts once every old
164 container is gone for good. `restart` ≠ recreate — a root container
165 that comes back up (crashed-and-restarted, or brought up by habit)
166 keeps writing and silently recreates volume files as `root:root`,
167 rolling your chown back without any error. If a writer came back up
168 for any reason, stop it and redo step 3 before proceeding.
1693. One-shot chown to the runtime user:
170 
171```bash
172# Replace <project>_api-data with your real volume name (docker volume ls).
173docker run --rm -v <project>_api-data:/data alpine \
174 sh -c 'chown -R 1000:1000 /data'
175```
176 
1774. Spot-check ownership before bringing services back:
178 
179```bash
180docker run --rm -v <project>_api-data:/data alpine \
181 sh -c 'ls -ln /data | head'
182# Expect uid/gid columns to show 1000 / 1000 for migrated paths.
183```
184 
1855. Read back the migration on the volume — must pass:
186 
187```bash
188docker run --rm -v <project>_api-data:/data alpine \
189 sh -c 'find /data ! -user 1000 | head'
190# Expect no output: zero files still owned by a non-1000 user.
191```
192 
1936. Pull/build the new images, then verify the API image declares the
194 runtime user **before** starting anything. Build the whole project
195 (`docker compose build` with no service name) — or at minimum `api`
196 and `ntfy-provision` together: they share one Dockerfile, and the
197 `--force-recreate` below re-runs the one-shot ntfy-provision container
198 too, so it must come from the same non-root image batch. A stale
199 root-based provision image re-running here would write the volume as
200 root and roll your migration back — the same failure this runbook
201 exists to prevent.
202 
203```bash
204docker inspect <new-api-image> --format '{{.Config.User}}'
205# Expect bun (uid 1000) — never empty/root.
206```
207 
208 Only then start everything (`docker compose up -d --force-recreate`
209 or your usual deploy path).
210 
211Do **not** reverse this order. Production chown is a deploy-window operation;
212it is not performed by the image entrypoint.
213 
214### Deploy-window recreate gotchas (measured 2026-09-21)
215 
2161. **`--force-recreate <service>` follows the dependency chain.** Measured on
217 the reference host: recreating `api` also recreated `mailserver` along
218 `api`→`mailserver`→`provision` (and `ntfy` pulls `ntfy-provision`) —
219 healthy, ~11s, no loss, but unintended. To recreate **only** the named
220 services, add `--no-deps`; full-project builds keep the plain form.
2212. **Measure override attribution — do not assume it.** Before editing
222 or removing any override, run `docker compose config` against **three
223 configurations**: base alone, base + each override, and compare the
224 resolved output field by field. On the reference host (2026-09-21)
225 this distinguished two siblings: `docker-compose.override.yml` was a
226 zero-contribution no-op (its patches had long been absorbed into the
227 base file) and was removed with a timestamped backup, while
228 `compose.override.yaml` is **live and must be kept** (adds the loopback
229 binding and widens allowed ports). A "Found multiple override files"
230 warning at recreate time is the cue to run this check. Scope note:
231 override auto-merging happens only on default-file invocations —
232 deployments selecting files explicitly (e.g. API-only
233 `docker compose -f compose.api-only.yaml ...`) auto-merge **no**
234 override at all and must pass every override explicitly.
235 
236### ntfy non-root upgrade (#278)
237 
238ntfy now runs as **UID 1000** and listens on container port **2587**. If an
239existing deploy previously ran ntfy as root, chown its data **before**
240`up -d` — otherwise ntfy fails to start and the API stays down (`depends_on`
241healthy):
242 
243```bash
244# Same volume naming as above; only the ntfy subtree is required here.
245docker run --rm -v <project>_api-data:/data alpine \
246 sh -c 'chown -R 1000:1000 /data/ntfy'
247```
248 
249Then `docker compose up -d` (recreate `ntfy` and `api`). A prior full-volume
250`#93` migration already covers `/data/ntfy`; re-run only if that subtree is
251still root-owned.
252 
253<a id="use-it-from-your-agent-mcp"></a>
254## Connect your agent
255 
256### Choose the right credential
257 
258| Identity setup | Intended use |
259| --- | --- |
260| `scopes: ["read:messages"]` | Read/wait for permitted mail; not send or task operations |
261| Omit `scopes` | Current full **identity** permissions, subject to address/participant rules; suitable for task participants |
262| `scopes: []` | No API operation permissions |
263 
264These are current API semantics, not a proposal for new scopes. Full identity
265permissions are **not admin permissions**. Only an operator should create/list/manage
266identities. Keep admin keys out of agent client configurations. See
267[credential setup](docs/first-task-handoff.md#credentials-and-prerequisites).
268 
269### Local stdio MCP
270 
271The published MCP client requires **Node.js 20+**, matching its
272[package contract](packages/mcp/package.json). The API server runs on Bun inside its
273container. A generic local-client configuration is:
274 
275```json
276{
277 "mcpServers": {
278 "openagentemail": {
279 "command": "npx",
280 "args": ["-y", "@openagentemail/mcp"],
281 "env": {
282 "OPENAGENTEMAIL_API_URL": "http://localhost:3100",
283 "OPENAGENTEMAIL_API_KEY": "oa_replace_with_your_identity_token"
284 }
285 }
286 }
287}
288```
289 
290Use localhost only for an API on the same host or behind a local SSH tunnel. For a
291remote instance use its HTTPS base URL. Protect the client configuration; do not
292commit tokens. The stdio client never needs the mailserver's IMAP/SMTP credentials.
293 
294### Remote HTTP MCP
295 
296Clients supporting remote MCP can connect directly to the instance's **`/mcp`**
297endpoint without installing the stdio package. Configure a public HTTPS origin and
298follow the client's supported Bearer/OAuth flow. OAuth grants do not have the same
299mutation permissions as admin or direct identity credentials. Use the
300[client guide](docs/mcp-clients.md); protocol support is not a claim of automatic
301wakeup or a vendor partnership.
302 
303<a id="tools"></a>
304The [MCP reference](packages/mcp/README.md#tools) covers all registered tools,
305permissions and wait semantics. Each server wait is capped by `MCP_MAX_WAIT_SECONDS`
306(default **60 seconds**, configurable from 1 to 600). The MCP mail client can re-arm
307shorter segments within its requested total deadline; task waits are one capped turn.
308A wait timeout is not a failed job. After an uncertain create outcome, check the returned task ID/history rather
309than blindly creating another task.
310 
311## How it works
312 
313```mermaid
314flowchart TB
315 Agents["Existing agents / harnesses"] -->|"REST · HTTP MCP · OAuth"| API["OAE API: Bun + Hono"]
316 Agents --> Stdio["Node stdio MCP wrapper"]
317 Stdio -->|"REST"| API
318 Human["Humans via /ui"] --> API
319 API -->|"SMTP / IMAP"| Mail["Bundled mailserver or external catch-all"]
320 API --> State["DATA_DIR local state<br/>identities · auth · sessions · webhooks<br/>optional lease journal"]
321 Mail -->|"IMAP"| Watcher["Watcher in the API process"]
322 Watcher --> Delivery["ntfy and/or webhook delivery"]
323 Delivery --> Adapter["External adapter / receiver"]
324 Adapter -. "Activation is integration-specific" .-> Agents
325```
326 
327Tasks are reconstructed from authenticated mail records. Local files also hold
328operational state, including an **optional** pending lease journal. Execution stays
329in the agent's own runtime—OAE does not run the model. This is a single-process
330service with background loops, not a distributed worker runtime.
331 
332The core does not require Orca. The checked-in
333[webhook-wake example](examples/webhook-wake/README.md) currently targets Orca; it is
334not a universal replacement adapter. Already-recorded pending webhook deliveries
335can be recovered, but the watcher does not replay all mail that arrived while the
336API was stopped. Consumers should reconcile their task/mail state on startup or
337reconnect rather than treating a notification as the only record of work.
338 
339Read the [current architecture and data ownership](docs/architecture.md) for the
340SMTP/IMAP visibility overlay, task state and notification boundaries.
341 
342## Deployment and boundaries
343 
344**Own the data path, not just the server.** A self-hosted instance avoids a required
345OAE SaaS control plane. An external mailbox provider, SMTP relay, archive recipient
346or notification destination can still receive data according to your configuration.
347Mail returned to a remote model/client also leaves the server. Review that path
348before exposing credentials or message content.
349 
350**Capacity and limits are operational choices.** There is no per-identity software
351licensing charge. Mailbox capacity, provider policy, disk, memory and configurable
352rate limits still apply (sending defaults to 20 messages/hour per identity).
353Ordinary mail defaults to 30-day retention; task-marked mail is excluded from that
354sweeper. Back up the mail store, `DATA_DIR` and stable signing secrets together.
355 
356**Seen is shared state.** Marking a message read affects other mailbox consumers; it
357is not a private agent acknowledgement. Use a consumer-specific cursor and periodic
358reconciliation for independent processing. Reading alone does not mark mail seen.
359 
360<details>
361<summary>Optional leases: defaults and rollout boundaries</summary>
362 
363`TASK_LEASES_ENABLED` defaults to `false`. When enabled, use exactly one API process
364per mailbox. Claim/renew/release require the managed recipient's identity, not an
365admin impersonation. Do not enable multiple API writers against the same state.
366 
367- Optional `TASK_LEASES_EXPIRY_AUDIT_M3` (default false) decouples reclaim from expiry-audit SMTP; late matching expiry receipt tolerance is always on.
368- Optional `TASK_LEASES_OVERLAY_BOUND` (default false) stops public list/detail replay of unindexed lease overlay events after 15 minutes.
369- Optional `TASK_LEASES_PENDING_JOURNAL` (default false, requires `TASK_LEASES_ENABLED`) preserves pending generation fences across restart and records or defers expiry-audit work.
370- Even with journal off (production default), `claim` returns **409 `lease_overlay_pending_index`** while a fresh (`≤15 min`) `release`/`renew` has been SMTP-accepted but not yet absorbed into the durable rebuild. Callers should retry after indexing. If the receipt is permanently lost, the fence ages out after **15 minutes** (mirrors `TASK_LEASES_OVERLAY_BOUND`) and claim proceeds; divergence is contained by the #305 read-side degrade (#308).
371 
372Production expiry-audit emission remains hard-disabled; these flags do not enable
373it. Read [the journal operating guide](docs/task-lease-journal.md) before provisioning
374or enabling journal writes. Provisioning is not a wipe/recovery procedure. After a
375`claim_lost` tombstone exists, rollback to an old reader is unsafe. Leases do not
376undo external file changes, commits or other side effects.
377 
378</details>
379 
380## Examples and documentation
381 
382| I want to… | Start here |
383| --- | --- |
384| Hand a task to another identity | [First task handoff](docs/first-task-handoff.md) |
385| Understand task state and persistence | [Architecture](docs/architecture.md) |
386| Deploy, configure TLS or inspect the UI | [Operator guide](docs/operator-guide.md) |
387| Connect a client / inspect tool permissions | [Client guide](docs/mcp-clients.md) · [MCP tool reference](packages/mcp/README.md) |
388| Integrate HTTP APIs or outbound events | [REST reference](docs/api.md) · [Webhook specification](docs/rfcs/0001-outbound-webhooks.md) |
389| Explore external wakeup and framework integration | [Orca wake example](examples/webhook-wake/README.md) · [Adapter examples](examples/adapters/README.md) |
390| Webhook → headless agent reply (recipe + templates) | [Agent responder recipe](docs/agent-responder-recipe.md) · [Templates](examples/agent-responder/README.md) |
391| Understand exposure and privacy | [Security guide](docs/security.md) · [Report a vulnerability](SECURITY.md) |
392 
393Framework examples and local fixtures are not evidence of a production end-to-end
394run. Their own READMEs identify which paths use fake services and which require an
395explicit live invocation.
396 
397<a id="roadmap"></a>
398## Direction and contribution
399 
400**Bring your own agents. Keep work inspectable. Own your infrastructure. Build the
401handoff, not another runtime.**
402 
403The next direction is simpler connection to existing working environments and clearer
404recovery/operating guidance—not a promise of a general scheduler, shared workspace
405manager, global federation or automatic code execution. Current capabilities are
406listed above; release history belongs in [CHANGELOG.md](CHANGELOG.md). The npm badge
407tracks the MCP package, not a unified server/deployment version.
408 
409See [#251](https://github.com/openagentemail/openagentemail/issues/251) for the README
410refresh and remaining live-demo/website follow-ups. The
411[website repository](https://github.com/openagentemail/website) owns public-site
412presentation and documentation.
413 
414<a id="contributing"></a>
415Issues and PRs are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md): substantial work
416starts with an issue; independent review and green CI are required before merge.
417Documentation changes should track actual code, not anticipated capabilities.
418 
419<details>
420<summary>Operator shortcuts and advanced deployment</summary>
421 
422## Using your own mail server
423 
424Already have a mail provider for your domain? Run the API by itself with
425[`compose.api-only.yaml`](compose.api-only.yaml), connected to that provider's
426catch-all mailbox. The [external mail server guide](https://openagent.email/docs/guides/external-mailserver/)
427covers the required catch-all setup, Portainer deployment, SMTP sender limits,
428and TLS certificate verification.
429 
430The standalone default project name is `openagentemail`. If the full
431`compose.yaml` stack also runs on the same host, the API-only stack must not
432share that default project: give it an explicitly different `-p` value or
433`COMPOSE_PROJECT_NAME` so the two stacks cannot adopt each other's resources.
434 
435To run multiple API-only instances on one host, give every instance its own
436environment file, unique Compose project, and host `API_PORT`. The API always
437listens on port 3100 inside its container; `API_PORT` changes only the host-side
438mapping. For example:
439 
440```bash
441mkdir -p ../oae-api-only-env
442cp .env.api-only.example ../oae-api-only-env/alpha.env
443cp .env.api-only.example ../oae-api-only-env/beta.env
444chmod 600 ../oae-api-only-env/*.env
445# Set API_PORT=3100 in alpha.env and API_PORT=3101 in beta.env.
446# Generate separate API_KEYS and TASK_SIGNING_SECRET values in each file.
447 
448docker compose -p oae-alpha --env-file ../oae-api-only-env/alpha.env -f compose.api-only.yaml up -d
449docker compose -p oae-beta --env-file ../oae-api-only-env/beta.env -f compose.api-only.yaml up -d
450```
451 
452You may set a unique `COMPOSE_PROJECT_NAME` for each command instead of using
453`-p`. The project names make Compose generate distinct container names and
454project-scoped named volumes. Each instance must have independently generated
455`API_KEYS` and `TASK_SIGNING_SECRET` values. Use distinct `API_PORT` values,
456separate data volumes and the intended independent mailbox/provider boundary;
457this is not a multi-writer recipe. Keep populated files outside the repository.
458 
459## Read mail in a browser
460 
461Open `/ui` through localhost, an SSH tunnel or HTTPS and log in with an appropriate
462token. Ordinary sessions and persisted **Trust this device** sessions have different
463lifetimes; restarting the API does not discard every trusted session. See
464[UI access and sessions](docs/operator-guide.md#ui-access-and-sessions).
465The dashboard UI supports `en` / `es` / `ja` / `ko` / `zh-CN` via the Settings language
466selector (`oa_lang` cookie); terminology follows [docs/i18n-glossary.md](docs/i18n-glossary.md).
467 
468<a id="admin-overview"></a>
469Overview counts are a bounded window, not lifetime totals. Cache timing, unknown
470counts and scan limits are described in the [operator guide](docs/operator-guide.md#overview-counts).
471 
472### Multi-domain support
473 
474`DOMAIN` plus `EXTRA_DOMAINS` defines domains managed by **one instance**. Explicit
475identities with the same localpart can coexist on different configured domains;
476full-address duplicates cannot. Additional domains still require provider/MTA
477routing and DNS/DKIM setup. This is not independent-instance federation. See
478[multi-domain operations](docs/operator-guide.md#multiple-domains).
479 
480<a id="public-tls-with-lets-encrypt-opt-in"></a>
481[Public mail TLS and renewal](docs/operator-guide.md#public-mail-tls) are opt-in;
482HTTPS for the API needs its own trusted reverse proxy or tunnel.
483 
484<a id="optional-compliance-archive"></a>
485[Optional compliance archive](docs/operator-guide.md#optional-compliance-archive):
486`ALWAYS_BCC` is off by default and creates an additional off-domain data recipient.
487 
488<a id="why-self-host"></a>
489Self-host for control of deployment, data paths and policies—not a promise of zero
490cost, unlimited hardware capacity or immunity from upstream provider policies.
491 
492<a id="server-requirements"></a>
493[Resource planning](docs/operator-guide.md#resource-planning) depends on the workload;
494this README does not publish an undated benchmark or a VPS-price guarantee.
495 
496<a id="comparison"></a>
497OAE is an open-source option for agent email and task handoffs. Detailed vendor
498comparisons need dated primary sources; the old unchecked feature matrix is retired.
499 
500<a id="docs"></a>
501[Documentation index](#examples-and-documentation).
502 
503</details>
504 
505## License
506 
507[Apache-2.0](LICENSE).
508 

Discussion

Alternatives