Ctrl K
ComunidadGuías…

docs/REMOTE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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 <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://<host>.<tailnet>.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://<name>:8443`. If all
   three are taken it says what uses them and stops;
3. runs `tailscale serve --bg [--https=<port>] <port>`, 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/<TUNNEL-ID>.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.