Verbs

agentproto serve

agentproto serve [--workspace <dir>] [--port <n>] [--bind <ip>]
                 [--profile <name>]
                 [--connect <url> [--token <jwt>] [--label <name>]]
                 [--allow-origin <url> …] [--auth-token <token>]
                 [--interactive | -i]

Runs the agentproto daemon in the foreground. Boots a local HTTP gateway (sessions, MCP, events SSE, optional PTY WS) on --port (default 18790) and, if --connect <url> is set, opens an outbound WebSocket tunnel to a host so cloud-side operators can drive this machine.

For running it as an OS-managed background service, see daemon.md — same binary, same flags, just wrapped in launchd / systemd.

Flags

FlagDefaultPurpose
--workspace <dir>, -wprocess.cwd() (or daemon.workspace in config)Workspace directory the daemon binds to. Must exist + be a directory.
--profile <name>—Load a profiles[] bundle from ~/.agentproto/config.json instead of the top-level keys.
--port <n>, -p18790HTTP port.
--bind <ip>, -b127.0.0.1Bind address. 0.0.0.0 if you want LAN reachability.
--connect <wss-url>, -coffTunnel host URL. When set, daemon connects outbound and adopts every cloud-driven spawn into its local registry.
--token <jwt>, -tfrom credentials.jsonTunnel bearer. See resolution order below.
--label <name>, -lusername@hostnameFriendly label sent in tunnel hello frames.
--allow-origin <url> (repeatable)localhost onlyBrowser origins trusted to drive mutating routes + the PTY WS. Merged with daemon.allowedOrigins from config.
--auth-token <token>—Bearer token gating the gateway itself (or daemon.authToken in config). Distinct from --token, which is the tunnel bearer for --connect.
--interactive, -ioffChain agentproto sessions --watch as a child in the same terminal — quit the TUI to tear the daemon down.

Token resolution (--connect mode)

  1. --token <jwt>
  2. $AGENTPROTO_TOKEN
  3. ~/.agentproto/credentials.json[<host>] (set by auth login)

If the credential is expired and stores a refreshToken, serve first tries a silent non-interactive refresh. It only warns and recommends agentproto auth login --host <host> when that refresh fails.

Origins

By default, any Origin that resolves to 127.0.0.1:* / localhost:* is trusted, plus anything in --allow-origin and daemon.allowedOrigins. Set daemon.strictOrigins=true (via config) to drop the localhost default and trust only the explicit allowlist — for hardened / shared-host setups.

The Bearer-token check on /mcp and other routes is independent of the Origin check — a valid token bypasses the Origin allowlist.

Modes

Local-only

agentproto serve
agentproto serve --workspace ~/code/my-project --port 18791

No tunnel, no --connect. Local clients (web spawn dialogs running in the browser, agentproto sessions, MCP clients) talk to http://127.0.0.1:18790. Discover the URL + token from ~/.agentproto/runtime.json (written at boot).

Tunnel

agentproto auth login --host wss://guilde.work
agentproto serve --connect wss://guilde.work/api/v1/agentproto/tunnel

Boots the same local gateway and opens a WS to the host. Every tunnel-driven spawn is also registered locally — operators dispatching from the cloud land in the same /sessions list as local ones, so the LocalDaemonSessionsCard, agentproto sessions, etc. all see them.

Reconnect-with-backoff (1s → 30s ceiling) is built in. The local gateway stays up across reconnects; only the WS cycles. Keep-alive pings every 30s prevent reverse-proxies (Cloud Run, …) from closing idle sockets.

End-to-end encryption (tunnel.e2e, opt-in)

By default the tunnel carries agentproto/tunnel/v1 frames in plaintext — the host terminates the WS and sees every spawn, stdout byte, and HTTP relay. Set tunnel.e2e = true in config (per-profile) to encrypt the tunnel end-to-end, so even the trusted host loses plaintext visibility:

// ~/.agentproto/config.json
{
  "tunnel": {
    "host": "wss://guilde.work/api/v1/agentproto/tunnel",
    "token": "apt_…",   // REQUIRED — the E2E handshake authenticates on it
    "e2e": true
  }
}

How it works: right after the WS opens — before any tunnel/v1 frame — the daemon and host run a short token-authenticated ephemeral handshake (X25519 + HKDF-SHA256, keyed by sha256(tunnel.token)), then wrap every frame in an AES-256-GCM box. The ephemeral exchange gives forward secrecy; binding the key schedule to the pre-shared tunnel.token authenticates both ends (a middle without the token derives different keys and is rejected at handshake time, not mid-stream). The hello, spawns, stdout, and HTTP relay all travel as ciphertext; a WS terminator sees only opaque envelopes, their sizes, and their timing.

The host must also support it. E2E is negotiated: the daemon offers it only when tunnel.e2e is set, and encrypts only if the host answers the handshake. Against a host that doesn't (an older host), the daemon transparently falls back to the plaintext tunnel after a brief negotiation timeout — nothing breaks. With tunnel.e2e unset, the behaviour is byte-identical to today. A wrong tunnel.token on either side (or a tampered handshake) fails closed — the daemon never downgrades to plaintext, it errors and reconnects.

Requires a tunnel.token (the shared secret the handshake binds to); if e2e is set without one, the daemon warns and stays plaintext.

Interactive (--interactive / -i)

agentproto serve --workspace ~/code --interactive

Spawns the watch TUI as a child after boot, inheriting stdio. Quitting the TUI (q / Ctrl-C) tears the daemon down. Use the PTY-attach detach chord (Ctrl-] then q) to leave the TUI while keeping the daemon running.

Boot banner

─ agentproto · gateway up · http://127.0.0.1:18790 ─
  workspace    ~/code/my-project
  pty          enabled (node-pty)
  origins      localhost:* only (default)
  endpoints    /mcp · /sessions · /events · /sessions/:id/pty (WS) · /sessions/:id/terminal/input · /sessions/:id (PATCH)
  mode         tunnel → wss://guilde.work/api/v1/agentproto/tunnel

Each line is a quick sanity check:

  • pty — whether node-pty was loadable. Without it, PTY-backed session routes return 501. Install with npm i -g node-pty.
  • origins — STRICT when daemon.strictOrigins=true. Empty strict allowlist is highlighted red (everything 401s except Bearer-token requests).
  • mode — local-only or tunnel → <url>. When tunnel.e2e is set, a · e2e tag is appended (the encryption is negotiated per reconnect; see End-to-end encryption).

When to use serve vs daemon install

  • agentproto serve — run-as-foreground, Ctrl-C to stop. Good for development, debugging the tunnel, --interactive TUI.
  • agentproto daemon install — same code, run by launchd / systemd. Survives logout/login and crashes. Pick this for "always-on" tunnels.

Both read the same ~/.agentproto/config.json.

Shutdown

SIGINT, SIGTERM, and SIGHUP all trigger a clean shutdown sequence: stop accepting new requests → close the tunnel → stop the gateway → delete the daemon's runtime.json. The exit banner is ── shutting down (<signal>) ──.

Files written

PathWhat
<workspace>/.agentproto/runtime.jsonDaemon discovery descriptor: { port, bind, token, pid }. Other CLI invocations (agentproto sessions) read this to talk to the daemon. Deleted on clean shutdown.

Stale runtime.json files (dead PID) in registered workspaces are swept at boot — they confuse discovery otherwise.

AGENTPROTO_JOIN — auto-register as a host at boot

A box daemon (e.g. a CI reviewer sandbox or a cloud computer) can auto-register itself as a host on a controlling daemon without relaying an offer URL by hand. Set the AGENTPROTO_JOIN env var to a join-token URL minted by the controlling daemon:

# On the controlling daemon:
agentproto devices join-token create ci-runner

# On the box daemon's startup env:
export AGENTPROTO_JOIN="agentproto://join?v=2&…"
agentproto serve

At boot, if AGENTPROTO_JOIN is set, agentproto serve dials the minting daemon, hands over a self-offer URL, and the controlling daemon registers this box as a host automatically (visible in agentproto devices list with role: host). The join is best-effort and non-blocking — a missing, expired, or revoked token logs a warning but never prevents the daemon from booting.

Optional companion env vars let the box report metadata to the controlling daemon:

Env varWhat it sets
AGENTPROTO_JOIN_NAMEDisplay name for the registered host device.
AGENTPROTO_JOIN_PROVIDERSelf-reported sandbox provider (e.g. box, e2b).
AGENTPROTO_JOIN_SANDBOX_IDSelf-reported sandbox ID.
AGENTPROTO_JOIN_LABELSComma-separated key=value pairs, e.g. env=prod,region=us-east-1.

Mint and manage join tokens with agentproto devices join-token.