返回资讯中心
外部精选
软件工程
#开发者实时流

Show HN: Nuvrail – make an AI agent ask before it sends, moves or deletes email

The approval layer between AI agents and your inbox.

Hacker News 最新mmodahl2026/10/28 · 7 天前30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

The approval layer between AI agents and your inbox.

An agent tries to delete 38 emails at 3am. Nothing happens until a human says yes.

The agent's EXPUNGE gets OK Noted and is never forwarded; the staged delete is rejected with one tap. Recorded in CI against a local test mail server; the "agent" is a short script, and any IMAP client behaves the same through the proxy. Reproduce it yourself · MP4 (67 s)

Email is the most dangerous thing you can hand an AI agent. Nuvrail makes it safe to try — reads flow through instantly, every write waits for one human tap. If that sounds useful, ★ star the repo and self-host it in 60 seconds.

AI agents that can send email are useful. AI agents that can send email without any human check are a liability. Nuvrail sits between your AI agent and your real mail server. The agent sees a normal IMAP/SMTP server. Every write — every move, delete, flag change, and outbound send — is staged for your approval before it touches the real mailbox. Reads pass through instantly; nothing blocks the agent from working. Only destructive or irreversible actions require a human to say yes.

AI agent Nuvrail Real mail server ──────── IMAP/SMTP ─────── IMAP/SMTP ──────────────── (Claude, ──────────────► gateway ──────────────► (Gmail, iCloud, Cursor, reads: pass │ on approval Outlook, or any custom) through │ only IMAP/SMTP server) instantly │ writes: staged → human reviews & approves

Agent writes a command — e.g. UID MOVE 42 Trash or sends an email via SMTP.

Gateway intercepts it — returns OK [STAGED] immediately so the agent can keep working.

You get a notification — a push notification on your phone or browser shows you exactly what the agent wants to do, with the subject, sender, and destination.

You approve or reject — one tap. On approval the operation executes against the real server. On rejection the proxy reverts its local state and silently surfaces the rejection to the agent on its next command.

The agent can never permanently delete mail. EXPUNGE is blocked at the gateway layer, and a delete (\Deleted) is staged like any other write — the worst the agent can do is ask to delete a message, and even that requires your sign-off.

📖 Why this exists: Why email is the most dangerous thing you can give an AI agent — the failure modes, why scoped OAuth and read-only tokens don't close them, and why the fix belongs on the wire.

🧭 Guides: Stop an AI agent from deleting your emails · Add human approval before an AI agent sends email · Human in the loop for LLM email agents

Wiring up a specific agent? Step-by-step recipes: Claude Desktop · Cursor · LangChain.

Just want to see it work? You need Docker with the Compose plugin — nothing else. No domain, no TLS, no nginx. This runs everything on localhost for evaluation.

git clone https://github.com/agens-field/nuvrail.git cd nuvrail cp .env.example .env

docker compose up --build

🔌 Port 3000 already taken? Set NUVRAIL_HOST_WEB_PORT=3400 in .env (likewise NUVRAIL_HOST_API_PORT, NUVRAIL_HOST_IMAP_PORT, NUVRAIL_HOST_SMTP_PORT) and use that port in the URLs below. Ports are published host-side only — nothing about the host or port is baked into the web bundle, so docker compose up -d picks the change up without a rebuild.

⏱️ First build takes several minutes — this is normal, not a hang. The initial --build compiles both images from scratch (Python deps for the gateway, a Vite build for the web PWA), so a true fresh clone with a cold cache is typically ~5–9 min on first run — not 60 seconds. Later docker compose up runs start in seconds. If the terminal sits quiet mid-build, it is compiling, not stuck; let it finish.

Sign-up is closed by default (NUVRAIL_SIGNUP_MODE=closed), so the web app has no "create account" button. Create your first account from the CLI once the containers are up (it prompts for a password):

docker compose exec gateway python3 scripts/manage_users.py create you@example.com --name "You"

Then open http://localhost:3000 and log in. (Prefer self-serve sign-up? See Account creation.) The API is on http://localhost:8080:

Give the containers ~10–20s after the build finishes to come up healthy,

then this returns {"status": "ok", ...}. A connection-refused here just

means the gateway is still starting — wait a moment and retry.

curl -s http://localhost:8080/health # → {"status": "ok", ...}

That's it — the approval UI, REST API, and IMAP/SMTP proxies (localhost:10143 / localhost:10587) are all running. Point a test IMAP/SMTP client at the proxy ports and watch writes get staged for approval in the UI.

🌐 Running it on a remote box? The browser never calls the API directly — the web container proxies /api/ to the gateway over the compose network — so only the web port needs publishing. Set NUVRAIL_BIND_ADDR=0.0.0.0 in .env, rerun docker compose up -d, and open http://<host>:3000. No rebuild, no VITE_API_URL, no CORS entry. Do this on a trusted network only: it is plain HTTP carrying bearer tokens — see Deploying to production for the TLS path.

ℹ️ Local trial only. With NUVRAIL_MASTER_KEY left blank the gateway auto-generates a key on first run (fine for kicking the tires, not for real mail). To connect a real mailbox with agents on other machines, follow Deploying to production below — that adds TLS, nginx, and a persisted master key.

Like what you see? ★ Star the repo — it's the fastest way to help other people find it.

Internet │ ├──► :993 IMAPS ──► nginx (TLS) ──► IMAP proxy (10143) ──► upstream IMAP ├──► :465 SMTPS ──► nginx (TLS) ──► SMTP proxy (10587) ──► upstream SMTP └──► :443 HTTPS ──► nginx (TLS) ──► Web PWA + REST API (3000/8080)

All external TLS terminates at nginx. Docker containers are bound to 127.0.0.1 only. No plain-text port is reachable from outside the host.

Components:

The 60-second path above is for evaluation on localhost. To run Nuvrail as a real service — reachable by an agent on another machine, terminating TLS, connecting a real mailbox — follow the full deployment below.

Ubuntu 22.04 LTS (or equivalent Debian-based distro)

DNS A record pointing at your server:

nuvrail.example.com → server IP (approval UI + proxy)

Git, Docker Engine + Compose plugin, nginx with stream module, certbot

Docker Engine

curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # log out and back in

nginx with stream module + certbot

sudo apt install -y nginx libnginx-mod-stream certbot python3-certbot-nginx

Step 1 — Clone and configure

sudo mkdir -p /opt/nuvrail && sudo chown $USER:$USER /opt/nuvrail git clone https://github.com/agens-field/nuvrail.git /opt/nuvrail cd /opt/nuvrail cp .env.example .env nano .env

Required environment variables:

⚠️ Back up NUVRAIL_MASTER_KEY. It encrypts every upstream credential in the database. Losing it makes all stored agent credentials unrecoverable.

Choose your signup mode now — it decides who can create an account on this deployment and it is fail-closed, so a fresh .env is already private. Pick one and put the matching lines in .env:

closed (DEFAULT) — no public sign-up; you provision users from the admin CLI.

This is also what you get if you set nothing at all. Best for a private /

single-tenant instance.

NUVRAIL_SIGNUP_MODE=closed

invite — public /auth/register works ONLY with a valid single-use invite code

that you mint from the admin CLI. Best for onboarding a known group.

NUVRAIL_SIGNUP_MODE=invite

open — anyone can self-register. REQUIRES the explicit ack below, or the API

refuses to start. Only for a genuinely public deployment.

NUVRAIL_SIGNUP_MODE=open NUVRAIL_ALLOW_OPEN_SIGNUP=1

You operate each mode in Step 6 — Create your account; the full reference is Account creation (signup modes). You can change the mode later on a live box — see Switching modes on a running deployment.

Certificates don't exist yet, so we bring nginx up with a minimal HTTP-only config first, let certbot obtain the certs against the running nginx, and only then add the TLS listeners and the mail stream proxies (Step 3). Doing it in this order avoids the chicken-and-egg trap where nginx refuses to start because it references fullchain.pem files that haven't been issued yet.

/etc/nginx/sites-available/nuvrail — start with just this:

server { listen 80; server_name nuvrail.example.com;

certbot's --nginx plugin will add the ACME challenge location here,

then insert the TLS server block in Step 2b.

location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }

ℹ️ Ports and bind address. The block above reaches the containers on 127.0.0.1:3000 and 127.0.0.1:8080 — the compose defaults. If you remapped either with NUVRAIL_HOST_WEB_PORT / NUVRAIL_HOST_API_PORT, use your numbers here. Leave NUVRAIL_BIND_ADDR at its 127.0.0.1 default: nginx reaches the containers over loopback, so nothing should be published to the network. Note the SPA calls /api/ on its own origin, so the location /api/ block above is what serves it — there is no VITE_API_URL to set.

sudo ln -s /etc/nginx/sites-available/nuvrail /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx

2b — Obtain certificates (certbot drives nginx)

sudo ufw allow 80 # ACME HTTP-01 challenge sudo certbot --nginx
-d nuvrail.example.com
--non-interactive --agree-tos --redirect
-m you@yourdomain.com

certbot --nginx obtains the cert, rewrites the port-80 block to redirect to HTTPS, and adds a listen 443 ssl block wired to the freshly issued fullchain.pem / privkey.pem. It also installs a systemd renewal timer. Verify:

sudo certbot certificates # confirm the cert exists sudo nginx -t # config still valid

The mail proxies (:993 / :465) reuse this same certificate — that's why we mint it before configuring the stream module below.

With certificates now in place, add the TLS-terminating stream proxies for IMAP/SMTP and confirm the web server block is correct.

Add at the top level of /etc/nginx/nginx.conf (outside http {}):

stream { include /etc/nginx/stream.d/*.conf; }

sudo mkdir -p /etc/nginx/stream.d

/etc/nginx/stream.d/nuvrail-proxy.conf

IMAP SSL (:993) → gateway container (:10143)

server { listen 993 ssl; ssl_certificate /etc/letsencrypt/live/nuvrail.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nuvrail.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; proxy_pass 127.0.0.1:10143; proxy_timeout 3600s; proxy_connect_timeout 5s; }

SMTPS (:465) → gateway container (:10587)

server { listen 465 ssl; ssl_certificate /etc/letsencrypt/live/nuvrail.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nuvrail.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; proxy_pass 127.0.0.1:10587; proxy_timeout 300s; proxy_connect_timeout 5s; }

/etc/nginx/sites-available/nuvrail (final web config)

certbot already rewrote this file in Step 2b into an HTTP→HTTPS redirect plus a listen 443 ssl block for nuvrail.example.com. Confirm it matches the shape below; add the hardening headers if certbot didn't:

server { listen 80; server_name nuvrail.example.com; return 301 https://$host$request_uri; }

server { listen 443 ssl; server_name nuvrail.example.com; ssl_certificate /etc/letsencrypt/live/nuvrail.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nuvrail.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; add_header Referrer-Policy no-referrer;

REST API + health probe → API backend on :8080.

These MUST precede the location / SPA catch-all, o

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →