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
8 changes: 4 additions & 4 deletions .agents/skills/mock-agentmail-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ R_AGENTMAIL_API_KEY=mock-agentmail-api-key # any value; the harness a
AGENTMAIL_API_BASE_URL=http://127.0.0.1:3015 # reroutes ALL outbound AgentMail API calls to the harness
```

Pods are supported: `POST /v0/pods` (idempotent per `client_id`) creates one, and the pod-scoped management routes (`/v0/pods/{pod_id}/inboxes...`, `/v0/pods/{pod_id}/webhooks...`) only see resources inside that pod, while the organization-level routes see everything. To exercise a pod-scoped setup, seed a pod in the scenario file (`pods: [{ pod_id: 'pod_acme' }]`) and set `R_AGENTMAIL_POD_ID=pod_acme` for the app. Webhook updates follow the real API: `add_inbox_ids` / `remove_inbox_ids` (and `add_pod_ids` / `remove_pod_ids` at organization level), a non-empty `event_types` list replaces the subscription, and the `url` is immutable.
The app registers its webhook through the inbox-scoped routes (`/v0/inboxes/{inbox_id}/webhooks...`), which is what an inbox-scoped API key can reach; the harness serves those alongside the organization-level and pod-scoped (`/v0/pods/{pod_id}/...`) routes, and the app resolves its inbox from `GET /v0/inboxes`, so seed exactly one inbox unless you are testing the "key sees several inboxes" refusal. Webhook updates follow the real API: `add_inbox_ids` / `remove_inbox_ids` (and `add_pod_ids` / `remove_pod_ids` at organization level), an inbox-scoped webhook only changes `event_types`, a non-empty `event_types` list replaces the subscription, and the `url` is immutable.

Webhook secrets need no manual wiring: when the app registers its webhook through `POST /v0/webhooks`, the harness mints the `whsec_...` secret and returns it, exactly like real AgentMail. If the app relies on a pre-provisioned secret (`R_AGENTMAIL_WEBHOOK_SECRET`), seed a webhook with that secret in the scenario file instead — deliveries are signed with whatever secret the registration holds.
Webhook secrets need no manual wiring: when the app registers its webhook through `POST /v0/inboxes/{inbox_id}/webhooks`, the harness mints the `whsec_...` secret and returns it, exactly like real AgentMail. If the app relies on a pre-provisioned secret (`R_AGENTMAIL_WEBHOOK_SECRET`), seed a webhook with that secret in the scenario file instead — deliveries are signed with whatever secret the registration holds.

## Step 2: Create a scenario file

Expand Down Expand Up @@ -159,8 +159,8 @@ To reset between scenarios, `POST /mock/state` with a fresh state object (it rep
- **`duplicate-delivery`** — `duplicate: true` → same svix-id twice → exactly-once handling
- **`oversize-payload`** — `oversize: true` → app must re-fetch the message body by id before acting
- **`auto-submitted-loop-guard`** — `autoSubmitted: true` → automated senders must not trigger reply loops
- **`webhook-registration`** — app boots, registers its webhook via `POST /v0/webhooks` (idempotent per `client_id`), and the secret round-trips into signature verification
- **`pod-scoped-setup`** — with `R_AGENTMAIL_POD_ID` set and a seeded pod, the app provisions its inbox and webhook under `/v0/pods/{pod_id}/...` and deliveries for inboxes outside the pod never reach it
- **`webhook-registration`** — app boots, registers its webhook via `POST /v0/inboxes/{inbox_id}/webhooks` (idempotent per `client_id`), and the secret round-trips into signature verification
- **`inbox-scoped-setup`** — with one seeded inbox, the app resolves it from the key and registers its webhook under `/v0/inboxes/{inbox_id}/webhooks`; with two seeded inboxes the save is refused and names both
- **`reply-idempotency`** — app retries a reply with the same `Idempotency-Key` → exactly one outbound message in `/mock/state`
- **`bounce-suppression`** — `kind: 'bounce'` (Permanent) / `kind: 'complaint'` → the recipient lands in `agentmail_suppressions` and outbound-initiated email to them is refused; `bounceType: 'Transient'` must NOT suppress

Expand Down
5 changes: 2 additions & 3 deletions apps/docs/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,9 @@ as per-task auth tokens or workspace paths.
| `R_DISCORD_GATEWAY_SECRET` | Discord | Shared internal secret for Discord event delivery between BullMQ and API. Auto-generated when Discord is saved in the UI if unset, and auto-healed when Discord is already configured without one. |
| `DISCORD_API_BASE_URL` | Optional | Discord REST API base URL override, primarily for testing. |
| `R_EMAIL_CHANNEL_ENABLED` | Optional | Set to `true` to enable the email (AgentMail) channel. Without it, email is absent from settings, inbound webhooks are ignored, and Roomote never sends email. Enabling it also turns on account email verification. |
| `R_AGENTMAIL_API_KEY` | Optional | AgentMail API key for email. Overrides the value saved in the settings UI. |
| `R_AGENTMAIL_API_KEY` | Optional | AgentMail API key for email, created from inside the deployment's inbox (inbox-scoped). Overrides the value saved in the settings UI. |
| `R_AGENTMAIL_WEBHOOK_SECRET` | Optional | AgentMail webhook secret. Overrides the value managed by the settings UI. |
| `R_AGENTMAIL_INBOX_ID` | Optional | AgentMail deployment inbox. Overrides the inbox connected in the settings UI. |
| `R_AGENTMAIL_POD_ID` | Optional | AgentMail pod the inbox and webhook live in; required with a pod-scoped API key. Overrides the pod entered in the settings UI. |
| `R_AGENTMAIL_INBOX_ID` | Optional | AgentMail deployment inbox. Normally derived from the key on save; set it to pin the inbox for env-var-only setups. |
| `AGENTMAIL_API_BASE_URL` | Optional | AgentMail API base URL override, primarily for testing. Defaults to `https://api.agentmail.to`. |
| `R_MICROSOFT_CLIENT_ID` | Microsoft sign-in | Microsoft OAuth client ID. |
| `R_MICROSOFT_CLIENT_SECRET` | Microsoft sign-in | Microsoft OAuth client secret. |
Expand Down
66 changes: 29 additions & 37 deletions apps/docs/providers/communications/agentmail.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,53 +29,45 @@ is never blocked on verification.

## Connect an AgentMail inbox

Create an AgentMail account and API key at
[console.agentmail.to/dashboard/api-keys](https://console.agentmail.to/dashboard/api-keys).
The API key must carry these AgentMail permissions (or be a full-access
key): `inbox_read`, `inbox_create`, `inbox_update`, `webhook_read`,
`webhook_create`, `webhook_update`, `webhook_delete`, `message_read`, and
`message_send`. Missing permissions fail at save time with an error naming
the refused step, except `message_send`, which has no side-effect-free
check and is exercised on the first reply.

In the Roomote UI (**Settings > Communications > Email (AgentMail)**),
paste the API key, then save. Roomote proposes a deployment inbox address such as
`roomote-yourhost-a1b2c3@agentmail.to`; edit the address before creation, or
supply the address of an existing AgentMail inbox instead. On save, Roomote
creates the inbox and registers a webhook for `message.received`,
`message.bounced`, and `message.complained` events automatically. The
connected inbox address and the registered webhook URL are shown in
settings.
Create an AgentMail account at [console.agentmail.to](https://console.agentmail.to),
then:

1. **Create the inbox** Roomote should receive mail at (or pick an existing
one). AgentMail's free tier includes 3 inboxes and 100 emails per day and
adds a "Sent via AgentMail" footer to outbound mail; custom domains, with
AgentMail-managed SPF/DKIM/DMARC, need a paid plan and are recommended for
production.
2. **Create an API key from inside that inbox**, so the key is scoped to it.
Give it these permissions (or full access): `inbox_read`, `inbox_update`,
`webhook_read`, `webhook_create`, `webhook_update`, `webhook_delete`,
`message_read`, and `message_send`.
3. In the Roomote UI (**Settings > Communications > Email (AgentMail)**),
paste the key and save.

Roomote uses the inbox the key is scoped to; there is nothing else to enter.
On save it validates the key, registers a webhook on the inbox for
`message.received`, `message.bounced`, and `message.complained` events, and
shows the connected address and webhook URL in settings. Missing permissions
fail at save time with an error naming the refused request, except
`message_send`, which has no side-effect-free check and is exercised on the
first reply.

An inbox-scoped key is the intended shape: it cannot read other inboxes or
create new ones, so a leaked key exposes one mailbox rather than the
account. An organization-level key is accepted only while the account has
exactly one inbox, since Roomote would otherwise have no way to tell which
inbox is for this deployment.

For self-hosted env-var configuration instead of the UI, all values are
optional overrides of the settings UI:

```sh
# Optional — configure email entirely from settings when unset:
# R_AGENTMAIL_API_KEY=<agentmail-api-key>
# R_AGENTMAIL_API_KEY=<inbox-scoped-agentmail-api-key>
# R_AGENTMAIL_WEBHOOK_SECRET=<webhook-secret>
# R_AGENTMAIL_INBOX_ID=<inbox-address>
# R_AGENTMAIL_POD_ID=<pod-id>
```

### Pods and pod-scoped keys

AgentMail [pods](https://docs.agentmail.to/documentation/core-concepts/pods)
isolate inboxes, domains, and mail per tenant inside one AgentMail
organization, and a pod-scoped API key can only reach its own pod. If the
deployment was handed a pod-scoped key, or you want Roomote's inbox and
webhook kept inside a particular pod, enter the pod id in the **AgentMail
Pod ID** field (or set `R_AGENTMAIL_POD_ID`). Roomote then lists, creates,
and updates the inbox and webhook through the pod's endpoints; inbound and
outbound mail is unaffected. Leave the field empty to use the organization's
inboxes directly. Inboxes cannot move between pods, so changing the pod id
later means choosing or creating an inbox inside the new pod.

AgentMail's free tier allows 3 inboxes and 100 emails per day and adds a
"Sent via AgentMail" footer to outbound mail. Custom domains, with
AgentMail-managed SPF/DKIM/DMARC, require a paid AgentMail plan. For
production use, a paid plan with a custom domain is recommended.

## Email Roomote

Send an email to the deployment inbox from an email address on your Roomote
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,14 @@ export function ProviderSetupInstructions({
if (providerId === 'agentmail') {
return (
<div className={cn('space-y-3 max-w-xl', className)}>
<InstructionText heading="API key">
In the AgentMail console, create an API key and paste it below.
<InstructionText heading="Inbox">
In the AgentMail console, create the inbox Roomote should receive mail
at (or pick an existing one).
</InstructionText>
<InstructionText heading="Inbox email address">
Leave the address blank and Roomote provisions an inbox for this
deployment automatically, or enter an existing AgentMail inbox address
to use it instead.
<InstructionText heading="API key">
Open that inbox and create an API key from inside it, so the key is
scoped to the inbox. Paste it below; Roomote uses the inbox the key is
for.
</InstructionText>
<InstructionText heading="Webhook">
Roomote registers the AgentMail webhook for incoming mail
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/(onboarding)/setup/providerSetupCopy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const PROVIDER_SETUP_COPY: Record<ProviderSetupCopyId, ProviderSetupCopy> = {
setupLabel: 'Discord bot',
},
agentmail: {
creationHref: 'https://console.agentmail.to/dashboard/api-keys',
setupLabel: 'AgentMail API key',
creationHref: 'https://console.agentmail.to/dashboard/inboxes',
setupLabel: 'AgentMail inbox',
},
};

Expand Down
Loading
Loading