Skip to content

feat: wire the BitMind gateway to the container supervisor - #5

Open
IshmaelRogers wants to merge 3 commits into
mainfrom
feat/gateway-supervisor-wiring
Open

feat: wire the BitMind gateway to the container supervisor#5
IshmaelRogers wants to merge 3 commits into
mainfrom
feat/gateway-supervisor-wiring

Conversation

@IshmaelRogers

Copy link
Copy Markdown

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_computers was a hardcoded false literal in gateway.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 /health on every call (unauthenticated, matching the supervisor's own design) rather than caching a value that could go stale exactly when it matters. isolated_computers is true only while a configured supervisor is actually answering right now.
  • POST /bitmind/v1/computer/{agent_id}/ensure, proxying to the supervisor's own ensure verb. 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-computer already 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/lint clean, server typecheck clean, full bun test server/tests --timeout 30000 → 1708 pass / 0 fail.

🤖 Generated with Claude Code

bitcloud-dev and others added 2 commits September 3, 2026 21:15
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>
@IshmaelRogers

Copy link
Copy Markdown
Author

Reworked at b480a9d. Partway through adding control-transfer and screenshot routes, found server/src/computer/ already has exactly this seam — ComputerProvider/createDockerSupervisorProvider (talks to the identical supervisor over the identical /computers/{id}/ensure verb the first commit had reimplemented), ComputerGateway (locate/status/screenshot/takeControl/releaseControl with real address validation and an audit trail), and COMPUTER_SUPERVISOR_URL/SUPERVISOR_TOKEN/COMPUTER_TOKEN config index.ts already parses and builds a gateway from today, for the product's own UI.

So this now depends on that instance instead of building a second one:

  • BITMIND_SUPERVISOR_URL/TOKEN removed. createBitmindGateway takes an optional ComputerGateway, threaded from index.ts's own already-built one (undefined when no computer provider is configured — no change for anyone not running BitMind).
  • attestation()'s isolated_computers now checks computerGateway.provider.list() against the real seam instead of a bespoke unauthenticated /health probe I'd written by hand.
  • POST .../computer/{agent_id}/ensure calls locate() (validates the resolved address) then status().
  • New: GET .../computer/{agent_id} (observe without starting), GET .../computer/{agent_id}/screenshot (matches bit-bot#58's own stated baseline — "a snapshot URL, not a video pipe", with streaming named there as a later upgrade), POST .../computer/{agent_id}/control {action} against agent-computer's existing take/release state machine, actor carried in x-bitmind-actor-id since BitMind's callers have no row in this deployment's users table.

Still deliberately not done: relaying the live CDP frame stream (/stream) through this gateway — separate design question (websocket relay vs. polling, state across a control transfer), and bit-bot#58 itself treats it as a follow-up to the snapshot baseline.

Tests rewritten around a fakeComputerGateway where every method throws unless overridden (same partial-fake-cast pattern computer-routes.test.ts already uses for this interface), so a test exercising two methods can't silently pass by reaching a third.

Gate on b480a9d: biome format/lint clean, server typecheck clean, full bun test server/tests --timeout 30000 → 1709 pass / 0 fail.

🤖 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 IshmaelRogers left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /health is 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. %2F decodes to a / inside agentId, but createDockerSupervisorProvider interpolates with encodeURIComponent(botId) on every verb, so it cannot traverse the supervisor's path. The supervisor validates bot ids of its own accord as well.
  • isolated_computers is computed per request, never cached. A stale true is 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.ts passes the same ComputerGateway the 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 300001710 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants