From 18250284d9c1bd5d5ac9b2bf84fd0c680fc5a484 Mon Sep 17 00:00:00 2001 From: Dipak Date: Sat, 11 Jul 2026 03:55:07 +0530 Subject: [PATCH] docs: complete configuration reference, verified against the code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New docs/configuration.md: every env var RoamCode reads — one table per area (core, ROAMCODE_*, advanced/installer), each row with the default, effect, and source file, all verified against server-config.ts, data-dir.ts, start.ts, updater.ts, terminal-process.ts, config.ts, origin-check.ts, auth.ts, and scripts/install.sh. Drift found and fixed while verifying: - README and --help advertised VAPID_SUBJECT, but the code only reads ROAMCODE_VAPID_SUBJECT (or the legacy REMOTE_CODER_VAPID_SUBJECT) - README's TRUST_PROXY row only mentioned 1/true; the code's preferred form is a specific proxy IP/CIDR (trusting every hop is spoofable) README's config table links to the full reference; --help points at it too. Closes #71 --- README.md | 6 ++-- docs/configuration.md | 72 ++++++++++++++++++++++++++++++++++++++++ packages/cli/src/args.ts | 4 ++- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 docs/configuration.md diff --git a/README.md b/README.md index cac4e21..ebd5706 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,8 @@ Open the printed `https://…` link on your phone, paste the token (or use the ` `node packages/cli/dist/index.js install` writes a per-user service unit (**macOS** LaunchAgent / **Linux** `systemd --user`) and prints the one command to enable it — nothing auto-starts until you opt in. It runs as **you**, not root. On macOS it runs while you're logged in (Claude's subscription auth needs a real login session). +The common variables (full reference, every var verified against the code → [docs/configuration.md](docs/configuration.md)): + | Var | Default | Purpose | |---|---|---| | `PORT` | `4280` | Listen port (`0` = OS-chosen). | @@ -194,13 +196,13 @@ Open the printed `https://…` link on your phone, paste the token (or use the ` | `MAX_UPLOAD_BYTES` | `26214400` | Upload size cap (25 MiB). | | `ROAMCODE_DATA_DIR` | `~/.config/roamcode`¹ | SQLite DB, token, VAPID keys, **logs** (mode 0700). | | `ROAMCODE_PUBLIC_URL` | _(bind URL)_ | Your user-facing origin (the tunnel URL). **Set this** behind a tunnel: it's the click-target for push notifications and an allowed Origin. | -| `TRUST_PROXY` | `false` | `1`/`true` = honor `X-Forwarded-For` behind a reverse proxy, so the per-client lockout/rate-limit key on the real client IP (not the proxy's). | +| `TRUST_PROXY` | _(off)_ | Honor `X-Forwarded-For` behind a reverse proxy, so the per-client lockout/rate-limit key on the real client IP (not the proxy's). Prefer a specific proxy IP/CIDR (e.g. `127.0.0.1`) over `1`/`true`, which trusts every hop and is spoofable. | | `ROAMCODE_ALLOWED_ORIGINS` | _(empty)_ | Comma-separated extra Origins the CSWSH guard allows (beyond same-origin/loopback/`PUBLIC_URL`). | | `ROAMCODE_RATE_LIMIT_RPM` | `600` | Sustained requests/minute per client. `0` **disables** the limiter. | | `ROAMCODE_RATE_LIMIT_BURST` | `120` | Instantaneous burst allowance (token-bucket). | | `ROAMCODE_MAX_SESSIONS` | `25` | Max concurrent **live** `claude` sessions; new spawns get `429` at the cap. `0` = unbounded. | | `CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI to spawn (must be on the service's PATH). | -| `VAPID_SUBJECT` | `mailto:roamcode@localhost` | `mailto:`/URL contact in the Web Push VAPID claim. | +| `ROAMCODE_VAPID_SUBJECT` | `mailto:roamcode@localhost` | `mailto:`/URL contact in the Web Push VAPID claim. | | `WEB_DIR` | _(bundled)_ | Override the path to the built PWA (`packages/web/dist`). | | `XDG_CONFIG_HOME` | _(unset)_ | When `ROAMCODE_DATA_DIR` is unset, the data dir is `$XDG_CONFIG_HOME/roamcode`. | | `ROAMCODE_SERVICE_MANAGER` / `_LABEL` | _(auto)_ | Override which service the OTA self-updater restarts (`launchd`/`systemd` + label). Normally read from `service.json`. | diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..4745e24 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,72 @@ +# Configuration reference + +Every environment variable RoamCode reads, with its default and effect — **verified against the code** +(the source file for each row is linked). The short table in the [README](../README.md) covers the +common ones; this page is the complete list. + +Flags on the `roamcode` CLI map onto the first three core vars: `--port` → `PORT`, `--bind` → +`BIND_ADDRESS`, `--no-token` → `NO_TOKEN=1` ([`packages/cli/src/index.ts`](../packages/cli/src/index.ts)). + +> **Integer parsing rule** (applies to `PORT`, `MAX_UPLOAD_BYTES`, `SESSION_IDLE_TTL_MS`, and the +> `ROAMCODE_RATE_LIMIT_*` / `ROAMCODE_MAX_SESSIONS` vars): an **absent or unparseable** value quietly +> falls back to the default, but a parseable value **outside the allowed range is a boot error** — +> the server refuses to start rather than running with a config you didn't intend +> ([`server-config.ts`](../packages/server/src/server-config.ts)). + +## Core + +| Var | Default | Effect | Source | +| --------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `PORT` | `4280` | TCP listen port. `0` = let the OS pick a free port. Range `0–65535`; out of range refuses to start. | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `BIND_ADDRESS` | `127.0.0.1` | Address to bind. A **non-loopback** bind with no access token **refuses to start** (spec §9) — put a tunnel in front instead of binding wide. | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `ACCESS_TOKEN` | _(generated)_ | Use this token verbatim instead of the persisted/generated one. Wins over `/token` and is **never written to disk**. Without it, a token is generated on first run (32-byte CSPRNG, base64url), printed once, and persisted to `/token` (mode 0600). | [`data-dir.ts`](../packages/server/src/data-dir.ts) | +| `NO_TOKEN` | _(unset)_ | Exactly `1` = tokenless dev mode: no token generated, stored, or required. **Loopback binds only** — combined with a non-loopback `BIND_ADDRESS` the server refuses to start. | [`start.ts`](../packages/server/src/start.ts) | +| `TRUST_PROXY` | _(off)_ | Trust `X-Forwarded-*` so lockout/rate-limit keys on the real client IP behind a reverse proxy. **Preferred form: a specific IP/CIDR (comma-list allowed)**, e.g. `127.0.0.1` for a same-host cloudflared/Caddy — trusts only that hop. `1`/`true` trusts **every** hop (spoofable: a client can prepend to XFF). Anything else (`0`, `false`, unset) = off. | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `FS_ROOT` | `$HOME` (else cwd) | Root the file picker / fs endpoints (browse, upload, download, mkdir, search) are confined to. **Does not sandbox the `claude` agent itself** — see the README's Security section. | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `MAX_UPLOAD_BYTES` | `26214400` (25 MiB) | Upload size cap. Minimum `1`. | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `SESSION_IDLE_TTL_MS` | `0` (disabled) | Opt-in reaper for detached sessions: kill running terminals with **no attached client** idle longer than this many ms. `0` keeps the default behavior — detached sessions survive indefinitely for later reattach. When enabled, the sweep runs every `min(max(TTL, 30s), 5min)`. | [`server-config.ts`](../packages/server/src/server-config.ts) · [`transport.ts`](../packages/server/src/transport.ts) | + +## `ROAMCODE_*` + +Every variable in this table also accepts its legacy `REMOTE_CODER_*` name (pre-rename installs keep +working across an OTA update without touching their service env); the `ROAMCODE_*` name wins when both +are set ([`server-config.ts`](../packages/server/src/server-config.ts), +[`data-dir.ts`](../packages/server/src/data-dir.ts), [`updater.ts`](../packages/server/src/updater.ts)). + +| Var | Default | Effect | Source | +| ------------------------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| `ROAMCODE_DATA_DIR` | `~/.config/roamcode` ¹ | Host data dir (mode 0700) for the SQLite DBs, access token, VAPID keys, `service.json`, per-session MCP/hook files, and service logs. | [`data-dir.ts`](../packages/server/src/data-dir.ts) | +| `ROAMCODE_PUBLIC_URL` | _(unset)_ | Your user-facing origin (the tunnel URL). Allow-listed by the Origin/CSWSH guard and used as the click-target for push notifications. **Set this when serving behind a tunnel.** | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `ROAMCODE_ALLOWED_ORIGINS` | _(empty)_ | Comma-separated **extra** Origins the CSWSH guard allows, beyond same-origin / loopback / `ROAMCODE_PUBLIC_URL`. | [`origin-check.ts`](../packages/server/src/origin-check.ts) | +| `ROAMCODE_RATE_LIMIT_RPM` | `600` | Sustained requests/minute per client (token bucket). `0` **disables** the limiter entirely. | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `ROAMCODE_RATE_LIMIT_BURST` | `120` | Instantaneous burst allowance. Minimum `1` (a 0-size bucket would block everything). | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `ROAMCODE_MAX_SESSIONS` | `25` | Max concurrent **live** `claude` sessions; `POST /sessions` gets `429` at the cap. `0` = unbounded. | [`server-config.ts`](../packages/server/src/server-config.ts) | +| `ROAMCODE_VAPID_SUBJECT` | `mailto:roamcode@localhost` | `mailto:`/`https:` contact in the Web Push VAPID claim (web-push requires one). An invalid subject **disables push** with a boot warning rather than killing the server. | [`start.ts`](../packages/server/src/start.ts) | +| `ROAMCODE_SERVICE_MANAGER` / `ROAMCODE_SERVICE_LABEL` | _(auto)_ | Which service the OTA self-updater restarts (`launchd`/`systemd` + unit label). Resolution order: `/service.json` (written by `roamcode install`) → these env vars → a platform default. | [`updater.ts`](../packages/server/src/updater.ts) | + +¹ Full resolution order: `ROAMCODE_DATA_DIR` → `REMOTE_CODER_DATA_DIR` → `$XDG_CONFIG_HOME/roamcode` → +`~/.config/roamcode` → `./.roamcode` — and at each default location, an **existing** pre-rename +`remote-coder` directory is preferred over creating a fresh `roamcode` one, so upgraded installs keep +their token, `service.json`, and session index ([`data-dir.ts`](../packages/server/src/data-dir.ts)). + +## Advanced / internal + +| Var | Default | Effect | Source | +| ----------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| `CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI to spawn (must resolve on the **service's** PATH — the installed unit prepends the node dir and common tool locations for exactly this reason). | [`config.ts`](../packages/server/src/config.ts) | +| `WEB_DIR` | _(bundled)_ | Override the directory the built PWA is served from (default: the repo's `packages/web/dist`). A path that doesn't exist just disables static serving — the API still runs. | [`start.ts`](../packages/server/src/start.ts) | +| `RC_TMUX_SOCKET` | `remote-coder` | The dedicated tmux server socket that isolates RoamCode's sessions from your own tmux. Override so a **second** instance (tests, a verification server) gets its own socket and can't reap the primary's sessions. The default keeps the pre-rename name **on purpose** — live sessions exist on that socket and an OTA restart must find them again. | [`terminal-process.ts`](../packages/server/src/terminal-process.ts) | +| `XDG_CONFIG_HOME` | _(unset)_ | When `ROAMCODE_DATA_DIR` is unset, the data dir resolves under `$XDG_CONFIG_HOME/roamcode` (see the resolution order above). | [`data-dir.ts`](../packages/server/src/data-dir.ts) | +| `ROAMCODE_DIR` | `~/roamcode` ² | **Installer only** (`scripts/install.sh`): where the repo is cloned/updated. | [`install.sh`](../scripts/install.sh) | +| `RC_NO_START` | _(unset)_ | **Installer only**: `1` = clone, install, and build, but don't start the server. | [`install.sh`](../scripts/install.sh) | + +² The installer also honors the legacy `REMOTE_CODER_DIR`, and prefers an existing `~/remote-coder` +checkout over cloning a duplicate. `PORT` is respected by the installer too when it starts the server. + +## Not configurable (by design) + +- The **access token never enters argv** — it is passed to per-session MCP/hook helpers via mode-0600 + files, never on a command line ([`config.ts`](../packages/server/src/config.ts)). +- `ANTHROPIC_API_KEY` is always **stripped** from the spawned `claude` (subscription auth only). +- The token-rotation grace window (old token honored briefly after `POST /token/rotate`) is a fixed + **60 s**. diff --git a/packages/cli/src/args.ts b/packages/cli/src/args.ts index 7c913f4..d2e8903 100644 --- a/packages/cli/src/args.ts +++ b/packages/cli/src/args.ts @@ -72,9 +72,11 @@ export function helpText(): string { " ACCESS_TOKEN Use this token instead of the generated/persisted one.", " FS_ROOT Root dir the file picker is confined to (default $HOME).", " ROAMCODE_DATA_DIR Where the SQLite DBs + access token are stored.", - " VAPID_SUBJECT mailto:/https: subject for Web Push (default mailto:roamcode@localhost).", + " ROAMCODE_VAPID_SUBJECT mailto:/https: subject for Web Push (default mailto:roamcode@localhost).", " WEB_DIR Override the served PWA dir (default the built packages/web/dist).", "", + "Full reference (every variable, verified against the code): docs/configuration.md", + "", "On first run an access token is generated, stored in the data dir, and printed ONCE with", "the open URL. For remote access, put it behind an HTTPS tunnel (Cloudflare Tunnel / Tailscale)", "— Web Push and the installable PWA require a secure context. See the README.",