# Workspace 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.

## 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=…
   ▼
nginx ── upgrade ──► spoon-agent-worker :3921
                        │  verifyTerminalToken(token, jobId, secret)
                        │  dockerode exec -t  →  bash/tmux PTY
                        ▼
                     spoon-agent-term-<jobId>  (job image, mounts the workspace)
```

- 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.
- 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).

## Configuration

| Where    | Variable                                | Required? | Notes                                                                                                                                                                                                                                                                                                                                                        |
| -------- | --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 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`).                                                                                                                                                                                                                                                                                                               |

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
worker over nginx (prod).

## Exposing the worker (prod, nginx)

The worker and nginx are on the same `nginx-bridge` network, so nginx can reach
`spoon-agent-worker:3921` directly — no published port needed. Add a server block
that upgrades WebSockets:

```nginx
server {
  listen 443 ssl;
  server_name worker.spoon.gbrown.org;
  # ssl_certificate ... ; ssl_certificate_key ... ;

  location / {
    proxy_pass http://spoon-agent-worker:3921;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 86400s;   # keep idle terminals open
    proxy_send_timeout 86400s;
  }
}
```

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.

## Dev testing (no nginx)

The dev worker runs on the host at `localhost:3921` (`bun dev:next:worker`), so
the browser can hit it directly:

```
NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL=ws://localhost:3921
```

Note: the terminal uses **dockerode** against the Docker socket. In dev with
Podman, point it at the Podman socket (run `podman system service` and set
`DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock`), or run the worker in
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
  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.
