const READY_ATTEMPTS = 60; const READY_DELAY_MS = 500; /** * `start()` returns before the container accepts `exec()`, which throws "exec() requires a running container" * until it has booted; probe with a no-op until it answers. */ export async function waitForContainer(container: Container): Promise { for (let attempt = 0; attempt < READY_ATTEMPTS; attempt++) { try { if (container.running) { const probe = await container.exec(["true"], { stdout: "ignore", stderr: "ignore" }); if ((await probe.exitCode) === 0) return; } } catch { // still booting } await new Promise((resolve) => setTimeout(resolve, READY_DELAY_MS)); } throw new Error("The runner container didn't start in time"); } /** Host cloud jobs use to reach the forge API; requests to it never leave workerd (see `startRunnerContainer`). */ export const INTERNAL_API_HOST = "gitfor.internal"; export const INTERNAL_API_URL = `http://${INTERNAL_API_HOST}`; const intercepted = new WeakSet(); /** * Starts the runner container if needed and routes its HTTP requests for INTERNAL_API_HOST to `api` (the * JobApiGateway entrypoint, which serves only the job API), so the job API (cache, action archives) works without PUBLIC_URL being reachable from inside the * container (in local development it is `localhost`, which is the container itself). The interception is * re-applied once per Durable Object instance, since a restarted object may find the container still running. * * Egress is deliberately open (CI-P9): jobs install packages and call arbitrary APIs like GitHub-hosted runners, * so any code a job runs can reach the internet with whatever secrets it was given. A host allowlist isn't * enforced because `interceptOutboundHttp` only sees plain HTTP, so it can't gate HTTPS or raw TCP; the * protections are instead that fork and untrusted runs get no secrets and CI tokens are job-scoped and short-lived. */ export async function ensureRunnerContainer(container: Container, api: Fetcher): Promise { if (!container.running) { container.start({ enableInternet: true }); await waitForContainer(container); intercepted.delete(container); } if (intercepted.has(container)) return; await container.interceptOutboundHttp(INTERNAL_API_HOST, api); intercepted.add(container); }