Commit Graph
158 Commits
Author SHA1 Message Date
Gabriel Brown ea56b29453 refactor(web): extract reusable XtermSession from WorkspaceTerminal 2026-07-11 12:43:08 -04:00
Gabriel Brown 27019dc1d3 feat(web): /api/box status, lifecycle, tree, file, terminal-token routes 2026-07-11 12:36:56 -04:00
Gabriel Brown 11e93ade5f feat(web): box terminal token mint + ownership proxy helpers 2026-07-11 12:32:10 -04:00
Gabriel Brown 2c6a605646 feat(worker): user-scoped /box/terminal websocket bridge
Add a USER-scoped `/box/terminal` WebSocket that acquires the user's box
and opens a login shell rooted at `~`. Extract the shared real-TTY PTY
bridging (buffered input, dockerode exec/start, resize, forward, cleanup)
out of the job `bridge()` into `runShellBridge`, parameterized by how the
box is acquired, the working directory, and the env; `bridge()` and the
new `bridgeBox()` both call it so the job path is unchanged.

The `/box/terminal` upgrade reads the username FROM the token
(`parseBoxTokenUsername`, part[2] of the 4-part token), then verifies the
whole token with `verifyBoxTerminalToken`; on success `bridgeBox` derives
`boxHomePaths(username)`, seeds a minimal `.bash_profile` via a shared
`ensureBashProfile` (extracted from `materializeUserHome`), acquires the
box, and opens the shell at `containerHome` with `TERM`+`HOME` only (no
per-job secrets). Cleanup releases the box handle.

Manual verification (dockerode PTY I/O is daemon-dependent):
- `bun run test:unit` 105 passed (incl. new `parseBoxTokenUsername` tests),
  `bun run typecheck` clean.
- Raw upgrade against a live server: valid box token -> `101 Switching
  Protocols`; bad token -> refused (no upgrade). Job `/jobs/:id/terminal`
  branch unchanged.
- `bridgeBox` path exercised: `acquireUserBox` created `spoon-box-<user>`
  and `ensureBashProfile` seeded `~/.bash_profile`. Interactive typing/
  resize could not be observed on this host because dockerode cannot reach
  the podman API socket (same mechanism the job terminal uses, so no
  regression) — full browser flow is covered by later tasks.

Claude-Session: https://claude.ai/code/session_01ScyZ1NhfN84LAnHpwhfEQk
2026-07-11 12:25:32 -04:00
Gabriel Brown 192f547be5 feat(worker): /box status, lifecycle, tree, and file HTTP routes
Wire the box helpers into the worker HTTP server as /box/* routes,
authed by the internal bearer token like every other worker route.
Adds a boxRoute matcher (/^\/box\/(status|lifecycle|tree|file)$/) and
dispatches before jobRoute; missing user -> 400, unknown lifecycle
action -> 400, containment throws -> 400 (server keeps serving).

Manual verification (harness on :3922, internal token, user gabriel):
  GET  /box/status?user=gabriel        -> 200 {"status":{...running:false...}}
  GET  /box/status  (no user)          -> 400 {"error":"Missing user"}
  (no Authorization header)            -> 401
  POST /box/lifecycle {"action":"bogus"} -> 400 {"error":"Unknown action"}
  GET  /box/tree?user=gabriel          -> 200 {"tree":{"name":"~",...}}
  PUT  /box/file {"path":"spoon-test.txt","content":"hi from task4"} -> 200 {"success":true}
  GET  /box/file?path=spoon-test.txt   -> 200 {"path":...,"content":"hi from task4"}
  GET  /box/file?path=../../../../../etc/passwd -> 400 {"error":"Refusing to access path outside home: ..."}
  GET  /box/status after escape        -> 200 (no crash)
  GET  /box/other?user=gabriel         -> 404 {"error":"Not found"}
2026-07-11 12:13:50 -04:00
Gabriel Brown bf0276b1f3 feat(worker): home-scoped box file tree/read/write with realpath containment 2026-07-11 12:05:04 -04:00
Gabriel Brown 6439e5200b feat(worker): per-user box status and lifecycle helpers
Centralize per-user box home-path derivation, status inspection, and
lifecycle (start/stop/restart) in a new box.ts so the HTTP routes and the
terminal WS bridge share one source of truth. Add inspectUserBoxStatus to
docker.ts (single `inspect --format` call, all-null/false on non-zero exit)
and resetBox to user-container.ts (clears the idle timer and drops the
registry entry so a stopped box isn't treated as held).

Manual verification (docker required, after Task 4 wires the routes): with
the worker running and a box up,
`curl -H "Authorization: Bearer $TOKEN" localhost:3921/box/status?user=<you>`
shows running:true + image + startedAt; `POST /box/lifecycle {"action":"stop"}`
flips status to running:false; `start` brings it back.
2026-07-11 11:57:57 -04:00
Gabriel Brown d4303e652e feat(worker): user-scoped box terminal token verification 2026-07-11 11:52:26 -04:00
Gabriel Brown 432e3b501e fix(worker): don't double-append streamed answer when completed event carries content 2026-07-11 11:47:11 -04:00
Gabriel Brown 2311679802 fix(worker): chmod materialized env file to 0600 even when it pre-exists 2026-07-11 11:36:03 -04:00
Gabriel Brown 58c815a7a7 fix(worker): materialize env files 0600, re-materialize per run, delete on stop 2026-07-11 11:36:03 -04:00
Gabriel Brown 424307c5af build(agent-job): pin Claude Code CLI; smoke one turn per runtime
Add @anthropic-ai/claude-code@2.1.207 to the job image global npm install,
extend the smoke script with claude --version plus a documented per-runtime
turn block, and reconcile OpenCode/Claude adapter flags against the real CLIs
(all assumed flags confirmed). Document the anthropic_oauth_json credential
snapshot pattern in the README.
2026-07-11 11:25:44 -04:00
Gabriel Brown 702e53a4ba feat(web): per-thread/per-spoon runtime picker and Anthropic OAuth provider profile 2026-07-11 11:20:33 -04:00
Gabriel Brown 6ec2e51312 feat(backend): migration to retire openai_direct runtime 2026-07-11 11:05:12 -04:00
Gabriel Brown 0716a224ef fix(worker): codex prepareAuth skips auth.json for api_key profiles 2026-07-11 11:00:08 -04:00
Gabriel Brown c0f107c4cf feat(backend): queue-time runtime/profile validation; job.runtime authoritative in worker 2026-07-11 10:58:05 -04:00
Gabriel Brown 70396feccc feat(backend): add claude runtime, anthropic OAuth auth kind, and profile→runtime derivation 2026-07-11 10:42:48 -04:00
Gabriel Brown 76886621b5 feat(worker): ClaudeCodeAdapter runs claude -p stream-json in the per-user box 2026-07-11 10:35:56 -04:00
Gabriel Brown c9ee2e6cde refactor(worker): delete per-job OpenCode container path, host_port split, and dead docker helpers
Claude-Session: https://claude.ai/code/session_01ScyZ1NhfN84LAnHpwhfEQk
2026-07-11 10:26:58 -04:00
Gabriel Brown ff0425cf4b feat(worker): OpenCodeAdapter runs opencode in the per-user box (no side container) 2026-07-11 10:07:58 -04:00
Gabriel Brown 949a036d13 fix(worker): surface nonzero-exit agent turns as failed even when text streamed 2026-07-11 10:07:14 -04:00
Gabriel Brown e5bba720b4 refactor(worker): move Codex turn logic into CodexAdapter behind AgentRuntime 2026-07-11 02:53:26 -04:00
Gabriel Brown c1e816741d fix(worker): marked-command survives tail-exec; kill script excludes itself 2026-07-10 18:53:00 -04:00
Gabriel Brown bab87cc2d0 feat(worker): in-box process-group marker + kill helpers for agent turns 2026-07-10 18:53:00 -04:00
Gabriel Brown 613a6c8a2d fix(agent-jobs): narrow recovery-cron guard so abandoned stops still recover 2026-07-10 18:34:15 -04:00
Gabriel Brown 38431155b2 refactor(worker): extract AgentRuntime interface, shared provider helpers, and adapter registry 2026-07-10 18:28:41 -04:00
Gabriel Brown 697fb7286d fix(agent-jobs): recovery cron skips already-terminal jobs 2026-07-10 18:27:53 -04:00
Gabriel Brown 60dd5d3836 fix(agent-jobs): stop heartbeat from resurrecting terminated jobs 2026-07-10 18:13:28 -04:00
Gabriel Brown ac68484059 fix(worker): make cancellation teardown failure-safe 2026-07-10 18:04:40 -04:00
Gabriel Brown 470af043fa fix(agent-jobs): add heartbeat cancel and stale recovery 2026-07-10 18:03:45 -04:00
Gabriel Brown cbbb168e7c docs: add implementation handoff for Claude 2026-07-10 17:54:06 -04:00
Gabriel Brown 8af33fc9e0 fix(worker): always release stopped workspaces 2026-07-10 17:35:33 -04:00
Gabriel Brown 00f96a126e fix(agent-jobs): close maintenance jobs after decision 2026-07-10 17:35:33 -04:00
Gabriel Brown 1e1435e656 fix(agent-jobs): recover failed claim decryption 2026-07-10 17:26:17 -04:00
Gabriel Brown cf45404b7d fix(agent-jobs): validate claims before claiming 2026-07-10 17:26:17 -04:00
Gabriel Brown 926008fd3a fix(workspace): skip queries for missing jobs 2026-07-10 17:24:32 -04:00
Gabriel Brown 2beeeb1732 fix(app): handle nullable detail consumers 2026-07-10 17:24:32 -04:00
Gabriel Brown ed21126eea fix(app): add friendly not-found handling 2026-07-10 17:24:32 -04:00
Gabriel Brown c4d0f5219a fix(terminal): use real TTY exec and resize 2026-07-10 17:21:52 -04:00
Gabriel Brown 906f906ad3 fix(backend): use generated import extension 2026-07-10 17:15:34 -04:00
Gabriel Brown bdb2964e34 fix(sync): honor per-spoon auto-sync settings 2026-07-10 17:15:20 -04:00
Gabriel Brown 57c175aecd fix(worker): contain workspace paths against symlinks 2026-07-10 17:11:00 -04:00
Gabriel Brown 2bc090c378 chore: remove terminal task scratch report 2026-07-10 17:06:44 -04:00
Gabriel Brown d4a3856083 fix(workspace): key shell by job 2026-07-10 17:06:33 -04:00
Gabriel Brown 020ffbfb23 fix(worker-auth): unify token checks and bind job environment 2026-07-10 17:06:33 -04:00
Gabriel Brown feb3723753 fix(terminal): preserve animation-frame first fit 2026-07-10 17:06:33 -04:00
Gabriel Brown 75d06394ce fix(terminal): refit after fonts load 2026-07-10 17:06:33 -04:00
Gabriel Brown 0b5a2c0411 fix(worker): ensure reconciled boxes before acquire 2026-07-10 17:04:41 -04:00
Gabriel Brown 284f7ea52b fix(worker): serialize box failure and idle reaping 2026-07-10 17:00:22 -04:00
Gabriel Brown 490c05329b chore: remove implementation scratch report 2026-07-10 16:56:07 -04:00