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 <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-18 12:31:54 +02:00 committed by GitHub
parent a0bdf6ddc8
commit a27cdf733c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 0 deletions

View File

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

View File

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

View File

@ -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<string, GithubBrokerSession>();
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,