From 7452b8c6d2120de74391947df2e8ae69440c8644 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:49:34 +0200 Subject: [PATCH] Reach the GitHub broker over a mounted socket instead of the network (#213) Computers on evo could not reach the broker over TCP: the git shim reported that evOo's GitHub connection was unreachable, so cloning private repositories still failed. Container-to-host traffic depends on the host firewall and on which Docker network the computer joined. - On a Linux host the supervisor also serves the broker on a unix socket in a host directory (SANDBOX_GITHUB_BROKER_SOCKET_DIR, default ~/.rakazo/github-broker), and mounts that directory read-only into each new computer once the socket listens. - The shim uses the socket when it is present and falls back to the network otherwise; connections time out after 5 seconds and the unreachable message names the curl exit code. Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ Co-authored-by: Claude --- infra/sandboxes/computer/bin/rakazo-broker | 14 ++++++-- .../supervisor/src/computer-spec.test.ts | 24 +++++++++++++ .../sandboxes/supervisor/src/computer-spec.ts | 16 +++++++++ infra/sandboxes/supervisor/src/index.ts | 35 +++++++++++++++++-- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/infra/sandboxes/computer/bin/rakazo-broker b/infra/sandboxes/computer/bin/rakazo-broker index f3ece77..1fde5e2 100755 --- a/infra/sandboxes/computer/bin/rakazo-broker +++ b/infra/sandboxes/computer/bin/rakazo-broker @@ -4,7 +4,7 @@ set -euo pipefail endpoint="$1" payload="$2" post() { - curl -fsS -X POST "$1/$endpoint" \ + curl -fsS --connect-timeout 5 -X POST "$1/$endpoint" "${@:2}" \ -H "Authorization: Bearer ${RAKAZO_GITHUB_BROKER_RUN_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary "$payload" @@ -26,11 +26,19 @@ sys.exit(1) ' "$RAKAZO_GITHUB_BROKER_URL" } unreachable() { - echo "evOo could not reach its GitHub connection from this computer. Tell the user GitHub access is broken on the evOo server; do not ask for a token." >&2 + echo "evOo could not reach its GitHub connection from this computer (curl exit $status). Tell the user GitHub access is broken on the evOo server; do not ask for a token." >&2 exit 1 } status=0 -response="$(post "$RAKAZO_GITHUB_BROKER_URL" 2>/dev/null)" || status=$? +socket=/run/rakazo-github-broker/broker.sock +if [[ -S "$socket" ]]; then + response="$(post http://broker/internal/github-broker --unix-socket "$socket" 2>/dev/null)" || status=$? +fi +# 7: nothing listening on the socket either; then try the network. +if [[ ! -S "$socket" || "$status" == 7 ]]; then + status=0 + response="$(post "$RAKAZO_GITHUB_BROKER_URL" 2>/dev/null)" || status=$? +fi # 6: unresolved host, 7: connection refused, 28: timeout. Anything else is the broker answering. if [[ "$status" == 6 || "$status" == 7 || "$status" == 28 ]]; then fallback="$(gateway_url)" || unreachable diff --git a/infra/sandboxes/supervisor/src/computer-spec.test.ts b/infra/sandboxes/supervisor/src/computer-spec.test.ts index 3dbd8a8..2b09b80 100644 --- a/infra/sandboxes/supervisor/src/computer-spec.test.ts +++ b/infra/sandboxes/supervisor/src/computer-spec.test.ts @@ -107,6 +107,30 @@ describe("graphical computer spec", () => { expect(options.HostConfig.NetworkMode).toBe("rakazo_default"); }); + it("mounts the GitHub broker socket read-only when the supervisor serves one", () => { + const base = { + name: "rakazo-bot-abc", + image: COMPUTER_IMAGE, + botId: "abc", + spaceId: "ws", + githubBrokerSocketDir: "/srv/rakazo/github-broker", + }; + expect( + containerCreateOptions({ ...base, homePath: "/var/rakazo/homes/abc" }).HostConfig.Binds, + ).toContain("/srv/rakazo/github-broker:/run/rakazo-github-broker:ro"); + const projectMounts = containerCreateOptions({ + ...base, + homePath: "/var/rakazo/homes/abc", + projectWorkspacePath: "/srv/projects/evoo", + }).HostConfig.Mounts; + expect(projectMounts).toContainEqual({ + Type: "bind", + Source: "/srv/rakazo/github-broker", + Target: "/run/rakazo-github-broker", + ReadOnly: true, + }); + }); + it("still publishes host ports when NetworkMode is a per-bot isolated network", () => { const networkMode = computerNetworkNameFor("bot_isolation"); const options = containerCreateOptions({ diff --git a/infra/sandboxes/supervisor/src/computer-spec.ts b/infra/sandboxes/supervisor/src/computer-spec.ts index 90775e5..56b182f 100644 --- a/infra/sandboxes/supervisor/src/computer-spec.ts +++ b/infra/sandboxes/supervisor/src/computer-spec.ts @@ -245,8 +245,13 @@ export interface ComputerCreateInput { controlToken?: string; networkMode?: string; publishControlPort?: boolean; + /** Host directory holding the GitHub broker socket, mounted read-only into the computer. */ + githubBrokerSocketDir?: string; } +/** Where computers find the GitHub broker socket; the git shim prefers it over the network. */ +export const GITHUB_BROKER_SOCKET_MOUNT = "/run/rakazo-github-broker"; + interface PointerInput { kind: "pointer"; x: number; @@ -281,6 +286,14 @@ function computerVolumeMounts(input: ComputerCreateInput): Docker.MountSettings[ ReadOnly: false, }); } + if (input.githubBrokerSocketDir) { + mounts.push({ + Type: "bind", + Source: input.githubBrokerSocketDir, + Target: GITHUB_BROKER_SOCKET_MOUNT, + ReadOnly: true, + }); + } return mounts; } @@ -321,6 +334,9 @@ export function containerCreateOptions(input: ComputerCreateInput) { ...(input.projectWorkspacePath ? [`${input.projectWorkspacePath}:/home/rakazo/project`] : []), + ...(input.githubBrokerSocketDir + ? [`${input.githubBrokerSocketDir}:${GITHUB_BROKER_SOCKET_MOUNT}:ro`] + : []), ], Mounts: undefined, }), diff --git a/infra/sandboxes/supervisor/src/index.ts b/infra/sandboxes/supervisor/src/index.ts index ccccb2f..e641c5a 100644 --- a/infra/sandboxes/supervisor/src/index.ts +++ b/infra/sandboxes/supervisor/src/index.ts @@ -1,13 +1,14 @@ import { randomUUID } from "node:crypto"; -import { existsSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, rmSync } from "node:fs"; import { mkdir } from "node:fs/promises"; import http from "node:http"; +import os from "node:os"; import path from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import type { ReadableStream as NodeReadableStream } from "node:stream/web"; import { fileURLToPath } from "node:url"; -import { serve } from "@hono/node-server"; +import { createAdaptorServer, serve } from "@hono/node-server"; import { boundedSandboxCommandTimeoutMs, readBoundedJsonResponse, @@ -152,6 +153,31 @@ export function needsBridgeBrokerListener( ); } +// A socket bind-mounted into each computer reaches the broker without crossing the host firewall. +const githubBrokerSocketDir = path.resolve( + process.env.SANDBOX_GITHUB_BROKER_SOCKET_DIR ?? + path.join(os.homedir(), ".rakazo", "github-broker"), +); +// Set once the socket listens, so computers only get the mount when there is something behind it. +let githubBrokerSocketReady = false; + +function listenGithubBrokerSocket(logger: ReturnType) { + mkdirSync(githubBrokerSocketDir, { recursive: true, mode: 0o755 }); + const socketPath = path.join(githubBrokerSocketDir, "broker.sock"); + rmSync(socketPath, { force: true }); + const server = createAdaptorServer({ fetch: brokerApp.fetch }); + server.on("error", (error) => { + logger.warn("github broker socket unavailable", { "error.message": error.message }); + }); + server.listen(socketPath, () => { + // Computers may run as a different uid; the run token is what authorizes a request. + chmodSync(socketPath, 0o666); + githubBrokerSocketReady = true; + logger.info("github broker listening", { "server.socket": socketPath }); + }); + return server; +} + async function bridgeGatewayAddress(): Promise { const network = await docker.getNetwork("bridge").inspect(); return network.IPAM?.Config?.find((entry: { Gateway?: string }) => entry.Gateway)?.Gateway; @@ -293,6 +319,7 @@ app.post("/computers", async (c) => { networkMode, controlToken: randomUUID(), publishControlPort: controlViaLoopback, + ...(githubBrokerSocketReady ? { githubBrokerSocketDir } : {}), }), ); await container.start(); @@ -875,6 +902,9 @@ function startSupervisor() { logger.info("supervisor listening", { "http.host": hostname, "http.port": port }); }); let brokerServer: ReturnType | undefined; + const brokerSocketServer = needsBridgeBrokerListener(hostname) + ? listenGithubBrokerSocket(logger) + : undefined; if (needsBridgeBrokerListener(hostname)) { void bridgeGatewayAddress() .then((gateway) => { @@ -894,6 +924,7 @@ function startSupervisor() { if (stopping) return; stopping = true; if (brokerServer) await closeListeningServer(brokerServer); + if (brokerSocketServer) await closeListeningServer(brokerSocketServer); await closeListeningServer(server); await logger.flush({ timeoutMs: 2_000 }); process.exit(0);