/** * Result cache for typed acceptance gates (T12621 · ADR-061). * * `tool:` evidence atoms have been cached since T1534; typed gates were not, so * `cleo verify --run` followed by the attesting `cleo verify --gate …` * executed a slow suite twice, and a retry after any unrelated failure executed * it a third time. This module gives typed gates the same content-addressed * cache, in the same directory (`.cleo/cache/evidence/`, `gate-` prefix). * * The key is `(gate definition hash, git HEAD, dirty-tree fingerprint, cwd, * inputs digest)`. HEAD and the fingerprint come from the exact helpers the tool * cache uses, so every tracked byte is unchanged on a hit. The inputs digest * covers what the fingerprint cannot: the captured invocation (including its * environment hash) and the bytes of every input the verifier binds — untracked * harness scripts and task files included. Like the tool cache, a directory that * is not a git checkout gets no caching at all rather than one answer forever * (gh#1419). * * NOT in the key (the gh#1221 trade-off, stated rather than hidden): modules an * untracked harness imports, `node_modules`, and an in-place toolchain upgrade. * A reused pass is therefore always marked `source: 'cache'` on the result and * on the receipt, and `evidence.allowCachedGates: false` in * `.cleo/project-context.json` turns reuse off and makes completion refuse it. * * ## Authentication * * Entries are plain JSON in the project, and every key input is public, so an * unauthenticated entry is a forgeable pass. Each entry therefore carries an * HMAC-SHA256 under a per-machine key at `/keys/evidence-cache.key` * (created `0600` on first use, outside every project and worktree). An entry * whose MAC does not verify is never served. This module can VERIFY an entry * given the key; only the gate runner can create one, from a result it executed * itself, and nothing exported from core loads the key. The key defends against * hand-written, tampered and copied entries; it cannot defend against a process * that reads the key file directly with the user's own privileges. * * Only a `pass` is ever written. A failure, an error or a timeout is not a * reusable fact about the tree — it is the thing the next run exists to change. * Only process-executing kinds (`test`, `command`, `lint`) are cacheable. * * @task T12621 */ import type { AcceptanceGate, AcceptanceGateResult } from '@cleocode/contracts'; import type { AcceptanceGateCacheInputs } from '@cleocode/contracts/acceptance-gate'; /** Schema version of an on-disk gate cache entry; bump to invalidate every entry. */ export declare const GATE_CACHE_SCHEMA_VERSION = 2; /** The observed runner result as stored, without batch position, binding or provenance. */ export type CachedGateObservation = Omit; /** On-disk shape of one cached typed-gate pass, before its MAC. */ export interface GateCacheEntryBody { schemaVersion: typeof GATE_CACHE_SCHEMA_VERSION; key: string; gateHash: string; head: string; dirtyFingerprint: string; cwd: string; inputsHash: string; /** When the gate actually executed and the entry was sealed. */ createdAt: string; result: CachedGateObservation; /** * The parts behind `inputsHash`, for a later miss to explain itself (T13693). * Outside the MAC: diagnostic only, never used to serve a pass. */ inputs?: AcceptanceGateCacheInputs; } /** On-disk shape of one sealed entry. */ export interface GateCacheEntry extends GateCacheEntryBody { /** HMAC-SHA256 (hex) of {@link gateCacheEntryMacPayload} under the machine key. */ mac: string; } /** * Repo state a batch of gates is keyed against. * * `null` when the directory is not a git checkout: such a tree has no content * fingerprint, so nothing is read from or written to the cache. */ export interface GateCacheState { /** Git HEAD sha of the execution root. */ head: string; /** sha256 of the tracked uncommitted changes (see `captureDirtyFingerprint`). */ dirtyFingerprint: string; /** Symlink-resolved execution root. */ cwd: string; } /** Outcome of looking a gate up in the cache. */ export type GateCacheLookup = { status: 'hit'; observation: CachedGateObservation; createdAt: string; } | { status: 'miss'; } | { status: 'invalid'; reason: string; }; /** * Whether a gate kind's pass may be served from the cache. * @param gate - Gate to classify. * @returns `true` for `test`, `command` and `lint` gates. * @example * ```typescript * isCacheableGate({ kind: 'manual', description: 'look', prompt: 'ok?' }); // false * ``` * @task T12621 */ export declare function isCacheableGate(gate: AcceptanceGate): boolean; /** * Capture the repo state gate cache keys are computed against. * @param projectRoot - Directory the gates execute in. * @returns The state, or `null` when the directory is not a git checkout. * @example * ```typescript * const state = await captureGateCacheState('/project'); * ``` * @task T12621 */ export declare function captureGateCacheState(projectRoot: string): Promise; /** * Absolute path of the cache entry for a gate against a repo state. * @param projectRoot - Project whose `.cleo/cache/evidence/` holds the entry. * @param gate - Gate definition. * @param state - Repo state from {@link captureGateCacheState}. * @param inputsHash - Digest of the captured invocation and input artifacts. * @returns The entry path (which may not exist). * @example * ```typescript * const path = gateCacheEntryPath(root, gate, state, inputsHash); * ``` * @task T12621 */ export declare function gateCacheEntryPath(projectRoot: string, gate: AcceptanceGate, state: GateCacheState, inputsHash: string): string; /** * Path of the per-machine key that authenticates evidence cache entries. * @returns `/keys/evidence-cache.key`. * @example * ```typescript * evidenceCacheKeyPath(); // ~/.local/share/cleo/keys/evidence-cache.key * ``` * @task T12621 */ export declare function evidenceCacheKeyPath(): string; /** * Build the unsealed entry body for an observed pass. * @param gate - Gate the result was observed for. * @param state - Repo state captured BEFORE the gate executed. * @param inputsHash - Inputs digest captured BEFORE the gate executed. * @param result - The runner's observed result. * @param inputs - The parts behind `inputsHash`, kept for miss diagnostics (T13693). * @returns The body to seal, or `null` for anything but a cacheable pass. * @example * ```typescript * const body = buildGateCacheEntryBody(gate, state, inputsHash, observed); * ``` * @task T12621 */ export declare function buildGateCacheEntryBody(gate: AcceptanceGate, state: GateCacheState, inputsHash: string, result: AcceptanceGateResult, inputs?: AcceptanceGateCacheInputs): GateCacheEntryBody | null; /** * Why a gate has no cached pass: which key parts differ from the newest stored * pass of the same gate definition (T13693). A refusal that only said "no * cached pass for the current HEAD, working tree and inputs" could not tell a * moved HEAD from a test that rewrote a tracked file or an environment that * changed between two commands. Entries are read without authentication: this * explains, it never serves. * * @param projectRoot - Project whose `.cleo/cache/evidence/` holds the entries. * @param gate - The gate that missed. * @param state - The repo state the lookup used. * @param inputsHash - The inputs digest the lookup used. * @param inputs - The parts behind `inputsHash`, when the caller has them. * @returns A sentence naming the differing parts, or null when no pass of this * gate is stored at all. * @example * ```typescript * const why = describeGateCacheMiss(root, gate, state, inputsHash, inputs); * ``` * @task T13693 */ export declare function describeGateCacheMiss(projectRoot: string, gate: AcceptanceGate, state: GateCacheState, inputsHash: string, inputs?: AcceptanceGateCacheInputs): string | null; /** * Canonical bytes an entry's MAC covers: every body field, in a fixed order. * @param body - Entry body (a `mac` field, if present, is ignored). * @returns The string to MAC. * @example * ```typescript * const payload = gateCacheEntryMacPayload(body); * ``` * @task T12621 */ export declare function gateCacheEntryMacPayload(body: GateCacheEntryBody): string; /** * Seal an entry body with the machine key. * @param body - Entry body from {@link buildGateCacheEntryBody}. * @param key - Machine key; the caller is the only holder. * @returns The sealed entry. * @example * ```typescript * const entry = sealGateCacheEntry(body, key); * ``` * @task T12621 */ export declare function sealGateCacheEntry(body: GateCacheEntryBody, key: Buffer): GateCacheEntry; /** * Look a gate up in the cache and authenticate what is found. * @param projectRoot - Project whose `.cleo/cache/evidence/` holds the entry. * @param gate - Gate definition; any change to it changes the key. * @param state - Repo state from {@link captureGateCacheState}. * @param inputsHash - Digest of the captured invocation and input artifacts. * @param key - Machine key the entry must be sealed under. * @returns `hit` with the observation, `miss` when there is no entry, or * `invalid` when an entry exists but is unauthenticated, tampered or not a pass. * @example * ```typescript * const lookup = readGateCacheEntry(root, gate, state, inputsHash, key); * ``` * @task T12621 */ export declare function readGateCacheEntry(projectRoot: string, gate: AcceptanceGate, state: GateCacheState, inputsHash: string, key: Buffer): GateCacheLookup; /** * Project policy: may a typed gate pass be reused from the cache? * * Reads `evidence.allowCachedGates` from `.cleo/project-context.json` with a * bare read, like `evidence.gitRoot`. Only an explicit `false` disables reuse. * * @param projectRoot - CLEO store root holding `.cleo/`. * @returns `false` only when the project declares `evidence.allowCachedGates: false`. * @example * ```typescript * if (!readAllowCachedGates(root)) cache = 'off'; * ``` * @task T12621 */ export declare function readAllowCachedGates(projectRoot: string): boolean; //# sourceMappingURL=gate-result-cache.d.ts.map