Worker: interactive terminal WebSocket bridge (PTY in workspace container)

- attachTerminalServer() upgrades /jobs/:id/terminal WS connections, verifying a
  short-lived job-scoped HMAC token (verifyTerminalToken) so the browser never
  holds the worker secret
- Bridges the socket to a bash PTY via dockerode exec (Tty) in a persistent
  per-job shell container (spoon-agent-term-<id>) mounting the workspace; binary
  frames = stdin, JSON text frames = resize; idle containers reaped after 30m
- New env: SPOON_AGENT_TERMINAL_IMAGE/SECRET/IDLE_MS (secret falls back to the
  shared worker internal token)
This commit is contained in:
Gabriel Brown
2026-06-24 08:16:39 -04:00
parent 1072cf10cd
commit c1263b2e69
10 changed files with 388 additions and 7 deletions
+13
View File
@@ -32,6 +32,19 @@ export const env = {
: 'network',
jobImage:
process.env.SPOON_AGENT_JOB_IMAGE?.trim() ?? 'spoon-agent-job:latest',
// Interactive terminal: image for the persistent shell container (defaults to
// the job image), the secret shared with the Next app for verifying terminal
// tokens, and how long an idle terminal container survives before cleanup.
terminalImage:
process.env.SPOON_AGENT_TERMINAL_IMAGE?.trim() ??
process.env.SPOON_AGENT_JOB_IMAGE?.trim() ??
'spoon-agent-job:latest',
terminalSecret:
process.env.SPOON_AGENT_TERMINAL_SECRET?.trim() ??
process.env.SPOON_AGENT_WORKER_INTERNAL_TOKEN?.trim() ??
process.env.SPOON_WORKER_TOKEN?.trim() ??
'',
terminalIdleMs: intEnv('SPOON_AGENT_TERMINAL_IDLE_MS', 1_800_000),
workdir: process.env.SPOON_AGENT_WORKDIR?.trim() ?? '.local/agent-work',
hostWorkdir: process.env.SPOON_AGENT_HOST_WORKDIR?.trim(),
network: process.env.SPOON_AGENT_NETWORK?.trim(),