/** * `claudeCode()` — the Claude Agent SDK behind the frozen Harness contract. * * The flagship proof that "who thinks" is swappable: real bash hands over a * materialized workspace copy, a native session that survives across turns, and * NOT ONE new safety mechanism. Every tool call still lands in * `turn.tools.call()` — one guard, one audit row, one mirror, exactly like * `vendo()` — because the box's toolset is a projection, never an execution site. * * The ~250MB SDK never enters the host's node_modules on the sandbox path: it * lives in the box image, and this subpath is a thin driver. `machine: "local"` * loads it by dynamic import from an OPTIONAL peer. * * Read with: build contract §1 (the contract), §1.4 (approvals), §3.5 * (materialization), and design §3 / §8 / §9. */ import { type BeatPhase, type Harness } from "@vendoai/core"; import type { BeatPhase as LoopBeatPhase } from "./claude-turn.js"; import type { UIMessage } from "ai"; import { type HarnessAdapters } from "../harness-sandbox.js"; import { type SandboxAdapterLike } from "./box.js"; export { BOX_WORKSPACE_ROOT, boxEgress, boxMachine, disposeSessionMachines, inferenceEnv, type SandboxAdapterLike, type SandboxMachineLike, } from "./box.js"; /** The bound a build's dead-man timer has to clear: what the box itself gives * ONE message before giving up on it. */ export { MESSAGE_BUDGET_MS } from "./machine.js"; /** The knobs a TURN may still carry (harness-declared; see optionsSchema). */ export interface ClaudeCodeTurnOptions { maxTurns?: number; } /** * The beat phase union is declared TWICE, and this is what makes that safe. * * Core owns `BeatPhase` (contract §3.4). `claude-turn.ts` restates it * structurally because that file imports NOTHING — its module header explains * why: the emitted `dist/claude-turn.js` is copied verbatim into a machine image, * and a module that named its dependencies was reachable from every composed * host's build graph. * * The `yield event` below already compares the two unions, but only in ONE * direction: a mirror that is a SUBSET of core stays assignable, so a seventh * phase added to core would leave the box silently unable to ever emit it, with * nothing failing anywhere (verified: adding one to core left every typecheck * green). This map closes that direction — its keys are required by CORE's union * and its values are typed as the LOOP's, so drift either way fails here, by * name, instead of as an inference error 200 lines down. */ export declare const BEAT_PHASES: Record; /** v1 options, exactly (design §3): nothing else until asked. */ export interface ClaudeCodeOptions extends ClaudeCodeTurnOptions { /** Construction-time only (agents spec 2026-08-04 cut per-turn model/effort): * which model thinks binds when the harness is built, never per request. */ model?: string; effort?: "low" | "medium" | "high"; /** Run the SDK on the host's own server instead of a sandbox. Never default. */ machine?: "local"; /** * Provider template the conversation box boots from; defaults to * `VENDO_BOX_TEMPLATE`. Construction-time like `machine`: which image a box * runs is a deployment decision, never a request's. * * Sandbox path only: `machine: "local"` has no box to template. */ template?: string; /** * Extra outbound domains the box may reach, ADDED to the minimum set * ({@link boxEgress}). Bare hostnames, as `vendo.json`'s `egress` writes them. * * The box's outbound traffic is filtered against this list at the provider's * DOMAIN layer, so a host whose agent legitimately needs a third party — their * own API on another origin, an allowed vendor — names it here. (What that * filtering does and does not stop: see {@link boxEgress}.) * There is no approval flow on this list, unlike an app * document's `egress` (`egress-approval.ts`): that one is an ASK from generated * code, this one is the host developer's own source at boot, the same authority * that sets `implicitDomains`. * * Sandbox path only: `machine: "local"` has no network boundary to widen. */ egress?: string[]; } /** Host-side dependencies arrive by factory closure (design §3), which is how a * host who did not wire `createVendo({ sandbox })` hands one straight to the * harness. Composition fills the same slots through `provideHarnessAdapters`; * an explicitly passed value always wins (the adapter rule). The three apps * hooks are the app-document vocabulary this package no longer imports — * `HOT_PATH_WATCH`/`hotPathAppId`, `validateWrittenApps`, `repairInstruction` * in `@vendoai/apps`. Without them the driver still runs, minus the mid-turn * hot-path sync and the validate gate ({@link warnNoAppsHooksOnce}). */ export interface ClaudeCodeDeps extends Pick { sandbox?: SandboxAdapterLike; } /** * What the SDK is asked this turn. * * Resuming a native session, the SDK already holds everything before now, so the * prompt is only what the user just said. Starting fresh — a first turn, or a * mid-conversation swap from `vendo()` — the thread is re-seeded from OUR * transcript, which is what lets the swap continue the conversation rather than * restart it. The truth is always ours (design §3, "Harness state"). */ export declare function promptFor(messages: readonly UIMessage[], resuming: boolean): string; /** `turn.state` — the opaque blob (§1.3). Ours to shape, nobody else's to read. */ interface ClaudeState { /** * The SDK's native session id. * * This is the WHOLE of our recovery story now. It used to sit beside a * snapshot ref and a control token, because a swept box could be woken; a * conversation box is destroyed instead, so a session id is only resumable * while the box that owns it is still up. On a fresh box the id is stale and * the thread re-seeds from OUR transcript — which is the truth anyway * (design §3, "Harness state"). */ sessionId?: string; /** How long our transcript was when this session last answered — the ONLY * thing that makes a truncation detectable. */ covers?: number; } /** * §1.3's prefix truncation: did the user throw away the answer this session still * remembers? * * `covers` counts the answering turn's INPUTS — its reply lands at transcript * index `covers` — so a history that did not GROW means that reply is gone. That * is a REGENERATE or a delete-from-here; a real mid-history edit never reaches * here, because the runtime already CLEARS the state for one (`classifyHistory` * calls a differing overlap an arbitrary edit). * * This replaced a rewind LEDGER (`rewindFor`, `resumeSessionAt`, per-message * checkpoint uuids, a 24-entry history). That machinery was dead: the box door * never read `payload.resumeAt` and a warm session never reopened, so a * regenerate left the discarded answer in the model's memory — the exact failure * it existed to prevent. Dropping the session and re-seeding from OUR transcript * is never wrong, only slower, and regenerate is the rare path. */ export declare function truncated(state: ClaudeState, messageCount: number): boolean; export declare function claudeCode(options?: ClaudeCodeOptions & ClaudeCodeDeps): Harness; //# sourceMappingURL=index.d.ts.map