Server-side notifications
openagent.email v0.3 includes a private ntfy service inside the Docker stack. It is an interrupt channel, not another mailbox: agents still read mail through the normal protected API, while a notification only says that action may be needed.
The default is a server-only loop. It supports notify_verify, manual
agent/user notifications, and mail-arrival alerts without opening ntfy to the
internet. Phone delivery is optional and
uses a separate public HTTPS hostname. Webhooks are not part of this feature.
Bring it up safely
Section titled “Bring it up safely”.env.example includes the required values:
NTFY_ENABLED=trueNOTIFY_PUBLIC_URL=http://127.0.0.1:2586NTFY_UPSTREAM=trueNTFY_ADMIN_PASSWORD=replace-with-openssl-rand-hex-24NTFY_ADMIN_PASSWORD stays between Docker and the server. It is not returned
by the API and must never go in an agent’s MCP configuration. Protect the whole
file before starting the stack:
chmod 600 .envdocker compose up -d./deploy/doctor.shThe doctor calls POST /v1/notify/verify: it publishes a harmless check and
polls it back from ntfy’s cache. The ntfy port is bound to 127.0.0.1:2586 by
default. Keep it that way in v0.3.
Setting NTFY_ENABLED=false turns off notification API operations and the mail
watcher, but the ntfy container still runs. This is intentional for v0.3: the
fixed Compose graph is simpler and costs about 20 MB RAM. Set
NTFY_UPSTREAM=false if you do not want ntfy to forward unknown topics to
ntfy.sh.
Topics, without leaking topics to agents
Section titled “Topics, without leaking topics to agents”The server creates random-suffixed physical topics, for example:
| Logical route | Example physical topic | Who can use it |
|---|---|---|
user-alerts | user-alerts-x7k2 | server-side human alerts |
user-low | user-low-x7k2 | server-side low-priority human alerts |
agent:<localpart> | agent-qa-bot-x7k2 | the matching agent route |
The suffix and all ntfy tokens are stored only in the server’s private JSON
state. MCP clients use notify_user and notify_agent and never receive a
topic name or ntfy credential. ntfy is deny-by-default: the server publisher is
write-only, while reserved reader accounts are read-only. The optional phone
setup creates one separate read-only account for the two human topics; it has
no access to an agent route. Creating an identity in a running stack creates
its reader account and route before the API returns the new identity, and also
records it for the next ntfy restart.
Phone setup prints a password and the two random-suffixed human topic names. Treat them as credentials: do not paste them into a ticket, chat transcript, image, or shell history. The CLI path still prints those values and does not generate a QR code; the dashboard Add device flow does — see Phone notifications.
Who may interrupt whom
Section titled “Who may interrupt whom”notify_user is deliberately stronger than normal mail operations. An admin
key may use it. An identity token needs that identity to be created with
canNotifyUser: true, and it has its own NOTIFY_RATE_LIMIT rolling-hour
budget (default 10). It does not share the email send limit.
An identity may read notification history only for its own route:
identity qa-bot@example.com -> GET /v1/notify/messages?topic=selfThe server maps self to agent:qa-bot. Trying to read agent:other-agent,
user-alerts, or user-low returns 403, even if the caller guesses their
logical names.
Mail arrival policy
Section titled “Mail arrival policy”PUSH_POLICY=otp is the default: a new message causes a notification only when
the server finds an OTP code or verification link. Set all for every new
message, or none to disable the watcher.
How much of the mail appears in that human push is per-identity push content
tier (default 1):
| Tier | What the push contains |
|---|---|
1 | Interrupt only — e.g. qa-bot@example.com received new email (contains OTP or verification link). No sender, subject, preview, or code. |
2 | Tier 1 plus masked From / Subject |
3 | Interrupt line plus unmasked From / Subject, body preview, and extracted OTP codes/links. That content leaves this server for the ntfy channel. |
Admins set the tier with
PUT /v1/identities/:address/push-tier.
Tier 3 requires "confirm_risk": true or the API returns
confirm_risk_required. Identity tokens may read their own tier
(GET …/push-tier) but cannot change it. The Dashboard overview also exposes
the same control for admin sessions.
Mail from outside your own managed identities can alert the user, but it can
never wake an agent:<localpart> route. The IMAP watcher only publishes to
human topics; an agent wake-up is emitted only after the API has accepted a
server-authenticated send to another managed identity. That boundary does not
depend on email text or attacker-controlled headers.
API and MCP
Section titled “API and MCP”The REST shapes are in the API reference. The MCP package exposes four matching operations:
notify_user(title, message, level?, tags?)notify_agent(name, title, message, level?, tags?)notify_check(since?)notify_verify()
Levels map to ntfy priority conservatively: urgent → priority 5,
normal → 3, and low → 1. For user alerts, urgent and normal use the
normal human-alert route; low uses the separate low-priority route.