diff --git a/optional-skills/email/agentmail/SKILL.md b/optional-skills/email/agentmail/SKILL.md index 770ca8d551..c75297a1f5 100644 --- a/optional-skills/email/agentmail/SKILL.md +++ b/optional-skills/email/agentmail/SKILL.md @@ -1,128 +1,95 @@ --- name: agentmail -description: "Give the agent its own inbox: send and receive email." +description: Use when an agent needs AgentMail CLI email inboxes. version: 1.0.0 -author: teyrebaz33, Hermes Agent +author: Haakam Aujla (Haakam21), AgentMail license: MIT platforms: [linux, macos, windows] metadata: hermes: - tags: [email, communication, agentmail, mcp] - category: email + tags: [Email, CLI, AgentMail, Communication] + homepage: https://agentmail.to +prerequisites: + commands: [agentmail] +required_environment_variables: + - name: AGENTMAIL_API_KEY + prompt: AgentMail API key (starts with am_) + help: "Create one at https://console.agentmail.to — or run the CLI self-signup flow in references/signup.md to obtain a key without one." + required_for: "authenticating the agentmail CLI; not needed before self-signup" + optional: true --- -# AgentMail — Agent-Owned Email Inboxes +# AgentMail Skill -## Requirements +AgentMail gives an agent its own email inbox for sending mail, receiving +replies, completing email OTP flows, and running inbound email loops. Use it for +agent-owned inboxes, not a user's existing IMAP/SMTP mailbox. -- **AgentMail API key** (required) — sign up at https://console.agentmail.to (free tier: 3 inboxes, 3,000 emails/month; paid plans from $20/mo) -- Node.js 18+ (for the MCP server) +Use the `agentmail` CLI first. Use MCP only when the harness expects MCP tools; +use REST only when the CLI is missing a required operation. ## When to Use -Use this skill when you need to: -- Give the agent its own dedicated email address -- Send emails autonomously on behalf of the agent -- Receive and read incoming emails -- Manage email threads and conversations -- Sign up for services or authenticate via email -- Communicate with other agents or humans via email -This is NOT for reading the user's personal email (use himalaya or Gmail for that). -AgentMail gives the agent its own identity and inbox. +- The agent needs an email address it owns. +- The task involves email OTP flows, replies, threads, labels, or attachments. +- The agent needs webhook or WebSocket delivery for inbound mail. -## Setup +## Prerequisites -### 1. Get an API Key -- Go to https://console.agentmail.to -- Create an account and generate an API key (starts with `am_`) +- Run commands through the `terminal` tool. +- Install the CLI: -### 2. Configure MCP Server -Add to `~/.hermes/config.yaml` (paste your actual key — MCP env vars are not expanded from .env): -```yaml -mcp_servers: - agentmail: - command: "npx" - args: ["-y", "agentmail-mcp"] - env: - AGENTMAIL_API_KEY: "am_your_key_here" -``` - -### 3. Restart Hermes ```bash -hermes +npm install -g agentmail-cli@latest ``` -All 11 AgentMail tools are now available automatically. -## Available Tools (via MCP) +- Export an API key: -| Tool | Description | -|------|-------------| -| `list_inboxes` | List all agent inboxes | -| `get_inbox` | Get details of a specific inbox | -| `create_inbox` | Create a new inbox (gets a real email address) | -| `delete_inbox` | Delete an inbox | -| `list_threads` | List email threads in an inbox | -| `get_thread` | Get a specific email thread | -| `send_message` | Send a new email | -| `reply_to_message` | Reply to an existing email | -| `forward_message` | Forward an email | -| `update_message` | Update message labels/status | -| `get_attachment` | Download an email attachment | +```bash +export AGENTMAIL_API_KEY="am_..." +``` + +No API key yet? Use [signup.md](references/signup.md). + +## How to Run + +Use `--format json` whenever another command or script needs IDs. + +```bash +agentmail inboxes list --format json +``` + +## Quick Reference + +- [AgentMail agent reference](https://agentmail.md): hosted copy. +- [AgentMail](https://agentmail.to): product landing page. +- [Console](https://console.agentmail.to): API keys and account management. +- [Docs](https://docs.agentmail.to): full product documentation. +- [signup.md](references/signup.md): self-signup and OTP verification. +- [core.md](references/core.md): inboxes, messages, threads, labels, attachments. +- [webhooks.md](references/webhooks.md): events to a public HTTPS server. +- [websockets.md](references/websockets.md): events to a local agent process. +- [mcp.md](references/mcp.md): MCP integration. ## Procedure -### Create an inbox and send an email -1. Create a dedicated inbox: - - Use `create_inbox` with a username (e.g. `hermes-agent`) - - The agent gets address: `hermes-agent@agentmail.to` -2. Send an email: - - Use `send_message` with `inbox_id`, `to`, `subject`, `text` -3. Check for replies: - - Use `list_threads` to see incoming conversations - - Use `get_thread` to read a specific thread - -### Check incoming email -1. Use `list_inboxes` to find your inbox ID -2. Use `list_threads` with the inbox ID to see conversations -3. Use `get_thread` to read a thread and its messages - -### Reply to an email -1. Get the thread with `get_thread` -2. Use `reply_to_message` with the message ID and your reply text - -## Example Workflows - -**Sign up for a service:** -``` -1. create_inbox (username: "signup-bot") -2. Use the inbox address to register on the service -3. list_threads to check for verification email -4. get_thread to read the verification code -``` - -**Agent-to-human outreach:** -``` -1. create_inbox (username: "hermes-outreach") -2. send_message (to: user@example.com, subject: "Hello", text: "...") -3. list_threads to check for replies -``` +1. Install `agentmail-cli@latest` and verify `agentmail inboxes list --format json`. +2. If no API key is available, complete [signup.md](references/signup.md). +3. Use [core.md](references/core.md) for inbox, send, read, reply, forward, + label, thread, and attachment flows. +4. Add [webhooks.md](references/webhooks.md) or + [websockets.md](references/websockets.md) only when polling is not enough. ## Pitfalls -- Free tier limited to 3 inboxes and 3,000 emails/month -- Emails come from `@agentmail.to` domain on free tier (custom domains on paid plans) -- Node.js (18+) is required for the MCP server (`npx -y agentmail-mcp`) -- The `mcp` Python package must be installed: `pip install mcp` -- Real-time inbound email (webhooks) requires a public server — use `list_threads` polling via cronjob instead for personal use + +- Prefer `AGENTMAIL_API_KEY` over `--api-key`. +- Never expose `AGENTMAIL_API_KEY` in prompts, logs, URLs, or committed files. +- Use stable `client_id` values for retried create operations. +- Prefer `extracted_text` or `extracted_html` for LLM input when present. +- React to `message.received`, not messages the agent sent. ## Verification -After setup, test with: -``` -hermes --toolsets mcp -q "Create an AgentMail inbox called test-agent and tell me its email address" -``` -You should see the new inbox address returned. -## References -- AgentMail docs: https://docs.agentmail.to/ -- AgentMail console: https://console.agentmail.to -- AgentMail MCP repo: https://github.com/agentmail-to/agentmail-mcp -- Pricing: https://www.agentmail.to/pricing +```bash +agentmail inboxes list --format json +``` diff --git a/optional-skills/email/agentmail/references/core.md b/optional-skills/email/agentmail/references/core.md new file mode 100644 index 0000000000..6bc0ea5dee --- /dev/null +++ b/optional-skills/email/agentmail/references/core.md @@ -0,0 +1,116 @@ +# AgentMail Core + +Common CLI path: create inboxes, send mail, read incoming mail, reply in +threads, label work, and fetch attachments. Need a key first? Use +[signup.md](signup.md). Need realtime delivery? Use [webhooks.md](webhooks.md) +or [websockets.md](websockets.md). + +## Setup + +```bash +npm install -g agentmail-cli@latest +export AGENTMAIL_API_KEY="am_..." +agentmail inboxes list --format json +``` + +## Inboxes + +```bash +agentmail inboxes create \ + --username support \ + --display-name "Support Agent" \ + --client-id support-agent-primary \ + --format json + +agentmail inboxes get --inbox-id support@agentmail.to --format json +``` + +Omit `domain` to use `@agentmail.to`. Use stable `client_id` values for +retried create commands. + +## Send + +```bash +agentmail inboxes:messages send \ + --inbox-id support@agentmail.to \ + --to customer@example.com \ + --subject "Hello" \ + --text "Plain-text body." \ + --html "

Plain-text body.

" \ + --label outreach \ + --format json +``` + +Send both `text` and `html` when possible. Recipient limit is 50 total across +`to`, `cc`, and `bcc`. + +## Read and Reply + +```bash +agentmail inboxes:messages list --inbox-id support@agentmail.to --label unread --format json +agentmail inboxes:messages get --inbox-id support@agentmail.to --message-id --format json +agentmail inboxes:threads get --inbox-id support@agentmail.to --thread-id --format json +``` + +For LLM input, prefer `extracted_text` or `extracted_html`. Some email has +`html` but no `text`. + +```bash +agentmail inboxes:messages reply \ + --inbox-id support@agentmail.to \ + --message-id \ + --text "Thanks, I will take a look." \ + --format json + +agentmail inboxes:messages reply-all \ + --inbox-id support@agentmail.to \ + --message-id \ + --text "Thanks, everyone." \ + --format json + +agentmail inboxes:messages forward \ + --inbox-id support@agentmail.to \ + --message-id \ + --to teammate@example.com \ + --format json +``` + +## Labels + +Use labels as lightweight state: `unread`, `handled`, `needs-review`. + +```bash +agentmail inboxes:messages update \ + --inbox-id support@agentmail.to \ + --message-id \ + --add-labels handled \ + --remove-labels unread \ + --format json +``` + +## Attachments + +```bash +agentmail inboxes:messages get-attachment \ + --inbox-id support@agentmail.to \ + --message-id \ + --attachment-id \ + --format json +``` + +Fetch the returned download URL before it expires. Check +`agentmail inboxes:messages send --help` for attachment-send flags. + +## REST Notes + +Use REST only when the CLI is unavailable or missing a required operation. + +```bash +curl https://api.agentmail.to/v0/inboxes \ + -H "Authorization: Bearer $AGENTMAIL_API_KEY" +``` + +Base URLs: `https://api.agentmail.to/v0`, `https://api.agentmail.eu/v0`. + +Error bodies include `name` and `message`; validation errors include `errors`. +For `429`, honor `Retry-After` and back off. diff --git a/optional-skills/email/agentmail/references/mcp.md b/optional-skills/email/agentmail/references/mcp.md new file mode 100644 index 0000000000..ccdf6e86f2 --- /dev/null +++ b/optional-skills/email/agentmail/references/mcp.md @@ -0,0 +1,16 @@ +# AgentMail MCP + +Use MCP only when the user or harness already wants MCP tools. The CLI remains +the default path for AgentMail workflows. + +Hosted server: + +```text +https://mcp.agentmail.to/mcp +``` + +Authentication: + +- OAuth in compatible MCP clients. +- API key in `?apiKey=...`. +- API key in an `x-api-key` header. diff --git a/optional-skills/email/agentmail/references/signup.md b/optional-skills/email/agentmail/references/signup.md new file mode 100644 index 0000000000..3976f299b3 --- /dev/null +++ b/optional-skills/email/agentmail/references/signup.md @@ -0,0 +1,50 @@ +# AgentMail Self-Signup + +Use this when the agent does not have an AgentMail API key. A human must receive +and provide the OTP. + +## Sign Up + +```bash +npm install -g agentmail-cli@latest +agentmail agent sign-up \ + --human-email you@example.com \ + --username my-agent \ + --source agentmail-cli \ + --referrer hermes-agent \ + --format json +``` + +Export the returned `api_key`: + +```bash +export AGENTMAIL_API_KEY="am_..." +``` + +Verify with the OTP: + +```bash +agentmail agent verify --otp-code 123456 +``` + +## Notes + +- Use a real human email address for `--human-email`. +- `human_email` is the signup idempotency key, but signing up again with the + same email rotates the API key. +- Before verification, the account has one inbox, 10 sends/day, and can only + send to the signup human email. + +## First Check + +```bash +agentmail inboxes list --format json +agentmail inboxes:messages send \ + --inbox-id my-agent@agentmail.to \ + --to you@example.com \ + --subject "AgentMail verified" \ + --text "My AgentMail inbox is verified." \ + --format json +``` + +Continue with [core.md](core.md). diff --git a/optional-skills/email/agentmail/references/webhooks.md b/optional-skills/email/agentmail/references/webhooks.md new file mode 100644 index 0000000000..8bdbdba2e0 --- /dev/null +++ b/optional-skills/email/agentmail/references/webhooks.md @@ -0,0 +1,30 @@ +# AgentMail Webhooks + +Use webhooks when a public HTTPS server should receive AgentMail events. Use +[websockets.md](websockets.md) for local processes without a public URL. + +## Create + +```bash +agentmail webhooks create \ + --url https://your-app.example.com/webhooks/agentmail \ + --event-type message.received \ + --inbox-id support@agentmail.to \ + --client-id support-agentmail-webhook \ + --format json +``` + +Store the returned `secret` immediately. + +## Handle + +Headers: `svix-id`, `svix-timestamp`, `svix-signature`. + +1. Verify the raw request body with the webhook secret. +2. Dedupe by `svix-id` or `event_id`. +3. Return `200` quickly. +4. Process asynchronously. +5. For `message.received`, load the thread with the CLI and reply if needed. + +Act on `message.received` only. Treating `message.sent` or delivery events as +inbound work creates loops. diff --git a/optional-skills/email/agentmail/references/websockets.md b/optional-skills/email/agentmail/references/websockets.md new file mode 100644 index 0000000000..92dd25cede --- /dev/null +++ b/optional-skills/email/agentmail/references/websockets.md @@ -0,0 +1,55 @@ +# AgentMail WebSockets + +Use WebSockets when a local agent process needs realtime inbound mail. After an +event arrives, use the CLI for inbox, send, reply, label, and attachment work. + +## Python + +The Python example needs the separate AgentMail Python SDK, not the CLI: + +```bash +pip install agentmail +``` + +`AgentMail()` reads `AGENTMAIL_API_KEY` from the environment. If the SDK is +unavailable, use the `Raw` WebSocket below instead. + +```python +from agentmail import AgentMail, MessageReceivedEvent, Subscribe + +client = AgentMail() + +with client.websockets.connect() as socket: + socket.send_subscribe(Subscribe( + inbox_ids=["agent@agentmail.to"], + event_types=["message.received"], + )) + for event in socket: + if isinstance(event, MessageReceivedEvent): + print(event.message.subject, event.message.from_) +``` + +## Raw + +```text +wss://ws.agentmail.to/v0?api_key=$AGENTMAIL_API_KEY +``` + +EU region: + +```text +wss://ws.agentmail.eu/v0?api_key=$AGENTMAIL_API_KEY +``` + +Subscribe frame: + +```json +{ "type": "subscribe", "event_types": ["message.received"], "inbox_ids": ["agent@agentmail.to"] } +``` + +Omit `inbox_ids` and `pod_ids` for the API key scope. + +## Loop Rules + +- Dedupe every event by `event_id`. +- Reconnect with backoff and resubscribe after reconnecting. diff --git a/tests/tools/test_skill_env_passthrough.py b/tests/tools/test_skill_env_passthrough.py index be08539866..1988efb992 100644 --- a/tests/tools/test_skill_env_passthrough.py +++ b/tests/tools/test_skill_env_passthrough.py @@ -1,6 +1,8 @@ """Test that skill_view registers required env vars in the passthrough registry.""" import json +import shutil +from pathlib import Path from unittest.mock import patch import pytest @@ -8,6 +10,12 @@ import pytest import tools.env_passthrough as _ep_mod from tools.env_passthrough import clear_env_passthrough, is_env_passthrough +# Real optional AgentMail skill — its CLI-first workflow runs `agentmail` through +# the sandboxed terminal, which strips secrets unless the skill declares them. +_AGENTMAIL_SKILL_SRC = ( + Path(__file__).resolve().parents[2] / "optional-skills" / "email" / "agentmail" +) + @pytest.fixture(autouse=True) def _clean_passthrough(): @@ -78,3 +86,54 @@ class TestSkillViewRegistersPassthrough: assert result["success"] is True from tools.env_passthrough import get_all_passthrough assert len(get_all_passthrough()) == 0 + + +class TestAgentMailKeyPassthrough: + """The real AgentMail skill must declare AGENTMAIL_API_KEY so a profile-stored + key reaches the sandboxed terminal, while staying usable (self-signup) without + one.""" + + def _install_real_skill(self, tmp_path, monkeypatch): + dest = tmp_path / "email" / "agentmail" + shutil.copytree(_AGENTMAIL_SKILL_SRC, dest) + monkeypatch.setattr("tools.skills_tool.SKILLS_DIR", tmp_path) + return dest + + def test_agentmail_api_key_registered_for_sandbox_when_set(self, tmp_path, monkeypatch): + """A profile-stored AGENTMAIL_API_KEY must pass through to the terminal + sandbox — otherwise the documented CLI workflow fails authentication.""" + self._install_real_skill(tmp_path, monkeypatch) + monkeypatch.setenv("AGENTMAIL_API_KEY", "am_test_key_123") + + with patch("tools.skills_tool._secret_capture_callback", None): + from tools.skills_tool import skill_view + + result = json.loads(skill_view(name="agentmail")) + + assert result["success"] is True + assert is_env_passthrough("AGENTMAIL_API_KEY"), ( + "AGENTMAIL_API_KEY was set but not registered for sandbox passthrough — " + "the skill's frontmatter must declare it under required_environment_variables" + ) + + def test_agentmail_key_is_optional_so_self_signup_still_works(self, tmp_path, monkeypatch): + """With no key present the skill must NOT be blocked as setup_needed on the + key — the CLI self-signup flow obtains one without a pre-provisioned key.""" + self._install_real_skill(tmp_path, monkeypatch) + monkeypatch.delenv("AGENTMAIL_API_KEY", raising=False) + + with patch("tools.skills_tool._secret_capture_callback", None): + from tools.skills_tool import skill_view + + result = json.loads(skill_view(name="agentmail")) + + assert result["success"] is True + # `optional: true` means a missing key must NOT be reported as a blocking + # requirement — that is what keeps the CLI self-signup path viable. + assert "AGENTMAIL_API_KEY" not in result["missing_required_environment_variables"], ( + "AGENTMAIL_API_KEY must be declared optional so a missing key does not " + "block the self-signup path" + ) + assert result["setup_needed"] is False, ( + "an absent optional key must not force the skill into setup_needed" + )