Skip to content

feat(email): draft, reply, forward and send Gmail with approval - #6479

Open
tylerbea wants to merge 13 commits into
amd:mainfrom
tylerbea:claude/gaia-mail-parity-2
Open

tylerbea wants to merge 13 commits into
amd:mainfrom
tylerbea:claude/gaia-mail-parity-2

Conversation

@tylerbea

@tylerbea tylerbea commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

GAIA could read Gmail but not act on it. Asked to send an email, it said it had no way to. Now it can draft, reply, forward and send through the same Google sign-in, and it shows you the exact email and waits for your approval before anything goes out. Outlook gets the same tools through the same code, but only Gmail was tried on a real account.

Before

before

After (the address is hidden here; the prompt shows it)

after

Arabic, before and after: each line of the email now takes its own direction, so an English address and an Arabic body both read correctly.

arabic before arabic after

Small models don't get this yet. Gemma 4 keeps email turned off, because it fails the email tests. That work is tracked separately in #6478.

This does not finish #6107. That issue also needs gaia_email and gaia_tool_selection run on the Strix Halo pool, with the comparison posted, and neither has been run for this branch. A maintainer with pool access needs to start them, or they can run at the next RC.

Refs #6107

Test plan

  • python -m pytest tests/unit -k email
  • cd src/gaia/apps/webui && npm run test:rtl
  • gaia connectors connect google --grant-agent installed:gaia, ask GAIA to send an email to yourself, check that it waits for approval, then confirm the email arrives
  • Press Deny on a send and confirm nothing goes out
🔍 Technical details
  • Tested on a real Gmail account: a draft, an approved send, then search and read. The send and draft ran on DeepSeek through Fireworks. The read ran on local Gemma with GAIA_FEATURE_TIER=full.
  • Under Full access with no one able to answer a prompt, sending is refused. The packaged sidecar path that should ask instead is covered by unit tests only.
  • The unit suite on this branch has 12 failures and 3 errors outside email: PDF rendering, system pip and hub installer tests, which also fail on main, and test_skills_package_ships_in_the_wheel, caused by a local node_modules.

…ph write backends

Both providers and the confirmation preview now build from the same frozen message, so what a person approves cannot differ from what goes out. The offline fixture mailbox drafts and sends in memory.

Refs amd#6107
…n every surface

The guides, connector pages, integrator docs and triage skill all said the flagship could only read. They now describe the sixteen tools, the gmail.modify consent and what Full access does to a send.

Refs amd#6107
…after one consent

The flagship could only read mail. It now does the rest of the writing verbs on Gmail and Outlook, behind the existing confirmation. Each send shows its exact text; a decline, timeout or failure sends nothing and nothing retries. Where no person can answer, even pre-approved, a send is refused.

New Google consents declare gmail.modify. A gmail.readonly connection keeps reading and gets a reconnect error on writes.

Refs amd#6107
…ments

The prompt shows the message that will go out and has no line about left-out attachments.

Refs amd#6107
…oves

The prompt was cut to the terminal's height while y still approved, so a person could send mail they could not see. Mail prompts now scroll, and y only works once the last line has been on screen. Other tools are unchanged.
…at is sent

Disguised subjects, addresses and display names are refused before the prompt on both providers, and replies go to bare addresses. Body text keeps its characters, because Arabic and Hebrew mail needs them, but the prompt shows hidden controls as [U+XXXX].

International domains go out and show in ASCII. A send that may have gone out returns sent: "unknown", and its retry prompt warns. Changing a saved draft now asks and shows old and new. Multipart Gmail drafts show and compare their HTML. A forward with no recipients is refused, and forwards name the attachments they leave out.
An English address in the Arabic UI sat against the far edge of the approval preview, away from where a reader looks for it.
@github-actions github-actions Bot added documentation Documentation changes dependencies Dependency updates tests Test changes electron Electron app changes agents agent::email Email agent changes tui Go terminal UI (gaia-tui) daemon Daemon supervisor / sidecar control plane sidecar Agent sidecar contract / harness labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Verdict: Request changes (one small fix; the design itself is sound)

The PR lets GAIA draft, reply, forward and send Gmail and Outlook mail. Every send waits for a person to approve it, offers no "always" option, and is refused when nobody is there to ask. The safeguards are careful: GAIA builds the message once, the prompt shows that exact message, and that same message is what gets sent. If a draft changes after it was shown, the send stops.

Blocking issue

  • One new test can't pass. It checks that every email tool needing approval is gated on the flagship, then checks that "edit a draft" is not gated, but "edit a draft" is on the gated list. The code and docs both say editing a saved draft asks first, so the test's last check is out of date. Delete that line.

Worth knowing before merge

  • Outlook writes use the same code but haven't been tried on a real account, as the PR says. Sending a reply on Outlook takes a few steps (start a reply draft, fill it in, send it), and only stubbed tests cover them.
  • gaia_email is the eval category to watch at the next RC, as the PR notes.

Real-world evidence

N/A: no evidence bundle was produced for this run. The PR description embeds before/after screenshots of the approval prompt in English and Arabic, and says one draft and one approved send were tried on a real Gmail account. The verdict on the Outlook path and the TUI prompt rests on reading the code alone. I couldn't run the unit suite here because the runner has no Python environment, so the blocking finding comes from reading the code and should be confirmed by CI.

🔍 Technical details

🟡 Important

🟡 Self-contradicting assertion (tests/unit/agents/tools/test_email_write_tools.py:353): EMAIL_CONFIRMATION_TOOLS includes update_draft (src/gaia/agents/tools/email_tools.py:101). GaiaAgent.CONFIRMATION_REQUIRED_TOOLS unions it in, and confirmation_required_tools() (src/gaia/agents/base/agent.py:6448) returns that union unfiltered. Line 351 (EMAIL_CONFIRMATION_TOOLS <= …) therefore implies update_draft is present, and line 353 asserts it isn't. The root conftest.py puts hub/agents/gaia/python on sys.path, so importorskip won't skip the test. The module docstring and _PROMPT_INTROS["update_draft"] both describe the gated behaviour, so the assertion is what's wrong:

    assert EMAIL_CONFIRMATION_TOOLS <= GaiaAgent.confirmation_required_tools()
    assert "create_draft" not in GaiaAgent.confirmation_required_tools()

🟢 Minor

🟢 Prepared messages from declined prompts stay in memory (email_tools.py, _MailPreflight.__call__ / _prepared): an entry is only popped when the tool body runs, so a denied or timed-out prompt leaves its OutgoingMessage (whole body included) in _email_prepared for the rest of the session. It's harmless for safety, because the next identical call re-runs the preflight and overwrites the entry. Clearing it on denial would stop it accumulating.

🟢 A full-mailbox Google connection reads as read-only (scopes.py, WRITE_SCOPES): a connection holding only https://mail.google.com/ gets "can read mail but not send it". That scope does allow sending, so the message is wrong for this case. It's rare, but the error points the user at the wrong fix.

Dependency licence: idna>=3.0 is BSD-3-Clause and already arrives through httpx. Fine.

Strengths

  • The preflight builds the message before the prompt, the prompt renders it through EXACT_TEXT_ARG, and the body sends that same object, so what the person approves can't drift from what the provider receives. send_draft and update_draft re-read the draft and refuse if it changed while the prompt was open.
  • Unknown-outcome failures are handled with care. MailboxOutcomeUnknown covers mid-flight errors and 5xx, writes are never retried, and the next prompt for the same email warns that an earlier try may already have gone out.
  • Headers are hardened against injection: CR/LF and control characters are refused, display names lying about the address or carrying bidi controls are rejected, recipient names copied from inbound mail are stripped, and hidden characters show as [U+XXXX] in the preview.

Main added forget_everything, so the flagship now has 110 tools and 19
need approval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kovtcharov-amd kovtcharov-amd added the multilingual Language support: translations, right-to-left layout, Arabic and other non-English languages label Nov 6, 2026
…ail prompt

An email prompt now opens with the account the mail goes out from, and a send
stops if the connected mailbox changed while the prompt waited. A declined or
unanswered prompt leaves nothing behind that a later call could send, and a yes
to a reroute question no longer stands in for the exact-email prompt.

Resolves main's conflicts: tool counts are 113 (CORE 27 + 29 bundles), the
changelog entries move to Unreleased, and the TUI and Agent UI keep both sides.
@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Thanks @tylerbea, this is the piece that lets us retire the standalone email package. I merged main in and closed three gaps in the approval flow:

  • The email prompt now names the account the mail goes out from, and a send stops if the connected mailbox changes while the prompt is waiting.
  • A declined or unanswered prompt no longer leaves the message behind for a later call to send.
  • Saying yes to "allow another way after you said no" no longer skips the email prompt; you still see the exact message first.

The bot's blocking test finding was already fixed in your branch. The gaia_email and gaia_tool_selection evals still need to run on the next RC, as the description says.

🔍 Technical details
  • Conflicts: tool count is now 113 (CORE 27 + 29 bundles), with the matrix regenerated; the CHANGELOG entries moved from the released 0.3.4 section to Unreleased; the TUI titles and Agent UI prompt keep both sides.
  • OutgoingMessage.sender (left out of equality) comes from the connection record, so there's no extra network call. _approved_writer re-checks it before every write.
  • Agent._execute_tool calls the preflight's forget() on a denial or timeout, and treats an EXACT_TEXT_ARG prompt as one-time, so asked_and_allowed can't skip it.
  • Adversarial tests added in test_email_write_tools.py. Both new agent-side guards were checked by reverting them, and the tests went red.
  • Bot minor finding: a full-mailbox-only Google connection now names the missing scope instead of saying "can read but not send".
  • Locally: email tests, tests/unit/connectors, and the full vitest and tsc runs pass, and the TUI builds and its internal/ui tests pass. test_manifest_tools_count_matches_real_registry fails on this machine only because the browser extra isn't installed (8 tools), and it fails on main for the same reason.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent::email Email agent changes agents daemon Daemon supervisor / sidecar control plane dependencies Dependency updates documentation Documentation changes electron Electron app changes multilingual Language support: translations, right-to-left layout, Arabic and other non-English languages sidecar Agent sidecar contract / harness tests Test changes tui Go terminal UI (gaia-tui)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants