@clear-capabilities/agentic-security-scanner 0.161.0 → 0.162.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/agentic-security.js +11 -2
  3. package/dist/4113.index.js +59 -8
  4. package/dist/5613.index.js +500 -0
  5. package/dist/6239.index.js +469 -0
  6. package/dist/7838.index.js +38 -2
  7. package/dist/8798.index.js +1 -1
  8. package/dist/9207.index.js +5 -0
  9. package/dist/9560.index.js +36 -22
  10. package/dist/{1905.index.js → 9999.index.js} +265 -8
  11. package/dist/agentic-security.mjs +2 -2
  12. package/dist/agentic-security.mjs.sha256 +1 -1
  13. package/package.json +9 -4
  14. package/src/discovery/CLAUDE.md +9 -0
  15. package/src/discovery/index.js +28 -0
  16. package/src/discovery/memory.js +7 -5
  17. package/src/fix/apply-fix-service.js +33 -3
  18. package/src/mcp/CLAUDE.md +9 -1
  19. package/src/mcp/tools.js +17 -4
  20. package/src/posture/CLAUDE.md +174 -0
  21. package/src/posture/assurance/baseline-inventory.js +218 -0
  22. package/src/posture/assurance/baseline.js +322 -0
  23. package/src/posture/assurance/bounded-io.js +145 -0
  24. package/src/posture/assurance/config.js +299 -0
  25. package/src/posture/assurance/contracts.js +315 -0
  26. package/src/posture/assurance/identity.js +62 -0
  27. package/src/posture/assurance/migrations.js +259 -0
  28. package/src/posture/assurance/schema-kit.js +111 -0
  29. package/src/posture/assurance/verification-record.js +258 -0
  30. package/src/posture/attestation.js +5 -0
  31. package/src/posture/autopilot.js +28 -1
  32. package/src/posture/evidence-bundle.js +93 -7
  33. package/src/posture/execution-proof.js +11 -1
  34. package/src/posture/fix-verify-loop.js +20 -6
  35. package/src/posture/fix-verify.js +37 -2
  36. package/src/posture/oracles/adapters.js +360 -0
  37. package/src/posture/oracles/conformance.js +275 -0
  38. package/src/posture/oracles/oracle.js +433 -0
  39. package/src/posture/oracles/registry.js +76 -0
  40. package/src/posture/oracles/scenario-classes.js +223 -0
  41. package/src/posture/replay/replay.js +249 -0
  42. package/src/posture/verification/advisory-state.js +71 -0
  43. package/src/posture/verification/emit.js +257 -0
  44. package/src/posture/verification/hypothesis-promotion.js +211 -0
  45. package/src/posture/verification/patch-negative.js +214 -0
  46. package/src/posture/verification/patch-promotion.js +105 -0
  47. package/src/posture/verification/projection.js +183 -0
  48. package/src/posture/verification/repair-records.js +124 -0
  49. package/src/posture/verifier.js +16 -0
  50. package/src/report/index.js +49 -0
  51. package/src/sandbox/CLAUDE.md +41 -0
  52. package/src/sandbox/backend-namespace.js +7 -0
  53. package/src/sandbox/backend-userspace.js +68 -29
  54. package/src/sandbox/control-probes.js +215 -0
  55. package/src/sandbox/supervise.js +201 -0
  56. package/src/sandbox/trust-boundary.js +113 -0
  57. package/src/sandbox/trust-domains.js +171 -0
@@ -96,6 +96,8 @@ Wired in `bin/agentic-security.js` after every filter and after `makeDeterminist
96
96
 
97
97
  **`verifyRunAttestation` now has two real callers.** `agentic-security verify-attestation <file>` auto-detects whether the given JSON is an evidence bundle (`.finding`+`.signature`, verified via `evidence-bundle.js`'s Ed25519 path, unchanged) or a run attestation (`.digest`+`.canonicalisation`, either bare or embedded under a full `last-scan.json`'s `.attestation` field) and dispatches accordingly. A run attestation isn't self-contained the way a bundle is — verifying it means re-scanning the project (`--against <path>`, default `.`) and confirming the fresh scan reproduces the attested digest, which is the actual, meaningful claim this artifact makes ("does this codebase, scanned now, match what was attested earlier"). Separately, `scripts/release-check.mjs`'s `attestation-self-check` gate round-trips a synthetic finding set through compute→verify (and a mutated copy through verify, which must fail) on every release, catching a broken canonicalisation or signing path before it ships — independent of whether any project ever calls `verify-attestation` on a real artifact.
98
98
 
99
+ **Issuer and trust basis (CORE-003.AC03).** `evidence-bundle.js` signs an `issuance` block inside the canonical bytes (only when present, so pre-existing bundles keep their signatures): issuer id and key fingerprint, `trustBasis: 'self-issued-local-key'`, `independentlyCertified: false`, and a plain statement. `verifyEvidenceBundle` returns `trustBasis`/`issuer`/`trustNote`; a bundle with no `issuance` verifies and is reported `none-declared` (never as trusted); an unknown basis, an `independentlyCertified: true` claim, or an issuer fingerprint that is not the verifying key is rejected even when validly signed. The only basis this build can verify is the local one: a third-party basis needs a trust-root policy that does not exist here. A signed run attestation carries the same self-issued label. `verify-attestation` exit codes are unchanged. Signing keys are protected from workers by `sandbox/trust-boundary.js` (see `sandbox/CLAUDE.md`). Suite: `test/evidence-issuer.test.js`.
100
+
99
101
  **Integrity + signing** — `integrity.js` (per-install HMAC for `last-scan.json`), `rule-pack-signing.js`. The HMAC key lives at `$XDG_CONFIG_HOME/agentic-security/scan-key`; override via `$AGENTIC_SECURITY_HMAC_KEY`. Premortem-derived; do not regress to hostname-derived.
100
102
 
101
103
  **Rule lifecycle** — `custom-rules.js` (YAML pattern DSL), `rule-overrides.js` (`disable:` gated on signature), `rule-packs.js`, `rule-synthesis.js` (proposes suppressions from triage feedback), `ruleset-version.js`.
@@ -139,6 +141,178 @@ one was caught by the module's own test, not in review.
139
141
  - **No throwing.** Every annotation in `engine.js` is wrapped `try { … } catch (_) {}`. Your annotator must degrade gracefully — set `null` on the field and continue.
140
142
  - **Dead-module test.** `npm run test:lifecycle` fails the build if you export a public symbol from a posture module that no other source file imports. Wire it in `engine.js` (or allowlist it with a written reason in `test/no-dead-modules.test.js`).
141
143
 
144
+ ## Assurance contracts, baseline and rollout config — `assurance/` (9 modules)
145
+
146
+ Foundation for the differentiation work (CORE-001, CORE-002, CORE-004). All of it is pure or read-only, and **nothing in it is wired into `engine.js` or the scan/report path**: a default scan is byte-for-byte unaffected, which `test/posture/assurance-config.test.js` pins. Consumers (X-201 and later) import it directly.
147
+
148
+ - **`identity.js`**: ids for evidence records. Reuses `canonicalJson` (`evidence-bundle.js`) and `computeStableId` (`stable-id.js`, a hypothesis id is a finding's stable id). An id is a hash over an **allowlist** of semantic fields, so clocks (`createdAt`) and migration bookkeeping can never reach it. Prefixes `vrec/obind/capd/rlab/rev` are distinct from lineage's, and nothing here imports `lineage/` (a test checks that).
149
+ - **`schema-kit.js`**: validator primitives. Closed-world (unknown fields rejected), typed error codes, supported major version 1 only, `sha256:<64 hex>` digests.
150
+ - **`verification-record.js`**: the one verification record. Six distinct outcomes, a SEPARATE repair status, four evidence kinds. Structural rules: a model can only produce `inference`; `confirmed` needs referenced trusted-runtime-proof or independent-adjudication evidence; `confirmationLevel` must equal the level derived from the evidence (a forged level fails); `refuted` needs an applicable non-model oracle, valid preconditions and proving evidence.
151
+ - **`contracts.js`**: observation binding (references a lineage observation by id only; absence in a sampled observation cannot establish `blocked`), capability decision (only runner/proxy mediation with a backend and an active-probe digest may claim `enforced`; a hook or in-process check never can), routing label (a decided label must name an adjudication or trusted-execution source), release evidence (`complete` is derived, never trusted), and `validateRecordSet` for duplicate ids, dangling references and conflicting claims.
152
+ - **`migrations.js`**: explicit v1 adapters from legacy shapes (proof tier, boolean verified, egress decision, attestation, lineage observation) and a legacy view back out. Evidence can be lost, never invented: a legacy `true` is `inconclusive`, `proof-failed` is `inconclusive` not `refuted`, and the only route to `confirmed` is an explicit `trustedRunner` assertion bound to a commit. The legacy view's `verified` is `true` only for confirmed and `false` only for refuted, otherwise `null`.
153
+ - **`config.js`**: the unified feature configuration. Reuses the env/`.agentic-security/*.yml` mechanism. Precedence: kill switch (`AGENTIC_SECURITY_NO_ASSURANCE` or `AGENTIC_SECURITY_NO_<FEATURE>`) > explicit override > `AGENTIC_SECURITY_ASSURANCE_<FEATURE>` > `.agentic-security/assurance.yml` > default (everything off). **A project file can never enable a `high-risk-execution` feature** (the file lives in the scanned repo); invalid configuration fails closed; unsupported platforms return a typed `unsupported` with the disclosure. `runFeature` returns typed `blocked`/`unsupported`/`degraded`/`disabled` results and never prompts.
154
+ - **`bounded-io.js`**: `guardedModelCall` is the only route for new model/network calls. It owns no HTTP client (the transport is injected): feature gate, then `evaluateEgress` BEFORE the payload exists, then `redactPayload`, then the request-size limit, then deadline plus bounded retries, then output cap, with every decision appended to the existing egress audit chain. `readFileBounded`, `capOutput`, `withDeadline`, `retryBounded` enforce the limits `config.js` claims; `maxMemoryMiB` is carried for a runner and disclosed as not enforced here.
155
+ - **`baseline.js`, `baseline-inventory.js`**: baseline capture (`scripts/baseline-capture.mjs`, `npm run baseline:capture`). Read-only; records HEAD, dirty paths, exact source and bundle digests, tool versions, entry points, the seven mappings and an 18-entry capability inventory (implemented / partial / unsupported / unmeasured, every path and exported symbol verified at capture time, a missing one is a reported problem). Unknown facts are `{status:'unknown', reason}` with no value. `evaluateBaseline` invalidates exactly the capabilities whose cited files changed and lists unrelated user changes as retained/introduced. The manifest is machine-specific, so it is generated on demand and not committed. When you change a capability's source or tests, update its inventory entry in the same change.
156
+
157
+ Tests: `test/posture/assurance-{baseline,contracts,config}.test.js`, all in `test:posture`.
158
+
159
+ ## One verification record, trusted oracles and replay (X-201, X-202, X-203)
160
+
161
+ Built on `assurance/` (the record schema, migrations, config) and `sandbox/trust-boundary.js` (the boundary). Three
162
+ directories, all consumed rather than wired into the default scan: a default scan is unchanged, and every addition to an
163
+ existing output is an additive field.
164
+
165
+ **`verification/emit.js`: the one emit function (X-201).** `emitVerification(surface, source, ctx)` is what every
166
+ verification surface calls to get the version-1 record (`{ ok, record, legacy, errors }`, never throws, validated before it
167
+ returns). Surfaces and where they call it: `fix-verify.js` (`verificationRecord` on the result, which the MCP `verify_fix` and
168
+ `apply_fix` tools forward), `fix-verify-loop.js`, `execution-proof.js` (`proveFinding` result), `verifier.js`
169
+ (`verifierVerificationRecords`, called by the CLI `verify` command into its `verifier-runs/` run record, never onto the findings that
170
+ are written back to `last-scan.json`), `autopilot.js` (`rec.verificationRecord`, `commit` option), `discovery/index.js` (hunt:
171
+ `verificationRecords`, advisory) and `oracles/oracle.js` (the `oracle` surface). The rule for every mapping: evidence is lost,
172
+ never invented, so no legacy surface can reach `confirmed` or `refuted`. A PoC run under the older sandbox path is `inconclusive`
173
+ (not a trusted runner; `proof-failed` is a triage signal, not a refutation), a static re-scan with no exploit oracle is `not-run`,
174
+ a family with no oracle is `unsupported`, a harness failure is `error`, a model verdict is `inference` evidence only. Only
175
+ `oracle` emissions carry trusted-runner evidence, because only `oracles/oracle.js` builds them.
176
+
177
+ Legacy consumers: nothing existing was renamed or removed. For a consumer that wants a status or a boolean, `toLegacyVerificationView`
178
+ (`assurance/migrations.js`, also returned as `legacy`) maps `verified` to `true` only for `confirmed` and `false` only for `refuted`,
179
+ `null` for the other four, and carries `status` with the real outcome name, so a skipped check can never read as a pass and an unrun
180
+ check never as a failure. Native vocabularies keep their own fields (`proofTier`, `verifier_verdict`, autopilot `outcome`, fix-verify
181
+ `ok`); the record sits beside them as `verificationRecord`. A boolean `true` migrated from a legacy record is `inconclusive`.
182
+
183
+ **`oracles/` (X-202).** `oracle.js` is the adapter contract (`validateOracleSpec`, `defineOracle`) and the runner (`runOracle`).
184
+ `adapters.js` holds the five class adapters (injection-execution, authorization-decision, state-transition,
185
+ side-effect-reachability, parser-resource), `registry.js` the frozen registry and `oracleManifest()`, the machine-readable
186
+ description X-208 gates on (class, prerequisites, platforms with an honest per-platform status, budgets, negative controls,
187
+ fixture locations, limitations, logic digest). Fixtures: `test/fixtures/oracles/<class>/{positive,negative,inconclusive}`.
188
+ - A run is: feature gate (`verification-oracles`, off by default, operator-only), request validation (closed-world: labels,
189
+ verdicts and receipts are never accepted; reserved `__oracle_*` names and path escapes are refused), prerequisite check
190
+ (an unmet one is `unsupported` with a typed reason and the target is never executed), a fresh workspace plus a verifier-owned
191
+ evidence directory outside it, the target through `runInBoundary` ONLY (a test pins that `oracle.js` imports no other way to
192
+ run code), a digest check of the adapter's own harness (a target that rewrote it makes the run `error`), then verifier-side
193
+ interpretation of what the run left behind, read after the whole process tree is dead. The settled status comes from
194
+ `settleVerification` with `observedBy: 'verifier'` evidence; a decided result (`confirmed`/`refuted`) additionally needs an
195
+ exact commit, else it is held at `inconclusive`.
196
+ - `refuted` needs the adapter's own precondition (a benign control passed); a target that failed to load, exited early or never
197
+ reported is `inconclusive`. A run cut by its deadline decides nothing except for the parser/resource class, where the supervisor's
198
+ own deadline observation IS the measurement.
199
+ - Receipts are issued only by `runOracle` (a WeakSet records them; they are deep-frozen; a copy or hand-built one is not
200
+ `isIssuedReceipt`). They carry the observed output (capped), the logic digest, sanitized environment metadata (an allowlist: no
201
+ hostname, paths or environment values) and the record id. Unsigned: signing belongs to the signer domain.
202
+ - Platforms: macOS (userspace backend) is the only host these have been executed on. Linux is declared `unverified` in the
203
+ manifest and cannot be `supported`; today the boundary refuses to run on the namespace backend, which the runner reports as
204
+ `unsupported`. Changing what an adapter asserts means bumping its `version` (the harness text and version feed `logicDigest`).
205
+ - KNOWN LIMIT: the target is imported into the harness process. A target written to recognise the oracle's payload and forge the
206
+ effect artifact is not distinguishable from a real injection; the oracle bounds the effect it tests, it is not a defence
207
+ against a target built to fool it. Tamper controls cover what a worker or target can do to adapter logic, labels, receipts and
208
+ status text.
209
+
210
+ **`replay/replay.js` (X-203).** A replay manifest pins the repository commit, the patch hash, the fixture hash, the toolchain
211
+ (runtime version, platform, architecture, and a container digest when there is one), the oracle id, version and logic digest, the
212
+ inputs and the budgets. It is a description of an oracle run, not a second runner: `replayManifest` ends in `runOracle`, so the
213
+ supervised process tree, output cap and cleanup are the existing ones. It rejects, and nothing executes, on a content hash that does
214
+ not match, an oracle that changed since the manifest was made, a scope wider than "exactly the named oracle, no network,
215
+ workspace-only writes", a budget over the oracle's ceiling, an unsafe file path or an unknown field. Missing toolchain, container
216
+ runtime or image, a denied network (acquisition is never attempted: this build fetches nothing), a disabled feature or a boundary
217
+ that cannot run are typed prerequisites with `resumable` set honestly; the attempt is parked (`awaiting-prerequisites`) and
218
+ `resume` re-evaluates the prerequisites against the real host (a claim that they are met does nothing). Outcome while parked is
219
+ `not-run` (or `unsupported` when this build can never meet it, such as container execution, which is not implemented), with no
220
+ evidence; nothing is substituted for the execution that did not happen. Toolchain identity is exact. Persistence of attempts is
221
+ the caller's (the attempt object is plain data). Suites: `test/verification/{verification-record,oracle-adapters,replay-manifest}.test.js`
222
+ (`npm run test:verification`).
223
+
224
+ ## Patch-negative verification and non-taint classes (X-204, X-205)
225
+
226
+ Same posture as the three directories above: consumed, not wired into the default scan, every output addition additive, and
227
+ nothing changes with the feature flags off. Suites: `test/verification/{patch-negative,non-taint-classes}.test.js`
228
+ (`npm run test:verification`).
229
+
230
+ **`verification/patch-negative.js` (X-204).** `verifyPatchNegative(req, { config, runOptions })` is `verified-fix` only when four
231
+ `replayManifest` runs (so each is pinned, boundary-confined and receipted) show: the exploit oracle CONFIRMS on the original
232
+ revision, REFUTES on original-plus-diff, and the `functional-regression` oracle (the requester's declared cases) is clean on the
233
+ original (a baseline: wrong expectations are reported, not blamed on the patch) and on the patched revision. It stops at the first
234
+ non-passing step and names it: the result carries `incompleteStep` and a typed `failureCode` (`original-not-reproduced`,
235
+ `patched-still-exploitable`, `patched-build-broken`, `functional-omitted`, `functional-baseline-invalid`,
236
+ `functional-regression-detected`, `oracle-changed`, `revision-unbound`, `prerequisite-unmet` (resumable where stated),
237
+ `environment-mismatch`, `promotion-refused`) and a `summary` that says what was and was not exercised. Gate: the
238
+ `patch-negative-verification` feature (high-risk execution, operator-only, off by default) plus `verification-oracles`.
239
+ Wiring: `fix-verify.js` (`verifyFix({ patchNegative, assuranceConfig })` adds `patchNegative` and `fixStatus`, and `ok` requires
240
+ `verified-fix`), `fix-verify-loop.js` (a `patchNegative` leg and the `verified-fix` verdict) and `fix/apply-fix-service.js`
241
+ (`applyVerifiedFix({ patchNegative })` writes only a promoted patch and returns `repairRecords`). With the flag off or no request,
242
+ those return exactly what they did before.
243
+
244
+ **`verification/patch-promotion.js`.** `promotePatch({ proposal, receipts })` re-derives the diff digest and revision from the
245
+ PROPOSAL and refuses any receipt that was not issued by `runOracle` (`isIssuedReceipt`: copies and hand-built receipts fail), that
246
+ covers different content than the exact diff on the original revision, that was issued for another commit or hypothesis, whose
247
+ oracle or inputs differ between the original and patched runs, whose environment identity differs, or that is reused in two roles.
248
+ Receipts are valid inside the verifier's process only; carrying a promotion across processes needs a signing domain (not here).
249
+
250
+ **`verification/repair-records.js`.** The CORE-002 record has one repair status and no value for a rejection, so the history is an
251
+ append-only ledger of separate, frozen, chained records: `proposed`, `rejected`, `promoted` (only through `recordPromotion`, which
252
+ accepts nothing but a result `promotePatch` returned `ok`), `applied` (only after a promotion for that exact diff) and
253
+ `rolled-back`. `verificationRepairFor`/`withRepairStatus` map the ledger onto the CORE-002 `repair` field (`replay-verified` carries the
254
+ patched-negative record id); a rejection leaves it at `proposed` because the foundation enum is not widened here. The ledger is
255
+ returned to the caller and not persisted: persisting it would need an artifact-registry classification.
256
+
257
+ **New adapters.** `replay-idempotency` (a factory receiving a recording stand-in for the effect: a request and a distinct request
258
+ are delivered as a control, then the same request is redelivered, and the verifier counts effects) and `functional-regression`
259
+ (declared cases against expected JSON values; the hypothesis is that behaviour REGRESSED, so a clean patch is `refuted`). Both have
260
+ the usual positive, negative and inconclusive fixtures under `test/fixtures/oracles/`.
261
+
262
+ **`oracles/scenario-classes.js` (X-205).** The supported-class manifest for non-taint defects, surfaced as
263
+ `oracleManifest().nonTaint`: tenant authorization and privileged action (REUSE `authorization-decision`), workflow order (REUSES
264
+ `state-transition`), bounded resource exhaustion (REUSES `parser-resource`) and replay/idempotency (the new adapter). Each class
265
+ lists what the requester must supply, its fixtures (`test/fixtures/non-taint/<class>/{positive,negative,scenario.json}`, application-style
266
+ code), its limitations and the report lines a reader sees; every `runOracle` result additionally carries a `disclosure`
267
+ (prerequisites, platform statement, limitations, class report). Unsupported non-taint classes (concurrent races, CSRF, authentication
268
+ throttling) are named with a reason. `judgeNonTaintHypothesis` is the rule that taint absence is never an input to a refutation:
269
+ without an executed oracle of the class's own adapter with proven preconditions the outcome is `not-run` (or `unsupported`); the hunt
270
+ mapper in `verification/emit.js` uses it. `trustedNegativeDenominator` counts only decided results of an executed oracle of an
271
+ advertised class with proven preconditions and trusted runtime proof, and lists every exclusion with its reason (its runtime consumer is `verificationCoverage` in
272
+ `verification/projection.js`, which the JSON report, the CLI report and the MCP/autopilot surfaces use for the trusted-negative line).
273
+
274
+ Not verified: Linux (the oracle platform statements stay `unverified`); only the declared exploit scenario and functional cases are
275
+ exercised, never the patch's correctness elsewhere.
276
+
277
+ ## One projection for every interface, advisory/gating separation, conformance (X-206, X-207, X-208)
278
+
279
+ Same posture as the sections above: consumed, not wired into the default scan, every output addition additive and present only
280
+ when a verification record is. Suites: `test/verification/{interface-equivalence,advisory-gating,oracle-conformance}.test.js`
281
+ (`npm run test:verification`). Docs: `docs/guides/verification-schema-migration.md`, `docs/guides/verification-oracle-conformance.md`.
282
+
283
+ **`verification/projection.js` (X-206).** `projectVerification(record, { replay })` is the ONE place a record becomes something a
284
+ person or another tool reads: state, scope, oracle, evidence ids, replay prerequisites (derived from the record, plus the typed
285
+ ones a replay attempt reported), a what-was-verified / what-was-not summary, the legacy boolean view, and `text` lines. The JSON
286
+ report (`report/index.js` `normalizeFindings`), the CLI and Markdown reports (`verificationBlock`), MCP `explain_finding`,
287
+ `verify_fix` and `apply_fix`, and the autopilot response (`serializeAutopilotResult`, used by `scripts/autopilot.mjs --json`) all
288
+ call it; none formats its own verification text. The key is **`verificationView`**, never `verification`: a finding's existing
289
+ `verification` field is the producer/verifier separation record (`verification-separation.js`) and a first draft of this work
290
+ collided with it. An invalid record yields `verificationView: null` plus `verificationViewErrors`, never a guessed state. The text
291
+ never says "safe" or "fixed" for a partial result (a lint-style test covers every state, every repair status and the wording the real
292
+ surfaces emit; the fix-verify leg's native status word `fixed` is deliberately not echoed into a record reason). `verificationCoverage`
293
+ wraps `trustedNegativeDenominator`. Previous output schemas are pinned in `test/fixtures/verification-compat/`.
294
+
295
+ **`verification/advisory-state.js`, `verification/hypothesis-promotion.js` (X-207).** Hunt (`discovery/`) is advisory.
296
+ `writeAdvisoryState` is the only way advisory code writes: a closed file-name allowlist (the one hunt memory file), authoritative names
297
+ refused, write to a temp file and rename (a symlink or hard link planted at the target is replaced, never written through), the state
298
+ directory must resolve inside the project root. `discovery/memory.js` `saveMemory` uses it. `normalizeFindings` excludes any hunt
299
+ hypothesis (`parser: DISCOVERY` or a `discovery` object) from every finding list and exit code and records `advisoryExcluded`.
300
+ `promoteHypothesis({ hypothesis, result, policy })` is the only crossing: it verifies (not just checks presence of) a receipt issued by
301
+ `runOracle` (`isIssuedReceipt`), bound to the hypothesis and an exact commit and to a valid record settled `confirmed` at the
302
+ `runtime-confirmed` level, applies an EXPLICIT policy (fail-closed on an unknown key; the receipt rules are mandatory and cannot be
303
+ waived; `minConfidence`/`minAgreement` can only add a requirement), reuses `verification-separation.js` for hunter != verifier, and
304
+ returns a frozen audit record for every decision (`promoted`, `rejected`, `unsupported`, `inconclusive`) with no clock in it.
305
+ `runDiscovery` calls it only when `opts.promote = { policy, verify }` is given. The audit record is returned, not persisted:
306
+ persisting needs an artifact-registry classification and a signing domain, which are not decided here.
307
+
308
+ **`oracles/conformance.js` and `scripts/verification-conformance-check.mjs` (X-208).** One contract over every registered adapter:
309
+ static (class scope, budgets under `BUDGET_CEILINGS`, negative controls with an on-disk fixture, verifier-side evidence logic bound by
310
+ a digest, fixtures and logic pinned in `test/fixtures/oracles/conformance-pins.json`) and execution (state mappings, issued and frozen
311
+ receipts, three tamper attempts, an unavailable prerequisite, cancellation, replay). Wired into `scripts/release-check.mjs`
312
+ (`verification-conformance-gate`, in `RELEASE_GROUPS['benches-b']`) and, static half only, the pre-push gate
313
+ (`verification-conformance-static`). Where the boundary cannot run, execution is reported `not-run`, never passed; `--require-execution`
314
+ makes that a failure. A new adapter must be pinned deliberately (`--update-pins`).
315
+
142
316
  ## Execution-proof tiers (R2)
143
317
 
144
318
  `proof-tier.js` + `execution-proof.js` add a fourth axis to a finding's
@@ -0,0 +1,218 @@
1
+ // The recommendation mappings and the capability inventory that baseline capture
2
+ // (CORE-001) records. Pure data. Every path is relative to the repository root and
3
+ // is VERIFIED against the checkout at capture time: a path that does not exist is
4
+ // reported as a problem, never carried as if it did.
5
+ //
6
+ // Status vocabulary (one per capability, never combined):
7
+ // implemented the capability exists, has a source path and a test or artifact,
8
+ // and no gap against the differentiation PRD is known.
9
+ // partial it exists and is tested, and `gap` names exactly what the PRD
10
+ // still needs on top of it.
11
+ // unsupported the PRD calls for it and the checkout does not provide it; `source`
12
+ // is the existing extension point it would attach to, `evidence` the
13
+ // test that covers that neighbour, and `gap` says what is absent.
14
+ // unmeasured it exists but no current-revision measurement of it is recorded;
15
+ // `evidence` is the artifact that exists and `gap` says why it is not a
16
+ // current measurement.
17
+ //
18
+ // This is a statement about what was inspected, not a quality score. Moving an
19
+ // entry to a stronger status needs the new source and evidence paths to land first.
20
+
21
+ export const RECOMMENDATION_MAPPINGS = Object.freeze([
22
+ {
23
+ n: 1, title: 'Refresh real-code evaluation and repair measured misses', priority: 'P0', slice: 'QA-001..QA-008',
24
+ extensionPoints: ['bench/independent', 'scanner/src/posture/accuracy-scorecard.js', 'scripts/scorecard.mjs', 'docs/scorecard.json'],
25
+ },
26
+ {
27
+ n: 2, title: 'Unify verification and expand trusted runtime oracles', priority: 'P0', slice: 'X-201..X-208',
28
+ extensionPoints: ['scanner/src/posture/fix-verify.js', 'scanner/src/posture/execution-proof.js', 'scanner/src/posture/verifier.js', 'scanner/src/sandbox/index.js', 'scanner/src/posture/autopilot.js', 'scanner/src/mcp/tools.js'],
29
+ },
30
+ {
31
+ n: 3, title: 'Add a deployment-aware Code Boundaries graph', priority: 'P1', slice: 'X-301..X-308',
32
+ extensionPoints: ['scanner/src/lineage/runtime-observation.js', 'scanner/src/lineage/observation-adapters.js', 'scanner/src/lineage/observation-correlation.js', 'scanner/src/lineage/cross-repo-link.js', 'scanner/src/lineage/federation-loader.js', 'scanner/src/lineage/drift-policy.js'],
33
+ },
34
+ {
35
+ n: 4, title: 'Execute tenant and business invariants', priority: 'P1', slice: 'X-401..X-408',
36
+ extensionPoints: ['scanner/src/posture/business-logic.js', 'scanner/src/posture/logic-claims.js', 'scanner/src/posture/specification-mining.js', 'scanner/src/discovery'],
37
+ },
38
+ {
39
+ n: 5, title: 'Enforce coding-agent capabilities', priority: 'P1', slice: 'X-501..X-508',
40
+ extensionPoints: ['hooks/pre-bash-guard.js', 'hooks/pre-edit-bodyguard.js', 'hooks/dispatch-pre-tool.js', 'scanner/src/egress/policy.js', 'scanner/src/egress/redact.js', 'scanner/src/egress/audit.js', 'scanner/src/sandbox/index.js', 'scanner/src/mcp/server.js'],
41
+ },
42
+ {
43
+ n: 6, title: 'Calibrate model routing against outcomes', priority: 'P2', slice: 'X-601..X-608',
44
+ extensionPoints: ['scanner/src/posture/model-routing.js', 'scanner/src/posture/model-trust.js', 'scanner/src/posture/cache-economics.js', 'scanner/src/posture/provider-catalog.js'],
45
+ },
46
+ {
47
+ n: 7, title: 'Deliver portable release assurance and resumable portfolios', priority: 'P2', slice: 'X-701..X-708',
48
+ extensionPoints: ['scanner/src/posture/fleet.js', 'scanner/src/posture/scan-checkpoint.js', 'scanner/src/posture/attestation.js', 'scanner/src/posture/evidence-bundle.js', 'scanner/src/posture/integrity.js', 'scanner/src/posture/retention-policy.js'],
49
+ },
50
+ ]);
51
+
52
+ const t = (p) => ({ kind: 'test', path: p });
53
+ const a = (p) => ({ kind: 'artifact', path: p });
54
+
55
+ export const CAPABILITY_INVENTORY = Object.freeze([
56
+ {
57
+ id: 'poc-replay', workstream: 2, title: 'Proof-of-concept generation and confined replay',
58
+ status: 'partial',
59
+ source: ['scanner/src/posture/poc-generator.js', 'scanner/src/posture/poc-inprocess.js', 'scanner/src/posture/execution-proof.js', 'scanner/src/posture/proof-tier.js'],
60
+ entryPoints: [{ path: 'scanner/src/posture/execution-proof.js', symbol: 'proveFinding' }, { path: 'scanner/src/posture/proof-tier.js', symbol: 'attachProofTier' }],
61
+ evidence: [t('scanner/test/poc-generator.test.js'), t('scanner/test/execution-proof.test.js'), t('scanner/test/proof-artifact.test.js')],
62
+ gap: 'replay is JavaScript/taint oriented and records a proof tier on the finding; there is no class-specific oracle adapter, no patched-negative replay, and no single verification record',
63
+ prd: ['X-201', 'X-202', 'X-203', 'X-204', 'X-205'],
64
+ },
65
+ {
66
+ id: 'fix-verification', workstream: 2, title: 'Fix verification (rescan, lint, tests, honesty gate, PoC re-check)',
67
+ status: 'partial',
68
+ source: ['scanner/src/posture/fix-verify.js', 'scanner/src/posture/fix-verify-loop.js', 'scanner/src/fix/apply-fix-service.js', 'scanner/src/posture/fix-honesty-gate.js'],
69
+ entryPoints: [{ path: 'scanner/src/posture/fix-verify.js', symbol: 'verifyFix' }, { path: 'scanner/src/fix/apply-fix-service.js', symbol: 'applyVerifiedFix' }],
70
+ evidence: [t('scanner/test/fix-verify-loop.test.js'), t('scanner/test/fix-verify-tests.test.js'), t('scanner/test/apply-fix-service.test.js'), t('scanner/test/fix-honesty-gate.test.js')],
71
+ gap: 'results are per-surface shapes, not one versioned record; original-positive, patched-negative and functional-regression evidence are not bound together by digest',
72
+ prd: ['X-201', 'X-204', 'X-206'],
73
+ },
74
+ {
75
+ id: 'verifier-separation', workstream: 2, title: 'Independent verifier and verification separation',
76
+ status: 'partial',
77
+ source: ['scanner/src/posture/verifier.js', 'scanner/src/posture/verification-separation.js', 'scanner/src/posture/verifier-target.js', 'scanner/src/posture/verifier-ephemeral.js'],
78
+ entryPoints: [],
79
+ evidence: [t('scanner/test/verifier.test.js'), t('scanner/test/verifier-independence.test.js'), t('scanner/test/verification-separation.test.js')],
80
+ gap: 'separation is enforced for the existing verifier path; key and label isolation for the new oracle workers is the CORE-003 trust-boundary work',
81
+ prd: ['CORE-003', 'X-202'],
82
+ },
83
+ {
84
+ id: 'sandbox-confinement', workstream: 2, title: 'Confinement sandbox (namespace, userspace and disabled backends)',
85
+ status: 'partial',
86
+ source: ['scanner/src/sandbox/index.js', 'scanner/src/sandbox/capabilities.js', 'scanner/src/sandbox/limits.js', 'scanner/src/sandbox/backend-namespace.js', 'scanner/src/sandbox/backend-userspace.js', 'scanner/src/sandbox/backend-disabled.js'],
87
+ entryPoints: [{ path: 'scanner/src/sandbox/index.js', symbol: 'runConfined' }],
88
+ evidence: [t('scanner/test/sandbox.test.js'), t('scanner/test/sandbox-escape.test.js')],
89
+ gap: 'isolation is probed per backend; descendant-process cleanup, read confinement and per-backend active probes are the CORE-003 work, and enforcement is only advertised on Linux',
90
+ prd: ['CORE-003', 'X-502', 'X-503'],
91
+ },
92
+ {
93
+ id: 'runtime-observation-imports', workstream: 3, title: 'Runtime observation imports and correlation',
94
+ status: 'partial',
95
+ source: ['scanner/src/lineage/runtime-observation.js', 'scanner/src/lineage/observation-adapters.js', 'scanner/src/lineage/observation-store.js', 'scanner/src/lineage/observation-correlation.js', 'scanner/src/posture/telemetry-ingest.js'],
96
+ entryPoints: [{ path: 'scanner/src/lineage/runtime-observation.js', symbol: 'validateRuntimeObservation' }, { path: 'scanner/src/lineage/observation-store.js', symbol: 'persistObservationImport' }],
97
+ evidence: [t('scanner/test/lineage/runtime-observation.test.js'), t('scanner/test/lineage/observation-adapters.test.js'), t('scanner/test/lineage/observation-store.test.js'), t('scanner/test/lineage/observation-correlation.test.js')],
98
+ gap: 'one adapter (native-jsonl) is implemented; observations corroborate lineage edges but do not yet bind to vulnerability hypotheses, and freshness and sampling are not modelled as path states',
99
+ prd: ['X-302', 'X-303', 'X-305'],
100
+ },
101
+ {
102
+ id: 'cross-repo-federation-drift', workstream: 3, title: 'Cross-repository links, federation and drift policy',
103
+ status: 'implemented',
104
+ source: ['scanner/src/lineage/cross-repo-link.js', 'scanner/src/lineage/federation-loader.js', 'scanner/src/lineage/drift-policy.js', 'scanner/src/lineage/graph-diff.js'],
105
+ entryPoints: [{ path: 'scanner/src/lineage/cross-repo-link.js', symbol: 'validateCrossRepoLink' }],
106
+ evidence: [t('scanner/test/lineage/cross-repo-link.test.js'), t('scanner/test/lineage/federation-loader.test.js'), t('scanner/test/lineage/drift-policy.test.js')],
107
+ gap: null,
108
+ prd: ['X-304', 'X-306'],
109
+ },
110
+ {
111
+ id: 'business-logic-claims', workstream: 4, title: 'Business-logic detection, logic claims and specification mining',
112
+ status: 'partial',
113
+ source: ['scanner/src/posture/business-logic.js', 'scanner/src/posture/logic-claims.js', 'scanner/src/posture/specification-mining.js'],
114
+ entryPoints: [],
115
+ evidence: [t('scanner/test/business-logic.test.js'), t('scanner/test/logic-claims.test.js'), t('scanner/test/phase3-v3.test.js')],
116
+ gap: 'claims are advisory findings; there are no declarative approved invariants and no bounded stateful scenario execution',
117
+ prd: ['X-401', 'X-402', 'X-403', 'X-404'],
118
+ },
119
+ {
120
+ id: 'stateful-invariant-execution', workstream: 4, title: 'Approved invariants, stateful scenarios, state oracles and sequence shrinking',
121
+ status: 'unsupported',
122
+ source: ['scanner/src/posture/logic-claims.js', 'scanner/src/discovery'],
123
+ entryPoints: [],
124
+ evidence: [t('scanner/test/logic-claims.test.js')],
125
+ gap: 'no executable invariant contract, approval state, scenario generator, durable-effect oracle or minimizer exists in the checkout',
126
+ prd: ['X-401', 'X-403', 'X-404', 'X-405'],
127
+ },
128
+ {
129
+ id: 'egress-policy-redaction-audit', workstream: 5, title: 'Central egress policy, outbound redaction and tamper-evident audit',
130
+ status: 'implemented',
131
+ source: ['scanner/src/egress/policy.js', 'scanner/src/egress/redact.js', 'scanner/src/egress/audit.js'],
132
+ entryPoints: [{ path: 'scanner/src/egress/policy.js', symbol: 'evaluateEgress' }, { path: 'scanner/src/egress/redact.js', symbol: 'redactPayload' }, { path: 'scanner/src/egress/audit.js', symbol: 'recordEgressCall' }],
133
+ evidence: [t('scanner/test/egress-policy.test.js'), t('scanner/test/egress-redact.test.js'), t('scanner/test/egress-audit.test.js'), t('scanner/test/egress-policy-completeness.test.js')],
134
+ gap: null,
135
+ prd: ['CORE-004', 'X-504'],
136
+ },
137
+ {
138
+ id: 'agent-hooks', workstream: 5, title: 'Pre-edit, pre-bash and dispatch hooks',
139
+ status: 'partial',
140
+ source: ['hooks/pre-bash-guard.js', 'hooks/pre-edit-bodyguard.js', 'hooks/dispatch-pre-tool.js', 'hooks/hooks.json'],
141
+ entryPoints: [],
142
+ evidence: [t('scanner/test/pre-bash-guard.test.js'), t('scanner/test/bodyguard.test.js'), t('scanner/test/dispatch-pre-tool.test.js')],
143
+ gap: 'hooks explain and advise at the prompt layer; no runner mediates filesystem, command or network access, so a hook response is not an enforcement receipt',
144
+ prd: ['X-501', 'X-502', 'X-505', 'X-507'],
145
+ },
146
+ {
147
+ id: 'mcp-server', workstream: 2, title: 'MCP server tools (including the write-gated fix tools)',
148
+ status: 'partial',
149
+ source: ['scanner/src/mcp/server.js', 'scanner/src/mcp/tools.js', 'scanner/src/mcp/validate.js', 'scanner/src/mcp/audit.js'],
150
+ entryPoints: [],
151
+ evidence: [t('scanner/test/mcp.test.js'), t('scanner/test/mcp-audit.test.js'), t('scanner/test/mcp-protocol-smoke.test.js')],
152
+ gap: 'tool results are not yet emitted as the unified verification record',
153
+ prd: ['X-206'],
154
+ },
155
+ {
156
+ id: 'model-routing-and-trust', workstream: 6, title: 'Model routing, measured trust, cache economics and provider catalog',
157
+ status: 'partial',
158
+ source: ['scanner/src/posture/model-routing.js', 'scanner/src/posture/model-trust.js', 'scanner/src/posture/cache-economics.js', 'scanner/src/posture/provider-catalog.js'],
159
+ entryPoints: [{ path: 'scanner/src/posture/model-trust.js', symbol: 'createTrustLedger' }, { path: 'scanner/src/posture/model-routing.js', symbol: 'routeModelWithTrust' }],
160
+ evidence: [t('scanner/test/model-routing.test.js'), t('scanner/test/model-trust.test.js'), t('scanner/test/cache-economics.test.js'), t('scanner/test/providers.test.js'), a('bench/router-replay/baseline.json')],
161
+ gap: 'trust is a per-key miss-rate ledger; there are no adjudicated task strata, calibrated quality intervals, shadow validation or drift circuit breakers',
162
+ prd: ['X-601', 'X-603', 'X-604', 'X-605'],
163
+ },
164
+ {
165
+ id: 'fleet-checkpoints', workstream: 7, title: 'Fleet runs and scan checkpoints',
166
+ status: 'partial',
167
+ source: ['scanner/src/posture/fleet.js', 'scanner/src/posture/scan-checkpoint.js', 'scripts/fleet.mjs'],
168
+ entryPoints: [{ path: 'scanner/src/posture/scan-checkpoint.js', symbol: 'openCheckpoint' }],
169
+ evidence: [t('scanner/test/fleet.test.js'), t('scanner/test/scan-checkpoint.test.js')],
170
+ gap: 'checkpoints resume a single scan; there are no durable leased work units, dependency-hash invalidation or portfolio budgets',
171
+ prd: ['X-704', 'X-705', 'X-706'],
172
+ },
173
+ {
174
+ id: 'attestation-and-evidence-bundles', workstream: 7, title: 'Run attestation, signed per-finding evidence bundles, integrity and retention',
175
+ status: 'partial',
176
+ source: ['scanner/src/posture/attestation.js', 'scanner/src/posture/evidence-bundle.js', 'scanner/src/posture/integrity.js', 'scanner/src/posture/retention-policy.js'],
177
+ entryPoints: [{ path: 'scanner/src/posture/evidence-bundle.js', symbol: 'signEvidenceBundle' }, { path: 'scanner/src/posture/attestation.js', symbol: 'computeRunAttestation' }],
178
+ evidence: [t('scanner/test/attestation.test.js'), t('scanner/test/evidence-bundle.test.js'), t('scanner/test/retention-policy.test.js'), t('scanner/test/verify-attestation-cli.test.js')],
179
+ gap: 'bundles are per finding with a local signer; there is no release assurance manifest, portable package or explicit trust-root policy',
180
+ prd: ['X-701', 'X-702', 'X-703', 'CORE-003'],
181
+ },
182
+ {
183
+ id: 'accuracy-scorecard', workstream: 1, title: 'Published accuracy scorecard and corpus replay',
184
+ status: 'implemented',
185
+ source: ['scanner/src/posture/accuracy-scorecard.js', 'scripts/scorecard.mjs'],
186
+ entryPoints: [],
187
+ evidence: [t('scanner/test/accuracy-scorecard.test.js'), t('scanner/test/scorecard.test.js'), a('docs/scorecard.json')],
188
+ gap: null,
189
+ prd: ['QA-004'],
190
+ },
191
+ {
192
+ id: 'independent-accuracy-current-engine', workstream: 1, title: 'Current-engine accuracy on independent real code',
193
+ status: 'unmeasured',
194
+ source: ['bench/independent/runner.mjs', 'bench/independent/manifest.json'],
195
+ entryPoints: [],
196
+ evidence: [a('docs/scorecard.json'), a('docs/INDEPENDENT_POPULATION_ROOT_CAUSE.md')],
197
+ gap: 'the recorded independent result belongs to an older engine revision; no current-engine adjudicated measurement exists in the checkout',
198
+ prd: ['QA-001', 'QA-002', 'QA-003'],
199
+ },
200
+ {
201
+ id: 'loop-controller', workstream: 'foundation', title: 'Supervised background implementation controller (Haskell/Nix profile)',
202
+ status: 'partial',
203
+ source: ['scripts/loop-engineering/run.mjs', 'scripts/loop-engineering/profiles/haskell-nix.json'],
204
+ entryPoints: [],
205
+ evidence: [t('scripts/loop-engineering/test/loop-manifest.test.js'), t('scripts/loop-engineering/test/loop-watchdog.test.js'), t('scripts/loop-engineering/test/loop-recovery.test.js')],
206
+ gap: 'only the Haskell/Nix profile exists; the assurance-differentiation profile and its protected acceptance suites are not present',
207
+ prd: ['LOOP-001', 'LOOP-002', 'LOOP-003'],
208
+ },
209
+ {
210
+ id: 'lineage-graph', workstream: 3, title: 'Data Flow Explorer lineage graph (DataFlowGraph v1)',
211
+ status: 'implemented',
212
+ source: ['scanner/src/lineage/graph-builder.js', 'scanner/src/lineage/ids.js', 'scanner/src/lineage/schema.js', 'scanner/src/lineage/dataflow-graph.schema.json'],
213
+ entryPoints: [{ path: 'scanner/src/lineage/ids.js', symbol: 'graphId' }],
214
+ evidence: [t('scanner/test/lineage/schema.test.js'), t('scanner/test/lineage/ids.test.js')],
215
+ gap: null,
216
+ prd: ['X-301'],
217
+ },
218
+ ]);