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 <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-18 02:49:34 +02:00 committed by GitHub
parent f136451e22
commit 7452b8c6d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 84 additions and 5 deletions

View File

@ -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
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

View File

@ -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({

View File

@ -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,
}),

View File

@ -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<typeof createRootLogger>) {
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<string | undefined> {
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<typeof serve> | 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);