evOo-local/infra/sandboxes/supervisor/src/github-broker.test.ts
cynarAI 1611fe5fa2
Let computers push from project folders through the GitHub broker (#217)
Two gaps kept agents from pushing on a containerized supervisor:

- The git and gh shims called rakazo-broker by name, and login shells in
  the computer reset PATH, so the broker was "not found". The shims now
  call it next to their own resolved path, and /usr/bin/gh is a shim too.
- A project folder outside the supervisor container's mounts is invisible
  to it, so broker git ran against an empty directory. Such calls now run
  in a one-off computer-image container that binds the folder.


Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 15:04:20 +02:00

184 lines
6.7 KiB
TypeScript

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 {
explainGitFailure,
type GithubBrokerSession,
githubBrokerSessionsForTests,
githubHttpsRemote,
gitProcessIdentity,
hostGitInvocation,
hostPathFor,
} from "./github-broker.js";
import {
demuxDockerLogs,
helperContainerOptions,
readMountPoints,
visibleThroughMounts,
} from "./github-broker-helper.js";
import { hasValidBearerToken } from "./supervisor-logic.js";
const session: GithubBrokerSession = {
runId: "run-a",
workspacePath: "/srv/homes/chief",
allowedRemoteUrl: null,
defaultBranch: "main",
githubToken: "ghp_test",
containerRoot: "/home/rakazo",
pushApproved: false,
};
describe("github broker sessions", () => {
it("stores sessions for supervisor-authenticated registration", () => {
const token = "supervisor-test-token";
expect(hasValidBearerToken(`Bearer ${token}`, token)).toBe(true);
githubBrokerSessionsForTests().set("run-a", {
...session,
workspacePath: "/tmp/project",
allowedRemoteUrl: "https://github.com/example/demo.git",
});
const runToken = signGithubBrokerRunToken("run-a", token);
expect(runToken.startsWith("rgb1.")).toBe(true);
githubBrokerSessionsForTests().delete("run-a");
});
});
describe("github broker host git", () => {
it("maps computer paths onto the host workspace", () => {
expect(hostPathFor(session, "/home/rakazo")).toBe("/srv/homes/chief");
expect(hostPathFor(session, "/home/rakazo/repos/demo")).toBe("/srv/homes/chief/repos/demo");
expect(hostPathFor(session, "/home/rakazo/../etc")).toBeNull();
expect(hostPathFor(session, "/tmp")).toBeNull();
expect(hostPathFor(session, "/home/rakazoX")).toBeNull();
});
it("clones where the agent stands, with the token supplied as a git credential", () => {
const invocation = hostGitInvocation(
session,
["git", "clone", "https://github.com/example/demo.git", "/home/rakazo/repos/demo"],
"/home/rakazo/repos",
);
if ("error" in invocation) throw new Error(invocation.error);
expect(invocation.cwd).toBe("/srv/homes/chief/repos");
expect(invocation.argv[0]).toBe("/usr/bin/git");
expect(invocation.argv).toContain("credential.helper=");
expect(invocation.argv.slice(-3)).toEqual([
"clone",
"https://github.com/example/demo.git",
"/srv/homes/chief/repos/demo",
]);
expect(invocation.argv.join(" ")).not.toContain("ghp_test");
});
it("refuses paths outside the mounted workspace", () => {
expect(hostGitInvocation(session, ["git", "clone", "https://x/y.git"], "/tmp")).toEqual({
error: "Run git inside /home/rakazo.",
});
expect(
hostGitInvocation(session, ["git", "clone", "https://x/y.git", "/etc/y"], "/home/rakazo"),
).toEqual({ error: "Use a path inside /home/rakazo." });
});
});
describe("github broker remotes", () => {
it("sends GitHub remotes over HTTPS without pasted credentials", () => {
expect(githubHttpsRemote("git@github.com:example/demo.git")).toBe(
"https://github.com/example/demo.git",
);
expect(githubHttpsRemote("ssh://git@github.com/example/demo")).toBe(
"https://github.com/example/demo",
);
expect(githubHttpsRemote("https://x-access-token:rgb1.abc@github.com/example/demo.git")).toBe(
"https://github.com/example/demo.git",
);
expect(githubHttpsRemote("https://gitlab.com/example/demo.git")).toBe(
"https://gitlab.com/example/demo.git",
);
expect(githubHttpsRemote("--depth=1")).toBe("--depth=1");
});
it("points a rejected token at reconnecting GitHub", () => {
expect(explainGitFailure("remote: Invalid username or token.\n")).toContain(
"reconnect GitHub under Settings, Integrations",
);
expect(explainGitFailure("fatal: repository not found\n")).toBe(
"fatal: repository not found\n",
);
});
});
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 });
}
});
});
describe("github broker helper container", () => {
const mounts = readMountPoints(
[
"1 0 0:1 / / rw - overlay overlay rw",
"2 1 0:2 /srv/data /srv/data rw - ext4 /dev/sda1 rw",
"3 1 0:3 / /var/run/docker.sock rw - tmpfs tmpfs rw",
].join("\n"),
);
it("sees only paths below the supervisor's own mounts", () => {
expect(mounts).toEqual(["/", "/srv/data", "/var/run/docker.sock"]);
expect(visibleThroughMounts("/srv/data/homes/chief", mounts)).toBe(true);
// The parent of a bind mount exists in the container but is empty.
expect(visibleThroughMounts("/srv", mounts)).toBe(false);
expect(visibleThroughMounts("/home/owner/Projects/app", mounts)).toBe(false);
});
it("binds the workspace and runs the computer image's real binary", () => {
const options = helperContainerOptions(
{
argv: ["/usr/bin/git", "push", "origin", "fix"],
cwd: "/home/owner/Projects/app",
env: { GITHUB_TOKEN: "ghp_test" },
workspacePath: "/home/owner/Projects/app",
},
"rakazo/computer:local",
"1000:1000",
);
expect(options).toMatchObject({
Image: "rakazo/computer:local",
Entrypoint: ["/usr/local/libexec/rakazo/git"],
Cmd: ["push", "origin", "fix"],
WorkingDir: "/home/owner/Projects/app",
User: "1000:1000",
Env: ["GITHUB_TOKEN=ghp_test"],
HostConfig: { Binds: ["/home/owner/Projects/app:/home/owner/Projects/app"] },
});
});
it("splits Docker's multiplexed output into stdout and stderr", () => {
const frame = (stream: number, text: string) => {
const header = Buffer.alloc(8);
header[0] = stream;
header.writeUInt32BE(Buffer.byteLength(text), 4);
return Buffer.concat([header, Buffer.from(text)]);
};
expect(
demuxDockerLogs(Buffer.concat([frame(1, "out"), frame(2, "err"), frame(1, "!")])),
).toEqual({ stdout: "out!", stderr: "err" });
});
});