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
| Flag | Default | Purpose |
|---|---|---|
--workspace <dir>, -w | process.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>, -p | 18790 | HTTP port. |
--bind <ip>, -b | 127.0.0.1 | Bind address. 0.0.0.0 if you want LAN reachability. |
--connect <wss-url>, -c | off | Tunnel host URL. When set, daemon connects outbound and adopts every cloud-driven spawn into its local registry. |
--token <jwt>, -t | from credentials.json | Tunnel bearer. See resolution order below. |
--label <name>, -l | username@hostname | Friendly label sent in tunnel hello frames. |
--allow-origin <url> (repeatable) | localhost only | Browser 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, -i | off | Chain agentproto sessions --watch as a child in the same terminal — quit the TUI to tear the daemon down. |
Token resolution (--connect mode)
--token <jwt>$AGENTPROTO_TOKEN~/.agentproto/credentials.json[<host>](set byauth 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 18791No 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/tunnelBoots 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 --interactiveSpawns 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/tunnelEach line is a quick sanity check:
pty— whethernode-ptywas loadable. Without it, PTY-backed session routes return 501. Install withnpm i -g node-pty.origins—STRICTwhendaemon.strictOrigins=true. Empty strict allowlist is highlighted red (everything 401s except Bearer-token requests).mode—local-onlyortunnel → <url>. Whentunnel.e2eis set, a· e2etag 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,--interactiveTUI.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
| Path | What |
|---|---|
<workspace>/.agentproto/runtime.json | Daemon 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 serveAt 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 var | What it sets |
|---|---|
AGENTPROTO_JOIN_NAME | Display name for the registered host device. |
AGENTPROTO_JOIN_PROVIDER | Self-reported sandbox provider (e.g. box, e2b). |
AGENTPROTO_JOIN_SANDBOX_ID | Self-reported sandbox ID. |
AGENTPROTO_JOIN_LABELS | Comma-separated key=value pairs, e.g. env=prod,region=us-east-1. |
Mint and manage join tokens with
agentproto devices join-token.