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<void> {
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<Container>();
/**
* 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<void> {