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:
parent
a0bdf6ddc8
commit
a27cdf733c
@ -1,4 +1,7 @@
|
|||||||
FROM node:22-bookworm-slim
|
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
|
WORKDIR /app
|
||||||
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
|
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
|
||||||
COPY patches patches
|
COPY patches patches
|
||||||
|
|||||||
@ -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 { signGithubBrokerRunToken } from "@rakazo/core/node/github-broker-token";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
@ -5,6 +8,7 @@ import {
|
|||||||
type GithubBrokerSession,
|
type GithubBrokerSession,
|
||||||
githubBrokerSessionsForTests,
|
githubBrokerSessionsForTests,
|
||||||
githubHttpsRemote,
|
githubHttpsRemote,
|
||||||
|
gitProcessIdentity,
|
||||||
hostGitInvocation,
|
hostGitInvocation,
|
||||||
hostPathFor,
|
hostPathFor,
|
||||||
} from "./github-broker.js";
|
} 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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
import { statSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {
|
import {
|
||||||
evaluateGitShellPolicy,
|
evaluateGitShellPolicy,
|
||||||
@ -79,6 +80,20 @@ export function hostGitInvocation(
|
|||||||
return { argv: ["/usr/bin/git", ...GIT_CREDENTIAL_ARGS, ...mapped], cwd };
|
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>();
|
const sessions = new Map<string, GithubBrokerSession>();
|
||||||
|
|
||||||
export function githubBrokerSessionsForTests() {
|
export function githubBrokerSessionsForTests() {
|
||||||
@ -112,6 +127,7 @@ async function runHostGit(
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(argv[0]!, argv.slice(1), {
|
const child = spawn(argv[0]!, argv.slice(1), {
|
||||||
cwd,
|
cwd,
|
||||||
|
...gitProcessIdentity(session.workspacePath),
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
HOME: session.workspacePath,
|
HOME: session.workspacePath,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user