External and Inter-Agent Email
Vita has a guarded email protocol for messages sent to vita@ham.xyz. It keeps
unknown external mail out of the ordinary owner-mail pipeline, makes every
request visible in a dedicated Telegram topic, and gives response agents only
the read scope approved for that request. The same surface can send a new
message to another agent after the draft has been reviewed.
Status: active. The VitaScheduler polls every five minutes. Request state is durable under
data/email/external_requests/.
Inbound flow
Fastmail inbox
β
ββ internal / owner / known application mail β normal email processors
β
ββ unknown external sender
β
ββ persist request + announce arrival in βVita Emailβ
ββ sandboxed read-only triage
β ββ pure broadcast/FYI β auto_closed
ββ trusted + low risk β scoped auto-draft β human send review
ββ everything else
ββ full-read/no-write scoping agent proposes a grant
ββ human approves, narrows, broadens, or denies in Telegram
ββ grant compiles to a capability token
ββ response agent researches and drafts inside that token
ββ threaded Fastmail reply + sent confirmation
The general email processor deliberately leaves an unknown external sender
unclaimed. This prevents inbox homeostasis from archiving the message before the
external-email poller can create a request. Deduplication then happens by the
original Message-ID in the request ledger.
1. Poll and announce
The scheduler calls process_external_emails() every five minutes. A successful
poll normally looks back 24 hours. If polling was interrupted, the window widens
to cover the elapsed gap plus a two-hour buffer, capped at seven days. The poll
watermark advances only after a successful fetch.
Each new message is persisted before analysis and immediately announced in the
Telegram topic named Vita Email. Topic resolution is by name, so recreating the
topic does not require a code change; General is the fallback. The arrival notice
is intentionally upstream of triage and scoping so a later failure cannot make
the message invisible.
2. Sandboxed triage
The first agent runs with the email_triage profile. It cannot access personal
data, write files, or take actions. It returns a structured assessment:
- one-sentence summary and requested scope;
- whether the message is actionable;
low,medium, orhighrisk;informational,action, orredirectresponse type;- candidate read paths.
Only a non-actionable informational messageβsuch as a newsletter, scanner notice, or receiptβis auto-closed. An action or redirect request continues to review even when the classifier calls it non-actionable, because declining or redirecting a real request still deserves a reply. Missing or failed triage fails safe toward human review.
3. Trusted-sender fast path
data/email/known_agents.json is a small alias and trust registry. A trusted
sender gets the fast path only when triage also says the request is low risk and
not a redirect. Vita then creates a code-read-scoped draft and shows the exact
recipient, subject, and body for a one-tap send decision in Telegram.
Trust relaxes the scope ceremony, not content safety or send review. Medium/high risk, sensitive, redirect, and unknown-sender requests use the full path.
4. Scope and approval
The scoping agent can inspect Vita in read-only mode to recommend the minimum access grant needed for a useful answer. A grant has:
| Field | Meaning |
|---|---|
roots |
Filesystem roots the response agent may see |
categories |
code, docs, data, personal, or full |
level |
Normally read; read_write is available only as an explicit override |
sensitivity |
Risk annotation for the reviewer |
notes |
Why this access is needed |
Telegram presents the request summary, risk, response type, proposed grant, and
request ID. A natural-language reply is translated by the resident agent into
approve, deny, or an approval with grant_overrides. The approved grant is
compiled into a capability token; the execution agent is not simply given the
ambient session's access.
Denial sends a short threaded decline. Approval generates and sends a response; when a trusted fast-path draft was already reviewed, approval sends that same draft rather than silently regenerating different prose. Changing the grant causes a fresh draft under the changed scope.
5. Delivery and threading
Replies use the original Message-ID for In-Reply-To and References and a
Vita-owned message ID for the new message. Both HTML and plain-text bodies are
sent. Success and failure update the request state on disk; success also queues a
confirmation in the Vita Email topic.
Request lifecycle
The typical full-review lifecycle is:
pending_triage β triaged β scoped β pending_approval
β approved β executing β draft_ready β sent
Important side paths:
| Status | Meaning |
|---|---|
auto_closed |
Pure non-actionable informational broadcast |
pending_send |
Trusted-sender draft is waiting for review |
denied |
Human denied; decline attempted |
execution_failed |
Scoped response agent failed |
send_failed |
Draft exists but Fastmail delivery failed |
Requests in the open states are re-surfaced as a single stale digest when they are between two hours and 21 days old. The digest is self-gated to at most once per eight hours and lists at most ten requests, oldest first. This is the backstop for an arrival or approval notice that was read but never answered.
Conversational tool
Telegram sessions expose an external_email tool with five actions:
| Action | Purpose |
|---|---|
list_pending |
List requests still awaiting a decision |
get_request |
Show the durable record for one request |
approve |
Approve the recommendation or pass grant_overrides |
deny |
Deny and send the standard decline |
send |
Send a reviewed new message or link it to an inbound request |
For outbound mail, to can be an alias from known_agents.json or a literal
email address. request_id keeps a reply sent through the outbound tool in the
same inbound state machine. Every outbound draft must be shown with recipient,
subject, and body before send is called.
Reliability and safety properties
- No silent arrival: announce before triage; log queue-admission rejection.
- Outage catch-up: adaptive 24-hour-to-seven-day polling window.
- No double claim: the general processor defers unknown external senders.
- No duplicate request: original
Message-IDis checked against the ledger. - Least privilege: triage, scoping, and execution have distinct capability profiles; the execution token is compiled from the reviewed grant.
- No trust shortcut for sensitive asks: sender trust is necessary but not sufficient for the fast path.
- Human-reviewed sending: fast-path and new outbound drafts are reviewed; ordinary requests require grant approval before execution.
- Durable recovery: every transition is written to a request JSON file, and stale open requests are re-nudged.
The request store contains message metadata, summaries, grants, approvals, and draft response text. It is private operational data and must not be included in public reports or capability responses.
Operations
The loop is owned by VitaScheduler and should be operated through Vita's service runner:
./run api status
./run api log
./run api restart
The scheduler ledger records each external_email execution. The loop starts
after the API's initialization delay; repeated failures use the scheduler's task
policy and remain visible in API logs.
Daily collaboration watch
A separate leverage_watch scheduler task runs at 9am PT. It uses one GitHub
API directory listing to compare a configured collaborator's docs/future
files with data/email/leverage_watch_state.json; only new, modified, or removed
files produce a message in the Vita Email topic. New and modified files get a
short heading/prose preview. The watcher never emails the collaboratorβbio-zack
decides whether anything should go back.
The trigger has its own HIGH admission budget and the scheduler task uses the
shared task policy. The baseline can be seeded without surfacing historical
files, and --no-post supports a read/diff diagnostic:
uv run python scripts/integrations/leverage_watch.py --seed
uv run python scripts/integrations/leverage_watch.py --no-post
The watcher is deliberately cheap and state-based, not a full git clone or semantic comparison. Current limitation: after a detected change it advances the SHA baseline even if Telegram enqueueing fails, so the failed notification will not automatically replay on the next day.
File map
| Path | Responsibility |
|---|---|
scripts/external_email/external_handler.py |
Poll, triage, scope, execute, state machine |
scripts/external_email/approval.py |
Telegram approval and reviewed-draft handling |
scripts/external_email/known_agents.py |
Trusted sender and outbound alias resolution |
scripts/external_email/notify.py |
Arrival, approval, stale digest, topic routing |
scripts/external_email/outbound.py |
Reviewed outbound sends and inbound linking |
scripts/inference/tools/external_email.py |
Resident-agent tool contract |
scripts/integrations/leverage_watch.py |
Daily collaborator-spec SHA diff and topic notice |
scripts/mail/email_processor.py |
Defers unknown external senders to this protocol |
api/src/scheduler.py |
Five-minute scheduler loop |
data/email/known_agents.json |
Private trust and alias registry |
data/email/external_requests/*.json |
Durable request records |
Notification presentation (2026-07)
Three refinements landed in mid-July, all aimed at making a notice readable at a glance rather than requiring a click-through:
- Rich HTML cards for every email notification (
a6c50b233) β notices render as structured cards in the Telegram topic instead of plain text. - The arrival ping carries the triage summary (
d4c5be180) β the first notification already says what the message is and what it wants, so the common case needs no follow-up read. - Auto-close narrowed to informational broadcasts (
a1163371e) β a broadcast with nothing to do closes itself, but anything that redirects or asks for an action stays open. Auto-closing an actionable request was silently dropping work.
Leverage-Watch
A sibling change-triggered channel on the same topic. Shipped 2026-07-15
(7c029ed43): a daily diff of Rick Gorman's leverage/docs/future; when it
changes, the diff posts to the Vita Email topic.
It is a diff, not a digest β nothing posts on a no-change day. That makes it pull-shaped, satisfying the standing discriminator that a proactive surface must be requested and self-terminating rather than a recurring poll bio-zack learns to ignore.
Rick is a key peer node in the centaur mesh (see Agent Mastermind); his forward-looking docs are high-signal, but hand-checking them never actually happens.
Current limitations
- The trust registry is file-backed and manually curated; there is no Workshop editor or capability for it.
- Request records are per-file snapshots rather than an append-only event log.
- Topic delivery uses the proactive queue; admission rejection is logged and later stale-digest recovery is best effort.
- The catch-up window is capped at seven days, so a longer outage requires a manual inbox review.
- Sending a denial attempts the decline but currently leaves the request
deniedeven if Fastmail delivery of that decline fails.
Related documentation
- Telegram β named topics, resident sessions, and participant routing
- Capability Layer β envelopes, tokens, provenance, and transports
- Scheduling Engine β loop execution and liveness monitoring
- Proactive Outreach β the outbound queue used for notices
- Inbox Sweep β bulk triage for bio-zack's own inboxes
- Agent Mastermind β the other inter-agent channel