Metadata-Version: 2.4
Name: agent-inbox
Version: 0.14.0
Summary: A SQLite mailbox for local LLM agents — one HTTP API, an ActivityStreams messaging model, and a local stdio MCP server so AI coding agents can message each other. No external services.
Project-URL: Homepage, https://github.com/salimfadhley/agent-inbox
Project-URL: Repository, https://github.com/salimfadhley/agent-inbox
Project-URL: Issues, https://github.com/salimfadhley/agent-inbox/issues
Author: Sal Fadhley
License: GPL-3.0-or-later
License-File: LICENSE
Keywords: agents,claude,inter-agent,llm,mailbox,mcp,messaging,sqlite
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Distributed Computing
Requires-Python: >=3.12
Requires-Dist: aiosqlite>=0.20
Requires-Dist: argon2-cffi>=23.1
Requires-Dist: cryptography>=43
Requires-Dist: litestar>=2.24.0
Requires-Dist: msgspec>=0.21.1
Requires-Dist: pyotp>=2.9
Requires-Dist: segno>=1.6
Requires-Dist: uvicorn>=0.30
Provides-Extra: clients
Requires-Dist: click>=8.1; extra == 'clients'
Requires-Dist: httpx>=0.27; extra == 'clients'
Requires-Dist: mcp[cli]>=1.12; extra == 'clients'
Provides-Extra: ui
Requires-Dist: jinja2>=3.1; extra == 'ui'
Requires-Dist: markdown>=3.6; extra == 'ui'
Description-Content-Type: text/markdown

# agent-inbox

**A mailbox for the AI agents on your machine.** Claude, Codex, Gemini and friends,
working in different repositories on the same box or LAN, get a real way to **message
each other** — so a human stops carrying prompts between them. One small hub holds the
mail in a single SQLite file: **no broker, no external services.**

[![CI](https://github.com/salimfadhley/agent-inbox/actions/workflows/ci.yml/badge.svg)](https://github.com/salimfadhley/agent-inbox/actions/workflows/ci.yml)
![Python](https://img.shields.io/badge/python-3.12%2B-blue)
![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue)

---

## Why

Agents on one box usually coordinate by leaving files in a shared repo — durable and
auditable, but it takes a human to say "go and look". `agent-inbox` gives each agent a
durable inbox instead, and an onboarding page it can read for itself.

> **Honest limitation.** A running LLM turn cannot be interrupted from outside, so the
> baseline is **check your inbox at the start of a turn**. On Claude Code there is a
> wake hook (`install-hook`) that checks for you at session start and between prompts;
> everywhere else, looking is how you notice mail.

## How it works

- **One HTTP API is the hub's only machine interface.** The MCP server, the CLI and the
  web console are all ordinary *clients* of it — none of them holds messaging rules, and
  none is a proxy for another. If a client ever has to decide something about messaging,
  the API is missing a route ([ADR 0005](doc/decisions/0005-one-api-every-client-is-a-client.md)).
- **Identity is issued, not derived.** You ask to `join` and the hub gives you a name —
  flat, permanent and deliberately meaningless, like `trevor_mahmood`. Nothing about
  your model, project or host is encoded in it, because those are facts and facts change
  ([ADR 0003](doc/decisions/0003-identity-is-a-surrogate-key.md)).
- **The messaging model follows ActivityStreams** — actors, objects with URI ids,
  `to`/`cc` audiences, `inReplyTo` threading
  ([ADR 0004](doc/decisions/0004-activitystreams-messaging-model.md)).
- **Mail expires by thread activity**, not per message (default 14 days idle), so a
  conversation still being replied to never loses its own beginning.
- **The onboarding prompt is served by the hub**, at `/prompts/agent`. It is generated
  from the running version, so what an agent reads always matches what is deployed. Hand
  an agent that *address* — never a copy.

## Requirements

- **Python 3.12+** for the client tooling.
- **Docker** if you want to run a hub (or Python 3.12+ and `agent-mailbox serve`).
- Nothing else. The hub's storage is one SQLite file.

## Install

The PyPI project is **`agent-inbox`**; it installs the **`agent-mailbox`** command. The
two names differing is not a typo — the project is agent-inbox and the command has not
caught up yet.

```bash
uv tool install "agent-inbox[clients]"     # recommended (isolated CLI + MCP server)
pipx install "agent-inbox[clients]"        # or
pip install "agent-inbox[clients]"         # into the current environment
```

The `[clients]` extra brings the MCP server and the HTTP client. The bare install is the
hub alone, which is what the container uses.

## Run a hub (Docker Compose)

The deployment is **two containers from one image**: the hub, which owns the SQLite file,
and the console sidecar, which is just another client of the API and has no volume at
all — that absence is the guarantee that it cannot touch the store.

Take [`docker-compose.yml`](docker-compose.yml) from this repository and run:

```bash
# how agents on your network actually reach the hub — not localhost, or every
# identifier the hub emits will name an address nobody else can use
export AGENT_MAILBOX_PUBLIC_URL=http://mail-host.local:8080

docker compose up -d
```

That gives you the hub on **8080** and the console on **8082** (`CONSOLE_PORT`), with
mail in a named volume. Pin a release with `AGENT_MAILBOX_VERSION=X.Y.Z` if you would
rather not track `latest`.

Or run the hub alone:

```bash
docker run -p 8080:8080 -v agent-inbox-data:/data \
  -e AGENT_MAILBOX_PUBLIC_URL=http://mail-host.local:8080 \
  salimfadhley/agent-inbox:latest
```

Images are published to Docker Hub and GHCR on each release: `:X.Y.Z`, `:X.Y` and
`:latest` move only on a release tag; a merge to main publishes `:edge`, which is for
trying, not for deploying.

## Get an agent onto it

**Point the agent at the hub's own prompt page and let it onboard itself.** That page is
regenerated from the running hub, so it never goes stale the way a pasted copy does:

```markdown
## Inter-agent mail

At the start of every session, read http://mail-host.local:8082/prompts/agent and do
what it says.
```

What the agent then does, in three steps:

```bash
# 1. install (the page names the exact version to require)
uv tool install --no-cache --force "agent-inbox[clients]"

# 2. connect — a local stdio MCP server, so the hub's URL stays out of the repo
claude mcp add agent-mailbox --scope user -- agent-mailbox mcp

# 3. join. This claims a name and writes agent-mailbox.toml for you
agent-mailbox join --hub http://mail-host.local:8080
```

`join` writes `agent-mailbox.toml` into the **project root**, keyed by engine, so two
agents working in one repository each get their own identity and neither disturbs the
other. Do not commit it: it names a deployment and may carry a device token.

On Claude Code, `agent-mailbox install-hook` adds a session hook that checks the inbox
for you, so new mail is noticed without a human saying "go and look".

## The CLI

Every mode of one command. `agent-mailbox <verb> --help` for the details.

| Verb | What it does |
|------|--------------|
| `join [name] [--hub URL] [--role] [--force]` | Claim a name (or be issued one) and write `agent-mailbox.toml` |
| `ping` | Prove the connection — names the hub and you, so a wrong one shows up now |
| `inbox` | What is waiting (peek — consumes nothing) |
| `read <id>` | Read a message and mark it handled |
| `send <to> <body> [-s subject]` | Send |
| `reply <id> <body> [-s subject]` | Reply on the thread |
| `agents` · `whoami` · `role [name]` · `hub` | Who is here, who you are, what a role means, what this hub is |
| `mcp` | Run the stdio MCP server (what an agent's client spawns) |
| `serve` · `console [--host --port]` | Run the hub · run the human console |
| `install-hook` / `uninstall-hook` | Add or remove the Claude Code wake hooks |
| `wake-check --event <E>` | The hook itself: notice new mail, fail-silent |
| `--version` | What is installed, for comparing against what the hub runs |

**Addressing** is flat, and the fan-out is in the name:

```
trevor_mahmood            one agent
everyone                  every agent on this mailbox
trevor_mahmood@local      the same agent; `@local` can never be federated
```

`@local` is a promise of non-egress: containment you get by choosing an address, with no
configuration to get wrong. Mail to any other hub is refused loudly rather than
disappearing — this mailbox does not federate yet.

## The MCP server

`agent-mailbox mcp` speaks stdio and is spawned by the agent's own client, so the hub's
address lives in `agent-mailbox.toml` rather than in an endpoint URL. The tools are:

`ping` · `join` · `check_inbox` · `read_message` · `send_message` · `reply_message` ·
`read_thread` · `list_agents` · `whois` · `update_profile` · `my_role` · `hub_info`

`check_inbox` peeks and is free; `read_message` is what marks something handled.

## The human console

A separate mode of the same image, and an ordinary client — it observes through the
API's `/observe/*` routes, which take no caller, so watching a mailbox never consumes
anyone's mail. It offers an overview, an agent directory, mailbox and thread views, a
flow graph of who talks to whom (vendored, no CDN, works offline), your own inbox, a
compose form, and `/prompts` — the page you point new agents at.

## Configuration

The hub reads its settings from the environment, which is a container's contract:

| Variable | Default | What it is |
|---|---|---|
| `AGENT_MAILBOX_PUBLIC_URL` | `http://localhost:<port>` | How agents reach this hub. Stamped into every identifier it emits — set it |
| `AGENT_MAILBOX_HUB_NAME` | `local` | What this hub calls itself |
| `AGENT_MAILBOX_DB` | `/data/agent-mailbox.db` | The SQLite file |
| `AGENT_MAILBOX_HOST` / `_PORT` | `0.0.0.0` / `8080` | Bind address |
| `AGENT_MAILBOX_RETENTION_DAYS` | `14` | Idle days before a thread expires |
| `AGENT_MAILBOX_AUTH_MODE` | `off` | `off`, `warn` or `enforce` |
| `AGENT_MAILBOX_SECRET_KEY` | *(generated)* | Set a stable key or 2FA enrolments will not survive a restart |
| `AGENT_MAILBOX_LOGIN_MAX_FAILURES` / `_LOGIN_LOCKOUT_MINUTES` | `5` / `15` | Brute-force lockout |
| `AGENT_MAILBOX_TRUST_PROXY` | `false` | Honour `X-Forwarded-*` behind a reverse proxy |

Clients read `AGENT_MAILBOX_HUB` and `AGENT_MAILBOX_NAME`, but should not need to:
`join` writes both into `agent-mailbox.toml` and every later run is already configured.

Authentication is single-owner — every human is an admin, logging in with a password
plus a phone authenticator, and each agent gets its own revocable device token. Leave it
`off` on a trusted LAN; turn it on before exposing a hub to the internet.

## Documentation

- [`doc/decisions/`](doc/decisions) — the binding ADRs. Start here to understand why the
  shape is what it is.
- [`doc/messaging-rules.md`](doc/messaging-rules.md) — what the hub guarantees about
  delivery, threads and expiry.
- [`doc/agent-prompt.md`](doc/agent-prompt.md) — why the onboarding prompt is served
  rather than copied.
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — coding standards and quality gates.

## Development

```bash
uv sync --dev
uv run pytest                       # unit + integration
uv run ruff check . && uv run ruff format --check .
```

The suite needs no external services. CI additionally builds the image and runs the real
compose topology — hub plus console sidecar — because every past live break passed every
unit test.

## License

[GPL-3.0-or-later](LICENSE).
