@clear-capabilities/agentic-security-scanner 0.189.0 → 0.191.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- f5b8304272ffd3846d405b461331e001a6209c349b48d83353c266eeb34b9460 agentic-security.mjs
1
+ 2bc7bee99ef91ceb0fa07627f9983bb07c36827213e3876e898dac7062532e87 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.189.0",
3
+ "version": "0.191.0",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -93,7 +93,7 @@
93
93
  "test:lineage": "node --test test/lineage/analysis-completion.test.js test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js test/lineage/runtime-assurance.test.js test/lineage/runtime-edge-states.test.js test/lineage/runtime-explorer-view.test.js test/cli/runtime-explorer-cli.test.js test/cli/runtime-identity-cli.test.js test/cli/runtime-twin-cli.test.js test/lineage/cut-planner.test.js test/lineage/scc.test.js test/lineage/scc-solver.test.js test/graph/graph-views.test.js test/lineage/dag07-completion.test.js test/lineage/owned-build.test.js test/dataflow/boundary-propagation.test.js test/lineage/boundary-contract.test.js test/lineage/correlation-v2.test.js test/lineage/path-control-evidence.test.js test/lineage/smt-z3-backend.test.js test/cli/dag-commands.test.js",
94
94
  "test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js test/proof-corpus-lib.test.js test/proof-corpus-runner.test.js",
95
95
  "test:server": "node --test test/server/graph-loader.test.js test/server/security.test.js test/server/routes.test.js test/server/http-server.test.js test/server/cmd-explore.test.js test/server/cmd-dataflow-export.test.js test/server/static-assets.test.js test/server/dataflow-scenario-cli.test.js test/server/dataflow-impact-cli.test.js",
96
- "test:haskell": "node --test test/haskell/haskell-reachability.test.js test/haskell/haskell-alias-taint.test.js test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js test/haskell/haskell-boundary-propagation.test.js test/haskell/haskell-generalization.test.js test/haskell/haskell-trust-boundary.test.js test/haskell/haskell-comment-invariance.test.js test/haskell/haskell-remediation-classes.test.js test/haskell/haskell-trust-boundary-frameworks.test.js test/haskell/haskell-generic-detectors.test.js",
96
+ "test:haskell": "node --test test/haskell/haskell-reachability.test.js test/haskell/haskell-alias-taint.test.js test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-remediation-0190.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js test/haskell/haskell-boundary-propagation.test.js test/haskell/haskell-generalization.test.js test/haskell/haskell-trust-boundary.test.js test/haskell/haskell-comment-invariance.test.js test/haskell/haskell-remediation-classes.test.js test/haskell/haskell-trust-boundary-frameworks.test.js test/haskell/haskell-generic-detectors.test.js",
97
97
  "test:nix": "node --test test/nix/nix-option-types.test.js test/nix/nixos-hardening-daemon.test.js test/nix/nixos-hardening-extended.test.js test/nix/nix-build-trust.test.js test/nix/nix-eval-isolation.test.js test/nix/nix-input-inventory.test.js test/nix/nix-parser-ir.test.js test/nix/nix-remediation.test.js test/nix/nix-resolved-closure.test.js test/nix/nix-sca-patches.test.js test/nix/nix-sca-severity.test.js test/nix/nix-haskell-closure-names.test.js test/nix/nix-script-taint.test.js test/nix/nix-secrets-store.test.js test/nix/nixos-hardening.test.js test/nix/nixos-module-resolution.test.js test/nix/nix-fix-multifile.test.js test/nix/nixos-evaluator-forms.test.js test/nix/nix-advisory-feed.test.js test/nix/nix-fix-prewrite.test.js test/nix/nix-boundary-scope.test.js",
98
98
  "test:language": "node --test test/bench-haskell-precision.test.js test/bench-independent-haskell-panel.test.js test/language/capability-ledger.test.js test/language/language-accuracy-regression.test.js test/language/language-aibom.test.js test/language/language-assurance.test.js test/language/language-bom.test.js test/language/language-compliance.test.js test/language/language-contracts.test.js test/language/language-corpora-integrity.test.js test/language/language-discovery.test.js test/language/language-doc-coverage.test.js test/language/language-evidence-posture.test.js test/language/language-explorer.test.js test/language/language-gap-fixes.test.js test/language/language-gap-fixes-2.test.js test/language/language-governance.test.js test/language/language-integrations.test.js test/language/language-llm-agent.test.js test/language/language-local-ai.test.js test/language/language-parity-haskell.test.js test/language/language-polyglot-bridges.test.js test/language/language-privacy-lineage.test.js test/language/language-proof-remediation.test.js test/language/language-report-formats.test.js test/language/language-secrets.test.js test/language/language-trust-boundary.test.js",
99
99
  "test:language-stress": "node --test --test-concurrency=1 test/language/language-stress-offline.test.js",
@@ -7,7 +7,7 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
7
7
  ## Invariants (the ones you will get wrong without reading)
8
8
 
9
9
  - **Static by default.** A default scan never runs GHC, Cabal, Stack, `nix`, `Setup.hs`, Template Haskell or a build. Compile
10
- checks (`haskell-fix.js` `compileGate`) and evaluation (`nix-eval-isolation.js`) are opt-in, and a tool that is absent is a
10
+ checks (`haskell-fix.js` `compileGate`: requested by the FIX PREVIEW and by `validateHaskellFix({compile:true})`, never by a scan) and evaluation (`nix-eval-isolation.js`) are opt-in, and a tool that is absent is a
11
11
  stated `not-run` / `unsupported`, never a pass. A required criterion that needs a tool **fails** (and the support registry
12
12
  marks the capability `blocked`); it never skips.
13
13
  - **Never derive an answer from ground truth.** The engine must not read the corpus under `test/language/corpora/`, fixture
@@ -72,7 +72,12 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
72
72
  - **"No caller can exist" is a verdict with preconditions, and `null !== false`.** `haskell-ir.js` `projectReachability` stamps `fn.hs.reachability {status, rootKind, holeReason}` (`entrypoint-reachable | unreachable-in-project | external-api | unknown`); `haskell-reachability.js` `applyHaskellReachability` copies it onto each IR-TAINT finding as `f.reachability {status, reason, premise, roots, holeReason}`, a record SEPARATE from `trustBoundary` (that one says who controls the input, this one whether a caller can exist; either may be `unknown`). `unreachable-in-project` needs ALL of: `completeScan` (runScan/engine thread it; a changed-file, PR, subdirectory or caller-supplied scan is a subset, under which every export looks uncalled), no `policy-excluded`/`size-skipped` Haskell, `entrypointCount > 0` (a `main`; the sealed single-file corpora have none, so the rule is structurally inert on them), `exposedModules !== null` (`haskell-manifests.js` `exposedModuleIndex`: cabal `exposed-modules`, or hpack `exposed-modules`; an hpack library that exposes everything, `-main-is`, `-F/-pgmF`, a plugin, a non-test `build-tool-depends`, a foreign library, Backpack signatures are all `null`), the function's module not exposed (re-exports count: `reexportClosure`), not an instance method (instance methods are ROOTS, so what they call is reached, but their own verdict is `unknown/instance-method`), and not reached from a root. Roots: every `main`, framework-handler-typed functions (`hs.entryHandler`), `foreign export`s, instance methods, exported functions of exposed modules, and `withheld-caller` roots. Gaps are bounded where they can be and global where they cannot: Template Haskell/quasi-quotes the allow-lists did not accept (`preprocessForSyntaxInfo(...).opaque`), a foreign export, the function cap, an unresolved name nothing defines = global holes; a declaration the parser withheld or an expression it could not lower makes that module's own functions and the exported functions of every module it can name (re-export closure) roots, because withheld code can call only what its module can see. A call through a function VALUE is not a gap (its callee was mentioned in value position, which is a reference the walk follows). Holes are `unknown`, never `unreachable`; the order of `holeReason` is `HOLE_PRIORITY`. Pinned in `test/haskell/haskell-reachability.test.js`; the pin at `haskell-trust-boundary.test.js` (a lone `Svc.hs`) asserts `unknown/no-entrypoint`. **Severity:** only `unreachable-in-project` with `premise === 'caller-controlled-parameter'` (every recorded origin is the weak parameter label) is lowered to `info` (`lowerFinding`, guard `_reachLowered`, composes with `_trustLowered`); a finding with a recorded read keeps its severity and may only rank lower (`posture/relevance.js` `languageVerdict`). The verdict is recomputed in place per context (`buildHaskellIR(files, {manifests, completeScan, scanHoles})`; the BUILD cache is keyed by sources alone and the manifests are parsed once per context, counted by `EXPOSED_INDEX_WORK`). `fn.hs.reachedBy` and `fn.hs.paramEntry` record how a function was reached and what its parameters were before `paramAnnotations` is removed; the removal itself is unchanged. `callersUnknown(fn)` (`haskell-security-rules.js`) reads the verdict for the partial-function and unbounded-read rules; `external-api` is deliberately answered as before (turning it on added seven flagged sites and no true positive on the development set). Nothing reads a comment, a name or a path. Measured effect on real code is in `bench/dag/dev-haskell/results/README.md` (it changed no severity there) and the recorded baseline is gated by `bench/haskell-precision`.
73
73
  - **Comments never change a verdict, an id or a cluster.** `haskell-comments.js` finds Haskell comments lexically; a finding's `snippet` is `codeOfLine(line)` (code only), because the snippet feeds the stable id and the clone-shape hash, and both changed when someone edited a trailing comment. `test/haskell/haskell-comment-invariance.test.js` scans the examples and corpus-shaped cases as written, with every comment blanked, and with every comment (and a trailing comment on every line) replaced by text that lies in each direction; `bench/dag/dev-haskell/comment-invariance.mjs` does the same to a real project. An engine that varies is wrong.
74
74
  - **Rules that read the function around a call** (`haskell-security-rules.js`): `hs-weak-hash` is a vulnerability only where `weakDigestUse` finds a use that depends on the digest's strength (compared, hashed secret, credential-shaped binding or function, handed to a signing/verifying call); otherwise it is emitted at `info`. `hs-unbounded-input-read` needs a socket handle, or standard input in a function nothing calls (`readsOutsideInput`). Partial-function and allocation rules use a parameter as caller-supplied only for a framework-handler parameter or an uncalled function (`fn.hs.reached`). A salt is not an unpredictability-sensitive value. A cookie whose whole value is constant is not a credential. Own-property lookups only (`Object.hasOwn`): `constructor` and `toString` are legal Haskell names and were reported as "Unsafe boundary: function Object() { [native code] }".
75
- - **Fixes name the library the file uses.** `sqlDriverOf` picks `Database.PostgreSQL.Simple` or `Database.SQLite.Simple` from the file's own imports and refuses anything else; `hs-html-escape` needs blaze-html or a re-exporter; `hs-hash-sha256` refuses when the algorithm is named on another line (a type); `hs-path-takefilename` rewrites only a String/FilePath parameter spliced under a fixed directory in a FILE operation. Classes with no safe rewrite refuse with a class-level reason (`CLASS_REASONS`), `proposal: 'model-assisted'`.
75
+ - **Fixes name the library the file uses.** `sqlDriverOf` picks `Database.PostgreSQL.Simple` or `Database.SQLite.Simple` from the file's own imports and refuses anything else; `hs-html-escape` needs blaze-html or a re-exporter; `hs-hash-sha256` refuses when the algorithm is named on another line (a type); `hs-path-takefilename` rewrites only a String/FilePath parameter spliced under a fixed directory in a FILE operation. Classes with no safe rewrite refuse with a class-level reason (`CLASS_REASONS`), `proposal: 'model-assisted'`, `tier: 'guidance-only'`, and every reason ends `The decision needed: ...` (returned as `decision`). Never invent a library API: the TLS, cookie, entropy and wai shapes were written against the package documentation and their tests type-check against DECLARED STUBS of that API shape, which proves coherence with the documented shape, not a build against the real package.
76
+ - **Haskell plans carry `tier`, `consequences` and (for attribute fixes) `expectEffective`.** `planHaskellFix` returns `tier: 'full-source-edit'` and a `consequences` array for every plan (a patch that changes behavior must say how: stored digests, rejected connections, HTTP-only cookies, a request-body limit), the way `nix-fix.js` does. Ten fixers: the six rewrites above plus `hs-tls-validate` (CWE-295, the `connection` package's `TLSSettingsSimple` literal `True` to `False`; OpenSSL `VerifyNone` refused), `hs-cookie-attributes` (CWE-1004, four framework shapes; the scotty helper is written out as the record update because the wrapped form is invisible to the rule), `hs-random-os-entropy` (CWE-338, only `name = randomIO` of type `IO <fixed-width integer>` in a file that already imports `System.Entropy`) and `hs-request-body-bound` (CWE-770, a MITIGATION: wai `strictRequestBody` becomes a bounded reader defined in the file, with a 1 MiB limit that is a declared default, not a measured need). Password hashing (CWE-916), SSRF, argument injection, missing auth and hard-coded secrets keep refusing and name the decision; do not add a KDF fixer without a migration story for stored hashes.
77
+ - **The effective-change check is not the rescan.** `haskellEffectiveCheck` (wired as the lifecycle's `effectiveCheck`) re-reads the intended attribute from the patched file with readers independent of the editor (`tlsSettingsOf`, `cookieSitesOf`, call counts) and fails when the planned site did not change or another site did. A cookie renamed to a non-session name silences the rule without adding an attribute; the check says so (`haskell-remediation-0190.test.js`).
78
+ - **The compile gate is requested by the preview.** `languageFixPreview` passes `compile: true` (`compile: false` opts out). `compileGate` type-checks only the patched file(s) (`targets`; local imports found through the `module` header's source root) and takes the original tree as `baseline`: a project that does not type-check on this host (a package it lacks), or a patch that imports a module this host lacks, is `{ran:false, ok:null}` (not-run), a patch that breaks a compiling file is `{ran:true, ok:false}`. A missing GHC is not-run, never a pass. `requireCompile` turns not-run into a block.
79
+ - **Witnesses.** `witness.js` `haskellFixWitness(finding)` runs the original and the patched file under `ghc -e ':load *File' -e 'fn "<payload>"'` in the sandbox and judges them with `judgeFixWitness`. It is built only for command-injection fixes in a top-level `String -> IO` function whose file imports only `bootModuleSet` modules; every other class returns `verified: false` with the reason (a TLS peer, a framework response header, a database, a digest, a distribution cannot be shown by a program GHC runs alone). Opt in with `languageFixPreview(..., { witness: true })`.
80
+ - **A supplied patch goes through the same gates.** `verifyHaskellProposal` / `verifyNixProposal` are reached from `languageProposalPreview` (context.js) and the CLI `fix --finding <id> --proposal <file>`; verify-only, never writes.
76
81
  - **Pragma** (`pragma.js`): comment-aware, line-scoped, exact rule-id match, logged. A line-less finding cannot be suppressed.
77
82
  - **Scale: no per-use scan of the project.** Pandoc and cabal-install (hundreds to thousands of modules) did not finish a scan because of four superlinear costs, now fixed; do not reintroduce the shapes. (1) `haskell-ir.js`: anything a lowering step needs "from every module" (a constructor by name, the record types, a definition by qid, a name's resolution) is an index on `Project` / a per-module memo (`findCon`, `recordTypes`, `defByQid`, `RESOLVE_MEMO`), built once, never a loop over `proj.modules` per use. Only a top-level resolution (depth 0) is memoised: a resolution inside a re-export chain depends on the path taken. (2) `aibom.js` looks a file's imports up in a map, not `Object.values(perFile).find`. (3) `sast/pipeline.js`: the workflow-wide `env:` secret rule is a one-pass matcher (`scanEnvSecrets`), because the single regular expression it replaced backtracked catastrophically on a comment-blanked workflow (blanking leaves runs of blank lines). A detector expression that runs on comment-blanked text must not let `\s*`, `\n` and a lazy line loop claim the same blank lines. (4) `dataflow/engine.js` `exprTaint`: a call's argument taint is computed once per visit (`_argTaint`); computing it twice re-walked the subtree and cost 2^N for N nested calls. Guards: `test/pipeline-env-secret.test.js`, `test/dataflow-expr-taint-scaling.test.js`, `test/haskell/haskell-ir-scaling.test.js`, `test/mark-used-vuln-functions.test.js` compare work counters between two sizes, never a clock.
78
83
 
@@ -10,8 +10,8 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { STATE_DIR_NAME } from '../posture/state-dir.js';
12
12
  import { isLanguageSource, isLanguageManifest, isLanguageExcludedPath, buildImportGraph, importClosure } from './discovery.js';
13
- import { planHaskellFix, validateHaskellFix, unifiedDiff as hsDiff } from './haskell-fix.js';
14
- import { planNixFix, validateNixFix, planNixUpgrade } from './nix-fix.js';
13
+ import { planHaskellFix, validateHaskellFix, verifyHaskellProposal, unifiedDiff as hsDiff } from './haskell-fix.js';
14
+ import { planNixFix, validateNixFix, verifyNixProposal, planNixUpgrade } from './nix-fix.js';
15
15
  import { planHaskellUpgrade, unifiedDiff as _hsDiff } from './haskell-fix.js';
16
16
  import { writeWithBackup } from './fix-lifecycle.js';
17
17
  import { recordExternalFix } from '../posture/fix-history.js';
@@ -98,12 +98,16 @@ export function addedDatabaseImports(plan) {
98
98
  /**
99
99
  * A READ-ONLY fix preview for a Haskell/Nix finding: the planned patch, its tier, and what the verification gates say.
100
100
  * Never writes. `files` is the project's language sources (see loadLanguageProject).
101
+ *
102
+ * For Haskell the compile gate is requested (`compile: true`) unless the caller passes `compile: false`: GHC type-checks the patched file
103
+ * against the original (a host without GHC, or without the project's packages, records the gate as not-run, never as a pass).
104
+ * `witness: true` additionally runs the controlled Haskell fix witness where one can honestly be built (see witness.js `haskellFixWitness`).
101
105
  */
102
- export async function languageFixPreview(finding, files, { apply = false, root = null } = {}) {
106
+ export async function languageFixPreview(finding, files, { apply = false, root = null, compile = true, witness = false } = {}) {
103
107
  const lang = languageOfFinding(finding);
104
108
  if (!lang) return { ok: false, status: 'unsupported', reason: 'not a Haskell or Nix finding' };
105
109
  const plan = lang === 'haskell' ? planHaskellFix(finding, files) : planNixFix(finding, files);
106
- if (!plan || plan.ok === false) return { ok: false, status: (plan && plan.status) || 'unsupported', tier: (plan && plan.tier) || null, reason: (plan && plan.reason) || 'no deterministic fix', proposal: (plan && plan.proposal) || null };
110
+ if (!plan || plan.ok === false) return { ok: false, status: (plan && plan.status) || 'unsupported', tier: (plan && plan.tier) || null, reason: (plan && plan.reason) || 'no deterministic fix', decision: (plan && plan.decision) || null, proposal: (plan && plan.proposal) || null };
107
111
  if (lang === 'haskell') {
108
112
  // A static rescan cannot see a wrong library, so this is decided here, before any gate can call the patch verified.
109
113
  const added = addedDatabaseImports(plan);
@@ -112,7 +116,13 @@ export async function languageFixPreview(finding, files, { apply = false, root =
112
116
  }
113
117
  }
114
118
  let res;
115
- try { res = lang === 'haskell' ? await validateHaskellFix(finding, { files, apply, root }) : await validateNixFix(finding, { files, apply, root }); } catch (e) { res = { status: 'blocked', reason: `verification failed: ${String((e && e.message) || e)}` }; }
119
+ try {
120
+ if (lang === 'haskell') {
121
+ let witnessHook;
122
+ if (witness) { const w = await import('./witness.js'); witnessHook = w.haskellFixWitness(finding); }
123
+ res = await validateHaskellFix(finding, { files, apply, root, compile: compile !== false, witness: witnessHook });
124
+ } else res = await validateNixFix(finding, { files, apply, root });
125
+ } catch (e) { res = { status: 'blocked', reason: `verification failed: ${String((e && e.message) || e)}` }; }
116
126
  return {
117
127
  ok: res.status === 'verified' || res.status === 'applied',
118
128
  status: res.status, applied: res.applied === true, backup: res.backup || null,
@@ -126,6 +136,23 @@ export async function languageFixPreview(finding, files, { apply = false, root =
126
136
  };
127
137
  }
128
138
 
139
+ /**
140
+ * Runs a patch SUPPLIED BY SOMEONE ELSE (a person, a model) through the same gates as a deterministic fix, for one Haskell or Nix finding:
141
+ * syntax, a rescan in which the original finding is gone and nothing new appears at medium or above, and (Haskell) a GHC type-check against the
142
+ * original. `proposal` is the full new text of `file`. Never writes. This is what `agentic-security fix <id> --proposal <file>` calls.
143
+ */
144
+ export async function languageProposalPreview(finding, files, { file, proposal, compile = true }) {
145
+ const lang = languageOfFinding(finding);
146
+ if (!lang) return { ok: false, status: 'unsupported', reason: 'not a Haskell or Nix finding' };
147
+ if (!file || typeof proposal !== 'string') return { ok: false, status: 'blocked', reason: 'a proposal needs the file it replaces and its full new text' };
148
+ if (typeof files[file] !== 'string') return { ok: false, status: 'blocked', reason: `${file} is not in the project tree` };
149
+ let res;
150
+ try { res = lang === 'haskell' ? await verifyHaskellProposal(finding, { files, file, proposal, compile: compile !== false }) : await verifyNixProposal(finding, { files, file, proposal }); }
151
+ catch (e) { res = { status: 'blocked', reason: `verification failed: ${String((e && e.message) || e)}` }; }
152
+ const ok = res.status === 'verified';
153
+ return { ok, status: res.status, applied: false, label: 'WORKAROUND', tier: ok ? 'proposal-verified' : 'blocked', file, before: files[file], after: proposal, diff: res.preview || hsDiff(file, files[file], proposal), gates: res.gates || null, reason: ok ? null : (res.reason || null), explanation: 'A patch supplied by someone else, run through the same gates as a deterministic fix; it is not a rewrite this tool derived.', consequences: ['Not derived by the scanner: its consequences are the author\'s to state.'] };
154
+ }
155
+
129
156
  /** A minimal line-range text edit turning `before` into `after` (for an LSP/editor workspace edit). */
130
157
  export function minimalEdit(before, after) {
131
158
  const a = before.split('\n'); const b = after.split('\n');