/** `loop.json`'s schema tag. Gates forward compatibility of the definition file. */ export declare const LOOP_BUNDLE_SCHEMA = "hasna.loop.bundle.v1"; /** `manifest.json`'s schema tag. */ export declare const LOOP_BUNDLE_MANIFEST_SCHEMA = "hasna.loop.bundle-manifest.v1"; /** The definition file. Always present, always listed in `files[]`. */ export declare const LOOP_JSON_FILE = "loop.json"; /** The manifest itself. Never listed in `files[]` — it cannot contain its own digest. */ export declare const MANIFEST_FILE = "manifest.json"; /** Local-only provenance marker. Never packed, never uploaded. */ export declare const PULL_MARKER_FILE = ".loops-bundle.json"; /** Executables live here and only here; the mode rule keys on this prefix. */ export declare const SCRIPTS_DIR = "scripts"; /** Modes are contract, not umask: data 0600, scripts 0700. Nothing else is representable. */ export declare const MODE_DATA = 384; export declare const MODE_SCRIPT = 448; export declare const MODE_DIR = 448; /** Caps. Exceeding any of them is a client refusal AND a server 413. */ export declare const MAX_BUNDLE_FILES = 512; export declare const MAX_FILE_BYTES: number; export declare const MAX_UNPACKED_BYTES: number; export declare const MAX_ARCHIVE_BYTES: number; /** `manifest.json` is parsed as text under its own limit, never the JSON body limit. */ export declare const MAX_MANIFEST_BYTES: number; /** * Bundle names are an S3 key segment AND a CLI argument AND a directory name, * so the charset is the intersection of what all three can carry unambiguously. * `.` and `..` are excluded by the anchors (a name must start and end with * [a-z0-9]), and case is fixed lowercase because macOS would otherwise let * `Drain` and `drain` name the same directory but two different S3 prefixes. */ export declare const BUNDLE_NAME_PATTERN: RegExp; /** Coded failure for every integrity refusal. CLI maps `code` to an exit status. */ export declare class BundleIntegrityError extends Error { readonly code: string; constructor(code: string, message: string); } export interface BundleManifestFile { /** POSIX-relative to the bundle root. See {@link assertSafeBundlePath}. */ path: string; sha256: string; /** 384 (0o600) for data, 448 (0o700) for `scripts/**`. */ mode: number; size: number; } export interface BundleManifestSource { station: string; agent: string; packageVersion?: string; reason?: string; } export interface BundleManifest { schema: typeof LOOP_BUNDLE_MANIFEST_SCHEMA; /** 0 is a never-pushed local draft; >= 1 is a server-allocated version. */ version: number; loopId: string; name: string; bundleDigest: string; /** Absent in the on-disk copy (the archive does not exist there), present in the S3 copy. */ archiveSha256?: string; createdAt: string; files: BundleManifestFile[]; source: BundleManifestSource; /** Server-derived, never trusted from the client: true when `loop.json` carries an agent prompt. */ carriesPrompt?: boolean; } export declare function isBundleName(value: unknown): value is string; export declare function assertBundleName(value: unknown, label?: string): string; /** * Refuse any path that could escape the bundle root, or that spells the same * file two ways. * * Enforced on WRITE and on READ. The writer is ours; the reader's input is an * archive downloaded from a server, which is the side that matters — this is * the zip-slip gate, and it runs before a single byte is written to disk. */ export declare function assertSafeBundlePath(path: unknown): string; /** * The mode a path is REQUIRED to carry. `scripts/**` executes; nothing else does. * * Matched case-insensitively. On macOS `Scripts/run.sh` and `scripts/run.sh` * are the same file, so a case-sensitive prefix test would let one spelling * claim the executable directory while being validated as inert data. */ export declare function requiredModeFor(path: string): number; /** Byte-order sort. `localeCompare` would order differently per locale and change the digest. */ export declare function compareBundlePaths(a: string, b: string): number; export declare function sha256Hex(bytes: Uint8Array): string; /** * The canonical, framing-independent content digest. * * For each file, in path-sorted byte order, one line: * * \n * * then `sha256:` + sha-256 over the UTF-8 concatenation. Mode is in the digest * because a script losing its executable bit is a real change that a * content-only digest would call identical. */ export declare function computeBundleDigest(files: readonly BundleManifestFile[]): string; /** * Parse and validate an untrusted manifest. * * Fails closed on every axis the digest depends on — path safety, mode enum, * size cap, sort order, self-exclusion — and finally re-derives `bundleDigest` * from `files[]` and compares. A manifest whose declared digest disagrees with * its own file list is refused here, before any caller can act on either. */ export declare function validateBundleManifest(value: unknown): BundleManifest; /** Serialise a manifest the one way, so a round trip through disk is byte-stable. */ export declare function serializeBundleManifest(manifest: BundleManifest): string;