The problem
Ask the flagship GAIA agent to triage your inbox and it cannot. It has no Gmail tool, no Outlook tool, and no Google or Microsoft connector grant — 67 registered tools and not one of them touches mail. Email lives in a separate installable agent with its own sidecar process, its own REST API, its own npm package, and its own 66-tool surface. A user who wants “read my mail and tell me what needs me” alongside “search my documents” has to run two agents and cannot ask one question that spans both. After this change,triage my inbox works in the Go TUI against the same flagship
agent that already does documents, web, shell, and memory — and the standalone
email agent becomes redundant rather than load-bearing.
This is the architectural direction CLAUDE.md already states: per-task agents were
deleted and their capability became the flagship’s tool surface driven by a
SKILL.md in hub/skills/. Email is the last major holdout.
Position against the prior art
Four issues touch this area. This proposal is deliberately narrower than all of them.
File this as a new issue narrowing #2672, not as an amendment. #2672’s goal is
portability of email capability into arbitrary agents; its acceptance test is
literally “a non-email agent installs an extracted skill and uses its tools”, and
it is blocked on a sandbox that does not exist. This proposal has one consumer (the
flagship), needs no sandbox, and its success condition is the deletion of a
package. Folding a decommission plan into a portability issue would bury it behind
a blocker it does not have.
The constraint that picks the architecture — verified, and it is not what it looks like
The premise that local-capability permissions are refused is true but does not apply here, for two independent reasons. Both were verified in code. 1.shell is no longer a local-capability domain. It was, when #2672 was
written. It is now binary-bridged:
filesystem, database, desktop, env — not
shell. That is why hub/skills/github-triage/SKILL.md can declare
shell:execute:gh and load: a scoped binary grant resolves against
BINARY_POLICIES in src/gaia/skills/binaries.py (only gh and pytest have
policies today). Unscoped shell:execute is still refused, because that asks for
the whole shell. There is no allowlist exception and no bundled-vs-installed
distinction — the gate is the same for every skill.
2. The gate only ever runs on skill permissions, never on agent code. Every
call site of refuse_unbridged_permissions passes skill.parsed_permissions()
(agent.py:1619, loader.py:62, install.py:353, publish.py:137,
migrate.py:821). A tool mixin composed into an agent class is ordinary Python in
the agent — it is not a skill, declares no permissions, and is never gated.
Consequence: a first-class email tool mixin is available now. The sandbox
in #2863 is not on its critical path. What #2863 blocks is shipping email tools as
a skill-provided tools.py, because the reversible-action ledger needs
database:write — and that is precisely why #2672 is stuck.
Recommended architecture — A: tool mixin + skill
A newgaia.agents.tools.email_tools.EmailToolsMixin, registered in
KNOWN_TOOLS, orchestrated by hub/skills/inbox-triage/SKILL.md.
The skill carries judgment (what “urgent” means, when to escalate, how to word a
digest). The mixin carries capability (authenticated Gmail/Graph calls). This is
exactly the split every shipped starter skill already uses — document-brief
names RAGToolsMixin’s tools in tools_required without any code coupling.
Why not the alternatives
B — MCP connector. Available today viamcp:connect, but there is no
Gmail or Outlook server in src/gaia/connectors/catalog/mcp_servers.py (only
mcp-github, mcp-tavily, mcp-memory, mcp-git). Adopting it means handing a
third-party npx subprocess standing access to the user’s entire mailbox, and
moving OAuth credentials out of the OS keyring into subprocess env vars — wrong
for a local-first product, and it discards gmail_backend.py / outlook_backend.py,
which are tested and already authenticate through gaia.connectors.api. Keep MCP
as a bring-your-own-provider escape hatch, not the primary path.
C — skill drives the email agent’s REST API. Hides the agent instead of
retiring it, so it fails the stated goal. It is not even cheap: the flagship has no
HTTP POST tool, and shell:execute:curl has no binary policy, so it needs new
plumbing to deliver strictly less.
D — a gaia-mail CLI plus a binary policy, mirroring gh. Structurally the
closest analogue to github-triage, but it means building the same Gmail client and
then wrapping it in argv parsing and a second permission layer. Strictly more work
than A for the same result.
Why A is cheaper than it looks
The connector plumbing is already shared.gmail_backend.py and
outlook_backend.py obtain tokens via
gaia.connectors.api.get_access_token_sync(provider=..., scopes=..., agent_id=...)
— the same call a core mixin makes. Grants are enforced centrally by
check_agent_grant(). Moving the backends into src/gaia/agents/tools/ is largely
a relocation, not a rewrite.
New and changed files
Capability parity
A decommission is only credible if parity is explicit. Two views: the 27 externally exposed operations fromhub/agents/email/python/CAPABILITY_MATRIX.md, and the
66 internal agent-loop tools.
The 27 exposed operations
Headline: 15 must survive, 6 defer, 6 drop. The 15 collapse to 11 distinct
capabilities once the four MCP tools fold into their REST twins.
The 66 internal tools → ~19
Do not port 66 tools. The flagship already registers 67; adding 66 more would give it 133 and wreckToolLoader’s per-turn selection — which is the same problem
#2835 and
#2836 are already solving inside the
email agent. Parity is at the operation level, not the tool level.
The hard parts
These are not prompt-shaped and aSKILL.md cannot replicate them. Each needs real
code in the mixin:
- Classification pipeline — two-tier heuristic → LLM escalation
(
triage_heuristics.py,llm_triage.py,triage_condense.py), 5-category taxonomy tied to Gmail label semantics. Must move as code. - Batch safety gate —
ORGANIZE_BATCH_OP_THRESHOLD = 5/ORGANIZE_BATCH_SENDER_THRESHOLD = 3(agent.py:782-783): more than 5 organize mutations across more than 3 distinct senders in one turn forces confirmation. Must move verbatim. - Undo ledger —
action_store.pypersistsemail_actions/email_draftsto~/.gaia/email/state.dbwith a 120s default window, and enforces the ordering invariant that the API call succeeds before the row is recorded (a phantom undo row is state corruption). This is thedatabase:writedependency that blocks #2672; as agent code it is unblocked. - Context budgeting —
context_budget.py,thread_fold.py,body_normalize.py. Partly generic, but the flagship’s prompt is larger, so the budget needs recalculating, not copying. - Attention dedup —
attention_cache.pydedupes signals across four sources. - Briefing scheduling —
scheduler.py/schedule_store.pyare daemon-managed. This is why briefing is Phase 3, not Phase 0.
Permission and connector model
Reads: one consent, then silent
The mixin declaresREQUIRED_CONNECTORS on the flagship agent class, mirroring the
email agent’s existing declaration (gaia_agent_email/agent.py:748-760). The user
consents once when connecting the account; reads then run without prompting —
the same shape as gh issue list running silently under shell:execute:gh.
Scopes stay exactly as the email agent requests them, including the deliberate
exclusion of https://mail.google.com/ (full-mailbox permanent delete,
#2533). Do not widen scopes during the
move. build_tui.yml already path-triggers on scopes.py as a drift guard; that
guard must follow the file to its new home.
Writes: confirmation, and a higher bar than gh
gh issue comment is public and retractable. Sending mail is private and
irreversible. The gate must be stricter, not equivalent.
The mechanism already exists and is wired: TOOLS_REQUIRING_CONFIRMATION
(agent.py:198) is unioned at runtime with the agent’s own
CONFIRMATION_REQUIRED_TOOLS ClassVar (agent.py:3184-3193), and a listed tool
causes _execute_tool to call console.confirm_tool_execution and block
(agent.py:3436).
Do not route this through
src/gaia/governance/. The layer exists and is
well-designed, but it has zero consumers: @govern appears only inside its own
docstring, and GovernedAgentMixin is mixed into no agent in the repo. Wiring
email writes into an unexercised path would be the first real user of a layer with
no production evidence. Use the confirmation mechanism that already ships. If
governance is later adopted, email writes are the obvious first
@govern(risk="review") case — as a follow-up, not a prerequisite.
Acceptance bar
Blocker first: #2983 — the flagship has no scorecard and no release eval gate. Committed baselines exist only forrag_quality, tool_selection, and context_retention
(tests/fixtures/eval_baselines/gemma-4-e4b-d71cd914/). An email category for the
flagship has to exist before “no regression” means anything.
To justify decommissioning, the flagship plus skill must:
The sleeper risk, and the one nobody would think to test: adding ~19 tools to a
67-tool agent can degrade selection for non-email work. Every phase must re-run
gaia eval agent --category tool_selection against the flagship’s existing
committed baseline. A win on email that costs document Q&A is not a win.
Evals run serially — never two gaia eval agent processes at once. Confirm
ps aux | grep "gaia eval" | grep -v grep | wc -l prints 0 before starting.
Phased plan
Phase 0 — read-only Outlook triage from the TUI. One PR.EmailToolsMixin
registers check_mailbox_access, list_inbox, search_email, read_email, and
list_mail_folders in KNOWN_TOOLS; the inbox-triage skill supplies the triage
procedure. The Microsoft connector requests Mail.ReadWrite to match its existing
consent contract, while these five tools expose reads only. No writes, no state
DB, no Gmail, no calendar. Acceptance: triage my inbox works end-to-end in
gaia tui against a real Outlook account, and tool_selection does not regress
against the committed baseline.
Phase 1 — organize + undo. Brings the action ledger and the batch threshold.
Phase 2 — draft, send, trash, behind confirmation.
Phase 3 — briefing, tasks, scheduling. Needs daemon-managed jobs.
Phase 4 — Gmail / Google connector.
Phase 5 — deprecate. Not before Phases 0–4 have shipped and the eval bar is met.
Decommission blast radius
This is a multi-release effort, and the removal is larger than the build.
The cost nobody has priced: the email agent is the only proven sidecar. It is
the reference implementation for the entire daemon-supervised sidecar
architecture — frozen binaries on four platforms, SSE translation, caller auth,
forwarded credentials. Deleting it deletes that reference. A defensible middle
path is to move the capability to the flagship and keep the package as an
architectural reference, unlisted on the hub. That would also resolve the conflict
with #2982’s non-goal without abandoning either position.
Non-goals
- Not delivering the Phase 2 sandbox (#2863). This proposal routes around it.
- Not skills-v2 adaptive learning (#2683).
- Not the C++ runtime (#1543, #2799).
- Not calendar, phishing SLM, or voice-profile capability in Phases 0–4.
- Not deleting the sidecar architecture — only this consumer of it.
- Not widening OAuth scopes. The
https://mail.google.com/exclusion is permanent.
Open questions for the owner
- #2982 conflict. It names the email agent a permanent non-goal. Does this proposal supersede that, or does the package survive as an unlisted reference?
- Move or duplicate? #2672 asks the same question and never settled it.
Duplicating creates two implementations to drift; moving is a breaking change
for anyone importing
gaia_agent_email. Recommendation: move, with the package retained at a frozen version until Phase 5. - Does #2672 get closed, narrowed, or left alone once this lands? Its portability goal survives independently, but its email framing would be stale.
- Eval decoupling. Five eval modules import the email package directly. Do they retarget to the flagship in Phase 0, or keep dual baselines through Phase 4?
- npm consumers. Is
@amd-gaia/agent-emailknown to have external users? That decides deprecation-window length.