/** * Sandbox-parity dry-run (design §5c.3; build-plan slice 3.6) — the real verifier. Runs the * PACKED BUNDLE once in a fresh Linux container that mirrors the hosting sandbox contract, * PER FRAMEWORK, so the failures no static scan can see — missing env values, a handler * written against the wrong contract, deps that never install — surface BEFORE upload: * * openclaw — the week-1 spike, verbatim: node-slim + the pinned OpenClaw release + * bundle-as-workspace + shim; the deliverable is the LLM run's answer. * node / python — hosting's own generated-image contract (hangar generateDockerfile, * mirrored constant-for-constant: node:24-slim / python:3.12-slim, deps * install ONLY when package-lock.json / requirements.txt ships in the * bundle): the runner imports the seller's `entry` and calls the handler * the way the hosted shim will — node: the default export; python: the * module-level `handler(job)`. A stdin/stdout script (the `clustly run * --exec` self-hosted contract) fails HERE with the contract spelled out, * instead of as a silent 120s timeout after deploy (field incident * 2026-08-27: a paying seller shipped exactly that and reverse-engineered * this file to find out why). * * Env injected by NAME at run — never baked. Docker-gated: absent docker reports itself as * skipped, never as a pass. * ponytail: egress capture needs a proxy sidecar — deferred to `clustly test` (§5c.4), * which runs in the REAL sandbox behind the real egress proxy. */ import { type ClustlyManifest } from "./manifest"; /** Injectable process runner (unit tests script it; the real one is the system docker/tar). */ export type ExecFn = (cmd: string, args: string[], opts?: { input?: string; timeoutMs?: number; }) => string; /** Hosting's generated-image bases + conditional dep installs — MUST mirror hangar's * blaxel-build.ts (NODE_RUNTIME_BASE_IMAGE / PYTHON_RUNTIME_BASE_IMAGE / *_DEP_INSTALL): * "deps only when the lockfile ships" is exactly the failure class this dry-run exists to * catch, so the dry-run must fail the same way hosting would. */ export declare const NODE_RUNTIME_BASE_IMAGE = "node:24-slim"; export declare const PYTHON_RUNTIME_BASE_IMAGE = "python:3.12-slim"; /** The build-time `packages:` install line — MUST mirror hangar's `systemPackagesInstall` * (blaxel-build.ts) so the dry-run image carries exactly what the hosted one will. */ export declare function systemPackagesInstall(packages: readonly string[]): string; /** The model the sample job runs on when the workspace doesn't say otherwise. */ export declare const DRYRUN_DEFAULT_MODEL = "anthropic/claude-sonnet-4-6"; /** The sample job — cheap, deterministic-ish, still exercises the full headless loop. */ export declare const DRYRUN_SAMPLE_MESSAGE = "This is a deployment smoke test. Reply with the single word READY and nothing else."; /** True when a usable docker daemon answers. */ export declare function dockerAvailable(exec?: ExecFn): boolean; export interface DryrunReport { /** False = the container run itself failed (Block-grade). */ ok: boolean; /** Manifest env names with NO local value to inject — the run proceeded without them. */ missingEnv: string[]; /** The extracted deliverable, when the run produced one. */ deliverable?: string; /** Tail of the run output — the actionable part of a failure. On a failed run this IS the * agent's trace (their stderr, ours filtered out), which the wizard hands on verbatim. */ log: string; /** Failed branch only: WHOSE failure it was — the catalog code the wizard reports it under. */ code?: DryrunFailureCode; } /** The catalog codes a dry-run can end in — agent-origin (their code, their deps) or the one * platform case (docker could not produce OUR image). See error-catalog.ts. */ export type DryrunFailureCode = "AGENT_CRASHED" | "AGENT_CONTRACT" | "AGENT_TIMEOUT" | "AGENT_BUILD_FAILED" | "SANDBOX_UNAVAILABLE"; export interface DryrunInput { workspacePath: string; manifest: ClustlyManifest; /** The packed bundle — the dry-run boots EXACTLY what would ship. */ tarPath: string; /** Required for the openclaw framework only (the vetted engine pin). */ openclawVersion?: string; } /** * The actionable part of a failed run. Engines print the ROOT CAUSE first and die in a * stack trace — a plain tail shows only the trace (live-test finding, 2026-07-29). Pull the * distinct error-ish lines (skipping stack frames), fall back to the tail when none match. */ export declare function failureLog(raw: string): string; /** Whose failure a docker BUILD was: the workspace's install step, or our image (base pull, * daemon, disk). The log tail is attached either way. */ export declare function classifyBuildFailure(raw: string): "AGENT_BUILD_FAILED" | "SANDBOX_UNAVAILABLE"; export declare function classifyRunFailure(err: unknown, raw: string): DryrunFailureCode; /** * Build + run the sandbox container against the packed bundle. Env values are injected by * NAME from the workspace's local .env / the caller's environment — values never persist * into the image (they ride `docker run -e`, exactly as production injects at spawn). */ export declare function sandboxDryRun(input: DryrunInput, exec?: ExecFn): DryrunReport;