import { z } from "zod"; import type { Runner } from "../internals/proc.js"; import { type HostTuple } from "./host-tuple.js"; import type { DimensionReport, ResolvedSource } from "./scan-gate.js"; import type { BindingSource, FrameworkId } from "./schema.js"; /** * D12 scan cache tiers (W7 design §C) — the two derived, rebuildable caches that * sit ABOVE the fast-scan cache, plus the two async deep-scanner dimensions. * * Two keys, both a sha256 over CANONICAL JSON so byte-identical inputs always * digest identically and any field change changes the key: * - {@link deepScanKey} — `framework + sourceId + treeDigest + scannerVersion * + policyVersion`. Content identity, host-independent. * - {@link runtimeQualKey} — the deep-scan fields PLUS `selectedProfile`, * `adapterVersion`, and the host tuple's * `claudeCode / osBuild / arch / node / bun`. The tuple * is IN the key, so a Linux / older-CLI qualification * computes a DIFFERENT key and can never be read under * the pinned Windows tuple — off-tuple never satisfies, * STRUCTURALLY (design §C.2). Defense in depth: the read * additionally re-checks the stored tuple with the SAME * {@link classifyTuple} semantics the D16 doctor uses. * * The runtime-qual key deliberately mirrors the FIXED host-tuple semantics * (`host-tuple.ts`, O5 addenda): `osBuild` is the Windows BUILD number only — the * monthly UBR patch is PROVENANCE and is NEVER in the key; `claudeCode` (the CLI * version) IS in the key (D12), so a CLI bump is a different key = a cache miss * even when every hard fact is equal; and the read-time tuple guard gates RAM only * DOWNWARD (a rollback below the qualified class misses; the recorded dynamic-memory * balloon above it is drift, not off-tuple) and counts LOGICAL vCPUs. * * Both tiers are read/written ONLY inside an explicit provision / acceptance / * doctor flow — NOTHING here scans at session start (D12). Every read is * fail-safe: a corrupt, schema-invalid, or guard-mismatched record is a MISS * (a recompute), never a throw and never a served-wrong artifact; every write is * best-effort atomic (temp -> rename) like the fast-scan cache. */ /** Bump on ANY deep-scanner ruleset change — a different value re-keys both tiers. */ export declare const DEEP_SCANNER_VERSION: 1; /** Bump on ANY decision-policy change — a different value re-keys both tiers. */ export declare const SCAN_POLICY_VERSION: 1; /** The five content-identity fields both tiers key on. */ export interface DeepScanKeyInput { framework: FrameworkId; /** git commitSha | npm "package@exactVersion". */ sourceId: string; treeDigest: string; /** Defaults to {@link DEEP_SCANNER_VERSION}. */ scannerVersion?: number; /** Defaults to {@link SCAN_POLICY_VERSION}. */ policyVersion?: number; } /** * `sha256(canonicalJson({ framework, sourceId, treeDigest, scannerVersion, * policyVersion }))`. Content identity only — host-independent, so the same tree * digests identically on any machine. */ export declare function deepScanKey(input: DeepScanKeyInput): string; /** The runtime-qual key inputs — the deep-scan fields plus the host qualification axis. */ export interface RuntimeQualKeyInput extends DeepScanKeyInput { selectedProfile: string; adapterVersion: number; tuple: HostTuple; } /** * `sha256(canonicalJson({ ...deepScanFields, selectedProfile, adapterVersion, * claudeCode, osBuild, arch, node, bun }))`. The host tuple flows in as five fields * with the FIXED semantics: `osBuild` is the Windows BUILD number (`tuple.windowsBuild`) * — the UBR patch (`tuple.windowsUbr`) is provenance and is NEVER keyed; `claudeCode` * is `tuple.claudeCode.measuredOn` and IS keyed (a CLI bump = a different key = a miss, * D12). RAM class and vCPU class are deliberately NOT in the key — they are enforced * on read by {@link readRuntimeQualification}'s {@link classifyTuple} guard (RAM gates * downward only, vCPU exact), so the recorded dynamic-memory balloon never re-keys. */ export declare function runtimeQualKey(input: RuntimeQualKeyInput): string; /** git commitSha | npm "package@exactVersion" — the `sourceId` both tiers key on. */ export declare function sourceIdOf(source: BindingSource): string; /** * The `{ sourceId, treeDigest }` a resolved source contributes to the deep-scan key. * Fails closed for an npm source whose tree was never acquired (identity-only * resolution has no `treeDigest`): a deep scan can never key on an absent tree. */ export declare function deepScanIdentityOf(resolved: ResolvedSource): { sourceId: string; treeDigest: string; }; /** Fail-closed error for the tier caches (only thrown by the fail-closed identity guard). */ export declare class ScanCacheTierError extends Error { constructor(message: string); } declare const CoverageEntrySchema: z.ZodObject<{ dimension: z.ZodString; status: z.ZodEnum<{ missing: "missing"; produced: "produced"; }>; reason: z.ZodOptional; }, z.core.$strict>; export type CoverageEntry = z.infer; declare const DeepScanRecordSchema: z.ZodObject<{ schemaVersion: z.ZodLiteral<1>; deepScanKey: z.ZodString; framework: z.ZodString; sourceId: z.ZodString; treeDigest: z.ZodString; scannerVersion: z.ZodNumber; policyVersion: z.ZodNumber; scannedAt: z.ZodString; dimensionReports: z.ZodArray; reason: z.ZodOptional; findings: z.ZodArray; detail: z.ZodString; coverage: z.ZodEnum<{ complete: "complete"; incomplete: "incomplete"; }>; path: z.ZodOptional; }, z.core.$strict>>; }, z.core.$strict>>; coverage: z.ZodArray; reason: z.ZodOptional; }, z.core.$strict>>; }, z.core.$strict>; export type DeepScanRecord = z.infer; /** * Read the deep-scan cache for `key`. A HIT requires the record to PARSE and its * identity fields (`deepScanKey`, `framework`, `sourceId`, `treeDigest`, * `scannerVersion`, `policyVersion`) to equal the request — the same digest-guard * re-check the fast-scan cache applies. Any mismatch, corruption, or absence is a * MISS (undefined), never a throw. */ export declare function readDeepScanCache(cacheHome: string, input: DeepScanKeyInput): DeepScanRecord | undefined; /** The context an async deep-scanner dimension runs against. */ export interface DeepDimensionContext { treePath: string; runner: Runner; /** Generous per-scan budget; defaults to {@link DEEP_SCAN_TIMEOUT_MS}. */ timeoutMs?: number; } /** * An async deep-scanner dimension. Unlike the fast `DimensionInspector` (synchronous, * in-process), a deep dimension spawns an external tool through the injected runner, * so its `run` is async. It NEVER throws: an unavailable tool, a spawn failure, a * non-zero exit, or unparseable output all resolve to `status: "missing"` with a * reason — never a fabricated pass, never an automatic failure (design §C.3, H9). */ export interface DeepDimensionInspector { dimension: string; run(ctx: DeepDimensionContext): Promise; } /** A generous default deep-scan budget — these tools scan a whole framework tree. */ export declare const DEEP_SCAN_TIMEOUT_MS = 300000; /** * The Cisco AI Defense skill-scanner deep dimension (design §C.3, O6 — `cisco@uvx`). * Runs the pinned `cisco-ai-skill-scanner` console-script (`skill-scanner`) through * `uvx`, OFFLINE, emitting SARIF. Available on AIH-DEV, so it PRODUCES here. Its * output maps deterministically to `trust.cisco-finding` findings; any spawn / exit * / parse failure reports the dimension MISSING (never a fabricated pass). */ export declare const ciscoSkillScannerInspector: DeepDimensionInspector; /** * The SkillSpector deep dimension (design §C.3, O6 — `skillspector@docker`). Requires * docker, which is ABSENT on this VM, so it reports MISSING here (incomplete coverage, * the designed first-class path — never a false green). Where docker exists it runs the * pinned image over the mounted tree and maps its SARIF like the Cisco dimension. */ export declare const skillspectorInspector: DeepDimensionInspector; /** The deep dimensions wired in W7 (O6): cisco (uvx, produced) + skillspector (docker, missing here). */ export declare const DEEP_DIMENSION_INSPECTORS: readonly DeepDimensionInspector[]; /** Input to {@link runDeepScanTier} — the identity, the tree, and the async runner. */ export interface DeepScanTierInput { cacheHome: string; framework: FrameworkId; sourceId: string; treeDigest: string; treePath: string; runner: Runner; /** Defaults to {@link DEEP_DIMENSION_INSPECTORS}. */ inspectors?: readonly DeepDimensionInspector[]; timeoutMs?: number; scannerVersion?: number; policyVersion?: number; } /** Output of {@link runDeepScanTier} — the deep dimensions to fold + the key + hit flag. */ export interface DeepScanTierResult { deepScanKey: string; dimensionReports: DimensionReport[]; coverage: CoverageEntry[]; /** True when the result came from the cache (no deep scanner ran). */ cacheHit: boolean; } /** * Run the deep-scan tier for one source: consult the deep-scan cache first, and on a * HIT return the cached dimensions WITHOUT running any scanner (so a warm second run * spawns nothing — the runner-call capture in the tests proves it). On a MISS, run the * async deep inspectors, write the cache best-effort, and return the produced/missing * dimensions. The returned `dimensionReports` are folded through the SAME * `decide()`/coverage path as the fast dimensions by the caller (the one scan-gate * seam: `runFastScanGate(..., { deepDimensionReports })`). Never throws on scan errors — * a failed dimension is `missing`, i.e. incomplete coverage. */ export declare function runDeepScanTier(input: DeepScanTierInput): Promise; /** The three qualification outcomes recorded for a host (D12). */ export type RuntimeQualResult = "qualified" | "incomplete" | "blocked"; declare const RuntimeQualRecordSchema: z.ZodObject<{ schemaVersion: z.ZodLiteral<1>; runtimeQualKey: z.ZodString; framework: z.ZodString; sourceId: z.ZodString; treeDigest: z.ZodString; scannerVersion: z.ZodNumber; policyVersion: z.ZodNumber; selectedProfile: z.ZodString; adapterVersion: z.ZodNumber; tuple: z.ZodObject<{ claudeCode: z.ZodObject<{ measuredOn: z.ZodString; }, z.core.$strict>; windowsBuild: z.ZodString; windowsUbr: z.ZodOptional; arch: z.ZodString; node: z.ZodString; bun: z.ZodString; ramClassGb: z.ZodNumber; vcpuClass: z.ZodNumber; }, z.core.$strict>; result: z.ZodEnum<{ blocked: "blocked"; incomplete: "incomplete"; qualified: "qualified"; }>; evidence: z.ZodString; qualifiedAt: z.ZodString; }, z.core.$strict>; export type RuntimeQualRecord = z.infer; /** Input to {@link recordRuntimeQualification} — the qualification a provision/acceptance flow writes. */ export interface RecordRuntimeQualificationInput extends RuntimeQualKeyInput { cacheHome: string; result: RuntimeQualResult; evidence: string; } /** * Write a runtime-qualification record (design §C.2). The provision / acceptance flow * calls this AFTER a deep scan qualifies (or fails to qualify) a host — there is no * adapter wiring in this phase; this is the API W8's acceptance flow drives. Returns * the written record (with its key). Best-effort atomic like the deep-scan cache. */ export declare function recordRuntimeQualification(input: RecordRuntimeQualificationInput): RuntimeQualRecord; /** Input to {@link readRuntimeQualification} — the key inputs plus the cache home. */ export interface ReadRuntimeQualificationInput extends RuntimeQualKeyInput { cacheHome: string; } /** * Read a runtime-qualification record. A HIT requires: * 1. the FULL key to match (so a Linux / older-CLI / different-profile / different * adapter-version request computes a different key and cannot find this file), AND * 2. every keyed field on the record to equal the request (digest-guard re-check), AND * 3. the stored tuple to still qualify the REQUESTED tuple under the D16 * {@link classifyTuple} semantics — NOT off-tuple. So a RAM rollback below the * qualified class, or any hard-fact drift the key did not already exclude, MISSES; * the recorded dynamic-memory balloon above the class (version-drift) still HITS. * Any corruption or guard failure is a MISS (undefined), never a throw. Off-tuple can * never satisfy — structurally (the key) and defensively (the guard). */ export declare function readRuntimeQualification(input: ReadRuntimeQualificationInput): RuntimeQualRecord | undefined; export {};