import { z } from "zod"; import type { Posture } from "../config/posture.js"; import { AihError } from "../errors.js"; import type { Runner } from "../internals/proc.js"; import { type TrustFileInventory } from "../trust/inventory.js"; import { type ClosureSpec, type FindingClassification, type HostLoadFacts, type ProfileClosure, type Reachability } from "./closure/profile-closure.js"; import { type BindingDeclaration } from "./schema.js"; import { type TypographyAdvisory } from "./visible-typography.js"; export interface ResolvedGitSource { kind: "git"; repository: string; commitSha: string; treeDigest: string; /** Derived, rebuildable checkout path (machine cache); never recorded identity. */ treePath: string; /** * The exact leaf files (source-relative POSIX paths) folded into `treeDigest`. * Derived from the digest computation — NOT persisted identity — so the scan can * assert it inspected every byte the digest pins (D7 / CM-27 coverage invariant). * Optional only so hand-constructed test sources need not restate it; * {@link resolveGitSource} — the sole production producer — ALWAYS populates it, * so the coverage cross-check is always active on real resolutions. */ files?: readonly string[]; } export interface ResolvedNpmSource { kind: "npm"; package: string; exactVersion: string; integrity: string; /** * Acquisition fields — populated by `acquireNpmTree` (`./npm-source.ts`) once * the EXACT tarball has been SRI-verified, contained-unpacked, and digested. * Absent on a bare identity resolution ({@link resolveNpmSource}); a hand-built * npm source that omits them is not scannable ({@link scannableFromNpm} fails * closed). `treeDigest` uses the SAME tree-hash idiom git uses ({@link * resolveGitSource} via `hashComponentTree`), so an identical tree digests to an * identical value regardless of source kind. `gitHead` is present only when an * `expectedGitHead` provenance assertion was made. */ treeDigest?: string; /** Derived, rebuildable content-addressed tree path (machine cache); not identity. */ treePath?: string; /** The exact leaf files (source-relative POSIX) folded into `treeDigest`. */ files?: readonly string[]; gitHead?: string; } export type ResolvedSource = ResolvedGitSource | ResolvedNpmSource; /** The exact source digest a disposition is bound to (git tree digest / npm integrity). */ export declare function resolvedSourceDigest(resolved: ResolvedSource): string; /** * The re-provision D7 cross-check: a resolved source must match the committed * declaration's exact identity on every field (git: repository/commitSha/ * treeDigest; npm: package/exactVersion/integrity). A first-bind flow has no * declaration yet and simply does not call this; a re-provision that drifts from * the committed authority fails closed. */ export declare function assertResolvedMatchesDeclaration(declaration: BindingDeclaration, resolved: ResolvedSource): void; /** A source made scannable: a content digest plus the on-disk tree to inspect. */ export interface ScannableSource { digest: string; treePath: string; /** * The exact files the digest covers. When present, the gate requires the scan * inventory to cover every one of them; any identity file the inventory did not * see forces coverage INCOMPLETE (fail-closed), so a disposition can never * attest bytes no inspector examined. */ identityFiles?: readonly string[]; } export declare function scannableFromGit(resolved: ResolvedGitSource): ScannableSource; /** * Make an acquired npm source scannable — the npm mirror of {@link * scannableFromGit}. The scannable digest is the SRI `integrity` (what {@link * resolvedSourceDigest} already binds an npm disposition to), NOT the tree digest: * an npm tree and a byte-identical git tree share a `treeDigest` but must NEVER * share a scan-cache entry, so keying on the npm-namespaced integrity keeps them * disjoint by construction. Fails closed if the source was never materialized * (identity-only resolution), so a disposition can never attest an absent tree. */ export declare function scannableFromNpm(resolved: ResolvedNpmSource): ScannableSource; /** Fail-closed scan-gate error (resolution, digest mismatch, forged token, …). */ export declare class BindingScanError extends AihError { constructor(message: string); } /** A capability deferred to a later work item (e.g. a deep external scanner dimension). */ export declare class BindingNotSupportedError extends AihError { constructor(message: string); } /** `/.aih/binding` — repo convention: HOME || USERPROFILE || homedir(). */ export declare function bindingCacheHome(env: NodeJS.ProcessEnv): string; export interface GitResolveRequest { repository: string; /** Resolution input only (ref/tag/branch/HEAD) — never stored as identity (D7). */ ref?: string; /** An exact commit SHA input, skipping the ref round-trip. */ commitSha?: string; /** Optional subset of top-level paths to hash; defaults to all but `.git`. */ declaredPaths?: readonly string[]; } export interface GitResolveDeps { runner: Runner; cacheHome: string; } /** * Resolve a git source to exact identity (D7): an exact 40-char commit SHA (from * a ref or a SHA input), a checkout into the derived cache, and a sha256 tree * digest via {@link hashComponentTree}. Refs and tags are resolution inputs only * and never appear in the returned identity. */ export declare function resolveGitSource(request: GitResolveRequest, deps: GitResolveDeps): Promise; export interface NpmResolveRequest { package: string; version?: string; } export interface NpmRegistryMetadata { version: string; integrity: string; } export type NpmMetadataFetcher = (packageName: string, version: string | undefined) => Promise | NpmRegistryMetadata; /** * Resolve npm identity from injected registry metadata into an exact version + * SRI integrity (D7). The resolved identity is validated against the declaration * schema, so a range/dist-tag version or a non-SRI integrity fails closed. */ export declare function resolveNpmSource(request: NpmResolveRequest, deps: { fetchMetadata: NpmMetadataFetcher; }): Promise; export type ScanVerdict = "allow" | "block"; export type ScanCoverage = "complete" | "incomplete"; export type ScanSeverity = "info" | "low" | "medium" | "high" | "critical"; /** * The two independent (a2) outcomes. `rawSourceScan` describes the WHOLE hashed * tree (honest: findings present or not); `selectedProfileGate` is the actionable * verdict for the selected install/runtime closure — the only one that authorizes * provisioning. `ALLOW_WITH_CONDITIONS` means "allowed because the named accepted * runtime findings were in force" (see {@link assertProvisionAuthorized}). */ export type RawSourceOutcome = "FINDINGS_PRESENT" | "CLEAN"; export type SelectedProfileGate = "ALLOW" | "ALLOW_WITH_CONDITIONS" | "BLOCK"; export interface ScanFinding { code: string; severity: ScanSeverity; detail: string; coverage: ScanCoverage; /** Source-relative POSIX path of the scanned file. Present only on * content-risk findings, where it lets an accepted-baseline entry pin the * finding; inspectors whose findings must never be acceptable (e.g. * malicious-code) deliberately do not set it. */ path?: string; /** sha256 of the scanned file's UTF-8 text, CRLF-normalized to LF — the * acceptance content pin. Normalized so a checkout's platform line-ending * drift (core.autocrlf) cannot void or forge-break an acceptance while any * substantive edit still does. */ contentSha256?: string; /** Set by the policy decision when a maintainer-accepted baseline entry * matched this finding exactly (code + path + content hash). */ accepted?: boolean; /** Closure classification of this finding's file — present ONLY when a closure * spec was applied. Absent ⇒ legacy full-tree behavior (every finding blocks). */ classification?: FindingClassification; /** Precise reachability under {@link classification} (audit granularity; * `unknown` rolls up INTO `closure` for the gate but is disclosed distinctly). */ closureReachability?: Reachability | "non-materialized"; /** Gate-layer visible-typography demotion overlay (W5 rule-8): present ONLY on a * `trust.hidden-unicode` finding whose file is ALL advisory typography under a * seeded closure. The raw `severity` stays "high" (raw counts unaffected); a * finding carrying this is treated as NON-gating by {@link decide}. */ advisory?: TypographyAdvisory; } export interface DimensionInspectionContext { treePath: string; inventory: TrustFileInventory; } export interface DimensionReport { dimension: string; status: "produced" | "missing"; reason?: string; findings: readonly ScanFinding[]; } export interface DimensionInspector { dimension: string; run(ctx: DimensionInspectionContext): DimensionReport; } /** * The W2 fast-inspection registry. Every one of the eleven D12 FAST-tier * dimensions is genuinely inspected here (native `src/trust/` seams for content * and execution; inventory-driven checks for structure/scripts/binaries/hooks/ * MCP/licenses; bounded static pattern scans for network/telemetry/write * destinations). Deep external scanners (W7) add SEPARATE dimensions later; the * incomplete-coverage machinery in {@link runFastScanGate} exists for those. */ export declare const W2_DEFAULT_INSPECTORS: readonly DimensionInspector[]; export interface InspectTreeDeps { inspectors?: readonly DimensionInspector[]; inventoryFactory?: (root: string) => TrustFileInventory; } /** * Run the fast inspection over an on-disk tree, returning one report per D12 * dimension. Split out from {@link runFastScanGate} so the inspection is * independently testable and reusable; the gate applies policy on top. */ export declare function inspectTree(treePath: string, deps?: InspectTreeDeps): DimensionReport[]; declare const scanDispositionBrand: unique symbol; /** * The rule-9 five-way disclosure: raw findings, closure findings, inert findings, * accepted runtime findings, and residual risk — each counted separately so a * Framework Card can show them without conflation. Computed on every decision; * for a legacy (no-closure) disposition, `closureFindings` is the whole set and * `inertFindings` is empty. */ export interface FrameworkCardDisclosure { rawFindings: { total: number; high: number; bySeverity: Record; }; closureFindings: { total: number; high: number; unknownReachability: number; }; inertFindings: { total: number; high: number; }; acceptedRuntimeFindings: { total: number; }; /** Rule-8 visible-typography demotions: high `trust.hidden-unicode` findings whose * file is all advisory typography, reported (non-blocking) rather than accepted. */ visibleTypographyAdvisories: { total: number; files: number; }; residualRisk: { blockingUnaccepted: number; unknownReachability: number; inertReported: number; }; } export interface ScanDisposition { readonly [scanDispositionBrand]: "ScanDisposition"; readonly digest: string; /** Legacy verdict, always derived: `selectedProfileGate === "BLOCK" ? "block" : "allow"`. */ readonly verdict: ScanVerdict; readonly findings: readonly ScanFinding[]; readonly posture: Posture; readonly producedAt: string; /** Whole-tree outcome (descriptive; never authorizes on its own). */ readonly rawSourceScan: RawSourceOutcome; /** The actionable gate for the selected profile — what `assertProvisionAuthorized` reads. */ readonly selectedProfileGate: SelectedProfileGate; /** Closure identity + conditions; absent for a legacy full-tree disposition. */ readonly closure?: { profile: string; classifierVersion: number; closureDigest: string; hostFactsDigest: string; /** Acceptance keys that neutralized a blocking ≥high finding — the conditions * `ALLOW_WITH_CONDITIONS` requires still be in force at provision time. */ requiredAcceptanceKeys?: readonly string[]; }; readonly disclosure: FrameworkCardDisclosure; } /** * One maintainer-accepted content finding, pinned to the exact file content: * `fileSha256` is the sha256 of the file's UTF-8 text, so ANY edit to the file * voids the acceptance and the finding blocks again until re-reviewed. * `repository` is audit metadata only — the match key is (code, path, * fileSha256), which is already content-exact without it. * * `profile` scopes the entry to ONE selected-profile closure (a2): a scoped entry * only applies under that profile and never neutralizes a finding in a file * outside its blocking closure. ABSENT ⇒ the entry applies under any closure (the * W4 full-tree default, so pre-a2 entries need no `profile`). Because an inert * finding never gates, an acceptance whose file is out-of-closure is structurally * incapable of widening the gate — enforced by construction, not by a check. */ export interface AcceptedContentFinding { repository: string; code: string; path: string; fileSha256: string; profile?: string; /** Audit metadata (rule-8): the human-reviewed class this acceptance falls under * (e.g. EXPECTED_SKILL_WORKFLOW_CONTROL). Does NOT affect the match key. */ acceptanceClass?: string; /** Audit metadata (rule-8): the runtime conditions the acceptance is contingent on. */ conditions?: readonly string[]; } declare const ScanAcceptanceArtifactSchema: z.ZodObject<{ schemaVersion: z.ZodLiteral<2>; reason: z.ZodString; accepted: z.ZodArray; acceptanceClass: z.ZodOptional; conditions: z.ZodOptional>; }, z.core.$strict>>; }, z.core.$strict>; export type ScanAcceptanceArtifact = z.infer; /** * Parse the shipped scan-acceptance artifact. Fail-closed: a malformed * artifact yields ZERO acceptances (the gate stays at full strictness), never * a widened gate — and the artifact's own unit test fails loudly on shape * drift so a malformed ship cannot go unnoticed. */ export declare function readScanAcceptanceArtifact(): ScanAcceptanceArtifact; /** Read-only hygiene report of an acceptance set against a computed closure. */ export interface ScanAcceptanceReport { /** Entries (matching the closure's profile, or unscoped) whose file IS in the blocking closure. */ applicable: readonly AcceptedContentFinding[]; /** Scoped/unscoped entries whose file is absent or inert for this closure — reported, never * auto-deleted, and already harmless (an out-of-closure acceptance cannot widen the gate). */ staleOutOfClosure: readonly AcceptedContentFinding[]; } /** * Compare an acceptance set against a profile closure (pure — never writes). * Entries scoped to a DIFFERENT profile are skipped entirely; the remainder split * into `applicable` (file present AND in the blocking closure) and * `staleOutOfClosure` (file removed, or materialized-inert). Mirrors the * acceptance hygiene model: staleness is a note, not a gate change — the gate * already fails toward "the entry does nothing." */ export declare function scanAcceptanceReport(closure: ProfileClosure, accepted?: readonly AcceptedContentFinding[]): ScanAcceptanceReport; export interface FastScanPolicy { posture: Posture; /** Only meaningful at vibe: permit provisioning despite incomplete coverage. */ allowIncompleteAtVibe?: boolean; /** * Maintainer-accepted content findings; when absent, the shipped * scan-acceptance artifact applies. Acceptance can only match a finding * that carries a content pin (path + contentSha256 — content-risk findings * only) and never reaches critical severity. */ acceptedFindings?: readonly AcceptedContentFinding[]; /** * The selected-profile closure spec (a2). ABSENT ⇒ legacy full-tree behavior: * every finding blocks exactly as before. PRESENT ⇒ closure-aware disposition: * findings are classified and only closure (+ unknown-reachability) findings gate. */ closureSpec?: ClosureSpec; /** Injected host-load facts for the closure's model-load axis. Absent ⇒ fail closed. */ hostFacts?: HostLoadFacts; /** * Phase-2 (W7 §C) deep-scanner dimensions, PRE-COMPUTED by the caller. The deep * scanners are ASYNC (they spawn uvx/docker through a runner) and cannot run inside * this synchronous gate, so the provision/doctor flow runs them via * `scan-cache-tiers.ts` (`runDeepScanTier` — which consults and writes the deep-scan * cache) and passes the produced/missing dimensions here. They are folded through the * SAME {@link decide} / coverage path as the fast dimensions: a `missing` deep * dimension yields an incomplete-coverage finding exactly like a missing fast * dimension. ABSENT ⇒ byte-identical to the pre-Phase-2 gate — the exact same report * array is decided, so every existing caller is unaffected. Deep tiers are opt-in; * NOTHING scans at session start. */ deepDimensionReports?: readonly DimensionReport[]; } export interface FastScanDeps { cacheHome: string; inspectors?: readonly DimensionInspector[]; inventoryFactory?: (root: string) => TrustFileInventory; } /** * Run the fast scan for a scannable source and produce a brand-protected * disposition. The inspection reports (the expensive part) are cached per exact * digest under the derived scan cache; the cheap policy decision re-runs each * call so posture changes are honored. Deleting the cache only forces a * recompute — it never changes the validation outcome. */ export declare function runFastScanGate(source: ScannableSource, policy: FastScanPolicy, deps: FastScanDeps): ScanDisposition; /** * The gate every adapter's `provision` MUST pass before running any upstream * code: the disposition must be genuine (branded by this module), its digest must * equal the EXACT resolved source digest being provisioned, and its * SELECTED-PROFILE gate must authorize. `BLOCK` fails closed; `ALLOW` passes; * `ALLOW_WITH_CONDITIONS` passes only when every condition it named is still in * force — i.e. each `requiredAcceptanceKeys` entry corresponds to a finding the * disposition actually carries as accepted (a tamper/integrity check). A legacy * disposition with no `selectedProfileGate` falls back to its `verdict`. */ export declare function assertProvisionAuthorized(disposition: ScanDisposition, expectedDigest: string): void; export {};