Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,27 @@ REGISTRATION_POLICY=open
# Comma-separated domains whose DID:web agents are auto-approved
# DID_WEB_ALLOWED_DOMAINS=example.com,partner.io

# Mailgun Outbox (optional — enables outbound email for agents)
MAILGUN_API_KEY=
MAILGUN_API_URL=https://api.mailgun.net/v3
MAILGUN_WEBHOOK_SIGNING_KEY=
# Email (Resend + Cloudflare)
# Outbound email via Resend (https://resend.com)
RESEND_API_KEY=
# Secret for verifying Resend webhook delivery status events (Svix-signed)
RESEND_WEBHOOK_SECRET=

# Inbound email via Cloudflare Email Routing → Worker → ADMP
# Shared secret between the Cloudflare Worker and this server
INBOUND_EMAIL_SECRET=
# Domain used to construct agent email addresses
INBOUND_EMAIL_DOMAIN=agentdispatch.io

# Cloudflare API — used by scripts/provision-resend-dns.js and src/lib/cloudflare.js
# Token requires DNS:Edit (and Email Routing:Edit for inbound setup)
CLOUDFLARE_API_TOKEN=
# Zone ID for the sending/inbound domain (optional — looked up automatically if omitted)
CLOUDFLARE_ZONE_ID=

# Resend DNS values — copy exact values from Resend dashboard → Domains → DNS Records
# Only needed when running scripts/provision-resend-dns.js
RESEND_DKIM_CONTENT=
RESEND_MX_CONTENT=
RESEND_SPF_CONTENT=
RESEND_MX_PRIORITY=10
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,64 @@ See `.env.example` for all configuration options.
| `API_KEY_REQUIRED` | false | Enable API key auth |
| `MASTER_API_KEY` | - | Master API key (if auth enabled) |

### Email

ADMP supports bidirectional email for agents using [Resend](https://resend.com) for outbound and Cloudflare Email Routing for inbound.

#### Agent Email Addresses

Every agent gets a platform email address:

```
{agentId}@agentdispatch.io
acme.alice@agentdispatch.io ← agent "alice" in tenant/namespace "acme"
```

The address format is controlled by the `INBOUND_EMAIL_DOMAIN` env var.

#### Inbound Email

1. A catch-all rule on `agentdispatch.io` in Cloudflare Email Routing forwards all mail to the `admp-email-ingestion` Cloudflare Worker.
2. The Worker parses the recipient address, reads the MIME body with `postal-mime`, and POSTs to `POST /api/webhooks/email/inbound`.
3. The ADMP server applies inbound policy:
- **Trusted sender** (`agent.metadata.email_trusted_senders`) -> auto-approved to `queued`
- **Unknown sender** -> quarantined as `review_pending` until approved
4. Approved messages are delivered via normal inbox pull.

**Trusted sender management endpoints (agent-authenticated):**

- `GET /api/agents/:agentId/email/trusted-senders`
- `POST /api/agents/:agentId/email/trusted-senders` with `{ "email": "trusted@example.com" }`
- `DELETE /api/agents/:agentId/email/trusted-senders` with `{ "email": "trusted@example.com" }`

**Review endpoint (internal policy/model worker):**

- `POST /api/webhooks/email/inbound/:messageId/review`
- Requires `X-Webhook-Secret: <INBOUND_EMAIL_SECRET>`
- Body: `{ "decision": "approve" | "reject", "reason"?: "...", "model_verdict"?: {...} }`

See [`workers/email-ingestion/README.md`](workers/email-ingestion/README.md) for Cloudflare setup.

**Required env vars:**

| Variable | Description |
|----------|-------------|
| `INBOUND_EMAIL_SECRET` | Shared secret between Cloudflare Worker and ADMP server |
| `INBOUND_EMAIL_DOMAIN` | Domain for agent email addresses (default: `agentdispatch.io`) |

#### Outbound Email

Agents can send email via `POST /api/agents/:agentId/outbox/send` after configuring a custom domain.

**Required env vars:**

| Variable | Description |
|----------|-------------|
| `RESEND_API_KEY` | Resend API key for outbound delivery |
| `RESEND_WEBHOOK_SECRET` | Validates Resend delivery status webhooks (Svix-signed) |

Custom domain setup: `POST /api/agents/:agentId/outbox/domain` then `POST /api/agents/:agentId/outbox/domain/verify`.

### Production Checklist

- [ ] Set `NODE_ENV=production`
Expand All @@ -883,6 +941,10 @@ See `.env.example` for all configuration options.
- [ ] Set up log aggregation (JSON logs via `pino`)
- [ ] Configure resource limits (memory, CPU)
- [ ] Set up HTTPS reverse proxy (nginx, Caddy)
- [ ] Configure `RESEND_API_KEY` and `RESEND_WEBHOOK_SECRET` for outbound email
- [ ] Configure `INBOUND_EMAIL_SECRET` and deploy Cloudflare Worker for inbound email

See [docs/EMAIL-SETUP.md](docs/EMAIL-SETUP.md) for a step-by-step email setup checklist (env vars, Worker deploy, DNS, validation).

## Architecture

Expand Down
203 changes: 203 additions & 0 deletions docs/AGENT-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ All request and response bodies are JSON (`Content-Type: application/json`).
9. [Approval Workflow](#9-approval-workflow)
10. [Known Limitations and Security Notes](#10-known-limitations-and-security-notes)
11. [Best Practices](#11-best-practices)
12. [Email](#12-email)

---

Expand Down Expand Up @@ -576,3 +577,205 @@ The pull response includes `"auto_acked": true` when the hub auto-acked the mess
- **Pull in a loop** with a reasonable `visibility_timeout` (30-60s) to avoid re-processing.
- **Use webhooks** (`POST /api/agents/:id/webhook`) for push-based delivery if your agent has a public endpoint. This eliminates polling latency.
- **Send heartbeats** (`POST /api/agents/:id/heartbeat`) at your configured interval (default 60s) to maintain `online` status.

---

## 12. Email

Every ADMP agent has a built-in email address. Humans and external systems can email your agent directly, and agents with verified custom domains can send email outbound.

### Your Agent's Email Address

Email addresses follow this format:

| Agent setup | Email address |
|-------------|--------------|
| Agent with namespace/tenant | `{namespace}.{agent_id}@agentdispatch.io` |
| Agent without namespace | `{agent_id}@agentdispatch.io` |

**Examples:**
- Agent `alice` in tenant `acme` → `acme.alice@agentdispatch.io`
- Agent `alice.v2` in tenant `acme` → `acme.alice.v2@agentdispatch.io`
- Agent `alice` with no tenant → `alice@agentdispatch.io`

Retrieve your agent's email address from the API:

```http
GET /api/agents/<agentId>
```

Response includes:
```json
{
"agent_id": "alice",
"email_address": "acme.alice@agentdispatch.io",
...
}
```

---

### Receiving Email

When someone sends an email to your agent's address, ADMP ingests it as an `email` message.

**Message type:** `email`

**Default safety flow (unknown sender):**

1. Message is stored as `review_pending` (not pullable yet).
2. A policy/review decision must approve or reject it.
3. On approve, status becomes `queued` and the message is pullable.
4. On reject, status becomes `failed`.

**Trusted sender fast-path:**

If the sender email is in the agent's trusted sender allowlist, ADMP auto-approves and queues immediately.

Configure trusted senders:

```http
GET /api/agents/<agentId>/email/trusted-senders
POST /api/agents/<agentId>/email/trusted-senders
DELETE /api/agents/<agentId>/email/trusted-senders
```

`POST` body:
```json
{ "email": "trusted.sender@example.com" }
```

`DELETE` body:
```json
{ "email": "trusted.sender@example.com" }
```

Review endpoint (typically called by your policy/model worker):

```http
POST /api/webhooks/email/inbound/<messageId>/review
X-Webhook-Secret: <INBOUND_EMAIL_SECRET>
Content-Type: application/json

{
"decision": "approve", // or "reject"
"reason": "optional rejection reason",
"model_verdict": { "risk_score": 0.12, "reason": "no phishing indicators" }
}
```

**Pull approved email and inspect it:**

```http
POST /api/agents/<agentId>/inbox/pull
```

Response:
```json
{
"message_id": "...",
"from": "email:sender.at.example.com",
"to": "alice",
"type": "email",
"subject": "(no subject)",
"body": {
"subject": "Hello from email",
"from_email": "sender@example.com",
"text": "Plain-text body of the email",
"html": "<p>HTML body, if present</p>"
},
"metadata": {
"source": "email",
"raw_size": 4096
}
}
```

**Notes:**
- The `from` field encodes the sender email as `email:{local}.at.{domain}` (replacing `@` with `.at.`) to satisfy ADMP's agent ID format constraints.
- Inbound email records include provenance fields such as `ingress_channel`, `ingress_trust`, and `review_status`.
- Acknowledge the message after processing with `POST .../ack`.
- Inbound email is sent with `retain_until_acked=true`; explicit ack is required.

---

### Sending Email

Agents with a **verified custom domain** can send outbound email. Three steps are required before sending:

**Step 1: Register your domain**

```http
POST /api/agents/<agentId>/outbox/domain
Content-Type: application/json

{ "domain": "yourdomain.com" }
```

Response includes DNS records to add:
```json
{
"domain": "yourdomain.com",
"status": "pending",
"dns_records": [
{ "type": "MX", "name": "@", "value": "..." },
{ "type": "TXT", "name": "_dmarc", "value": "..." }
]
}
```

**Step 2: Add DNS records** to your domain registrar, then trigger verification:

```http
POST /api/agents/<agentId>/outbox/domain/verify
```

**Step 3: Send email**

```http
POST /api/agents/<agentId>/outbox/send
Content-Type: application/json

{
"to": "recipient@example.com",
"subject": "Hello from my agent",
"body": "Plain-text body of the email",
"html": "<p>Optional HTML body</p>",
"from_name": "My Agent"
}
```

Response (202 Accepted):
```json
{
"id": "...",
"status": "queued",
"to": "recipient@example.com",
"subject": "Hello from my agent"
}
```

**Check delivery status:**

```http
GET /api/agents/<agentId>/outbox/messages/<messageId>
```

Status values: `queued` → `sent` → `delivered` (or `failed`)

**Domain management endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/agents/:id/outbox/domain` | Register a custom domain |
| `GET` | `/api/agents/:id/outbox/domain` | Get domain config and DNS records |
| `POST` | `/api/agents/:id/outbox/domain/verify` | Trigger DNS verification |
| `DELETE` | `/api/agents/:id/outbox/domain` | Remove domain config |
| `POST` | `/api/agents/:id/outbox/send` | Send an email |
| `GET` | `/api/agents/:id/outbox/messages` | List sent messages |
| `GET` | `/api/agents/:id/outbox/messages/:msgId` | Get message delivery status |

**Requirements:**
- `RESEND_API_KEY` must be configured on the ADMP server.
- Domain must be fully verified before sending.
- All outbox endpoints require agent authentication (HTTP Signature or API key).
72 changes: 72 additions & 0 deletions docs/EMAIL-SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Email Setup Checklist

Operator guide for enabling inbound and outbound email on an ADMP server.

## 1. Server environment variables

Set these where the ADMP server runs (e.g. `.env` or deployment config).

| Variable | Required for | Description |
|----------|---------------|-------------|
| `RESEND_API_KEY` | Outbound | Resend API key for sending email |
| `RESEND_WEBHOOK_SECRET` | Outbound | Validates Resend delivery webhooks (Svix-signed) |
| `INBOUND_EMAIL_SECRET` | Inbound | Shared secret; Worker sends this in `X-Webhook-Secret` |
| `INBOUND_EMAIL_DOMAIN` | Inbound (optional) | Domain in agent addresses (default: `agentdispatch.io`) |

- **Outbound only:** set `RESEND_API_KEY` and `RESEND_WEBHOOK_SECRET`.
- **Inbound:** set `INBOUND_EMAIL_SECRET` (and optionally `INBOUND_EMAIL_DOMAIN`). The same secret must be set in the Cloudflare Worker (see below).

## 2. Cloudflare Worker (inbound)

The Worker receives mail via Cloudflare Email Routing and POSTs to the ADMP server.

- **Full steps:** see [workers/email-ingestion/README.md](../workers/email-ingestion/README.md).

**Summary:**

1. **Deploy the Worker**
```bash
cd workers/email-ingestion
bun install
wrangler deploy
```

2. **Set Worker secrets** (must match server)
```bash
wrangler secret put ADMP_URL # e.g. https://api.yourdomain.com
wrangler secret put INBOUND_EMAIL_SECRET
```

3. **DNS & Email Routing** (Cloudflare dashboard)
- Enable **Email Routing** on the zone for your inbound domain.
- Add a **Catch-all** rule: Action = **Send to Worker** → `admp-email-ingestion`.

## 3. Resend (outbound)

- Create a Resend account and add/verify your sending domain.
- In Resend dashboard: create an API key → set as `RESEND_API_KEY`.
- Create a webhook endpoint pointing to your server:
`POST https://your-admp-server/api/webhooks/resend`
Copy the signing secret → set as `RESEND_WEBHOOK_SECRET`.

### Resend DNS records in Cloudflare

Resend’s “Fill in your DNS Records” screen shows records for **domain verification (DKIM)**, **sending (SPF/MX)**, and optional **DMARC**. To add them in Cloudflare:

1. In **Cloudflare Dashboard** → your zone → **DNS** → **Records**, add each record with the **Type**, **Name**, and **Content** (or **Target**) shown in Resend. Use **TTL** Auto unless you need a specific value.
2. **DKIM:** one TXT record (e.g. name `resend._domainkey`, content the long `p=MIGfMA...` string).
3. **SPF / sending:** MX and TXT for the subdomain Resend gives (e.g. `send`); set the **Priority** for the MX as shown (e.g. 10).
4. **DMARC (optional):** one TXT record name `_dmarc`, content e.g. `v=DMARC1; p=none;`.

If your DNS is managed by a partner (e.g. CircleInbox), ask them how to add these records in their Cloudflare setup.

## 4. Validation checklist

- [ ] **Outbound:** Register an agent with `email_address` (or ensure agent has one). Send a message via the outbox API; confirm the email is received and (optional) that a Resend delivery webhook is received.
- [ ] **Inbound:** Send an email to an agent address (e.g. `acme.alice@your-inbound-domain`). If the sender is not in the trusted-senders list, approve the message via `POST /api/webhooks/email/inbound/:messageId/review` with `decision: approve`, then pull from the inbox and confirm the message appears.
- [ ] **Trusted sender (optional):** Add a sender with `POST /api/agents/:agentId/email/trusted-senders` and send from that address; confirm the message is `queued` (no review step).

## 5. References

- Inbound policy, trusted senders, and review endpoint: [AGENT-GUIDE.md](AGENT-GUIDE.md#email-receiving) and [README](../README.md).
- Worker implementation and address format: [workers/email-ingestion/README.md](../workers/email-ingestion/README.md).
Loading