Agent Email
Give your agent its own real email address on a shared domain — claiming a name, deciding who may start a session by writing in, holding blocked senders for review, how mail flows in and out, and the quiet arrival notice it raises in your inbox.
What it is
Give your agent its own real email address. People (or other systems) can email it to start or continue a session, and your agent can reply back out — powered by the Cloudflare-backed email service Omniscio runs.
What the reply contains. The reply is the agent's final answer to that email, nothing else. If another agent messages the session, or you type into it, after the email turn ends, that later exchange never goes out to the sender. Formatting the agent writes (bold, lists, links) arrives as a properly formatted email, with a plain-text copy alongside for mail apps that want one.
The domain half of the address is server config, not a constant in the app
(app_config/agent_email): the panel asks the desktop for it, so the pre-claim preview always
shows the real address. On the hosted install that domain is omnisciomail.com.
Where to find it
Where it lives
Agent Email is a sidebar entry nested under Omniscio → Agent Tools (alongside CLI Tools, Skills, MCP Servers, and API Keys). Click the Agent Email row to open its panel.
It is an in-development feature, so it ships hidden. Reveal it on a machine via
Settings → Lab → "Agent Email" (or the AMC_SHOW_AGENT_EMAIL=1 env var in a dev
build). Until revealed, the sidebar row does not appear. There is also a slim
Settings → Agent Email screen, but it only holds the on/off toggle — all claiming and
managing happens in the sidebar panel.
Agent Email is the primary, zero-setup way to receive email into Omniscio. The older "Email Inbound" path (forward mail in from your own AgentMail inbox) still exists, but is now framed as an "Advanced — use your own inbox" option under Settings → Email & Summaries — the same capability, for people who want to bring their own inbox.
Setting it up (a guided three-step flow)
Open Agent Tools → Agent Email. Before you have an address the panel walks you through three steps, one thing on each:
Step 1 — Choose your address. Type the name you want — the part before the @. As you type,
a live preview shows the full address, e.g. your-agent → your-agent@omnisciomail.com. The
preview is exactly what you'll get: letters, numbers, and hyphens only (spaces/punctuation become
hyphens, everything lowercases). The domain half is read from the mail service rather than
hardcoded, so the preview cannot advertise a domain the service does not use. Validation is
instant — the name must be at least 3 characters and not reserved. Reserved covers role words
(admin, support, billing, security, no-reply, …), well-known brands (paypal, apple,
google, … — also as one word of a longer name, so paypal-billing is refused but pineapple is
fine), and anything containing omniscio. Lookalike spellings count too: supp0rt, suport and
pay-pal are refused the same way. The Claim this address button stays disabled with a short
reason until the name is valid, and the server enforces the same rule (only an Omniscio
owner/admin may hold a reserved name). The address is globally unique across everyone on the
shared domain: if someone already took that name — or it is still held after its previous owner
gave it up (see below) — you get a clean "That name is already taken — please choose another"
and pick a different one (it does not silently append random characters). Claiming is
idempotent: one address per account.
Step 2 — Send yourself a test. The address is yours; this step proves it works. Send a test email sends a real message from your agent's address to your own account inbox — the recipient is resolved on the desktop side from your signed-in account, so it can only ever email you, never a stranger. Check your inbox (and spam): replying to that message starts a session, so you can watch the whole round trip. Skip for now moves on without sending one.
Step 3 — Choose who can write in. Only me / People I approve / Anyone, explained in plain words (see below). Finish setup hands over to the management screen.
You must be signed in to your Omniscio account (Google) to claim — the address is tied to your account. When signed out, the panel shows a real Sign in with Google button right there (the same Google sign-in the app's sign-in gate uses) on the desktop, or a "sign in on your computer" note on the web/mobile client (the OAuth opens a desktop browser and can't be completed from a phone) — never a dead-end pointer to go find another screen.
When Omniscio takes an address back
The Terms of Service (§15) reserve the right to suspend or reclaim an address. In practice:
- Suspended — an Omniscio admin (or the automatic spam-complaint limit) stops an address: it cannot send, and mail to it bounces, but you still hold the name. Restoring it brings it back unchanged. Sending shows why: spam reports, a support suspension, or "no longer on your account". You get an email either way.
- Taken back — for abuse, impersonation, a trademark claim, or other reasons. The address leaves your account and you are emailed. The name is held for 12 months; a name taken back for impersonation is never handed out again.
- Unused (free accounts only) — an address with no use for 12 months gets two warning emails over 30 days, is then paused, and is released 30 days after that (held 12 months, and you can still claim it back). Sending or receiving mail, or simply having Omniscio open with Agent Email on (it checks in once a day), counts as use and cancels the process. Paid, admin and support addresses are never reclaimed this way.
- Deleting your account releases its address into the same 12-month hold, with no link back to you.
Admins act from Settings → User Management → a user's ⋯ menu → Agent Email (Suspend
Address / Restore Address / Take Back… with a reason), or from the control server
(POST /admin/users/agent-email/:action).
Managing it afterwards
Once you hold an address, the same panel is the management screen, and everything about the address lives there in one place:
- Your address, with a Copy button, and Change or release address beneath it.
- Receiving — the on/off switch for incoming mail.
- Who can write in — the same three-way control as step 3, plus your approved senders.
- Mail for you — one-step ways into held mail and Spam, so neither is somewhere you have to remember to look.
- Send a test email — the connectivity check, available any time.
- Where email sessions open and Notify me in the inbox — the session destination and the arrival notice.
- Public support address — the separate public door, always shown here so a first one can be set up (the guided setup only shows it once one already exists).
Changing or releasing your address
There is no in-place rename. Change or release address releases the address, and you then claim a new name — that is how you "change" it. Because releasing is irreversible it asks you to type RELEASE to confirm; then the address stops working at once and any conversation in progress ends. The released name is held for 12 months so nobody else receives your mail: mail to it bounces, nobody else can claim it, and only you can take it back during that time. You can change your address at most 3 times a day.
Who can write in (receiving setup)
Receiving turns on the moment you claim an address — no restart, no extra step. The management screen has a plain Receiving on/off switch (on by default) so you can pause incoming mail at any time and turn it back on later, all live. Pausing keeps your address; it just stops new mail from starting sessions. (This switch is honest — it actually controls the inbound checker, unlike the in-development feature toggle, which only reveals the panel.)
The "Who can write in" control decides who may start a NEW session by emailing the address. It is the SAME control in both places you meet it — step 3 of the setup and the management screen — so the two can never describe the rule differently. It defaults to the safest choice:
- Only me (default) — only mail from your own account email starts a session. The control names exactly which address that is.
- People I approve — add specific email addresses; only those (plus you) get through.
- Anyone — open intake. Anyone who learns the address can start a session (the AI safety screen still applies, and so does the spam filter if you have turned it on). Shown with a "keep this restricted" caution.
It also closes, in plain words, with what the choice actually does — see the note below.
Replies always get through. The policy gates only brand-new conversations — a reply in a thread your agent already started is always accepted, whatever the setting, so the agent can hold a back-and-forth it began. That holds even after you archive the session behind it: archiving says you are done with that conversation, not that the person is a stranger, so their next reply starts a fresh session rather than being held for review. Each inbound email that starts a session uses your AI usage like any other session (the panel says so).
What this control does, and what it does not do. For "Only me" and "People I approve", a message only counts if the sending domain proves it really came from there — it has to pass email authentication — so a forged From is refused, not admitted on spelling. What it never does is judge the mail itself: it decides who can START a conversation, and every email is still checked by the AI safety screen before a session runs, with sessions sandboxed besides. The UI says exactly this, in plain words, on both screens that show the control.
How it behaves
Held for review (blocked senders — and the safety screen — are captured, never dropped)
When someone the policy DOESN'T allow (a stranger under "Only me") emails your address, their message is not thrown away — it is held for your review. The same tray also catches any email, on any route, that the built-in safety screen flagged or could not check; those rows carry a "Held by safety screen" badge, the screen's plain-language reason, and which route the mail came in on, so you can tell the two kinds of hold apart at a glance. An email the spam filter caught is held too, but it waits in the Inbox's Spam list instead of here.
The "Held for review" tab in the Agent Email panel lists every held email — sender, subject, and a short preview for a blocked-sender row, plus the badge and reason for a safety-screen row — so you can see exactly what's waiting and decide, instead of the mail vanishing. The queue stays a list down the left; picking one opens that email in the pane beside it, the way a mail app works. A single quiet inbox notice tells you how many blocked-sender emails are waiting (it updates in place — never one ping per email), with a "Review emails" button that opens that Held-for-review tab in one click — on your phone as well as the desktop. Safety-screen holds get their OWN separate "Emails held by the safety screen" card with a "Review held emails" button that opens a standalone review dialog listing just those, laid out the same way — the list with the email beside it, or one at a time on a phone — and because that dialog talks to the held-email list directly, it works even with the Agent Email panel switched off. A message that fails sender authentication (DMARC) — a possible spoofed "from yourself" — is still captured but shown with an "Unverified" badge so it is never mistaken for the real sender.
This is the honest middle ground between "Only me" (which used to silently drop everyone else) and "Anyone" (open intake): strangers wait in a review tray you clear, and nothing is ever lost.
Reading and clearing the tray
Click a held email to open it in the pane beside the list and read the whole message, not just the preview line. On a phone the email takes the screen instead — one half at a time — and carries a Back control to return to the queue, which is also where you land once you have dealt with it. Two buttons then sit under the message:
- Approve & start session — lets that email through. A session starts from it exactly as if the sender had been allowed all along, your agent works on it and replies by email, and the app jumps you straight to that session. The safety screen does not run a second time on a message you approved, since you've now looked at it yourself — which holds for a blocked-sender email exactly as for one the screen itself held. An email an AGENT approves over the command line is not you looking at it, so that one is still screened. From then on the conversation is a normal thread: their reply continues the same session and is never held again.
- Dismiss — takes it off the list. For an ordinary blocked-sender hold, nothing is sent to the sender — deliberately, because replying "rejected" would tell a spammer the address is real and read by a person. A safety-screen hold is different: dismissing one asks you to confirm first, then emails the sender that their message couldn't be processed (and tells you if even that notice failed to send). Either way, the message itself is kept, not deleted, until you act.
Agents can only ask, never decide. If an agent working through the command line asks to release or to dismiss an email the safety screen held, it raises an approval card for you instead of doing it — the same as any other high-risk action. Nothing holding the app's own command-line key can release or drop a safety-screen hold except you. One exception, on dismissal only: an email the spam filter caught is refused outright to a command-line caller, with no card, because letting an agent clear a catch would hand it exactly the thing the filter exists to stop. Asking to release one still raises a card, and releasing it runs the safety screen before any session starts.
The "N waiting for review" notice updates itself after each decision and disappears once the tray is empty.
Two things worth knowing:
- Attachments and reply headers come along. Approving replays the message as it originally arrived — its reply headers and any attachments, not just the text — so the resulting session sees what was actually held and its first reply threads correctly (an email held before this shipped may still replay without its attachments, since no copy of the original was kept for those older rows). The stored text is cut at 256 KB, so an unusually long body is held — and later released — cut at that length; attachments themselves are not shortened.
- Approving works from the phone too (owner decision, 2026-09-29). The review pane on a phone offers the same Approve & start session action as the desktop: it was previously hidden on the reasoning that approving spends money by spawning a session, but the phone already cold-starts paid sessions through Quick Launch, and what actually bounds a phone release is the tunnel's owner sign-in plus the desktop-approval device gate — both unchanged. A CLI caller, by contrast, still cannot release a safety-screen hold on its own.
Approving one email does not add the sender to your approved list — a new, unrelated email from them is held again. If you want someone through permanently, switch "Who can email it" to Approved senders and add their address.
How mail flows
- Inbound: mail sent to your agent address (on the shared mail domain) arrives via Cloudflare Email Routing → an email worker → a Firebase function that drops it into an inbound queue Mission Control picks up. The inbound checker runs whenever you have a claimed address AND the Receiving switch is on — claiming (or flipping the switch) starts/stops it immediately, with no restart. A message to a known address starts or continues a session; mail to a random/unknown address on the domain is rejected with a bounce by design. Before a NEW session spawns, the "who can write in" policy (above) is applied — a sender it blocks is held for your review (captured in full, not silently dropped — see "Held for review" below; no session, no reply is sent); a reply on an existing thread skips the check.
- Outbound: your agent can reply in a thread it's already in, OR compose a brand-new email
to anyone from its address (the latter via the
POST /agent/email/sendCLI route — the Omniscio-native replacement for a third-party outbound sender like AgentMail). Outbound has abuse guards — per-account daily limits, a distinct-recipient-per-day cap, no spoofing (thefromis always your own agent address, set server-side), and auto-suppression of recipients that bounce or complain (to protect the shared domain's reputation). - Mailing-list mail gets a one-click unsubscribe: a send marked as list mail (a Newsletter issue, for example) carries the standard unsubscribe headers, which most mail apps show as an Unsubscribe button next to the sender. The link is made by Omniscio's email service for that one recipient. When someone uses it, they stop getting list mail from you through Omniscio; ordinary one-to-one email to them still works, and other people's mail to them is not affected. The record keeps no email address, only a one-way code, for up to two years.
When the agent's answer is held (a reply written for someone else)
One email in, one reply out. So before Omniscio mails anything back, it asks who the agent's finished turn was actually written for — and a turn can be written for an internal reader rather than the person who wrote in: a crew member's report back to a coordinator, a dev-pipeline gate report, an overseer status update, a mission summary. Those are real, finished pieces of writing, but mailing one to your correspondent would spend their single reply on text addressed to somebody else.
Omniscio holds it instead, and tells you — a card in your inbox: "A reply is being held, and someone is still waiting." It names the session, says plainly that the person who wrote in has been sent nothing, and stays there until a reply actually reaches them; it clears itself the moment one goes out. One card per session, so a second correspondent waiting on a second agent does not get hidden behind the first card you dismiss. Nothing is lost by the hold: the agent's next finished turn that IS written for the correspondent goes out normally.
An email the agent writes by hand counts as the reply. When a session emails the person itself (its own send, or a message added to that person's Help Desk ticket), Omniscio marks everything that person sent the session as answered and clears the card — so a correspondent the agent already answered is never shown as still waiting, and is never mailed a second, stray reply later. A newsletter-style mailing does not count, and an email to someone else leaves this person's reply owed.
A turn that only paused — the agent stopped to wait on something and has not finished — sends nothing and raises no card: no answer is owed yet, and the reply slot is deliberately left untouched so the real answer can go out when the turn actually finishes.
Seeing your mail + arrival alerts
The Agent Email panel's Emails tab is a real inbox view: each row shows the sender, the subject, a short preview of the message, and the date it arrived (older emails that predate this fall back gracefully to the session name). Click a row to open the session it started.
When an email arrives and starts or continues a session, a quiet entry also appears in your main Omniscio inbox — "New email from <sender>" with the subject and the full message — so you can read the email right there instead of opening the session to find out what it said. Quoted reply history and signatures are stripped, and very long mail is capped, but the message itself arrives whole. Lines the sender's mail program wrapped are flowed back into full paragraphs, so a wrapped paragraph reads as one line, while the lines the sender typed — a greeting, a sign-off under a finished sentence, a list, a forwarded message's header, a pasted log or code block — keep their own line; mail that was never wrapped is normally left exactly as sent. It is a calm inbox row, not an OS pop-up or a sound, and it links straight to the session. Bug reports / feedback emails do not raise it (they have their own triage flow). A "Notify me in the inbox" switch in the panel turns it off (default on), and your global notification setting still gates it.
Reading a conversation
A conversation is drawn by who sent what, so someone else's words are never mistaken for yours:
- Mail from the other person arrives on the right, inside the same indented violet channel Omniscio uses for messages agents send each other, headed "emailed by <sender>" with the subject underneath. It is not drawn as one of your own messages.
- Your agent's own messages stay ordinary agent messages, on the left and unchanged.
- A reply that actually went out as email is not repeated in the session: it shows one line — "Emailed <recipient>" — and clicking it opens the full exchange. That line appears only when Omniscio can point at the record of the send, so it never claims a delivery that did not happen.
- Anything you type keeps the accent-coloured bubble, so the accent colour always means you.
An email conversation opens on its Email tab — the mail both ways and nothing else, with a line naming the address as your agent's and stating that a reply on that thread reaches your agent, not a person. The Session tab beside it is the full working transcript: the agent's thinking, its tool calls, and everything else.
For agents
For developers / agents
- Sidebar built-in: registered in the integration registry
(
src/shared/integrations/agent-email.ts) as anamc-builtin,tier: 'native',parentGroupId: 'agent-tools', virtual project__agent_email__. SPAWNABLE — the panel hosts a dual-tab sidebar (Emails | Sessions) following the KMS pattern: the Sessions tab usesuseProjectSessionHost+SessionHostSidebar; the Emails tab lists received emails via theagent-email:list-emailsIPC channel. Clicking an email navigates to its session. The panel yields to session chat when a session is displayed (shouldYieldToSessionChat). - Captured inbox content: each inbound email's sender, subject, and a de-quoted preview
(
buildEmailPreviewinsrc/main/services/agentmail-parsing.ts) are stamped intoemail_inbound_tracking(nullablefrom_address/subject/previewcolumns, ledger migration20260812193000) at the create AND continue seams viatrackInboundMessage(…, meta);listAgentEmailsreturns them (+created_at), so each Emails row shows sender · subject · preview · received-date, with a session-name fallback for legacy/NULL rows.I16-inbound-capture-columnsin.claude/memory/contracts/agent-email-contract.md. - Main-inbox arrival notice: when an email starts (after launch) or continues a session,
maybeNotifyEmailArrival(src/main/services/email/email-inbound-session-create.ts) raisesraiseEmailArrivalAlert(src/main/services/email/email-inbound-alerts.ts) — a quietraiseAgentAlertinbox row (DB insert +ALERT_CREATEDonly, so NO OS notification/sound; survives Focus Mode as the durable record). Per-message dedupKeyemail-arrived:<messageId>(one row per email, updates-in-place),sourceSessionIdfor the click-through (I11 provenance link, no new action kind), and DIRECTIVE-FREE copy so it needs no action button (I20). Gated by the per-typeemailArrivalNotificationsEnabledsetting (default on, toggled in the panel) at the call site — soemail-inbound-alerts.tsstays a pure leaf. The bug-intakespawnBugReportSessionpath never calls it. The card carries the wholecleanEmailBodyoutput (de-quoted, 16k-capped there) via thebodyparam, passed throughflowHardWrappedEmail(src/main/services/email/email-body-flow.ts, spectests/unit/services/email-body-flow.test.ts) so the sender's client wraps become paragraphs — NOTbuildEmailPreview, whose 140-char cut and whitespace-collapsing turned the card into a fragment;buildEmailPreviewbelongs on list rows only (theI16-inbound-capture-columnscapture above).I17-arrival-notice-inbox-row-onlyin the contract. - Session destination is configurable:
agentEmailSessionProjectPathin the channels settings schema (default empty = Agent Email project). The settings panel exposes a<Select>dropdown listing all spawnable projects. The inbound service falls back to the Agent Email project if the configured project doesn't exist. - Visibility is gated by the
agent-emailunreleased feature (UNRELEASED_PROJECT_GATESinproject-visibility.ts), not a raw settings flag. - Signed-out card + auth reliability: the panel gates on
Boolean(useGlobalAuthStore().uid). When signed out it renders an inline Sign in with Google button (desktop — runs the gate'slogin('google')) or a desktop-instruction message on a web client (!isElectron), never a dead-end pointer. The renderer's global-auth store also self-heals its hydration —fetchStatus()retriesGLOBAL_AUTH_STATUSon a bounded backoff and App re-syncs on reconnect — so a signed-in user is never stuck showing "signed out" from a raced startup fetch. See.claude/memory/contracts/global-auth-gate-contract.md→ "Renderer auth-state is self-healing". - Provisioning lives in Main (
provisionAgentEmailinsrc/main/services/email/agent-email-provisioning.ts), reached via theagent-email:provisionIPC channel with an optional{ handle }. Handle sanitization + validation are the pure helpers insrc/shared/agent-email-address.ts(shared by Main and the renderer's live preview); global uniqueness is enforced by an atomic Firestore.create(). The server domain comes from theapp_config/agent_emailconfig doc. - The preview's domain comes from Main, never a renderer literal: the input-less
agent-email:get-domainchannel (handleAgentEmailGetDomaininsrc/main/ipc/agent-email-handlers.ts) resolves it from the CLAIMED address first (which carries its own domain, no network), then the cachedagentEmailDomainsetting, then the relay'sget-config. Each success refreshes the cache, and an unresolvable domain answers''so the UI shows a placeholder rather than a wrong address. A hardcoded domain in the renderer is what once made the preview advertise a domain the mail service never used. - Setup and management are separate faces of one panel:
AgentEmailView.tsxroutes toAgentEmailSignInCard,AgentEmailSetupWizard(the three-step guided setup) orAgentEmailManagePanel(the management screen).AgentEmailSenderPolicyControlis shared by the setup's last step and the management screen so the two cannot describe the same rule differently. - Release / change is the inverse:
releaseAgentEmail(same file), via the input-lessagent-email:releaseIPC channel (and the parity CLI routePOST /agent/email/release), deletes the caller's ownagent_email_addressesdoc through the relay's uid-scopedrelease-addressop — freeing the handle (no Cloudflare action needed; the catch-all just 404s a freed address). It is idempotent ({ released:false }when there was none). The renderer gates it behind a typed-RELEASEConfirmDialogand, on success, clears the cachedagentEmailAddressso the panel drops back to the guided setup ("changing" the name is release-then-reclaim) — see.claude/memory/contracts/agent-email-contract.mdI14-release-frees-the-handle. - No shipped Firebase admin key (Phase B): the desktop's per-user Firestore access —
address lookup + the
.create()claim (provisioning) and the inbound queue read/ack (receiving) — runs through the user-tokenagentEmailRelayCloud Function (firebase/functions/src/agent-email-relay.ts, hosting rewrite/t/agent-email), which verifies the caller's Firebase ID token and scopes every op to that uid server-side. Agent Email data is private per signed-in user, so it uses the user-token relay shape (likesendEmail/shareRelay), NOT a ship-in-app deterrent token. Client seam:src/main/services/email/agent-email-relay-client.ts.user-token-relay-onlyin.claude/memory/contracts/agent-email-contract.md. - Sender policy (who may start a NEW session):
agentEmailSenderPolicy(owner/approved/anyone, defaultowner) +agentEmailApprovedSenders. The pure decision isisAgentEmailSenderAllowed(src/main/services/email/agent-email-sender-policy.ts); the hosted poller passes it to the shared inbound core as an opt-innewThreadSenderGateconsulted ONLY for new threads, so the AgentMail / bug-intake path (which passes no gate) stays byte-identical. Invariants + tests:.claude/memory/contracts/agent-email-sender-policy-contract.md. Keyed on the From address (forgeable) — a convenience gate, not a security boundary. - Screen holds share the row with sender-policy holds:
agent_email_heldgainedsource/hold_kind/hold_reason/held_message_json(migration20260924164245);hold_kindisNULLfor a sender-policy hold and one of the closedsrc/shared/screen-hold-kind.tsvalues for a safety-screen hold.held_message_jsonis the serializedHeldEmailCopy(src/main/services/email/held-email-copy.ts) — the source-shaped message an Approve replays, cleared back toNULLthe moment the row leavespending.AgentEmailHeldRow.tsxbadges a non-nullhold_kindas "Held by safety screen";HeldEmailReviewDialog.tsxrendersAgentEmailHeldList kind="screen"as a standalone surface, independent of the panel's own enabled state, so the "Emails held by the safety screen" card (SCREEN_HELD_DEDUP_KEYinemail-inbound-alerts.ts) works with Agent Email off. - Screen-hold release is owner-only, enforced twice:
approveHeldEmail/declineHeldEmail(src/main/services/email/agent-email-held-actions.ts) take areviewedByactor (AGENT_EMAIL_REVIEW_ACTOR) and refuse a screen hold for anything but.ui(the desktop IPC click) or.approvalCard(a card a person approved). The ONE axis the two actions still differ on is a spam catch:.approvalCardcannot dismiss one (assertOwnerCanRelease'sspamCatch), because clearing a catch is the spam view's job for a person in the app, while RELEASING one is allowed and runs the safety screen before any session starts. The CLI routes (POST /agent/email/held/approve|decline) never claim.ui/.approvalCardthemselves: a screen-hold approve raisesagent_email.release_screen_heldand a screen-hold decline raisesagent_email.dismiss_screen_held, and only a spam catch's decline is answered 409 outright. The card is always approval-gated, never "always allow".tests/unit/lint/held-email-release-callers.test.tspins which files may call either function at all, and which of those may claim.ui/.approvalCard. Rules:.claude/memory/contracts/email-safety-screen-contract.md. - Receiving lifecycle (does inbound actually run?): gated on
agentEmailReceivingEnabled(defaulttrue) AND a claimedagentEmailAddress— the pure predicate isshouldRunAgentEmailReceiving(settings). The poller is started/stopped byreconcileAgentEmailService(src/main/services/email/agent-email-inbound-poll.ts), idempotent via aserviceStartedlatch and driven BOTH at boot (the startup task always records the shared ProcessManager ref FIRST, so a later runtime start needs no restart) AND by asettings-applyside-effect (agent-email-receiving-reconcile) — so claiming an address or flipping the Receiving switch reconciles live. The dedicatedagentEmailReceivingEnabledflag is deliberately separate from theagentEmailEnabledin-development feature toggle (overloading it would hide the panel on pause and make the toggle deceptive). Invariants + tests:.claude/memory/contracts/agent-email-contract.md. - Poll-failure alerting (transient-blip safe): the 30s inbound poll surfaces the deduped
"some cloud features are degraded (agent email)" inbox banner only after
POLL_FAILURES_BEFORE_DEGRADED_ALERT(3) CONSECUTIVErelayPollQueuefailures (~90s); a successful poll resets the streak, so a single transient timeout never alarms the user (the server-side queue is durable and re-read next tick). Thealerts-only-on-sustained-streakinvariant in.claude/memory/contracts/firebase-health-alert-contract.md. - Connectivity self-test: the post-claim panel's "Send a test email" button calls the
input-less
agent-email:send-testIPC channel.handleAgentEmailSendTest(src/main/ipc/agent-email-handlers.ts) resolves BOTH the recipient (the signed-in account email,getCachedAuth().email) and thefrom(the provisionedsettings.agentEmailAddress) in Main — the renderer sends nothing, so the test can only email the user themselves. It reuses the livesendAgentEmailpath (server-side ownership / per-tier daily quota / recipient-suppression all still apply) with aclientRequestIdfor idempotency, returns{ recipient }for the UI confirmation, and does NOT touch the inbound seam. - Compose / send to anyone (the AgentMail-outbound replacement): the CLI route
POST /agent/email/send(agentEmailSendSchema,cli-server-agent-email-routes.ts) sends a BRAND-NEW email from your agent address to a caller-supplied{ to, subject, text?, html? }. Thefromis resolved server-side fromsettings.agentEmailAddress(never the body — no spoofing) and it iscliTokenOnly(a scoped agent-session token is refused, unlike the self-only send-test). It reuses the livesendAgentEmailpath, so every server-side abuse guard applies (ownership, per-tier daily quota, distinct-recipient velocity cap, suppression). It is the headless outbound half; recipients are deliberately unrestricted (send-to-anyone is the product). Contractcompose-send-to-anyone. - Answering as Support (T-88): the route takes an optional
identity: 'agent' | 'support'. Omitted (or'agent') is the send above, unchanged.'support'is for anything a person reads as a Help Desk answer — a support request, a bug report, mail to someone who wrote in — and sends From and Reply-To the install's public support address (agentEmailSupportReplyFromAddress, falling back toagentEmailSupportAddress), so such mail never leaves from the operator's own address. It names a ROLE, never an address: both addresses are resolved in Main from settings, so a body-suppliedfromis still dropped. It fails closed with400— no support address configured, or a support address that IS the agent's own address — and withidentity: 'support'combined withlistUnsubscribe(a mailing is not a Help Desk answer, the mirror of the defect). A FACE that resolves back to the agent's own address is no longer a refusal: it is not a usable public face, so it is ignored — compared through the same normalizing parser the arrival rule uses — and the support address supplies both headers, because refusing would answer a reporter who is waiting with nothing. A recipient who already owns a Help Desk ticket is unaffected: the send routes onto their ticket's thread as before and reportsidentityUsed: 'ticket-arrival', since the ticket's own identity wins. The rule lives inresolveSupportOutboundIdentity(agent-email-reply-identity.ts) — the SAME resolver the three arrival legs delegate to, so the four can never drift. Contracta-reporter-is-answered-as-support. - List mail + one-click unsubscribe (privacy audit F080): an optional boolean
listUnsubscribeon that route (and onsendAgentEmail) marks the message as mailing-list mail. ThesendEmailfunction then mints an RFC 8058List-Unsubscribe(https link) plusList-Unsubscribe-Post: List-Unsubscribe=One-Clickfor that recipient; a caller can never supply the header or link. The publicagentEmailUnsubscribefunction records the opt-out (GET only confirms; POST writes) inagent_email_suppressionsunderlist-unsubscribe-<sha256 of sender uid + recipient>, with no address or uid stored and the collection's 730-day TTL. From then onsendEmailrefuses that sender's LIST mail to that person withrecipient unsubscribed, which the route answers as422 UNPROCESSABLEwithreason: "recipient-unsubscribed". Signed with the managed secretAGENT_EMAIL_UNSUB_SECRET. ContractI23-list-mail-carries-a-relay-minted-unsubscribeandI24-an-opt-out-names-nobody. - Voice guide integration seam (Voiceprint Studio): when a calibrated Voiceprint Studio
guide exists for the default profile (
'me'),resolveEmailVoiceGuide(db)insrc/main/services/email/agent-email-voice.tsreads it and passes the markdown tobuildSessionPrompt()as an optionalvoiceGuideparameter. The prompt builder appends aREPLY VOICEsection that tells the agent to match that writing style — but explicitly subordinates the voice guide to the HARD RESTRICTIONS and REPLY FORMAT rules. When no guide exists (the default while Voiceprint Studio is in-development), the parameter isnulland the prompt is byte-identical to the pre-seam version. The guide is capped at 4000 chars. Invariant I15 in.claude/memory/contracts/agent-email-contract.md.
Related
The other built-in Agent Tools integrations are described in Agent tools, and the bearer-token contract behind every route above lives in CLI Control. An agent that wants to email someone other than you is the same command-line pattern Agent-driven sessions uses to drive a session over HTTP. Bug reports and feedback emails deliberately take a different path (Bug report intake) and never raise the arrival notice described above. What the safety screen reads before any of this, why it holds rather than bounces, and how its picture/attachment limits work is covered in Email inbound prescreen.
Last verified 2026-10-05