docs: rewrite for box-first model + webhooks + notifications
This commit is contained in:
+44
-30
@@ -1,34 +1,44 @@
|
||||
# Workspace interactive terminal
|
||||
# Interactive terminal
|
||||
|
||||
A real shell inside the agent workspace, shown as the **Terminal** tab in the
|
||||
workspace UI. It's an xterm.js front end bridged to a bash/tmux PTY running in a
|
||||
persistent per-job container (the agent job image), mounting the same workspace
|
||||
the editor and agent use.
|
||||
A real shell inside the user's persistent box (`spoon-box-{username}`). There are
|
||||
two entry points, both bridging to the **same** box:
|
||||
|
||||
- The workspace **Terminal** tab — a shell opened at the thread's checkout
|
||||
(`~/Code/{spoon}/{branch}`).
|
||||
- The **My Machine** page (`/machine`) — a `~`-rooted shell into the same box.
|
||||
|
||||
Both are an xterm.js front end bridged to a bash/tmux PTY via `docker exec` into
|
||||
the box. There is no per-job `spoon-agent-term-*` container anymore — the box is
|
||||
long-running and shared by the agent, the editor, and both terminals.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
browser (xterm.js)
|
||||
│ 1. GET /api/agent-jobs/:id/terminal-token (Convex-auth'd, owner only)
|
||||
│ → { url: "wss://worker…/jobs/:id/terminal?token=…", expiresAt }
|
||||
│
|
||||
│ 2. WebSocket wss://worker.<domain>/jobs/:id/terminal?token=…
|
||||
│ 1a. GET /api/agent-jobs/:id/terminal-token (Convex-auth'd, owner only)
|
||||
│ → { url: "wss://worker…/jobs/:id/terminal?token=…", expiresAt }
|
||||
│ 1b. GET /api/box/terminal-token (Convex-auth'd; username
|
||||
│ → { url: "wss://worker…/box/terminal?token=…", expiresAt } derived
|
||||
│ server-side)
|
||||
│ 2. WebSocket wss://worker.<domain>/{jobs/:id|box}/terminal?token=…
|
||||
▼
|
||||
nginx ── upgrade ──► spoon-agent-worker :3921
|
||||
│ verifyTerminalToken(token, jobId, secret)
|
||||
│ dockerode exec -t → bash/tmux PTY
|
||||
│ verify HMAC token (job-scoped or username-scoped)
|
||||
│ docker exec -it → bash/tmux PTY
|
||||
▼
|
||||
spoon-agent-term-<jobId> (job image, mounts the workspace)
|
||||
spoon-box-{username} (Fedora box; persistent home mounted)
|
||||
```
|
||||
|
||||
- The browser **never** holds the worker secret. The Next app (which has already
|
||||
verified job ownership) mints a short-lived HMAC token; the worker verifies it.
|
||||
- The browser **never** holds the worker secret. The Next app mints a short-lived
|
||||
HMAC token; the worker verifies it. The job terminal token is minted only after
|
||||
a Convex ownership check; the box terminal token carries the username derived
|
||||
**server-side** from the authed user (never from the request).
|
||||
- Frames: **binary** = stdin/stdout bytes; **text JSON** `{type:"resize",cols,rows}`
|
||||
= resize. The token's 2-minute expiry is a _connect_ window; an established
|
||||
session persists.
|
||||
- The shell runs `tmux new-session -A -s spoon` (falls back to `bash -l`), so
|
||||
reconnecting reattaches the same session. Idle containers are removed after
|
||||
`SPOON_AGENT_TERMINAL_IDLE_MS` (default 30m).
|
||||
- The shell prefers `tmux new-session -A -s spoon` (falls back to `bash -l`), so
|
||||
reconnecting reattaches the same session. The box itself is idle-reaped after
|
||||
`SPOON_AGENT_BOX_IDLE_MS` (default 30m) once no job or terminal holds it.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -37,8 +47,7 @@ nginx ── upgrade ──► spoon-agent-worker :3921
|
||||
| Next app | `NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL` | **Yes** | Browser-facing worker WS base, e.g. `wss://worker.spoon.gbrown.org` (prod) or `ws://localhost:3921` (dev). **Build-time** (`NEXT_PUBLIC`): for the Docker image it must be passed as a build arg (wired in `docker/Dockerfile` + `docker/compose.yml`, sourced from the build env file), not a runtime env. Unset → the Terminal tab shows "not configured". |
|
||||
| Next app | `SPOON_AGENT_TERMINAL_SECRET` | No | HMAC secret for signing tokens. Falls back to `SPOON_AGENT_WORKER_INTERNAL_TOKEN`. |
|
||||
| Worker | `SPOON_AGENT_TERMINAL_SECRET` | No | Must match the Next app's. Falls back to `SPOON_AGENT_WORKER_INTERNAL_TOKEN` (already shared), so by default **no new secret is needed**. |
|
||||
| Worker | `SPOON_AGENT_TERMINAL_IMAGE` | No | Shell container image. Defaults to `SPOON_AGENT_JOB_IMAGE`. |
|
||||
| Worker | `SPOON_AGENT_TERMINAL_IDLE_MS` | No | Idle-container reap delay (default `1800000`). |
|
||||
| Worker | `SPOON_AGENT_BOX_IDLE_MS` | No | Idle-box reap delay, shared with agent jobs (default `1800000`). The terminal holds the box open while connected. |
|
||||
|
||||
Because the secret defaults to the already-shared worker token, the **only**
|
||||
required step is setting `NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL` and exposing the
|
||||
@@ -70,9 +79,10 @@ server {
|
||||
|
||||
Then set on the Next app: `NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL=wss://worker.spoon.gbrown.org`.
|
||||
|
||||
> The worker's HTTP routes (`/jobs/:id/tree` etc.) require the internal bearer
|
||||
> token, so exposing the worker host only usefully exposes the token-gated
|
||||
> `/jobs/:id/terminal` upgrade. Still, restrict the server block to TLS.
|
||||
> The worker's HTTP routes (`/jobs/:id/tree`, `/box/*`, etc.) require the
|
||||
> internal bearer token, so exposing the worker host only usefully exposes the
|
||||
> token-gated `/jobs/:id/terminal` and `/box/terminal` upgrades. Still, restrict
|
||||
> the server block to TLS.
|
||||
|
||||
## Dev testing (no nginx)
|
||||
|
||||
@@ -90,15 +100,19 @@ Docker mode. Prod (Docker socket mounted) works as-is.
|
||||
|
||||
## Security
|
||||
|
||||
- Owner-only: the token route uses Convex auth + `assertOwned`.
|
||||
- Tokens are short-lived (2m connect window), job-scoped, HMAC-signed.
|
||||
- A shell in the workspace can reach the network and the repo's git credentials.
|
||||
This is intended for the single-user self-hosted deployment; do not expose the
|
||||
- Owner-only: the job token route uses Convex auth + ownership assertion; the
|
||||
box token derives the username server-side from the authed user.
|
||||
- Tokens are short-lived (2m connect window), scoped (job-scoped or
|
||||
username-scoped), and HMAC-signed.
|
||||
- A shell in the box can reach the network and the repo's git credentials. This
|
||||
is intended for the single-user self-hosted deployment; do not expose the
|
||||
worker domain without TLS, and keep the deployment single-tenant.
|
||||
|
||||
## Tools in the shell
|
||||
|
||||
The job image ships `bash`, `tmux`, `neovim`, `git`, `ripgrep`, `jq`, `python3`,
|
||||
`node`, `bun`, `pnpm`, `yarn`, `curl`/`wget`, `unzip`. Bring your own dotfiles by
|
||||
cloning them in-session (e.g. `git clone <dotfiles> ~/.config/...`); persistent
|
||||
auto-cloning of a dotfiles repo is a planned follow-up.
|
||||
The box image ships `bash`, `tmux`, `neovim`, `git`, `ripgrep`, `jq`, `python3`,
|
||||
`node`, `bun`, `pnpm`, `yarn`, `curl`/`wget`, `unzip`, plus QoL tooling (`fzf`,
|
||||
`fd`, `bat`, `eza`, `zoxide`, `gh`, `gum`, `oh-my-posh`) and the agent CLIs
|
||||
(`codex`, `opencode`, `claude`). Personalize the shell in **Settings → Dotfiles**
|
||||
(overlay files + an optional dotfiles repo), applied to the persistent home; see
|
||||
[dotfiles.md](dotfiles.md).
|
||||
|
||||
Reference in New Issue
Block a user