---
name: mailbox-bridge
description: |
  Use this skill when external coding agents (Claude Code, Aider, custom
  scripts) need to participate in the project's shared WrongStack mailbox,
  or when a user asks to "expose the mailbox", "let Claude Code read the
  mailbox", "external agent mailbox", "mailbox bridge", or "HTTP mailbox
  bridge". Starts a loopback HTTP façade over the same GlobalMailbox that
  WrongStack-internal agents already share, so any agent with curl or
  fetch can read, send, and acknowledge messages.
version: 1.0.0
---

# Mailbox Bridge — Expose the Shared Mailbox to External Agents

> **Bundled skill.** This file is shipped with `@wrongstack/core` and
> auto-discovered via `bundledSkillsDir`. To pin it to a specific project,
> run `wstack skill install <path-to-this-file>` once — the project-level
> manifest at `~/.wrongstack/projects/<slug>/installed-skills.json` will
> record that override.

## Overview

WrongStack-internal agents (CLI, TUI, WebUI, ACP) already share one
project-level mailbox at `~/.wrongstack/projects/<slug>/_mailbox.jsonl`.
This skill starts a thin loopback HTTP server that wraps that exact same
`GlobalMailbox` so external coding agents — Claude Code, Aider, Continue,
a user's own scripts — can read and send messages on the same channel
without going through the JSONL file or implementing the file-lock
protocol.

```
┌────────────────────────────────────────────────────────────────────┐
│                    WrongStack project dir                          │
│                                                                    │
│   ~/.wrongstack/projects/<slug>/                                  │
│   ├── _mailbox.jsonl            ← shared message store              │
│   ├── _mailbox.registry.json    ← agent heartbeats                  │
│   └── _mailbox.clients.json     ← REPL/TUI/WebUI/external clients   │
│                                                                    │
│              ▲             ▲             ▲             ▲            │
│              │             │             │             │            │
│       ┌──────┴───┐  ┌──────┴───┐  ┌──────┴───┐  ┌──────┴───┐       │
│       │ Leader A │  │ BugHunter│  │ WebUI    │  │ External │        │
│       │ (CLI)    │  │ (CLI)    │  │ (browser)│  │ agent    │        │
│       └──────────┘  └──────────┘  └──────────┘  └─────▲────┘       │
│                                                       │            │
│                                            HTTP POST │ /mailbox/* │
│                                                       │            │
└───────────────────────────────────────────────────────┼────────────┘
                                                        │
                                          ┌─────────────┴───────────┐
                                          │  wstack mailbox serve   │
                                          │  (this skill)           │
                                          │  wraps GlobalMailbox    │
                                          └─────────────────────────┘
```

The bridge does NOT introduce a parallel store. External calls go through
`GlobalMailbox`, so file locking, mtime-bounded reads, agent heartbeats,
read receipts, and HQ telemetry happen exactly as they do for
WrongStack-internal callers. An external agent and a WrongStack-internal
agent posting to the same `agentId` are indistinguishable to the rest of
the system — the WebUI's "online agents" panel will show them side by
side, with `source = 'http'` distinguishing the HTTP path.

## When to use this skill

- The user asks to "let Claude Code send/receive on the mailbox".
- The user wants to run a script (build bot, CI hook, alerting agent)
  alongside WrongStack that should participate in the project's inter-agent
  coordination.
- The user wants to debug or inspect mailbox traffic from another tool
  without granting it access to the JSONL file.

## When NOT to use this skill

- The external agent speaks MCP natively — use `wstack mcp serve` instead
  (it exposes WrongStack's tool registry, including the mailbox tool).
- The user wants SMTP/IMAP-style email integration — WrongStack's mailbox
  is internal-only and is not an email server. Reject that direction.
- The user wants the external agent to act on the wider file system or
  other WrongStack tools — the bridge exposes ONLY mailbox operations.

## Setup

Run from any terminal where `wstack` is on PATH and the project is the
working directory:

```
wstack mailbox serve
```

Or, if the user is already in a WrongStack REPL/TUI:

```
/mailbox-serve
```

The server prints its bind URL and writes the bearer token to
`~/.wrongstack/projects/<slug>/.mailbox.token` (mode `0600`). The token
is rotated on every server start, so external agents must read it
freshly each time they connect — never hardcode.

To pass it to the external agent, set two environment variables:

```
WRONGSTACK_MAILBOX_URL=http://127.0.0.1:7788
WRONGSTACK_MAILBOX_TOKEN=$(cat ~/.wrongstack/projects/<slug>/.mailbox.token)
```

### Flags

| Flag | Default | Notes |
|------|---------|-------|
| `--host <ip>` | `127.0.0.1` | Loopback by default. Pass `0.0.0.0` to expose on LAN — NOT recommended without a reverse proxy that re-authenticates and rate-limits. |
| `--port <n>` | `7788` | Requested port used when `--strict-port` is set. In non-strict mode the server deliberately passes port `0` so the OS assigns a free port, even when a port value was supplied. |
| `--strict-port` | off | Bind the requested/default port exactly and fail on `EADDRINUSE`; without it, bind an OS-assigned free port. |

## Routes

All routes take JSON bodies on POST (or no body on GET). All requests
require `Authorization: Bearer <token>`. All responses are JSON.

| Method | Path | Wraps |
|--------|------|-------|
| POST | `/mailbox/send` | `GlobalMailbox.send` |
| POST | `/mailbox/query` | `GlobalMailbox.query` |
| POST | `/mailbox/check` | convenience inbox check: direct/base/broadcast query plus optional read/completion batch ack |
| POST | `/mailbox/ack` | `GlobalMailbox.ack` |
| POST | `/mailbox/ack-many` | `GlobalMailbox.ackMany` (batch under one lock + rewrite) |
| POST | `/mailbox/unread-count` | `GlobalMailbox.unreadCount` |
| POST | `/mailbox/agents/register` | `GlobalMailbox.registerAgent` (`source = 'http'`) |
| POST | `/mailbox/agents/heartbeat` | `GlobalMailbox.heartbeat` |
| POST | `/mailbox/register-client` | `GlobalMailbox.registerClient` (`source = 'http'`) |
| POST | `/mailbox/heartbeat` | `GlobalMailbox.clientHeartbeat` |
| POST | `/mailbox/purge-clients` | `GlobalMailbox.purgeClients` |
| GET | `/mailbox/agents` | `GlobalMailbox.getAgentStatuses` |
| GET | `/mailbox/agents/online` | `GlobalMailbox.getOnlineAgents` |
| GET | `/mailbox/events` | authenticated SSE stream for mailbox events |
| GET | `/healthz` | liveness probe (no auth or rate limit) |

### Error shape

Every error response follows the WrongStack API convention:

```json
{ "error": { "code": "VALIDATION_ERROR", "message": "field \"from\" is required (string)" } }
```

| Code | HTTP | When |
|------|------|------|
| `VALIDATION_ERROR` | 400 | Missing/wrong-type field in request body, body too large, or invalid JSON. |
| `UNAUTHORIZED` | 401 | Missing or wrong bearer token. |
| `NOT_FOUND` | 404 | No route for the request method + URL. |
| `RATE_LIMITED` | 429 | More than 120 authenticated requests in the rolling 60-second window. |
| `INTERNAL_ERROR` | 500 | `GlobalMailbox` threw (e.g. file-lock contention, disk full). |

### Limits

- Body cap: **256 KB**. The mailbox message format is small; this leaves
  headroom for long bodies and base64 attachments while rejecting
  pathological payloads before they reach `JSON.parse`.
- Authenticated routes share a per-bearer sliding-window limit of **120
  requests per 60 seconds**. `/healthz` bypasses authentication and the limit.
  This bounds accidental flooding; it is not an identity or authorization
  boundary because every caller uses the same project token.

## The HQ dashboard writes to the same mailbox

The HQ command center (`wstack --hq`) shares this exact `GlobalMailbox`.
When an operator sends a prompt from the HQ screen, it lands in the same
`_mailbox.jsonl` an external agent reads through `/mailbox/query` — so an
external agent participating via this bridge sees HQ prompts too.

HQ delivers a prompt one of two ways:

- **`POST /api/command`** on the HQ server — routes to a *connected*
  client, which then calls `GlobalMailbox.send`.
- **`POST /api/mailbox-send`** on the HQ server — writes to the project
  mailbox **directly**, so the prompt is delivered even when no agent is
  connected. The HQ server resolves the target `projectRoot` from its
  `SessionRegistry` (never a browser-supplied path).

Either way, the resulting mailbox message carries one of the HQ **send
types**, which map onto the mailbox `type` field an external agent will
observe:

| HQ send type | Mailbox `type` | Intent for the receiver |
|--------------|----------------|-------------------------|
| `steer`      | `steer`        | Change course now.       |
| `btw`        | `btw`          | FYI / context — no course change demanded. |
| `queue`      | `note`         | A queued prompt; handle before the next step. |
| `broadcast`  | `broadcast`    | Sent to all agents on the project (`to: all`). |

An external agent does not need to distinguish HQ-originated messages —
they arrive with `from` set to `hq@<tag>` and are read, acked, and
completed through the same `/mailbox/query` + `/mailbox/ack` routes as
any other message. Filter on `from` if you want to treat HQ prompts
specially.

## Pairing with the external-facing skill

This internal skill describes how to run the server. The
`wrongstack-mailbox` skill (also bundled with `@wrongstack/core`) describes
how the external agent uses the routes. When configuring an external agent,
install both:

- In the WrongStack project: `bundledSkillsDir/mailbox-bridge/` (this file)
- In the external agent's project: copy
  `bundledSkillsDir/wrongstack-mailbox/SKILL.md` to the agent's skills
  directory (e.g. `.claude/skills/wrongstack-mailbox/SKILL.md`).
  The repo ships `scripts/install-mailbox-bridge-skills.sh` for this.

## Examples

### Start the bridge from REPL or TTY

```
$ wstack mailbox serve
WrongStack mailbox bridge listening on http://127.0.0.1:34827
Project dir:  ~/.wrongstack/projects/wrongstack-abc1234
Token file:   ~/.wrongstack/projects/wrongstack-abc1234/.mailbox.token (mode 0600)

Routes:
  POST /mailbox/send              send a message
  POST /mailbox/query             query messages
  ...
  GET  /healthz                   health probe (no auth)

Send the bearer token in: Authorization: Bearer <token>
Cat the token from another shell:
  cat ~/.wrongstack/projects/wrongstack-abc1234/.mailbox.token

Press Ctrl+C to stop.
```

### Send a message via curl

```
curl -X POST http://127.0.0.1:34827/mailbox/send \
  -H "Authorization: Bearer $(cat ~/.wrongstack/projects/wrongstack-abc1234/.mailbox.token)" \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "external-scout",
    "to": "*",
    "type": "broadcast",
    "subject": "Hello from outside",
    "body": "External agent has joined the conversation.",
    "audience": "leaders",
    "priority": "normal"
  }'
```

Use optional `"audience": "leaders"` when a project/session message is
operator context that only main agents should consume. Subagent inbox/check
delivery filters it out; omitting the field preserves normal all-agent delivery.

### Register so the external agent appears in the WebUI

```
curl -X POST http://127.0.0.1:34827/mailbox/agents/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{ "agentId": "claude-code-3941", "sessionId": "external",
        "name": "Claude Code", "role": "external", "pid": 3941 }'
```

The agent now shows up at `GET /mailbox/agents` and in the WebUI's
online-agents panel with `source: 'http'`.

### Heartbeat loop (keep the agent visible as "online")

```
# Every 30 s while alive:
curl -X POST http://127.0.0.1:34827/mailbox/agents/heartbeat \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{ "agentId": "claude-code-3941", "currentTask": "auditing auth layer" }'
```

Without heartbeats the agent flips to offline after 60 s.

### Query with a poll

```
# Every 5–10 s, only new messages since last poll:
curl -X POST http://127.0.0.1:34827/mailbox/query \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{ "to": "claude-code-3941", "since": "2026-06-27T08:50:00.000Z", "limit": 50 }'
```

### Acknowledge many in one batch

```
curl -X POST http://127.0.0.1:34827/mailbox/ack-many \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{ "acks": [
    { "messageId": "msg_aaa", "readerId": "claude-code-3941", "read": true },
    { "messageId": "msg_bbb", "readerId": "claude-code-3941", "read": true, "completed": true, "outcome": "Handled in PR #42" }
  ]}'
```

The batch path takes a single file lock and does one rewrite — preferred
over N sequential `/mailbox/ack` calls.

## How it ends

`Ctrl+C` (SIGINT) or `SIGTERM` triggers a graceful shutdown: stop accepting
new connections, let in-flight requests finish, flush the mailbox cache,
unlink the token file. The `mailbox_serve_started` and
`mailbox_serve_stopping` JSON log lines on stdout are the deterministic
hooks for any log-shipper watching the process.

## Health watchdog

`packages/core/src/coordination/mailbox-health.ts` (bundled with the
bridge) provides a `MailboxHealthWatchdog` that periodically probes
`/healthz` and posts a `mailbox-bridge-down` status event to the project
mailbox if the bridge stops responding. Wire it up once via the
`mailbox:watchdog:start` slash command or by calling
`mailbox-health:start()` from a custom subcommand. Default probe interval
is 15 s.

## Security notes

- The token is the only credential. Anyone who can read
  `~/.wrongstack/projects/<slug>/.mailbox.token` AND reach the bind host
  can act on the project's mailbox. Loopback binding makes "reach"
  require shell access on the host machine.
- The shared bearer is **not bound to an agent identity or capability set**.
  An authenticated caller supplies message `from`/type, registration ids, and
  acknowledgement `readerId`; the bridge does not separately authorize
  `steer`/control messages or prevent impersonation. Add an identity-aware
  trusted proxy before exposing it beyond mutually trusted local clients.
- Token comparison uses `timingSafeEqual`.
- The bridge does NOT log message bodies. The structured
  `mailbox_serve_started` event includes the bind URL, port, project dir,
  and token path — never the token itself.
- The HTTP server has no request logging at the access-log level. If
  audit trails of which external agent called which route are needed,
  the agent itself should log them client-side.

## Skills in scope

- `prompt-engineering` — for the external-facing `wrongstack-mailbox`
  skill that pairs with this one.
- `node-modern` — for `AbortSignal.timeout` patterns the external agent
  should use when calling these routes.
- `output-standards` — for the `<nextsteps>` shape in the paired
  external skill.
- `security-scanner` — for confirming the bridge's bearer-token handling
  matches project security conventions.
