Skip to content

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, or high risk;
  • informational, action, or redirect response 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-ID is 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

  1. The trust registry is file-backed and manually curated; there is no Workshop editor or capability for it.
  2. Request records are per-file snapshots rather than an append-only event log.
  3. Topic delivery uses the proactive queue; admission rejection is logged and later stale-digest recovery is best effort.
  4. The catch-up window is capped at seven days, so a longer outage requires a manual inbox review.
  5. Sending a denial attempts the decline but currently leaves the request denied even if Fastmail delivery of that decline fails.