Documentation Index
Fetch the complete documentation index at: https://alfred.black/docs/llms.txt
Use this file to discover all available pages before exploring further.
Sir’s email address
Every Alfred Black tenant is provisioned an inbox atalfred.<username>@mail.alfred.black. This is not a Composio-brokered Gmail integration — this is Alfred’s own email address, hosted on the shared AgentMail pod with per-tenant inbox-scoped credentials.
Sir’s username is derived from his account at provision time. Inbox addresses are unique across the fleet.
Mail addressed to this inbox routes through one of two paths depending on who sent it:
| Sender | Path | Effect |
|---|---|---|
| On the authorised list | Channel — full payload to ctrl-api /api/v1/channels/email/inbound | Spawns a one-shot openclaw session; the main agent reads the full thread (with quoted history) and replies, forwards, executes the request, or stays silent — per the alfred-email-channel skill |
| Not on the authorised list | Stream — extracted-text payload to /api/v1/streams/ingest with stream_type: "agentmail" | Zero-LLM ingest; the email becomes a vault event/ record, attached to relevant matters/people via the hourly enrichment pass |
Authorising a sender
The authorised list lives atvault/.auth/authorized_senders.json on Sir’s tenant and is managed via three endpoints:
How the dual dispatch works
When AgentMail receives a message for Sir’s inbox, it fires a Svix-signed webhook tohttps://alfred.black/webhooks/agentmail. The SaaS:
- Verifies the Svix signature using the shared webhook secret
- Looks up the tenant by
inbox_id - Extracts the bare email from the
fromfield (handles RFC 5322"Display Name" <addr@domain>"form) - Fetches the tenant’s authorised list (cached for 60s per tenant)
- Dispatches:
- Authorised: full message (including quoted history) → tenant
/api/v1/channels/email/inbound→ openclaw session spawn - Unauthorised: extracted-text only → tenant
/api/v1/streams/ingest→ vault event
- Authorised: full message (including quoted history) → tenant
StreamEvent row on the SaaS side as a fallback.
What the agent does on inbound
Thealfred-email-channel/SKILL.md is the agent’s playbook. The decision tree:
- Reply (plain) — Sir on
To:, no others onCc:, or others onCc:but the instruction is personal - Reply-all — Sir on
To:with others onCc:and the instruction implies group response - Forward — Sir is forwarding a third-party email asking Alfred to do something with it
- Execute the request, then confirm — Sir asked for a task; Alfred does it via other ctrl-api endpoints or Composio actions, then sends a short confirmation reply
- No-action — Alfred is on
Cc:purely as an observer; no direct question, no instruction
GET /api/v1/email/thread/<thread_id>) so quoted history is in context, searches the vault for related matters/people, and only downloads attachments if the request actually requires reading them.
Outbound — sending, replying, forwarding
Sir can send mail via the dashboard (“compose” UI), via the agent conversationally (“Alfred, please email Sarah about Q3”), or directly via API. All three paths converge on the same endpoints:| Endpoint | Purpose |
|---|---|
POST /api/v1/email/send | New email (new thread). Body: {to, subject, text, html?, cc?, bcc?, reply_to?, labels?, attachments?} |
POST /api/v1/email/reply | Reply within an existing thread. Body: {message_id, text, html?, reply_all?, attachments?} |
POST /api/v1/email/forward | Forward a message. Body: {message_id, to, subject?, text?, attachments?} |
GET /api/v1/email/message/:message_id | Fetch a single message (for cases where the webhook payload was truncated at 1 MB) |
GET /api/v1/email/thread/:thread_id | Fetch the full thread |
GET /api/v1/email/attachment/:message_id/:attachment_id | Download an attachment |
GET /api/v1/email/status | Confirm AgentMail is configured on this tenant |
Attachments
Send, reply, and forward all accept anattachments array:
content is base64-encoded bytes, no data-URL prefix. filename and content_type are optional but recommended.
PDF rendering
Alfred can render HTML to PDF using Playwright + Chromium, both pre-installed in the openclaw container. The pattern:- Write the report as HTML (with inline CSS) to
~/.openclaw/workspace/data/report.html - Spawn a subagent with a small Playwright script: load the file, call
page.pdf({path: "data/report.pdf", format: "A4", printBackground: true}) - Once the PDF exists, base64-encode it and include in the email’s attachments array
Limits and edge cases
- Attachment size — AgentMail rejects attachments above ~20 MB base64. Compress or paginate large content; for big files, prefer Slack uploads (
SLACK_FILES_UPLOADvia Composio) or the Drive integration. - Webhook payload truncation — AgentMail truncates inbound webhook bodies at 1 MB. If a long thread arrives truncated, Alfred fetches the full thread via
GET /api/v1/email/thread/:idbefore composing a reply. - Display-name parsing — Sender comparison handles RFC 5322 angle-bracket form:
"Sarah Jones" <sarah@acme.com>matches the baresarah@acme.comon the authorised list. - First Brief delivery — The First Brief at end of onboarding is sent to Sir’s Google address (the one he signed in with), not to a tenant inbox. Sir’s reply to that brief authorises his own Gmail going forward.
- No initiating cold outreach — Alfred responds; he doesn’t send unsolicited mail. This is enforced in the skill’s hard rules.
Configuration storage
Sir’s per-tenant AgentMail credentials live in two places (both on the LUKS-encrypted volume):- Tenant
.env:AGENTMAIL_API_KEY,AGENTMAIL_INBOX_ID,AGENTMAIL_INBOX_ADDRESS - Fallback file:
/mnt/encrypted/alfred/.agentmail-credentials.json
Voice Channel
The other premium channel — calls and SMS
SMS Channel
Authorised SMS gets the same dual-mode treatment
Connected Apps
Gmail-via-Composio is a different story — read here
Recipes
Copy-pastable email send and authorise examples