Add agent email send MCP tool
## Original request (verbatim)
> Migrate the emails system from GMail API to AWS SES. Use all the best practices. The quality of communication is of highest priority. Spare no time or expense.
>
> [jaah.ai](http://jaah.ai) MX is currently at GMail and will remain so, but we can carve out whatever we need
Epic context (BA PRD Task 9352, Architecture Task 9353) — settled: all system mail (send + receive) moves to SES on `mail.jaah.ai` (dev: `mail.dev.jaah.ai`, dev account, sends/receives real mail) with suppression, operator-only delivery log, RFC 8058 unsubscribe, DMARC monitoring, operator-approved template redesign; tenant Gmail (agent send, automation action, receive) is built in this epic behind one plan feature. All system AWS calls use the hand-signed SigV4 seam (`apps/api/app/aws/common.py` `signed_call`), no boto3. SES events and inbound mail are PULLED from SQS by system crons (precedent `apps/api/app/alarm_relay.py`, `apps/api/app/aws/sqs.py`); the only new public path is unsubscribe on the hooks edge.
## Scope of this child
C8 Agent email tool: a control MCP verb that lets a runner on an org's task send email from the org's connected Gmail via the CP, audited. Delivers PRD R10.
## Requirements (verbatim from the PRD)
### R10 — Agents send from tenant Gmail (A6, Q9, Q14)
A runner on that org's task can send an email (to, cc, subject, text/HTML) from the org's connected Gmail via a control MCP tool. No allow-list, cap or approval; every send is audited (who/which task, recipient, subject, time, result) and visible to the org's admins.
Q14: **No guardrails** (no allow-list, no cap, no approval) — every send still audited.
AS9. Tenant Gmail sends go **to any recipient** (no verified-address rule).
## Approach (from the settled architecture)
- `Verb("email.send", "POST", "/api/agent/tasks/email", Scope.TASK, tool=…)` in `apps/jaahd/agent_vocabulary.py` (near :1006), provider map `apps/jaahd/agent_providers.py:1191`; the route resolves the task's org and calls `tenant_gmail.send(..., task_id=<task>)` (S7) — C7 writes `tenant_email_sends` and the audit event; this child only passes `task_id`.
- Map S7 errors to the tool result: `NoConnection`/`AmbiguousConnection` 409, `PlanOff` 403, `GmailRefused` 502, with actionable messages.
- Update the enumerating tests (`test_agent_vocabulary.py:252`, `test_agent_vocabulary_tools.py:52`, `test_dispatch_compose_charter.py:177`).
- The credential never leaves the CP: the node only relays the verb.
Pinned seams (build against these exactly; other children build against them too):
- S7 tenant send (C7 → C8/C9): `TenantOutboundEmail(to: list[str], cc: list[str] = [], subject: str, text: str, html: str | None = None)` (1–50 recipients total); `tenant_gmail.send(session, *, org_id: UUID, connection_id: UUID | None, message: TenantOutboundEmail, actor: Actor, task_id: UUID | None = None, automation_run_id: UUID | None = None) -> SendResult`; `SendResult(send_id: UUID, gmail_message_id: str, from_address: str)`. `connection_id=None` = the org's only send-capable connection, else `AmbiguousConnection` (→ 409). Errors: `NoConnection` (→ 409), `PlanOff` (→ 403), `GmailRefused(.status)` (→ 502). C7 creates `tenant_email_sends` and writes it inside `send`; C8/C9 only pass `task_id`/`automation_run_id`. Both builders feed `mail_message.build(sender, to: list[str], cc: list[str], ...)`.
Credentials & reach:
| Oracle | Credential / identity / environment | Status |
|---|---|---|
| AC10 (non-operator 403), AC17 tenant side | `org/console/dev-orgb-login` (org B admin) | **verified** — `credential_get` ok (its recorded base_url is legacy; use `https://app.dev.jaah.ai`) |
| AC14/AC15/AC16/AC17(a) live (AC16 needs a connected Gmail too) | Jaah Google OAuth client (gmail.send) + a test Workspace SA with DWD | **deferred** — operator: "Build now, verify later" (I4) |
| AC5, AC18, unit/integration suites | worktree only | **verified** — nothing beyond the worktree |
Out of scope:
- Do NOT change `tenant_gmail.send`, the connection model or the audit table (C7); do NOT add the automation action (C9).
- Do NOT add an allow-list, cap or approval (Q14).
Epic out-of-scope (PRD §7, verbatim):
- MUST NOT change the apex `jaah.ai` MX, SPF or `_dmarc.jaah.ai` record, nor any Workspace mailbox, alias or routing.
- MUST NOT add SES Virtual Deliverability Manager, BIMI/VMC, or a dedicated IP / dedicated IP pool.
- MUST NOT request Google's restricted `gmail.readonly` (or any restricted Gmail scope) via OAuth; no CASA assessment.
- MUST NOT add open/click tracking pixels or link rewriting to system mail.
- MUST NOT let tenant Gmail replace system mail for an org's members (Q9 option not chosen).
- MUST NOT expose the delivery log or suppression list to tenant admins or users (operator only, Q7).
- MUST NOT relax the verified-address rule or invitation budgets for system mail (AS6).
- MUST NOT add boto3/botocore to `apps/api/requirements.txt` unless the Architect records why the hand-signed SigV4 seam (`apps/api/app/aws/`) cannot serve SES (the repo guards its absence).
- MUST NOT put a tenant Gmail credential, OAuth token or service-account key on any node or in a runner's environment.
- MUST NOT change Slack/Telegram delivery, the support chat, or site enquiries (no email today).
Done means:
- **AC15 (R10) Agent send.** → verify: run a task in that org whose prompt asks the agent to email an external address via the tool; the mail arrives From the tenant's Gmail address (visible in that mailbox's Sent); the org's audit shows the send with task, recipient, subject, result. `grep` the node's environment/files for the credential → absent. (deferred — live: operator "Build now, verify later" (I4). Substitute now: route tests against a stubbed `tenant_gmail.send` asserting task→org resolution, `task_id` recorded on the `tenant_email_sends` row, error mapping; a test that the verb's node-side relay carries no credential material; the enumerating vocabulary tests green.)
- A runner on a task of org A cannot send via org B's connection (test).
- Quick gate `./dc check quick` green and changed-area tests green; `python3 tools/check_context.py` green (any doc this child adds/edits is routed and single-homed).
Outcome
Built the email_send tool: a runner sends from its task's org Gmail through the CP, live-hold auth, every attempt audited. Fixed C7's uuid task_id with an expand-only int column. Quick gate green, review lane passed, PR #6753 queued. Live AC15 deferred (I4). Memory: none applied.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)