From a27cdf733c630ad2e3d1cf85575ca60314362c5d Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:31:54 +0200 Subject: [PATCH] Install git and gh in the sandbox supervisor image (#216) The GitHub broker runs git and gh for agent computers, but the supervisor image is node:bookworm-slim, which has neither. A containerized supervisor therefore failed every broker call. Install both, and when the supervisor runs as root, run git as the workspace owner so clones stay writable by the computer. Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ Co-authored-by: Claude --- infra/sandboxes/supervisor/Dockerfile | 3 +++ .../supervisor/src/github-broker.test.ts | 25 +++++++++++++++++++ .../sandboxes/supervisor/src/github-broker.ts | 16 ++++++++++++ 3 files changed, 44 insertions(+) diff --git a/infra/sandboxes/supervisor/Dockerfile b/infra/sandboxes/supervisor/Dockerfile index 1b2e11d..e69c84b 100644 --- a/infra/sandboxes/supervisor/Dockerfile +++ b/infra/sandboxes/supervisor/Dockerfile @@ -1,4 +1,7 @@ FROM node:22-bookworm-slim +# The GitHub broker runs git and gh on behalf of computers. +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates gh git \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ COPY patches patches diff --git a/infra/sandboxes/supervisor/src/github-broker.test.ts b/infra/sandboxes/supervisor/src/github-broker.test.ts index c842c13..e5d42ae 100644 --- a/infra/sandboxes/supervisor/src/github-broker.test.ts +++ b/infra/sandboxes/supervisor/src/github-broker.test.ts @@ -1,3 +1,6 @@ +import { chownSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { signGithubBrokerRunToken } from "@rakazo/core/node/github-broker-token"; import { describe, expect, it } from "vitest"; import { @@ -5,6 +8,7 @@ import { type GithubBrokerSession, githubBrokerSessionsForTests, githubHttpsRemote, + gitProcessIdentity, hostGitInvocation, hostPathFor, } from "./github-broker.js"; @@ -98,3 +102,24 @@ describe("github broker remotes", () => { ); }); }); + +describe("github broker host tools", () => { + it("ships git and gh in the supervisor image", () => { + const dockerfile = readFileSync(path.join(import.meta.dirname, "../Dockerfile"), "utf8"); + expect(dockerfile).toMatch(/apt-get install[^\n]* gh git/); + }); + + it("runs git as the workspace owner only from a root supervisor", () => { + const workspace = mkdtempSync(path.join(os.tmpdir(), "broker-owner-")); + try { + expect(gitProcessIdentity(workspace, 1000)).toBeUndefined(); + expect(gitProcessIdentity(path.join(workspace, "missing"), 0)).toBeUndefined(); + if (process.getuid?.() === 0) { + chownSync(workspace, 1000, 1000); + expect(gitProcessIdentity(workspace, 0)).toEqual({ uid: 1000, gid: 1000 }); + } + } finally { + rmSync(workspace, { recursive: true, force: true }); + } + }); +}); diff --git a/infra/sandboxes/supervisor/src/github-broker.ts b/infra/sandboxes/supervisor/src/github-broker.ts index 8c714ee..b799d7a 100644 --- a/infra/sandboxes/supervisor/src/github-broker.ts +++ b/infra/sandboxes/supervisor/src/github-broker.ts @@ -1,4 +1,5 @@ import { spawn } from "node:child_process"; +import { statSync } from "node:fs"; import path from "node:path"; import { evaluateGitShellPolicy, @@ -79,6 +80,20 @@ export function hostGitInvocation( return { argv: ["/usr/bin/git", ...GIT_CREDENTIAL_ARGS, ...mapped], cwd }; } +/** A root supervisor runs git as the workspace owner, so clones stay writable by the computer. */ +export function gitProcessIdentity( + workspacePath: string, + uid = process.getuid?.(), +): { uid: number; gid: number } | undefined { + if (uid !== 0) return undefined; + try { + const stat = statSync(workspacePath); + return stat.uid === 0 ? undefined : { uid: stat.uid, gid: stat.gid }; + } catch { + return undefined; + } +} + const sessions = new Map(); export function githubBrokerSessionsForTests() { @@ -112,6 +127,7 @@ async function runHostGit( return new Promise((resolve) => { const child = spawn(argv[0]!, argv.slice(1), { cwd, + ...gitProcessIdentity(session.workspacePath), env: { ...process.env, HOME: session.workspacePath,