Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

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/send CLI 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 (the from is 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 an amc-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 uses useProjectSessionHost + SessionHostSidebar; the Emails tab lists received emails via the agent-email:list-emails IPC 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 (buildEmailPreview in src/main/services/agentmail-parsing.ts) are stamped into email_inbound_tracking (nullable from_address / subject / preview columns, ledger migration 20260812193000) at the create AND continue seams via trackInboundMessage(…, meta); listAgentEmails returns 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-columns in .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) raises raiseEmailArrivalAlert (src/main/services/email/email-inbound-alerts.ts) — a quiet raiseAgentAlert inbox row (DB insert + ALERT_CREATED only, so NO OS notification/sound; survives Focus Mode as the durable record). Per-message dedupKey email-arrived:<messageId> (one row per email, updates-in-place), sourceSessionId for 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-type emailArrivalNotificationsEnabled setting (default on, toggled in the panel) at the call site — so email-inbound-alerts.ts stays a pure leaf. The bug-intake spawnBugReportSession path never calls it. The card carries the whole cleanEmailBody output (de-quoted, 16k-capped there) via the body param, passed through flowHardWrappedEmail (src/main/services/email/email-body-flow.ts, spec tests/unit/services/email-body-flow.test.ts) so the sender's client wraps become paragraphs — NOT buildEmailPreview, whose 140-char cut and whitespace-collapsing turned the card into a fragment; buildEmailPreview belongs on list rows only (the I16-inbound-capture-columns capture above). I17-arrival-notice-inbox-row-only in the contract.
  • Session destination is configurable: agentEmailSessionProjectPath in 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-email unreleased feature (UNRELEASED_PROJECT_GATES in project-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's login('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() retries GLOBAL_AUTH_STATUS on 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 (provisionAgentEmail in src/main/services/email/agent-email-provisioning.ts), reached via the agent-email:provision IPC channel with an optional { handle }. Handle sanitization + validation are the pure helpers in src/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 the app_config/agent_email config doc.
  • The preview's domain comes from Main, never a renderer literal: the input-less agent-email:get-domain channel (handleAgentEmailGetDomain in src/main/ipc/agent-email-handlers.ts) resolves it from the CLAIMED address first (which carries its own domain, no network), then the cached agentEmailDomain setting, then the relay's get-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.tsx routes to AgentEmailSignInCard, AgentEmailSetupWizard (the three-step guided setup) or AgentEmailManagePanel (the management screen). AgentEmailSenderPolicyControl is 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-less agent-email:release IPC channel (and the parity CLI route POST /agent/email/release), deletes the caller's own agent_email_addresses doc through the relay's uid-scoped release-address op — 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-RELEASE ConfirmDialog and, on success, clears the cached agentEmailAddress so the panel drops back to the guided setup ("changing" the name is release-then-reclaim) — see .claude/memory/contracts/agent-email-contract.md I14-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-token agentEmailRelay Cloud 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 (like sendEmail/shareRelay), NOT a ship-in-app deterrent token. Client seam: src/main/services/email/agent-email-relay-client.ts. user-token-relay-only in .claude/memory/contracts/agent-email-contract.md.
  • Sender policy (who may start a NEW session): agentEmailSenderPolicy (owner/approved/anyone, default owner) + agentEmailApprovedSenders. The pure decision is isAgentEmailSenderAllowed (src/main/services/email/agent-email-sender-policy.ts); the hosted poller passes it to the shared inbound core as an opt-in newThreadSenderGate consulted 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_held gained source / hold_kind / hold_reason / held_message_json (migration 20260924164245); hold_kind is NULL for a sender-policy hold and one of the closed src/shared/screen-hold-kind.ts values for a safety-screen hold. held_message_json is the serialized HeldEmailCopy (src/main/services/email/held-email-copy.ts) — the source-shaped message an Approve replays, cleared back to NULL the moment the row leaves pending. AgentEmailHeldRow.tsx badges a non-null hold_kind as "Held by safety screen"; HeldEmailReviewDialog.tsx renders AgentEmailHeldList 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_KEY in email-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 a reviewedBy actor (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: .approvalCard cannot dismiss one (assertOwnerCanRelease's spamCatch), 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/.approvalCard themselves: a screen-hold approve raises agent_email.release_screen_held and a screen-hold decline raises agent_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.ts pins 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 (default true) AND a claimed agentEmailAddress — the pure predicate is shouldRunAgentEmailReceiving(settings). The poller is started/stopped by reconcileAgentEmailService (src/main/services/email/agent-email-inbound-poll.ts), idempotent via a serviceStarted latch 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 a settings-apply side-effect (agent-email-receiving-reconcile) — so claiming an address or flipping the Receiving switch reconciles live. The dedicated agentEmailReceivingEnabled flag is deliberately separate from the agentEmailEnabled in-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) CONSECUTIVE relayPollQueue failures (~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). The alerts-only-on-sustained-streak invariant 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-test IPC channel. handleAgentEmailSendTest (src/main/ipc/agent-email-handlers.ts) resolves BOTH the recipient (the signed-in account email, getCachedAuth().email) and the from (the provisioned settings.agentEmailAddress) in Main — the renderer sends nothing, so the test can only email the user themselves. It reuses the live sendAgentEmail path (server-side ownership / per-tier daily quota / recipient-suppression all still apply) with a clientRequestId for 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? }. The from is resolved server-side from settings.agentEmailAddress (never the body — no spoofing) and it is cliTokenOnly (a scoped agent-session token is refused, unlike the self-only send-test). It reuses the live sendAgentEmail path, 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). Contract compose-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 to agentEmailSupportAddress), 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-supplied from is still dropped. It fails closed with 400 — no support address configured, or a support address that IS the agent's own address — and with identity: 'support' combined with listUnsubscribe (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 reports identityUsed: 'ticket-arrival', since the ticket's own identity wins. The rule lives in resolveSupportOutboundIdentity (agent-email-reply-identity.ts) — the SAME resolver the three arrival legs delegate to, so the four can never drift. Contract a-reporter-is-answered-as-support.
  • List mail + one-click unsubscribe (privacy audit F080): an optional boolean listUnsubscribe on that route (and on sendAgentEmail) marks the message as mailing-list mail. The sendEmail function then mints an RFC 8058 List-Unsubscribe (https link) plus List-Unsubscribe-Post: List-Unsubscribe=One-Click for that recipient; a caller can never supply the header or link. The public agentEmailUnsubscribe function records the opt-out (GET only confirms; POST writes) in agent_email_suppressions under list-unsubscribe-<sha256 of sender uid + recipient>, with no address or uid stored and the collection's 730-day TTL. From then on sendEmail refuses that sender's LIST mail to that person with recipient unsubscribed, which the route answers as 422 UNPROCESSABLE with reason: "recipient-unsubscribed". Signed with the managed secret AGENT_EMAIL_UNSUB_SECRET. Contract I23-list-mail-carries-a-relay-minted-unsubscribe and I24-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) in src/main/services/email/agent-email-voice.ts reads it and passes the markdown to buildSessionPrompt() as an optional voiceGuide parameter. The prompt builder appends a REPLY VOICE section 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 is null and 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