feat: wire the BitMind gateway to the container supervisor - #5
feat: wire the BitMind gateway to the container supervisor#5IshmaelRogers wants to merge 3 commits into
Conversation
Closes the gap between CopilotKit#20's two proven pieces: the gateway relays runs but always attested isolated_computers: false, and supervisor can create a real isolated computer but the gateway had no idea it existed. Neither was wrong on its own, but together they meant BitMind's activation gate could never legitimately turn on — there was no path from "a supervisor is running" to "the gateway says so." Adds: - BITMIND_SUPERVISOR_URL / BITMIND_SUPERVISOR_TOKEN, set together or not at all (same half-configured-fails-at-boot rule as every other pair in this config). Absent, behavior is unchanged from before a supervisor existed. - attestation() is now async and checks supervisor /health on every call (unauthenticated, matching the supervisor's own design) rather than caching a value that could go stale exactly when it matters most. isolated_computers is true only while a configured supervisor answers. - POST /bitmind/v1/computer/{agent_id}/ensure, proxying to the supervisor's own ensure verb. The response is a small named subset (status, started_at) — never the container name, the internal container-DNS url, or supervisor's raw identity/error text, none of which BitMind can use and none of which belongs on the wire to a different trust domain. The supervisor's own status code passes through on refusal (400/409/503 carry real meaning); its body does not. Deliberately not done here: control transfer and live frames. `agent-computer` already has both as real features (`/control/take`+`/control/release`, `/stream`'s CDP screencast) — relaying a live frame stream or a control-transfer session through this gateway is its own design question (does it proxy a websocket? poll? does the gateway hold state across a transfer?), not a small extension of the ensure route. Tracked as follow-up, not silently dropped. Tests: 3 new attestation cases (reachable, unreachable, unwell), 5 new computer-route cases (unconfigured, successful proxy + response trimming, status passthrough on refusal, unreachable supervisor, auth requirement), 2 new config cases (paired validation, credential/scheme rejection). Gate: biome format/lint clean, server typecheck clean, full `bun test server/tests --timeout 30000` → 1708 pass / 0 fail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The first commit on this branch built BITMIND_SUPERVISOR_URL/TOKEN and a
bespoke fetch-to-supervisor client from scratch. Partway through adding
control-transfer and screenshot routes, found that server/src/computer/
already has exactly this: ComputerProvider (createDockerSupervisorProvider
talks to the identical supervisor, over the identical /computers/{id}/ensure
verb this branch had reimplemented), ComputerGateway (locate/status/
screenshot/takeControl/releaseControl, with address validation, audit
trail, and error mapping none of which the first commit had), and
COMPUTER_SUPERVISOR_URL/SUPERVISOR_TOKEN/COMPUTER_TOKEN — config the main
server (index.ts) already parses and already builds a ComputerGateway from
today, for the product's own UI.
Reworked to depend on that instance instead of building a second one:
- BITMIND_SUPERVISOR_URL/TOKEN removed. BitMind's gateway now takes an
optional ComputerGateway, threaded through from index.ts's own
already-constructed one (undefined when no computer provider is
configured — unchanged deployment story for everyone who isn't running
BitMind).
- attestation()'s isolated_computers checks computerGateway.provider.list()
reachability, same as before, just against the real seam instead of a
bespoke unauthenticated /health probe.
- POST .../computer/{agent_id}/ensure now calls locate() (which validates
the resolved address, not just fetches it) then status().
- New: GET .../computer/{agent_id} (observe without starting), GET
.../computer/{agent_id}/screenshot (a snapshot, matching bit-bot#58's own
stated baseline of "a snapshot URL, not a video pipe" — streaming is
named there as a later upgrade, not required for this), POST
.../computer/{agent_id}/control {action:"take"|"release"} against
agent-computer's existing control-transfer state machine, with the actor
for its audit trail carried in x-bitmind-actor-id (BitMind's callers have
no row in this deployment's own `users` table).
Still deliberately not done: relaying agent-computer's live CDP frame
stream (`/stream`) through this gateway. That is a genuinely separate
design question (websocket relay vs. polling, whether state needs to
survive a control transfer) from reusing an existing HTTP seam, and
bit-bot#58's own scope treats it as a follow-up to the snapshot baseline,
not a blocker.
Tests rewritten around a fakeComputerGateway (every method throws unless
overridden, so a test exercising two methods cannot silently pass by
reaching a third) — same partial-fake-cast pattern computer-routes.test.ts
already uses for this exact interface. Net: 3 attestation cases, 9 computer
-route cases (unconfigured, auth, status, ensure, screenshot, take+release
with actor propagation asserted, missing actor, invalid action, generic
failure mapping), config tests for the removed supervisor pair deleted.
Gate: biome format/lint clean, server typecheck clean, full
`bun test server/tests --timeout 30000` → 1709 pass / 0 fail.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Reworked at So this now depends on that instance instead of building a second one:
Still deliberately not done: relaying the live CDP frame stream ( Tests rewritten around a Gate on 🤖 Generated with Claude Code |
`decodeURIComponent` throws on a path like `/bitmind/v1/computer/%`, and this fetch handler is the outermost frame, so an unguarded decode answered a bad path with an unhandled error instead of a status. It is not a route this gateway serves either way, so it now gets the same 404 as any other miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IshmaelRogers
left a comment
There was a problem hiding this comment.
Review (self-review — I wrote this branch, flagging that explicitly)
Reviewed adversarially and found one real defect, now fixed in 1f2b986.
Fixed: a malformed percent-escape crashed the handler. decodeURIComponent(agentId) was unguarded and this fetch handler is the outermost frame, so GET /bitmind/v1/computer/% threw a URIError out instead of answering. Confirmed against the pre-fix code:
THREW: URIError URI error
It is not a route this gateway serves, so it now answers 404 like any other miss, with a regression test covering %, %zz and a%2.
Checked and sound:
- Auth ordering. Only
/healthis exempt; the service-token check sits above every/bitmind/v1/*route including the four new computer ones. Verified by reading the dispatch order, not assumed. - Encoded slashes are contained.
%2Fdecodes to a/insideagentId, butcreateDockerSupervisorProviderinterpolates withencodeURIComponent(botId)on every verb, so it cannot traverse the supervisor's path. The supervisor validates bot ids of its own accord as well. isolated_computersis computed per request, never cached. A staletrueis the one thing that field must never say, since it is what turns BitMind's worker on.- Errors do not leak the seam. Every computer failure maps to a fixed 502 string; no
ComputerUnavailableError/supervisor message reaches a caller in another trust domain. - One gateway, not two.
index.tspasses the sameComputerGatewaythe product's own UI uses, so there is one supervisor, one audit trail, and no parallel notion of where a computer lives. - No secrets or hostnames in the diff — this is a public repo.
Gate on 1f2b986 (bun 1.3.14, real pgvector Postgres): format:check clean, lint clean, typecheck clean across all packages, bun test server/tests --timeout 30000 → 1710 pass, 0 fail across 114 files. An earlier run of the same suite showed 1708/2; the two were the host's documented timing-sensitive tests and passed on rerun, in files this branch does not touch.
Known gap, unchanged and deliberate: live frames (a websocket relay of agent-computer's CDP screencast) are not here. screenshot covers the polling baseline bit-bot#58 itself describes ("a snapshot URL, not a video pipe"); streaming is named there as a later upgrade.
Merging: this is what serves /bitmind/v1/computer/*, and bit-mind's merged CopilotKit#94/CopilotKit#107 surface calls it — until this lands, a deployment built from openbot main 502s every computer call.
Closes the gap between CopilotKit#20's two proven pieces (bit-mind CopilotKit#20, the rootless enclave): the gateway relays runs but always attested
isolated_computers: false, and supervisor (once #4 landed) can create a real isolated computer — but the gateway had no idea supervisor existed at all. Verified directly:isolated_computerswas a hardcodedfalseliteral ingateway.ts, not derived from anything. Together these meant BitMind's activation gate could never legitimately turn on.What this adds
BITMIND_SUPERVISOR_URL/BITMIND_SUPERVISOR_TOKEN, set together or not at all (the same half-configured-fails-at-boot rule as every other pair in this config). Absent, behavior is unchanged from before a supervisor existed — this is purely additive.attestation()is now async and checks the supervisor's/healthon every call (unauthenticated, matching the supervisor's own design) rather than caching a value that could go stale exactly when it matters.isolated_computersistrueonly while a configured supervisor is actually answering right now.POST /bitmind/v1/computer/{agent_id}/ensure, proxying to the supervisor's ownensureverb. Response is a small named subset (status,started_at) — never the container name, the internal container-DNS url, or supervisor's raw identity/error text. The supervisor's own status code passes through on refusal (400/409/503 carry real meaning to a caller); its response body does not.Deliberately not done here
Control transfer and live frames.
agent-computeralready has both as real, working features (/control/take+/control/release,/stream's CDP screencast) — relaying a live frame stream or a control-transfer session through this gateway is its own design question (websocket proxy? polling? does the gateway hold state across a transfer?), not a small extension of the ensure route. Tracked as a named follow-up, not silently dropped.Tests
3 new attestation cases (reachable / unreachable / unwell supervisor), 5 new computer-route cases (unconfigured, successful proxy with response trimming asserted field-by-field, status-code passthrough on refusal, unreachable supervisor, auth requirement), 2 new config cases (paired validation, credential/scheme rejection).
bunx biome format/lintclean, servertypecheckclean, fullbun test server/tests --timeout 30000→ 1708 pass / 0 fail.🤖 Generated with Claude Code