/** The claim image-api returns: the VETTED argv (operator free-text never reaches the runner) + the per-bake * ingest secret minted at claim (so a stale/rogue runner can't inject frames into another bake — §P2.4e). */ export interface ClaimedBake { bakeId: string; argv: string[]; push: boolean; dryRun: boolean; logs: boolean; ingestSecret: string; /** Denormalized so the runner can pick the profile-aware deadline without a second fetch. */ profile?: string | null; bands?: string[] | null; } /** The ingest/heartbeat response — image-api echoes whether a cancel was requested so the runner kills the group. */ export interface IngestAck { accepted: boolean; /** The durable cancel flag (§P2.11) — polled off EVERY ingest/heartbeat response (≤heartbeat-interval lag). */ cancelRequested?: boolean; /** False when the lease was lost/stolen (the runner aborts + kills the child — it no longer owns the build). */ leaseValid?: boolean; } /** A spawned build.sh child — abstracted so a test can drive stdout/stderr/exit without a real process. The handle * exposes the process-GROUP id (negative pid) so a kill signals the WHOLE group (the setsid-detached tree). */ export interface ChildHandle { /** Negated pgid for `process.kill(-pgid, sig)` — the group kill that actually stops a detached build (§P2.11). */ readonly pgid: number; /** Resolves with the child's exit code (or 124-style on a signal) once it exits. */ readonly exited: Promise<{ code: number | null; signal: string | null; }>; /** Send a signal to the process GROUP (SIGTERM then SIGKILL on the cancel/deadline path). */ killGroup(signal: NodeJS.Signals): void; } /** Spawn `build.sh --json` detached in its own process group; stdout/stderr are delivered LINE-BY-LINE via * the callbacks (the impl splits on newlines). The runner feeds ONLY stdout lines through the JSON parser; stderr * is the failure tail (§P2.5) — buffered, surfaced on a non-zero exit, never JSON.parsed. */ export interface ChildSpawner { spawn(opts: { argv: string[]; cwd: string; onStdoutLine(line: string): void; onStderrLine(line: string): void; }): ChildHandle; } /** The image-api HTTP client (claim/ingest) — injected so a test drives the control plane without a server. */ export interface ImageApiClient { /** Long-poll for + claim the next queued bake; null when none is claimable right now. */ claimNext(): Promise; /** POST one frame to `…/ingest` (carries the per-bake ingest secret header); returns the ack (cancel/lease). */ ingest(bakeId: string, ingestSecret: string, frame: Record): Promise; /** Heartbeat the lease forward (also surfaces cancel/lease state) — distinct from a frame ingest. */ heartbeat(bakeId: string, ingestSecret: string): Promise; } /** Host-side ops the runner needs that are NOT image-api calls (fs + a thin docker-push wrapper for the big-blob * retry — NOT a build.sh --replay). All injected so the orchestration is testable off build-host. */ export interface HostOps { /** Read the manifest BODY off the build-host FS (build.sh's done carries only a path — a cat/read is mandatory, §P2.10). */ readManifest(path: string): Promise; /** `df -P -BG /data` output for the pre-flight disk guard (§P2.7); the runner parses free GiB out of it. */ dfData(): Promise; /** Checkout the recipe at RECIPE_REF so build.sh's git rev-parse gitsha is REAL (§P2.12); returns the cwd. */ checkoutRecipe(ref: string): Promise; /** The thin `docker push` big-blob retry wrapper (§P2.12) — retries a flaky registry layer push (born on SWR — * deprecated; pool now docker.io/claybobby) WITHOUT re-running the * whole build (build.sh has no push-only re-entry; --replay would re-bake + hit the dup-check). Best-effort. */ dockerPushRetry?(tag: string, attempts: number): Promise<{ ok: boolean; digest: string | null; }>; } export interface Clock { now(): number; sleep(ms: number): Promise; } export interface BakeRunnerLogger { info(msg: string, fields?: Record): void; warn(msg: string, fields?: Record): void; error(msg: string, fields?: Record): void; } export interface BakeRunnerOpts { api: ImageApiClient; spawner: ChildSpawner; host: HostOps; clock: Clock; log: BakeRunnerLogger; /** The git ref build.sh's recipe checkout is pinned to (RECIPE_REF) — replay-sufficient gitsha (§P2.12). */ recipeRef: string; /** The absolute path to build.sh inside the recipe checkout (e.g. e2b-template/dev-sandbox/build.sh). */ buildShPath: string; /** Lease heartbeat cadence (default 30s) — must stay well under image-api's BAKE_LEASE_MS (90s) so a healthy * build never has its lease reaped (§P2.7). The docker-build progress tick + cancel poll ride the same cadence. */ heartbeatMs?: number; /** Idle poll backoff when no bake is claimable (default 5s). */ idlePollMs?: number; /** Minimum free GiB on /data at claim; below this the bake fails fast (507-class) rather than dying mid-build and * leaving a half-layer (k3s + docker containerd both on /data — a fill triggers kubelet GC eviction — §P2.7). */ minFreeGb?: number; } export declare class BakeRunner { private readonly o; private readonly heartbeatMs; private readonly idlePollMs; private readonly minFreeGb; private stopped; constructor(opts: BakeRunnerOpts); /** Cooperative shutdown — finishes the in-flight bake's current frame, then exits the claim loop. */ stop(): void; /** The long-poll loop: claim → run → release → poll next. Single-flight is enforced DB-side (the claim CAS), so a * second runner replica simply never wins a claim. Runs until `stop()`. */ loop(): Promise; /** Run a single CLAIMED bake end-to-end. Owns the lineOrd counter, the heartbeat/cancel/deadline timers, the * child process group, the stdout→ingest pump, and the terminal done synthesis. */ runOne(bake: ClaimedBake): Promise; /** Forward build.sh's own `done` line — but FIRST read the manifest BODY off disk and attach it so image-api can * auto-register (build.sh's done carries only a build-host path, NOT the body — §P2.10). A manifest read failure is * non-fatal: the done still forwards (the digest is on the line); image-api then registers indexId:null partial. * * BIG-BLOB PUSH RETRY (§P2.12): build.sh dies exit 13 on a flaky `docker push`. The retry is a THIN `docker push` * wrapper on the host (NOT build.sh --replay — that re-bakes + hits the dup-check exit 10). On a push-fail done * for a `--push` bake with a known tag, the runner retries the push; on success it rewrites the done to COMPLETE * with the recovered post-push digest (no manifest on disk — build.sh dies before the manifest write — so image- * api registers the built-and-pushed-but-unregistered partial, which the P3 reconciliation tick finalizes). */ private forwardDone; /** Kill the process GROUP: SIGTERM, then SIGKILL shortly after (a SIGKILL mid-docker-build can leave dangling * layers; the dup-check + lockhash prevent a poisoned re-bake, and a startup workdir sweep reclaims them — §P2.11). */ private killGroup; private stopSupervisor; /** Pre-flight free-GiB read; null (fail-open) on a df parse/exec miss — a df hiccup must not block a bake (the * reaper + a mid-build fill guard still protect the host). */ private freeGbOrNull; /** Ingest one frame, swallowing transient errors (the buffered-retry of unsent lines is the real-runner's job; * image-api's uq_line makes a re-send a no-op, so a dropped frame is recoverable, never a crash). */ private safeIngest; } /** Allocate the next per-bake lineOrd (monotonic from 1) — the at-least-once ingest dedupe key (§P2.10). */ export declare function nextOrd(ord: { n: number; }): number; //# sourceMappingURL=runner.d.ts.map