# Running Agent 007 remotely By default the server binds to `127.0.0.1` and is reachable only from the machine it runs on. This is the safe default: **the app spawns real shells, so anyone who can reach it effectively has terminal access to the host.** Follow this guide to reach it from another machine *without* exposing it to the public internet. > This guide enables *you* to reach *your own* server remotely. Per-user login > now exists (`npm run adduser` — see the README "Multiplayer & login" section), > but it establishes **identity, not isolation**: every logged-in user can still > spawn shells on the host, and read-only sharing of others' agents is a later > phase (`docs/designs/multiplayer.md`). Keep the server behind Tailscale or an > authenticating proxy (see below) and only issue tokens to people you'd give > an SSH login. Three ways in, all of which leave the server on `127.0.0.1`: - **(a) [Tailscale](#recommended-tailscale)**: `agent007 install --remote`. Your devices join a private network; nothing is public. - **(b) [Cloudflare Tunnel + Access](#cloudflare-tunnel--access)**: a real `https://agent.example.com` that Cloudflare signs people into (email code or Google) before any request reaches the machine. Good for a VM with a fixed address, or for people you would rather not add to a tailnet. - **(c) [WireGuard + a local certificate](#wireguard--a-local-certificate)**: your own WireGuard network, with caddy or nginx terminating https on the WireGuard address. (b) and (c) put a reverse proxy in front: `agent007 install --public-url ` sets that up (see [Behind a reverse proxy](#behind-a-reverse-proxy-public_url)). ### Security notes - **Never expose port 7007 directly**, to the internet or to a LAN you do not trust. Keep `HOST=127.0.0.1` (or the WireGuard address) and let the proxy or Tailscale be the only way in. - **The app assumes whatever is in front of it authenticates.** With user accounts off (the default), anyone the proxy lets through gets a terminal on the host. Cloudflare Access, a tailnet or a WireGuard peer list is the lock; Agent 007 adds none of its own. - The origin check (`ALLOWED_ORIGINS`, `PUBLIC_URL`) only stops other websites driving your browser; it is not access control. ## One command: `agent007 install --remote` With [Tailscale](https://tailscale.com/download) installed and logged in on the host and on your phone or laptop (same tailnet): ```bash agent007 install --remote # npm start -- install --remote in a clone ``` Then open `https://..ts.net` on any device in your tailnet. It: 1. finds the `tailscale` CLI (on `PATH`, else inside `/Applications/Tailscale.app` on macOS) and checks it is running and logged in. If not, it says what to do (install it, or `tailscale up`) and changes nothing; 2. reads `tailscale serve status`: a port already served is kept, and another site on the HTTPS port (443) is never replaced: Agent 007 takes the next free HTTPS port (8443, then 10000) and you open `https://:8443`. If all three are taken it says what uses them and stops; 3. runs `tailscale serve --bg [--https=] `, which tailscaled keeps across reboots. If Serve is not enabled on your tailnet, it prints the link Tailscale gives to enable it; 4. adds the host's ts.net name to `ALLOWED_ORIGINS` in `~/.agent-007/.env`, keeping any entries already there; 5. restarts Agent 007 if it is running, so the new setting takes effect. `--dry-run` prints the changes instead of making them. `agent007 install --all` does it after the service and voice, and plain `agent007 install` offers it when Tailscale is installed. `agent007 doctor` checks the result. The rest of this page is what it automates, and the other options. ## Recommended: Tailscale A home connection is almost always behind **CGNAT**, so forwarding a port on your router usually won't work at all, and exposing SSH/HTTP to the internet is a brute-force magnet. Tailscale sidesteps both: the host dials *out* and joins a private WireGuard mesh, so nothing on the router needs changing. **Quick CGNAT check:** compare the WAN IP shown in your router admin against [whatismyip.com](https://whatismyip.com). If they differ, you're behind CGNAT and port-forwarding can't work — use Tailscale. ### Setup (Mac mini host) 1. Install Tailscale on the **host** (the Mac mini) and on each **client** (your laptop, or your phone: the app fits a phone screen). Sign both into the same tailnet. 2. Note the host's tailnet name, e.g. `mac-mini.tailXXXX.ts.net`. 3. Start the server so it accepts connections from the tailnet, and tell it which remote origin the browser will use: ```bash HOST=0.0.0.0 \ ALLOWED_ORIGINS=mac-mini.tailXXXX.ts.net \ npm start ``` `HOST=0.0.0.0` binds all interfaces (Tailscale is the access boundary). `ALLOWED_ORIGINS` whitelists the hostname your browser reports — without it, the cross-origin check rejects the remote browser. 4. From the client, open `http://mac-mini.tailXXXX.ts.net:7007`. As a service (`agent007 install`), put these in `~/.agent-007/.env`: the service reads only that file, not the `.env` of the folder you used to start it from. `agent007 install` copies keys that file lacks from the current folder's `.env`, and `agent007 doctor` flags a `tailscale serve` hostname missing from `ALLOWED_ORIGINS`. ### Nicer: `tailscale serve` (HTTPS, no open port) Keep the server localhost-only and let Tailscale terminate TLS and proxy it: ```bash # terminal 1 — default localhost bind is fine npm start # terminal 2 tailscale serve 7007 ``` Tailscale serves it at `https://mac-mini.tailXXXX.ts.net` (port 443). Because the proxied request arrives from localhost, the default origin check passes — but if the browser's `Origin` is the tailnet hostname you may still need `ALLOWED_ORIGINS=mac-mini.tailXXXX.ts.net`. This is the most secure option: no extra port is open and traffic is encrypted end to end. > **Voice input needs this HTTPS setup.** Browsers only grant microphone access > in a secure context (HTTPS or localhost), so the in-app voice input (the > terminal's mic button, the Billion tab's mic, `Cmd+D`) works over `tailscale serve` but not over the plain > `http://...:7007` bind above. One boundary caveat: Web Speech recognition in > Chrome/Edge streams the microphone audio to Google/Microsoft servers for > transcription (Safari may process on-device), so dictated content leaves the > tailnet even though the app's own traffic doesn't — don't dictate secrets. > Also note the microphone permission is granted per origin: the tailnet > hostname prompts separately from localhost, and a denial there sticks until > you reset it in the browser's site settings for that origin. ### SSH into the host too Enable **Tailscale SSH** and reach the mini with `ssh you@mac-mini` — key-free and gated by tailnet ACLs. Useful for starting/restarting the server. ## Behind a reverse proxy (`PUBLIC_URL`) Any proxy that terminates https and forwards to `http://127.0.0.1:7007` works, as long as it passes WebSocket upgrades (the terminals, the board and Talk all run over one WebSocket). Tell Agent 007 the address the browser uses: ```bash agent007 install --public-url https://agent.example.com # the service, with PUBLIC_URL; no Tailscale # or, in a terminal: PUBLIC_URL=https://agent.example.com agent007 (or --public-url) ``` `install --public-url` writes `PUBLIC_URL` to `~/.agent-007/.env`, then installs and (re)starts the service bound to `127.0.0.1`. It warns if `HOST` is `0.0.0.0`, if the URL is not https, or if a `PUBLIC_URL` set elsewhere would win. With `--all` it takes the place of the Tailscale step. With `PUBLIC_URL` set, the server: - lets that hostname through the origin check for the API and the WebSocket, as an `ALLOWED_ORIGINS` entry would, and treats that page as your own browser even when the proxy sends its own `Host` header; - believes `X-Forwarded-Proto` and `X-Forwarded-For` from a proxy on the same machine (loopback) and no one else; - uses it for every link it sends out (Telegram round messages; `APP_URL`, its older name, still works). The page itself only uses relative URLs and builds its `wss://` address from the page's own, so nothing in the browser needs it. `agent007 status` says which mode the server is in and what the proxy last sent (`proxy headers: last forwarded request 2m ago: https from 203.0.113.9`); `agent007 doctor` checks `PUBLIC_URL`, `HOST` and that requests are arriving as https. **Voice input and Talk to Billion work behind the proxy.** The browser decides whether the microphone is allowed from the address it sees, so an `https://` page from the proxy is a secure context even though Agent 007 itself only sees plain http on localhost. The certificate must be one the browser trusts (Cloudflare's always is; a local one must be installed on each device, see (c)). An `http://` `PUBLIC_URL` gets no microphone. ## Cloudflare Tunnel + Access `cloudflared` on the host dials out to Cloudflare, so the machine needs no open port, and Cloudflare Access signs people in before a request reaches it. The Zero Trust free plan covers up to 50 users. You need a domain on Cloudflare. 1. Install `cloudflared` on the host and create the tunnel: ```bash cloudflared tunnel login cloudflared tunnel create agent-007 # prints the tunnel ID cloudflared tunnel route dns agent-007 agent.example.com ``` 2. `~/.cloudflared/config.yml` (or `/etc/cloudflared/config.yml` for the system service): ```yaml tunnel: agent-007 credentials-file: /home/you/.cloudflared/.json ingress: - hostname: agent.example.com service: http://127.0.0.1:7007 - service: http_status:404 ``` cloudflared passes WebSockets, the browser's `Host` and `X-Forwarded-Proto: https` without further settings. Run it with `cloudflared tunnel run agent-007`, or `sudo cloudflared service install` to keep it running. 3. **Set the Access policy before you open the URL.** In the Zero Trust dashboard: *Access → Applications → Add an application → Self-hosted*, domain `agent.example.com`. Add a policy with action **Allow** and an **Include** rule of *Emails* (yours, one per person) or *Emails ending in* `@yourcompany.com`. Sign-in is a one-time code by email unless you add Google under *Settings → Authentication → Login methods*. Leave no *Everyone* rule: that would hand a shell to anyone with an email address. 4. On the host: `agent007 install --public-url https://agent.example.com`. With Access in front, every request carries `Cf-Access-Authenticated-User-Email`. Agent 007 logs each email the first time it sees it, `agent007 status` shows the last one, and the Settings panel says *Signed in through Cloudflare Access as …*. **That is display only: the `Cf-Access-Jwt-Assertion` token is not verified**, so the email is whatever the header said. Access is what keeps people out; Agent 007's own user accounts stay off. Because the server listens on `127.0.0.1`, only programs on the host itself could send a forged header. ## WireGuard + a local certificate With your own WireGuard network (the host at, say, `10.8.0.1`), run a proxy on the WireGuard address that terminates https and forwards to the server on `127.0.0.1`. A plain `HOST=10.8.0.1` bind works too, but it is http, so the microphone is off. Give the host a name that resolves to `10.8.0.1` on your devices (a DNS record pointing at the private address, or each device's hosts file), e.g. `agent.wg.example`. **caddy** (`Caddyfile`), with its own local certificate authority: ``` https://agent.wg.example { bind 10.8.0.1 tls internal reverse_proxy 127.0.0.1:7007 } ``` Then install caddy's root certificate (`~/.local/share/caddy/pki/authorities/local/root.crt`, or `caddy trust` on the host) on each phone and laptop, or the browser will not grant the microphone. `mkcert` works the same way with any proxy. **nginx** needs the WebSocket, `Host` and forwarded headers spelled out, and a body limit that fits a Talk recording: ```nginx server { listen 10.8.0.1:443 ssl; server_name agent.wg.example; ssl_certificate /etc/nginx/agent.pem; ssl_certificate_key /etc/nginx/agent-key.pem; client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:7007; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 1d; } } ``` Then `agent007 install --public-url https://agent.wg.example`. ## Not recommended: router port-forwarding Only works if you have a real public IP (not CGNAT), and exposes the host to the internet. If you must, put it behind an authenticating reverse proxy — never expose the raw server. ## Environment variables | Variable | Default | Purpose | |-------------------|-------------|---------| | `PORT` | `7007` | Listen port | | `HOST` | `127.0.0.1` | Bind interface. `0.0.0.0` = all interfaces (use only behind Tailscale/trusted network) | | `PUBLIC_URL` | *(none)* | The https address a reverse proxy or tunnel serves the app at. Allowed as an origin, used in links, and turns on the proxy checks in `status` and `doctor`. Only the origin is used (no path prefix) | | `ALLOWED_ORIGINS` | *(none)* | Comma-separated extra origins allowed by the cross-origin check. Bare hostnames (`mac-mini.tailXXXX.ts.net`), `host:port` (`mac-mini:7007`), or full origins (`https://mac-mini:7007`); only the hostname is used. `*` disables the check for **any** origin — avoid it: even on the default localhost bind, `*` lets any website you visit drive this server through your browser (drive-by command execution) | Loopback origins (`localhost`, `127.0.0.1`, `[::1]`) are always allowed regardless of `ALLOWED_ORIGINS`, and so is `PUBLIC_URL`'s hostname. The origin check only blocks cross-origin **browser** requests — it is not access control. Non-browser clients (curl, native WebSocket) send no `Origin` and always pass. When `HOST` is remote, the network boundary (Tailscale / a trusted LAN) is what actually gates who can reach the server; behind a proxy, the proxy's sign-in does.