evOo-local/packages/adapters/src/github-broker-client.ts
cynarAI a36b45b791
Let computer git reach GitHub through the broker with working credentials (#210)
* Let computer git reach GitHub through the broker with working credentials

The broker URL handed to computers pointed at the supervisor's loopback
address, which a container cannot reach, and host git ignored the
integration token for HTTPS remotes. Computers now get the host through
host.docker.internal, host git receives the token via a one-off
credential helper, and clone paths and the working directory map from
the computer onto the host workspace.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

* Serve the GitHub broker to computers on a Linux host-run supervisor

A loopback-bound supervisor is unreachable from containers on Linux, so
it also serves the broker routes alone on the default bridge gateway,
which only containers on the same host can reach.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 16:38:25 +02:00

96 lines
3.3 KiB
TypeScript

import { resolveSupervisorToken } from "@rakazo/core";
import { signGithubBrokerRunToken } from "@rakazo/core/node/github-broker-token";
export type GithubBrokerSessionInput = {
runId: string;
workspacePath: string;
allowedRemoteUrl: string | null;
defaultBranch: string | null;
githubToken: string;
/** Where the workspace is mounted inside the computer. */
containerRoot: string;
};
function supervisorBaseUrl(): string | undefined {
const raw = process.env.SANDBOX_SUPERVISOR_URL?.trim();
return raw ? raw.replace(/\/$/, "") : undefined;
}
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
/** The computer runs in a container, so a loopback supervisor address must become the host gateway. */
export function githubBrokerBaseUrl(): string | undefined {
const configured = process.env.SANDBOX_GITHUB_BROKER_URL?.trim();
if (configured) return configured.replace(/\/$/, "");
const supervisor = supervisorBaseUrl();
if (!supervisor) return undefined;
const url = new URL(`${supervisor}/internal/github-broker`);
if (LOOPBACK_HOSTS.has(url.hostname)) url.hostname = "host.docker.internal";
return url.toString().replace(/\/$/, "");
}
function supervisorAuthHeaders(): Record<string, string> | undefined {
const token = resolveSupervisorToken(process.env);
return { authorization: `Bearer ${token}`, "content-type": "application/json" };
}
export function githubBrokerRunToken(runId: string): string {
return signGithubBrokerRunToken(runId, resolveSupervisorToken(process.env));
}
export function githubBrokerEnvironmentForRun(runId: string): Record<string, string> {
const base = githubBrokerBaseUrl();
if (!base) return {};
return {
RAKAZO_GITHUB_BROKER_URL: base,
RAKAZO_GITHUB_BROKER_RUN_TOKEN: githubBrokerRunToken(runId),
};
}
export async function registerGithubBrokerSession(input: GithubBrokerSessionInput): Promise<void> {
const base = supervisorBaseUrl();
const headers = supervisorAuthHeaders();
if (!base || !headers) return;
const res = await fetch(`${base}/internal/github-broker/sessions`, {
method: "POST",
headers,
body: JSON.stringify({
runId: input.runId,
workspacePath: input.workspacePath,
allowedRemoteUrl: input.allowedRemoteUrl,
defaultBranch: input.defaultBranch,
githubToken: input.githubToken,
containerRoot: input.containerRoot,
pushApproved: false,
}),
});
if (!res.ok) {
const detail = await res.text().catch(() => "");
throw new Error(`github broker session registration failed: ${res.status} ${detail}`.trim());
}
}
export async function setGithubBrokerPushApproved(
runId: string,
pushApproved: boolean,
): Promise<void> {
const base = supervisorBaseUrl();
const headers = supervisorAuthHeaders();
if (!base || !headers) return;
await fetch(`${base}/internal/github-broker/sessions/${encodeURIComponent(runId)}`, {
method: "PATCH",
headers,
body: JSON.stringify({ pushApproved }),
}).catch(() => undefined);
}
export async function deleteGithubBrokerSession(runId: string): Promise<void> {
const base = supervisorBaseUrl();
const headers = supervisorAuthHeaders();
if (!base || !headers) return;
await fetch(`${base}/internal/github-broker/sessions/${encodeURIComponent(runId)}`, {
method: "DELETE",
headers,
}).catch(() => undefined);
}