@clear-capabilities/agentic-security-scanner 0.161.0 → 0.163.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.
- package/CHANGELOG.md +46 -0
- package/bin/agentic-security.js +98 -2
- package/dist/1340.index.js +1871 -0
- package/dist/3477.index.js +727 -0
- package/dist/3499.index.js +23 -3
- package/dist/4113.index.js +59 -8
- package/dist/5014.index.js +833 -0
- package/dist/5756.index.js +2 -2
- package/dist/6257.index.js +1 -1
- package/dist/6907.index.js +248 -0
- package/dist/6994.index.js +1 -1
- package/dist/7033.index.js +33 -5
- package/dist/7439.index.js +1822 -0
- package/dist/7838.index.js +38 -2
- package/dist/8798.index.js +1 -1
- package/dist/9207.index.js +5 -0
- package/dist/9560.index.js +2336 -27
- package/dist/957.index.js +1 -1
- package/dist/{1905.index.js → 9999.index.js} +306 -13
- package/dist/agentic-security.mjs +12 -12
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +39 -7
- package/src/capabilities/CLAUDE.md +114 -0
- package/src/capabilities/address.js +73 -0
- package/src/capabilities/attack-coverage.js +93 -0
- package/src/capabilities/decide.js +256 -0
- package/src/capabilities/delegate.js +69 -0
- package/src/capabilities/egress.js +20 -0
- package/src/capabilities/hook-advice.js +50 -0
- package/src/capabilities/index.js +32 -0
- package/src/capabilities/manifest.js +344 -0
- package/src/capabilities/outbound.js +189 -0
- package/src/capabilities/paths.js +57 -0
- package/src/capabilities/probes.js +211 -0
- package/src/capabilities/proxy.js +182 -0
- package/src/capabilities/reasons.js +67 -0
- package/src/capabilities/receipts.js +356 -0
- package/src/capabilities/records.js +58 -0
- package/src/capabilities/recovery.js +367 -0
- package/src/capabilities/report.js +81 -0
- package/src/capabilities/runner.js +236 -0
- package/src/capabilities/secrets.js +26 -0
- package/src/capabilities/tool-gate.js +119 -0
- package/src/capabilities/tool-registry.js +60 -0
- package/src/dataflow/CLAUDE.md +29 -0
- package/src/dataflow/catalog.js +21 -0
- package/src/dataflow/engine.js +20 -0
- package/src/dataflow/guard-dominance.js +352 -0
- package/src/discovery/CLAUDE.md +18 -0
- package/src/discovery/index.js +43 -1
- package/src/discovery/lenses.js +18 -0
- package/src/discovery/memory.js +7 -5
- package/src/engine.js +80 -15
- package/src/fix/apply-fix-service.js +33 -3
- package/src/integrations/index.js +5 -2
- package/src/ir/CLAUDE.md +14 -0
- package/src/ir/implicit-handler-params.js +74 -0
- package/src/ir/parser-cs.js +20 -0
- package/src/ir/parser-go.js +30 -13
- package/src/ir/parser-java.js +11 -0
- package/src/language/nix-eval-isolation.js +33 -5
- package/src/lineage/CLAUDE.md +31 -0
- package/src/lineage/deployment/adapter-cloud.js +202 -0
- package/src/lineage/deployment/adapter-compose.js +90 -0
- package/src/lineage/deployment/adapter-kit.js +142 -0
- package/src/lineage/deployment/adapter-kubernetes.js +247 -0
- package/src/lineage/deployment/attack-paths.js +443 -0
- package/src/lineage/deployment/boundaries-run.js +223 -0
- package/src/lineage/deployment/boundary-graph.js +567 -0
- package/src/lineage/deployment/cross-repo.js +253 -0
- package/src/lineage/deployment/drift.js +383 -0
- package/src/lineage/deployment/ingest.js +221 -0
- package/src/lineage/deployment/projection.js +244 -0
- package/src/lineage/deployment/trace-correlation.js +541 -0
- package/src/lineage/source-registry.js +19 -0
- package/src/mcp/CLAUDE.md +14 -3
- package/src/mcp/invariant-tools.js +64 -0
- package/src/mcp/portfolio-tools.js +79 -0
- package/src/mcp/redact.js +15 -0
- package/src/mcp/server.js +24 -3
- package/src/mcp/tools.js +30 -5
- package/src/posture/CLAUDE.md +505 -0
- package/src/posture/accuracy-scorecard.js +95 -4
- package/src/posture/assurance/baseline-inventory.js +218 -0
- package/src/posture/assurance/baseline.js +322 -0
- package/src/posture/assurance/bounded-io.js +145 -0
- package/src/posture/assurance/config.js +299 -0
- package/src/posture/assurance/contracts.js +315 -0
- package/src/posture/assurance/identity.js +62 -0
- package/src/posture/assurance/migrations.js +259 -0
- package/src/posture/assurance/rollback.js +214 -0
- package/src/posture/assurance/rollout.js +200 -0
- package/src/posture/assurance/schema-kit.js +111 -0
- package/src/posture/assurance/verification-record.js +258 -0
- package/src/posture/attestation.js +5 -0
- package/src/posture/autopilot.js +28 -1
- package/src/posture/business-logic.js +26 -4
- package/src/posture/evaluation/bakeoff.js +148 -0
- package/src/posture/evaluation/custody.js +200 -0
- package/src/posture/evaluation/deployment-ablation.js +416 -0
- package/src/posture/evaluation/dev-cases.js +65 -0
- package/src/posture/evaluation/economics.js +85 -0
- package/src/posture/evaluation/gates.js +161 -0
- package/src/posture/evaluation/grouping.js +101 -0
- package/src/posture/evaluation/interval.js +132 -0
- package/src/posture/evaluation/invariant-ablation.js +325 -0
- package/src/posture/evaluation/labels.js +200 -0
- package/src/posture/evaluation/protocol.js +308 -0
- package/src/posture/evaluation/release-gate.js +209 -0
- package/src/posture/evaluation/report.js +158 -0
- package/src/posture/evaluation/runner.js +273 -0
- package/src/posture/evaluation/scan-child.js +80 -0
- package/src/posture/evaluation/score.js +174 -0
- package/src/posture/evaluation/synthetic.js +90 -0
- package/src/posture/evaluation/why-missed.js +219 -0
- package/src/posture/evidence-bundle.js +93 -7
- package/src/posture/execution-proof.js +11 -1
- package/src/posture/fix-verify-loop.js +20 -6
- package/src/posture/fix-verify.js +37 -2
- package/src/posture/fleet.js +11 -2
- package/src/posture/invariants/cli.js +95 -0
- package/src/posture/invariants/coverage.js +164 -0
- package/src/posture/invariants/export.js +178 -0
- package/src/posture/invariants/expressions.js +46 -0
- package/src/posture/invariants/lifecycle.js +289 -0
- package/src/posture/invariants/project-input.js +55 -0
- package/src/posture/invariants/repair.js +340 -0
- package/src/posture/invariants/run.js +45 -0
- package/src/posture/invariants/scenarios.js +311 -0
- package/src/posture/invariants/schema.js +167 -0
- package/src/posture/invariants/shrink.js +407 -0
- package/src/posture/invariants/state-assertions.js +165 -0
- package/src/posture/logic-claims.js +12 -0
- package/src/posture/model-routing.js +40 -0
- package/src/posture/oracles/adapters.js +531 -0
- package/src/posture/oracles/conformance.js +276 -0
- package/src/posture/oracles/oracle.js +453 -0
- package/src/posture/oracles/registry.js +76 -0
- package/src/posture/oracles/scenario-classes.js +223 -0
- package/src/posture/portfolio/backend.js +279 -0
- package/src/posture/portfolio/bundle.js +314 -0
- package/src/posture/portfolio/cli.js +142 -0
- package/src/posture/portfolio/manifest.js +263 -0
- package/src/posture/portfolio/progress.js +267 -0
- package/src/posture/portfolio/resume.js +130 -0
- package/src/posture/portfolio/retention.js +207 -0
- package/src/posture/portfolio/scheduler.js +488 -0
- package/src/posture/portfolio/signing.js +155 -0
- package/src/posture/portfolio/wording.js +59 -0
- package/src/posture/portfolio/work-units.js +405 -0
- package/src/posture/replay/replay.js +250 -0
- package/src/posture/routing/adapter.js +140 -0
- package/src/posture/routing/calibration.js +278 -0
- package/src/posture/routing/canary.js +141 -0
- package/src/posture/routing/control.js +35 -0
- package/src/posture/routing/decide.js +211 -0
- package/src/posture/routing/drift.js +137 -0
- package/src/posture/routing/economics.js +185 -0
- package/src/posture/routing/feedback.js +207 -0
- package/src/posture/routing/index.js +31 -0
- package/src/posture/routing/outcomes.js +281 -0
- package/src/posture/routing/promotion.js +291 -0
- package/src/posture/routing/receipts.js +59 -0
- package/src/posture/routing/report.js +142 -0
- package/src/posture/routing/shadow.js +53 -0
- package/src/posture/specification-mining.js +26 -0
- package/src/posture/verification/advisory-state.js +71 -0
- package/src/posture/verification/emit.js +257 -0
- package/src/posture/verification/hypothesis-promotion.js +211 -0
- package/src/posture/verification/patch-negative.js +226 -0
- package/src/posture/verification/patch-promotion.js +113 -0
- package/src/posture/verification/projection.js +183 -0
- package/src/posture/verification/repair-records.js +124 -0
- package/src/posture/verifier.js +16 -0
- package/src/report/index.js +100 -1
- package/src/sandbox/CLAUDE.md +51 -0
- package/src/sandbox/backend-namespace.js +7 -0
- package/src/sandbox/backend-userspace.js +151 -34
- package/src/sandbox/control-probes.js +215 -0
- package/src/sandbox/supervise.js +201 -0
- package/src/sandbox/trust-boundary.js +113 -0
- package/src/sandbox/trust-domains.js +171 -0
package/src/posture/CLAUDE.md
CHANGED
|
@@ -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,509 @@ 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/` (11 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
|
+
- **`rollout.js`, `rollback.js` (REL-002).** Staged rollout and rollback. `rollout.js`: the three stages (`offline-fixtures`, `shadow-canary`,
|
|
158
|
+
`opt-in-supported`), the documented gates per feature and stage (`gatesFor`, `describeRollout`), an append-only ledger (`newLedger`), `promote`
|
|
159
|
+
(one stage at a time, every gate needs evidence that names its own suite and carries a sha256; the kill switch and an unsupported platform
|
|
160
|
+
block it; policy versions are never reused) and `effectiveMode` (an enabled feature below opt-in runs as `fixtures` or `shadow` and can never
|
|
161
|
+
change an outcome). `rollback.js`: `READER_POLICY` (per record kind, whether a previous reader is retained; four kinds honestly say no),
|
|
162
|
+
`planRollback`/`applyRollback` (restore the last known-good stage, append a `rollback` event, FLAG evidence and claims `stale` or
|
|
163
|
+
`incompatible`, never delete or rewrite), `EvidenceStore`/`guardedFeatureRun` (append-only, a thrown feature records a failure and costs no
|
|
164
|
+
evidence), and the known break: `verifyWithPreCore003Reader` is a faithful copy of the verifier before the `issuance` block existed, which
|
|
165
|
+
rejects every newly signed bundle; `exportLegacyBundle` is the migration path (verifies the original first, re-signs without issuance,
|
|
166
|
+
returns a sidecar carrying the dropped trust basis). Pure, consumed by tests and `docs/guides/assurance-rollout-and-rollback.md`; nothing in
|
|
167
|
+
the scan path imports them. Suite: `test/release-closure/` (`npm run test:release-closure`). The release closure itself (the plan, the
|
|
168
|
+
commit-bound record, the judge) is `scripts/release-closure.mjs`, not a posture module.
|
|
169
|
+
|
|
170
|
+
Tests: `test/posture/assurance-{baseline,contracts,config}.test.js`, all in `test:posture`.
|
|
171
|
+
|
|
172
|
+
## One verification record, trusted oracles and replay (X-201, X-202, X-203)
|
|
173
|
+
|
|
174
|
+
Built on `assurance/` (the record schema, migrations, config) and `sandbox/trust-boundary.js` (the boundary). Three
|
|
175
|
+
directories, all consumed rather than wired into the default scan: a default scan is unchanged, and every addition to an
|
|
176
|
+
existing output is an additive field.
|
|
177
|
+
|
|
178
|
+
**`verification/emit.js`: the one emit function (X-201).** `emitVerification(surface, source, ctx)` is what every
|
|
179
|
+
verification surface calls to get the version-1 record (`{ ok, record, legacy, errors }`, never throws, validated before it
|
|
180
|
+
returns). Surfaces and where they call it: `fix-verify.js` (`verificationRecord` on the result, which the MCP `verify_fix` and
|
|
181
|
+
`apply_fix` tools forward), `fix-verify-loop.js`, `execution-proof.js` (`proveFinding` result), `verifier.js`
|
|
182
|
+
(`verifierVerificationRecords`, called by the CLI `verify` command into its `verifier-runs/` run record, never onto the findings that
|
|
183
|
+
are written back to `last-scan.json`), `autopilot.js` (`rec.verificationRecord`, `commit` option), `discovery/index.js` (hunt:
|
|
184
|
+
`verificationRecords`, advisory) and `oracles/oracle.js` (the `oracle` surface). The rule for every mapping: evidence is lost,
|
|
185
|
+
never invented, so no legacy surface can reach `confirmed` or `refuted`. A PoC run under the older sandbox path is `inconclusive`
|
|
186
|
+
(not a trusted runner; `proof-failed` is a triage signal, not a refutation), a static re-scan with no exploit oracle is `not-run`,
|
|
187
|
+
a family with no oracle is `unsupported`, a harness failure is `error`, a model verdict is `inference` evidence only. Only
|
|
188
|
+
`oracle` emissions carry trusted-runner evidence, because only `oracles/oracle.js` builds them.
|
|
189
|
+
|
|
190
|
+
Legacy consumers: nothing existing was renamed or removed. For a consumer that wants a status or a boolean, `toLegacyVerificationView`
|
|
191
|
+
(`assurance/migrations.js`, also returned as `legacy`) maps `verified` to `true` only for `confirmed` and `false` only for `refuted`,
|
|
192
|
+
`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
|
|
193
|
+
check never as a failure. Native vocabularies keep their own fields (`proofTier`, `verifier_verdict`, autopilot `outcome`, fix-verify
|
|
194
|
+
`ok`); the record sits beside them as `verificationRecord`. A boolean `true` migrated from a legacy record is `inconclusive`.
|
|
195
|
+
|
|
196
|
+
**`oracles/` (X-202).** `oracle.js` is the adapter contract (`validateOracleSpec`, `defineOracle`) and the runner (`runOracle`).
|
|
197
|
+
`adapters.js` holds the five class adapters (injection-execution, authorization-decision, state-transition,
|
|
198
|
+
side-effect-reachability, parser-resource), `registry.js` the frozen registry and `oracleManifest()`, the machine-readable
|
|
199
|
+
description X-208 gates on (class, prerequisites, platforms with an honest per-platform status, budgets, negative controls,
|
|
200
|
+
fixture locations, limitations, logic digest). Fixtures: `test/fixtures/oracles/<class>/{positive,negative,inconclusive}`.
|
|
201
|
+
- A run is: feature gate (`verification-oracles`, off by default, operator-only), request validation (closed-world: labels,
|
|
202
|
+
verdicts and receipts are never accepted; reserved `__oracle_*` names and path escapes are refused), prerequisite check
|
|
203
|
+
(an unmet one is `unsupported` with a typed reason and the target is never executed), a fresh workspace plus a verifier-owned
|
|
204
|
+
evidence directory outside it, the target through `runInBoundary` ONLY (a test pins that `oracle.js` imports no other way to
|
|
205
|
+
run code), a digest check of the adapter's own harness (a target that rewrote it makes the run `error`), then verifier-side
|
|
206
|
+
interpretation of what the run left behind, read after the whole process tree is dead. The settled status comes from
|
|
207
|
+
`settleVerification` with `observedBy: 'verifier'` evidence; a decided result (`confirmed`/`refuted`) additionally needs an
|
|
208
|
+
exact commit, else it is held at `inconclusive`.
|
|
209
|
+
- `refuted` needs the adapter's own precondition (a benign control passed); a target that failed to load, exited early or never
|
|
210
|
+
reported is `inconclusive`. A run cut by its deadline decides nothing except for the parser/resource class, where the supervisor's
|
|
211
|
+
own deadline observation IS the measurement.
|
|
212
|
+
- Receipts are issued only by `runOracle` (a WeakSet records them; they are deep-frozen; a copy or hand-built one is not
|
|
213
|
+
`isIssuedReceipt`). They carry the observed output (capped), the logic digest, sanitized environment metadata (an allowlist: no
|
|
214
|
+
hostname, paths or environment values) and the record id. Unsigned: signing belongs to the signer domain.
|
|
215
|
+
- Platforms: macOS (userspace backend) is the only host these have been executed on. Linux is declared `unverified` in the
|
|
216
|
+
manifest and cannot be `supported`; today the boundary refuses to run on the namespace backend, which the runner reports as
|
|
217
|
+
`unsupported`. Changing what an adapter asserts means bumping its `version` (the harness text and version feed `logicDigest`).
|
|
218
|
+
- KNOWN LIMIT: the target is imported into the harness process. A target written to recognise the oracle's payload and forge the
|
|
219
|
+
effect artifact is not distinguishable from a real injection; the oracle bounds the effect it tests, it is not a defence
|
|
220
|
+
against a target built to fool it. Tamper controls cover what a worker or target can do to adapter logic, labels, receipts and
|
|
221
|
+
status text.
|
|
222
|
+
|
|
223
|
+
**`replay/replay.js` (X-203).** A replay manifest pins the repository commit, the patch hash, the fixture hash, the toolchain
|
|
224
|
+
(runtime version, platform, architecture, and a container digest when there is one), the oracle id, version and logic digest, the
|
|
225
|
+
inputs and the budgets. It is a description of an oracle run, not a second runner: `replayManifest` ends in `runOracle`, so the
|
|
226
|
+
supervised process tree, output cap and cleanup are the existing ones. It rejects, and nothing executes, on a content hash that does
|
|
227
|
+
not match, an oracle that changed since the manifest was made, a scope wider than "exactly the named oracle, no network,
|
|
228
|
+
workspace-only writes", a budget over the oracle's ceiling, an unsafe file path or an unknown field. Missing toolchain, container
|
|
229
|
+
runtime or image, a denied network (acquisition is never attempted: this build fetches nothing), a disabled feature or a boundary
|
|
230
|
+
that cannot run are typed prerequisites with `resumable` set honestly; the attempt is parked (`awaiting-prerequisites`) and
|
|
231
|
+
`resume` re-evaluates the prerequisites against the real host (a claim that they are met does nothing). Outcome while parked is
|
|
232
|
+
`not-run` (or `unsupported` when this build can never meet it, such as container execution, which is not implemented), with no
|
|
233
|
+
evidence; nothing is substituted for the execution that did not happen. Toolchain identity is exact. Persistence of attempts is
|
|
234
|
+
the caller's (the attempt object is plain data). Suites: `test/verification/{verification-record,oracle-adapters,replay-manifest}.test.js`
|
|
235
|
+
(`npm run test:verification`).
|
|
236
|
+
|
|
237
|
+
## Patch-negative verification and non-taint classes (X-204, X-205)
|
|
238
|
+
|
|
239
|
+
Same posture as the three directories above: consumed, not wired into the default scan, every output addition additive, and
|
|
240
|
+
nothing changes with the feature flags off. Suites: `test/verification/{patch-negative,non-taint-classes}.test.js`
|
|
241
|
+
(`npm run test:verification`).
|
|
242
|
+
|
|
243
|
+
**`verification/patch-negative.js` (X-204).** `verifyPatchNegative(req, { config, runOptions })` is `verified-fix` only when four
|
|
244
|
+
`replayManifest` runs (so each is pinned, boundary-confined and receipted) show: the exploit oracle CONFIRMS on the original
|
|
245
|
+
revision, REFUTES on original-plus-diff, and the `functional-regression` oracle (the requester's declared cases) is clean on the
|
|
246
|
+
original (a baseline: wrong expectations are reported, not blamed on the patch) and on the patched revision. It stops at the first
|
|
247
|
+
non-passing step and names it: the result carries `incompleteStep` and a typed `failureCode` (`original-not-reproduced`,
|
|
248
|
+
`patched-still-exploitable`, `patched-build-broken`, `functional-omitted`, `functional-baseline-invalid`,
|
|
249
|
+
`functional-regression-detected`, `oracle-changed`, `revision-unbound`, `prerequisite-unmet` (resumable where stated),
|
|
250
|
+
`environment-mismatch`, `promotion-refused`) and a `summary` that says what was and was not exercised. Gate: the
|
|
251
|
+
`patch-negative-verification` feature (high-risk execution, operator-only, off by default) plus `verification-oracles`.
|
|
252
|
+
Wiring: `fix-verify.js` (`verifyFix({ patchNegative, assuranceConfig })` adds `patchNegative` and `fixStatus`, and `ok` requires
|
|
253
|
+
`verified-fix`), `fix-verify-loop.js` (a `patchNegative` leg and the `verified-fix` verdict) and `fix/apply-fix-service.js`
|
|
254
|
+
(`applyVerifiedFix({ patchNegative })` writes only a promoted patch and returns `repairRecords`). With the flag off or no request,
|
|
255
|
+
those return exactly what they did before.
|
|
256
|
+
|
|
257
|
+
**`verification/patch-promotion.js`.** `promotePatch({ proposal, receipts })` re-derives the diff digest and revision from the
|
|
258
|
+
PROPOSAL and refuses any receipt that was not issued by `runOracle` (`isIssuedReceipt`: copies and hand-built receipts fail), that
|
|
259
|
+
covers different content than the exact diff on the original revision, that was issued for another commit or hypothesis, whose
|
|
260
|
+
oracle or inputs differ between the original and patched runs, whose environment identity differs, or that is reused in two roles.
|
|
261
|
+
Receipts are valid inside the verifier's process only; carrying a promotion across processes needs a signing domain (not here).
|
|
262
|
+
|
|
263
|
+
**`verification/repair-records.js`.** The CORE-002 record has one repair status and no value for a rejection, so the history is an
|
|
264
|
+
append-only ledger of separate, frozen, chained records: `proposed`, `rejected`, `promoted` (only through `recordPromotion`, which
|
|
265
|
+
accepts nothing but a result `promotePatch` returned `ok`), `applied` (only after a promotion for that exact diff) and
|
|
266
|
+
`rolled-back`. `verificationRepairFor`/`withRepairStatus` map the ledger onto the CORE-002 `repair` field (`replay-verified` carries the
|
|
267
|
+
patched-negative record id); a rejection leaves it at `proposed` because the foundation enum is not widened here. The ledger is
|
|
268
|
+
returned to the caller and not persisted: persisting it would need an artifact-registry classification.
|
|
269
|
+
|
|
270
|
+
**New adapters.** `replay-idempotency` (a factory receiving a recording stand-in for the effect: a request and a distinct request
|
|
271
|
+
are delivered as a control, then the same request is redelivered, and the verifier counts effects) and `functional-regression`
|
|
272
|
+
(declared cases against expected JSON values; the hypothesis is that behaviour REGRESSED, so a clean patch is `refuted`). Both have
|
|
273
|
+
the usual positive, negative and inconclusive fixtures under `test/fixtures/oracles/`.
|
|
274
|
+
|
|
275
|
+
**`oracles/scenario-classes.js` (X-205).** The supported-class manifest for non-taint defects, surfaced as
|
|
276
|
+
`oracleManifest().nonTaint`: tenant authorization and privileged action (REUSE `authorization-decision`), workflow order (REUSES
|
|
277
|
+
`state-transition`), bounded resource exhaustion (REUSES `parser-resource`) and replay/idempotency (the new adapter). Each class
|
|
278
|
+
lists what the requester must supply, its fixtures (`test/fixtures/non-taint/<class>/{positive,negative,scenario.json}`, application-style
|
|
279
|
+
code), its limitations and the report lines a reader sees; every `runOracle` result additionally carries a `disclosure`
|
|
280
|
+
(prerequisites, platform statement, limitations, class report). Unsupported non-taint classes (concurrent races, CSRF, authentication
|
|
281
|
+
throttling) are named with a reason. `judgeNonTaintHypothesis` is the rule that taint absence is never an input to a refutation:
|
|
282
|
+
without an executed oracle of the class's own adapter with proven preconditions the outcome is `not-run` (or `unsupported`); the hunt
|
|
283
|
+
mapper in `verification/emit.js` uses it. `trustedNegativeDenominator` counts only decided results of an executed oracle of an
|
|
284
|
+
advertised class with proven preconditions and trusted runtime proof, and lists every exclusion with its reason (its runtime consumer is `verificationCoverage` in
|
|
285
|
+
`verification/projection.js`, which the JSON report, the CLI report and the MCP/autopilot surfaces use for the trusted-negative line).
|
|
286
|
+
|
|
287
|
+
Not verified: Linux (the oracle platform statements stay `unverified`); only the declared exploit scenario and functional cases are
|
|
288
|
+
exercised, never the patch's correctness elsewhere.
|
|
289
|
+
|
|
290
|
+
## One projection for every interface, advisory/gating separation, conformance (X-206, X-207, X-208)
|
|
291
|
+
|
|
292
|
+
Same posture as the sections above: consumed, not wired into the default scan, every output addition additive and present only
|
|
293
|
+
when a verification record is. Suites: `test/verification/{interface-equivalence,advisory-gating,oracle-conformance}.test.js`
|
|
294
|
+
(`npm run test:verification`). Docs: `docs/guides/verification-schema-migration.md`, `docs/guides/verification-oracle-conformance.md`.
|
|
295
|
+
|
|
296
|
+
**`verification/projection.js` (X-206).** `projectVerification(record, { replay })` is the ONE place a record becomes something a
|
|
297
|
+
person or another tool reads: state, scope, oracle, evidence ids, replay prerequisites (derived from the record, plus the typed
|
|
298
|
+
ones a replay attempt reported), a what-was-verified / what-was-not summary, the legacy boolean view, and `text` lines. The JSON
|
|
299
|
+
report (`report/index.js` `normalizeFindings`), the CLI and Markdown reports (`verificationBlock`), MCP `explain_finding`,
|
|
300
|
+
`verify_fix` and `apply_fix`, and the autopilot response (`serializeAutopilotResult`, used by `scripts/autopilot.mjs --json`) all
|
|
301
|
+
call it; none formats its own verification text. The key is **`verificationView`**, never `verification`: a finding's existing
|
|
302
|
+
`verification` field is the producer/verifier separation record (`verification-separation.js`) and a first draft of this work
|
|
303
|
+
collided with it. An invalid record yields `verificationView: null` plus `verificationViewErrors`, never a guessed state. The text
|
|
304
|
+
never says "safe" or "fixed" for a partial result (a lint-style test covers every state, every repair status and the wording the real
|
|
305
|
+
surfaces emit; the fix-verify leg's native status word `fixed` is deliberately not echoed into a record reason). `verificationCoverage`
|
|
306
|
+
wraps `trustedNegativeDenominator`. Previous output schemas are pinned in `test/fixtures/verification-compat/`.
|
|
307
|
+
|
|
308
|
+
**`verification/advisory-state.js`, `verification/hypothesis-promotion.js` (X-207).** Hunt (`discovery/`) is advisory.
|
|
309
|
+
`writeAdvisoryState` is the only way advisory code writes: a closed file-name allowlist (the one hunt memory file), authoritative names
|
|
310
|
+
refused, write to a temp file and rename (a symlink or hard link planted at the target is replaced, never written through), the state
|
|
311
|
+
directory must resolve inside the project root. `discovery/memory.js` `saveMemory` uses it. `normalizeFindings` excludes any hunt
|
|
312
|
+
hypothesis (`parser: DISCOVERY` or a `discovery` object) from every finding list and exit code and records `advisoryExcluded`.
|
|
313
|
+
`promoteHypothesis({ hypothesis, result, policy })` is the only crossing: it verifies (not just checks presence of) a receipt issued by
|
|
314
|
+
`runOracle` (`isIssuedReceipt`), bound to the hypothesis and an exact commit and to a valid record settled `confirmed` at the
|
|
315
|
+
`runtime-confirmed` level, applies an EXPLICIT policy (fail-closed on an unknown key; the receipt rules are mandatory and cannot be
|
|
316
|
+
waived; `minConfidence`/`minAgreement` can only add a requirement), reuses `verification-separation.js` for hunter != verifier, and
|
|
317
|
+
returns a frozen audit record for every decision (`promoted`, `rejected`, `unsupported`, `inconclusive`) with no clock in it.
|
|
318
|
+
`runDiscovery` calls it only when `opts.promote = { policy, verify }` is given. The audit record is returned, not persisted:
|
|
319
|
+
persisting needs an artifact-registry classification and a signing domain, which are not decided here.
|
|
320
|
+
|
|
321
|
+
**`oracles/conformance.js` and `scripts/verification-conformance-check.mjs` (X-208).** One contract over every registered adapter:
|
|
322
|
+
static (class scope, budgets under `BUDGET_CEILINGS`, negative controls with an on-disk fixture, verifier-side evidence logic bound by
|
|
323
|
+
a digest, fixtures and logic pinned in `test/fixtures/oracles/conformance-pins.json`) and execution (state mappings, issued and frozen
|
|
324
|
+
receipts, three tamper attempts, an unavailable prerequisite, cancellation, replay). Wired into `scripts/release-check.mjs`
|
|
325
|
+
(`verification-conformance-gate`, in `RELEASE_GROUPS['benches-b']`) and, static half only, the pre-push gate
|
|
326
|
+
(`verification-conformance-static`). Where the boundary cannot run, execution is reported `not-run`, never passed; `--require-execution`
|
|
327
|
+
makes that a failure. A new adapter must be pinned deliberately (`--update-pins`).
|
|
328
|
+
|
|
329
|
+
## Frozen evaluation protocol, label custody and layer ablations: `evaluation/` (QA-001, QA-002, QA-003)
|
|
330
|
+
|
|
331
|
+
Offline measurement tooling, consumed by `scripts/evaluation.mjs` (`npm run evaluation -- <cmd>`, `npm run evaluation:synthetic`) and
|
|
332
|
+
`test:evaluation`. Nothing here is wired into `engine.js` or the scan path, and a test pins that no module outside `evaluation/`
|
|
333
|
+
imports it. **It builds machinery, not evidence.** No real adjudicated population, no real sealed set and no independent human
|
|
334
|
+
review exist in this repository, so every real-code gate reads `insufficient-population` or `unmeasured`; the exercised suite
|
|
335
|
+
(`synthetic.js`, fixtures in `test/fixtures/evaluation-synthetic/`) is authored by the tooling's developers and flagged
|
|
336
|
+
`synthetic: true` everywhere, and `gates.js` refuses to count it toward any minimum. Do not quote a score from the synthetic suite
|
|
337
|
+
as engine accuracy.
|
|
338
|
+
|
|
339
|
+
- **`protocol.js` (QA-001).** `freezeProtocol` validates a draft and returns a deep-frozen protocol with `protocolHash` (a digest over
|
|
340
|
+
every semantic field). It pins engine/bundle digest, a clean measurement tree, tool and model versions, dataset licences, per-target
|
|
341
|
+
pre/post commits and tree digests, scope, matching policy, limits, thresholds and split membership. Thresholds are the PRD section 5
|
|
342
|
+
floor (`PREREGISTERED_THRESHOLDS`) and may be stricter, never looser or missing. `amendProtocol` yields a new versioned protocol with a
|
|
343
|
+
field diff and empty `comparableWith`; with ANY result bound to the protocol's hash it refuses (`POST_RESULT_CHANGE`), so a threshold,
|
|
344
|
+
matching rule or denominator cannot move after results are observed. `deriveSuccessorProtocol` is the only way forward and rejects a
|
|
345
|
+
sealed target a result already consumed (`SEALED_REUSE`). A target removed from the population must appear in `retired` with a reason
|
|
346
|
+
and stays in `originalTargetIds`. `assertBoundToProtocol` rejects a run scored under any other hash.
|
|
347
|
+
- **`grouping.js`.** Union-find over pair id, normalised upstream (a fork resolves to its parent), advisory ids, commits and template
|
|
348
|
+
fingerprints; `assignSplits` places whole groups by a salted hash, `splitStraddles` is the check the protocol validator runs.
|
|
349
|
+
- **`labels.js` (QA-002).** Defect labels need a root-cause id, affected commit, language, family, a reviewed location RANGE (a CWE
|
|
350
|
+
alone cannot be a label), evidence, and adjudication with at least two distinct reviewers who are not the proposer, recorded as
|
|
351
|
+
independent, plus non-model evidence. A candidate (advisory-only) label is valid data and never scoreable. Negatives are `safe-real`,
|
|
352
|
+
`patched` (post variant, paired to a defect) and `near-miss`. `matchFinding` matches by location plus family or CWE; CWE-only,
|
|
353
|
+
wrong-file, no-line and out-of-range each fail with a distinct reason. A finding with no line cannot be localised, so it never
|
|
354
|
+
matches (this is why the pattern-only layer scores zero on the synthetic suite: it reports no line, not a defect of the harness).
|
|
355
|
+
This module can validate that a label SAYS it was independently adjudicated, not that it was.
|
|
356
|
+
- **`custody.js` (QA-002.AC03).** The custodian (verifier domain, role `custodian`) is the only writer of the label store; read needs the
|
|
357
|
+
trust-domain `sealed-labels` read grant and the sealed hash to match. `protectedTermsFrom` / `guardPrompt` / `auditWorkspace` /
|
|
358
|
+
`stageWorkspace` keep advisory prose, evidence refs, benchmark names, fixed-source hints, answer-key file names, advisory ids and
|
|
359
|
+
symlinks out of anything the engine or a prompt sees; a leaking tree is quarantined (removed, recorded as an outcome, never scanned).
|
|
360
|
+
These are PATTERN checks over known terms, not proof against a paraphrase. Process-level read denial of the label directory is the
|
|
361
|
+
sandbox boundary's job (`runInBoundary({ labelDirs })`, which reports `blocked` where the host cannot prove the control).
|
|
362
|
+
- **`runner.js`, `scan-child.js` (QA-003).** `runEvaluation` records, per target and variant, status (`completed|timeout|error|unavailable|
|
|
363
|
+
quarantined`), findings, duration, cost (null when unmeasured, never zero), failure reason and input/output hashes. The engine runs in a
|
|
364
|
+
child process (killed by process group on timeout) over a staged copy, with a scrubbed environment. Run identity covers protocol,
|
|
365
|
+
engine, layer, provider, model, settings, cache, budget, seed, replicate, split and every target digest; labels are not part of it.
|
|
366
|
+
A tree that differs from the digest pinned in the protocol is refused. `runReplicates` checks deterministic reproduction and requires
|
|
367
|
+
at least three replicates for the stochastic (model-assisted) layer; `runAblations` runs `deterministic-only`, `deep-taint` and
|
|
368
|
+
`model-assisted` over identical targets and refuses incomparable configurations. `model-assisted` is `unavailable` without
|
|
369
|
+
`AGENTIC_SECURITY_LLM_ENDPOINT`, recorded as such.
|
|
370
|
+
- **`score.js`.** End-to-end scoring: a timeout, error, unavailable or quarantined known positive is a MISS. Conditional-on-completion
|
|
371
|
+
metrics are reported beside it, labelled, never instead of it. False positives are counted only on adjudicated negatives. Findings no
|
|
372
|
+
adjudicated label accounts for go to a review queue and `unscored` (with counts and reasons), never auto-FP. Targets with no
|
|
373
|
+
adjudicated label are `unlabeled-target`.
|
|
374
|
+
- **`gates.js`.** Judges a sealed-split score against the protocol's own thresholds. `pass` needs the population minimums (>=100 real,
|
|
375
|
+
adjudicated, non-synthetic sealed positives and negatives per core language, >=30 positives per family) AND a measured value over the
|
|
376
|
+
threshold; otherwise `insufficient-population` or `unmeasured`. A protocol failing its hash check, or a score from another protocol, is
|
|
377
|
+
`rejected`. The interval-lower-bound gate is `unmeasured` until the interval method is implemented (QA-004), so `overall` cannot be
|
|
378
|
+
`pass` in this build. `accuracy-scorecard.js` relays a gate report in an `evaluationGates` section (`summarizeEvaluationGates`),
|
|
379
|
+
forcing `unmeasured` for a synthetic or unmarked report; `scripts/scorecard.mjs` reads the gate report from the independent bench directory
|
|
380
|
+
when one has been written (none exists today).
|
|
381
|
+
|
|
382
|
+
Tests: `test/evaluation/{protocol,grouping,labels,custody,runner,gates-scorecard}.test.js` (`npm run test:evaluation`).
|
|
383
|
+
## Executable invariants, approval, scenarios and the business-state oracle: `invariants/` (X-401 to X-404)
|
|
384
|
+
|
|
385
|
+
Same posture as the verification directories above: consumed, not wired into the default scan, every output addition additive, and
|
|
386
|
+
everything off unless the **`invariant-scenarios`** feature (high-risk execution, operator-only, off by default) is enabled; running a
|
|
387
|
+
scenario also needs `verification-oracles`. Suite: `npm run test:invariants` (`test/invariants/`, executed tests skip loudly where the
|
|
388
|
+
trust boundary cannot run). Linux stays `unverified`: nothing here claims a Linux outcome. X-405 to X-408 (shrinking, repair
|
|
389
|
+
verification, coverage and export, the held-out ablation) are described in the section after this one.
|
|
390
|
+
|
|
391
|
+
- **`schema.js` + `expressions.js` (X-401).** The versioned invariant document: scope (an explicit application, entry, factory and
|
|
392
|
+
environment), tenants, actors, resources, allowed transitions, FORBIDDEN outcomes and an oracle binding. Five classes
|
|
393
|
+
(tenant-isolation, privilege-constraint, value-conservation, workflow-order, idempotency). Declarative only: forbidden outcomes are
|
|
394
|
+
expressions from a closed grammar (`EXPRESSIONS`), every string in them matches a conservative charset, so no field can carry code;
|
|
395
|
+
an op outside the grammar is rejected. Rejects ambiguous identities (duplicate actor/resource/transition/store key), dangling
|
|
396
|
+
references, a missing, unregistered or wrong-class oracle binding, and preserves author, revision and review state. The id is a
|
|
397
|
+
hash of the CONTENT (not the review), so an approval binds to exactly what was reviewed and an edit is a different contract.
|
|
398
|
+
`expressions.js` exists so the oracle adapter can validate expressions without importing the schema (the schema imports the
|
|
399
|
+
oracle registry; the registry imports the adapters).
|
|
400
|
+
- **`lifecycle.js` (X-402).** Inferred versus approved. `inferredInvariant` builds a PROPOSED skeleton (inferred, with source
|
|
401
|
+
evidence and an uncertainty, authored by a model or by code); miners reuse it: `specification-mining.js` (`mineInvariantProposals`),
|
|
402
|
+
`business-logic.js` (`mineWorkflowInvariants`), `logic-claims.js` (`invariantFromLogicClaim`) and the discovery lenses
|
|
403
|
+
(`discovery/lenses.js` `invariantProposalFor`, surfaced by `runDiscovery` as `invariantProposals` only when asked AND the feature is
|
|
404
|
+
on). The approval ledger is append-only, hash-chained and HMAC-signed under the per-install key (`integrity.js`): a transition
|
|
405
|
+
records who, why, from, to, the authority it rests on, and the previous record. Only a HUMAN named by an explicit local reviewer
|
|
406
|
+
policy (or verified by `fix/approver-registry.js`) can approve, reject or supersede; a model, code or the proposer cannot, and with
|
|
407
|
+
no policy nothing can be approved. `classifyViolation` calls a settled `confirmed` result an APPROVED violation (may gate) only
|
|
408
|
+
when the ledger verifies and says approved, else a CANDIDATE violation (advisory); a document that merely claims approval is a
|
|
409
|
+
candidate, and a result without a receipt `runOracle` issued is `unverified`. `violationReport` keeps the two groups apart.
|
|
410
|
+
Limits, stated: symmetric HMAC is tamper-evidence for the operator, not third-party non-repudiation; the reviewer identity is a
|
|
411
|
+
claim checked against the operator's list, not authenticated; dropping the LAST record is not detectable; persistence is the
|
|
412
|
+
caller's (the ledger is plain data).
|
|
413
|
+
- **`scenarios.js` (X-403).** `generateScenarios(invariant, { fixture, seed, bounds, config })`: five families (cross-tenant access,
|
|
414
|
+
privilege change, reordered workflow, duplicate request, concurrent operations), each with synthetic tenants and actors, a seeded
|
|
415
|
+
setup, a control sequence, an attack sequence, the forbidden outcomes and a cleanup. Bounded by actors, depth, requests,
|
|
416
|
+
scheduling (a parallel group is at most 4 steps with a SEEDED start order) and time (the budget becomes the run deadline); every
|
|
417
|
+
bound has a hard ceiling and an over-ceiling request is rejected, not clamped. Deterministic given the seed (no clock; ids are
|
|
418
|
+
hashes). Confined to contracts scoped to a `disposable-fixture` and to fixture files the caller supplies; a shared/production
|
|
419
|
+
scope, missing setup or a scenario that cannot be built is `unsupported` with its reason. `runScenario` builds a replay manifest
|
|
420
|
+
(`replay/replay.js`) so the environment, toolchain and oracle logic digest are pinned, then runs it. `run.js` (`verifyInvariant`)
|
|
421
|
+
is the one orchestration: generate, replay each scenario, classify against the ledger, report.
|
|
422
|
+
- **`business-state` oracle (X-404)** in `oracles/adapters.js` (class `business-state`, `requiresFeature: 'invariant-scenarios'`,
|
|
423
|
+
fixtures `test/fixtures/oracles/business-state/`, pinned like every adapter). The harness gives the application an in-memory
|
|
424
|
+
durable store and an effect recorder (`ctx.store`, `ctx.emit`), plays a control sequence and the attack sequence TWICE (a
|
|
425
|
+
disagreement is `inconclusive`, never a verdict), and logs every durable write and emitted effect. `state-assertions.js`
|
|
426
|
+
evaluates the forbidden outcomes VERIFIER-SIDE over that log after the tree is dead: durable writes, leaked content, totals,
|
|
427
|
+
state moves and repeated effects decide; the response status is recorded in the event sequence and never decides, so a handler
|
|
428
|
+
answering 403 after writing another tenant's record is a violation. A forbidden outcome reached by the control flow counts too.
|
|
429
|
+
`refuted` needs the control flow to have visibly worked (durable effect, no error, clean, cleaned up). The result carries a
|
|
430
|
+
bounded `report` (preconditions, sanitized snapshots with secret-looking fields redacted, the event sequence, per-assertion
|
|
431
|
+
results with stable `ievd:` evidence ids); the ids are record evidence and the report digest is bound into the receipt.
|
|
432
|
+
Two small additive changes to shared code: `oracle.js` supports an optional per-adapter `requiresFeature` and optional
|
|
433
|
+
`judged.report`/`judged.evidenceItems`; `conformance.js` enables an adapter's `requiresFeature` for its execution checks.
|
|
434
|
+
- **Limits, not hidden.** The application is a factory returning `(ctx, key, payload)` actions over an in-memory store: durable state
|
|
435
|
+
held in a real database, queue or file system is not observed. Concurrency is cooperative scheduling in one process (interleavings
|
|
436
|
+
at await points), not parallel execution. A clean result covers only the declared bounded scenario. A target that recognises the
|
|
437
|
+
oracle and forges its own log entries is not distinguishable from a real violation (the limit every adapter states).
|
|
438
|
+
- **`deployment-ablation.js` (X-308).** A frozen, SYNTHETIC deployment-context ablation: the same source deployed under an exploitable and a
|
|
439
|
+
non-exploitable boundary configuration (`test/fixtures/deployment-ablation/`, seven cases, fourteen instances, kubernetes and compose
|
|
440
|
+
deployments). `freezeAblationSet` hash-pins each source tree, each deployment tree, the labels and the graph arm's decision rule and binds
|
|
441
|
+
the cases into a QA-001 protocol (`synthetic: true`, never counted toward a real-population gate); `verifyFrozenSet` refuses any drift
|
|
442
|
+
before a measurement. `runPairedAblation` scores a source-only arm (the QA-003 child-process scan) and a graph-enabled arm (the shipped
|
|
443
|
+
`runBoundaries` path plus a frozen demotion rule: demote only on `blocked`, or `none-found` in a graph with no gap) on the same
|
|
444
|
+
instances and reports paired counts (false positives reduced, confirmed defects added, baseline confirmed defects lost, false positives
|
|
445
|
+
added), precision, recall, runtime and coverage (unresolved, stale and unbound reported apart). Uncertainty is a SIMPLE paired case
|
|
446
|
+
bootstrap, labelled as such: the grouped bootstrap of QA-004 is not in this base. `adapterValidation` marks an adapter validated only with
|
|
447
|
+
a positive and a negative fixture and no lost defect; `docs/guides/deployment-aware-support.md` may claim only those (a test compares).
|
|
448
|
+
CLI: `npm run evaluation -- deployment-ablation [--json] [--write-frozen]`. The cases, labels and rule are authored by the tooling's
|
|
449
|
+
developers and one case exists because an earlier rule lost a defect on it, so the set is a mechanism check, not an estimate.
|
|
450
|
+
|
|
451
|
+
Tests: `test/evaluation/{protocol,grouping,labels,custody,runner,gates-scorecard}.test.js` (`npm run test:evaluation`); the X-308 ablation is tested in `test/deployment/differentiation-ablation.test.js` (`npm run test:deployment`).
|
|
452
|
+
|
|
453
|
+
## Shrinking, repair verification, coverage and the held-out ablation (X-405 to X-408)
|
|
454
|
+
|
|
455
|
+
Same posture as above: consumed, not wired into the default scan, behind `invariant-scenarios`, every output addition additive. Suites:
|
|
456
|
+
`test/invariants/{scenario-shrinking,invariant-repair,business-coverage,invariant-ablation}.test.js`. Guide: `docs/guides/business-logic-invariants.md`.
|
|
457
|
+
|
|
458
|
+
- **`invariants/shrink.js` (X-405).** `shrinkScenario(scenario, { fixture, commit, config, budget })`: a greedy removal search (an attack step,
|
|
459
|
+
one member of a parallel group, a control step, a seed record), each candidate replayed through `runScenario` (so manifest, boundary,
|
|
460
|
+
receipt), kept only if it settles `confirmed` with every precondition held AND the same authoritative failing assertion (the first
|
|
461
|
+
violated outcome in the contract's order; identified by forbidden-outcome id and phase). Units a surviving step addresses, or that
|
|
462
|
+
carry an asserted marker, are guarded (recorded with a reason, never silently skipped). Budget: `maxRuns` and `timeBudgetMs`, each with
|
|
463
|
+
a hard ceiling and rejected, not clamped, above it; `minimal: true` only when a full pass kept nothing more. One confirming replay
|
|
464
|
+
sits outside the budget. `reproductionSignals` is a PATTERN check over the fixture text for clocks, randomness, timers, network and
|
|
465
|
+
package imports; `reproducibility.blockedBy` names concurrency (cooperative scheduling, two fresh runs disagreed), external
|
|
466
|
+
dependencies and prerequisites. `checkMinimization` re-derives from the two scenarios that the minimized one is a removal-subset
|
|
467
|
+
(invariant, fixture, actors, forbidden outcomes untouched; no step added; no sequence emptied; no referenced record dropped);
|
|
468
|
+
`verifyShrinkRecord` recomputes the digest, id and receipt links. `scenarios.js` gained `deriveScenario` (same pinned scenario, new
|
|
469
|
+
inputs, recomputed limits and id) and `exerciseOf`; `run.js` gained an optional `shrink` input and an `exercise` field per result.
|
|
470
|
+
- **`invariants/repair.js` (X-406).** `verifyInvariantRepair` composes `verifyPatchNegative` (business-state as the declared behaviour
|
|
471
|
+
check: `req.functional.oracleId` and `promotePatch`'s `functionalOracle` are the two small additive changes in `verification/`) with
|
|
472
|
+
preservation legs for every other APPROVED contract on the fixture (baseline and patched, a pre-existing violation is not blamed on
|
|
473
|
+
the patch). Needs the violated contract approved in a verifying ledger or nothing runs. Blockers carry codes from `REASON_CODES`;
|
|
474
|
+
`reasonCodeFor` maps every patch-negative failure code and sends an unknown one to `setup-failed`. A preservation failure after a
|
|
475
|
+
promotion appends a `rejected` repair record. `patch-negative.js` now also counts a failed business-state control flow
|
|
476
|
+
(`observed.preconditions`) as a broken revision. A verified repair builds a regression artifact (exact patch, invariant document and
|
|
477
|
+
approval evidence, scenario, one pinned manifest per leg with its expected outcome, secret-checked); `runRegressionArtifact`
|
|
478
|
+
re-executes it from the artifact alone, `validateRegressionArtifact` detects an edit without running. Receipts do not cross a
|
|
479
|
+
process; the artifact crosses by re-execution. The approval evidence in it is a digest and a transition id: checking the HMAC needs
|
|
480
|
+
the operator's key.
|
|
481
|
+
- **`invariants/coverage.js`, `export.js`, `project-input.js`, `cli.js` (X-407).** `businessCoverage` reports inventory by the VERIFIED
|
|
482
|
+
ledger, exercised actors/states/transitions from executed scenarios only, and gaps (`GAP_CODES`) that running more of the same cannot
|
|
483
|
+
remove; it always states it is a bounded sample. `invariantCoverageFields` is the one place a surface asks for the additive
|
|
484
|
+
`invariantCoverage` field (`report/index.js` JSON and CLI); flag off or nothing attached returns `{}`, pinned by
|
|
485
|
+
`test/fixtures/invariant-compat/`. `exportScenarios` builds the reproducible package (no fixture source; a scenario holding
|
|
486
|
+
secret-looking content is withheld by path; claims `exhaustive: false`); `verifyScenarioExport` detects edits. CLI:
|
|
487
|
+
`agentic-security invariants export|coverage|regress`; MCP: `invariant_scenario_export` (`mcp/invariant-tools.js`, read-only).
|
|
488
|
+
- **`evaluation/invariant-ablation.js` (X-408).** Three arms (source-only engine scan, inferred contract, approved contract) over a frozen
|
|
489
|
+
synthetic benchmark (`test/fixtures/invariant-benchmark/`, `manifest.json` + `pin.json`, loader fails closed), one denominator and one
|
|
490
|
+
budget, flags produced before any label is read, Wilson intervals, paired counts, unique findings, scenario cost, and per-class
|
|
491
|
+
claims that are `supported` only from executed evidence meeting the registered policy (nothing executed is `unmeasured`). Driver:
|
|
492
|
+
`npm run bench:invariant-ablation -- run|verify|pin` (`scripts/invariant-ablation.mjs`). SYNTHETIC: authored and labelled by the
|
|
493
|
+
developers, no independent adjudication; a figure from it is not engine accuracy and not a real-world benefit claim.
|
|
494
|
+
- **Limits, not hidden.** Same as the section above (in-memory store, cooperative concurrency, only declared outcomes). A shrunk
|
|
495
|
+
scenario is a smaller reproduction, not a root cause. A verified repair covers the declared scenario, workflows and the other
|
|
496
|
+
contracts' bounded scenarios only. Linux is not claimed anywhere here.
|
|
497
|
+
- **`interval.js`, `report.js` (QA-004).** `groupedBootstrap` is the registered `grouped-bootstrap-95` method: percentile bootstrap over GROUPS
|
|
498
|
+
(grouping.js), 2000 resamples, seed = first 32 bits of sha256(protocolHash + statistic name), `unmeasured` with the reason below 10 independent
|
|
499
|
+
groups. `wilsonInterval` covers the completion rate. `buildAccuracyReport` publishes raw counts first, then P/R/F1 by language and family,
|
|
500
|
+
micro and macro, completion with an interval, and every disclosed gap (unscored labels and findings, negatives whose scan failed). A stratum below
|
|
501
|
+
the protocol's floor is `unmeasured` with raw counts and a separate `descriptive` figure. `score.js` now carries `perTarget` tallies (what the
|
|
502
|
+
bootstrap resamples) and scores repeated alerts of one flaw as ONE false positive (`clusterAlerts`, `alertsCollapsed`); `alertBurden` reports the
|
|
503
|
+
raw alerts a reviewer reads beside the root causes, so deduplication never hides burden. `checkPopulationsDisjoint` refuses a target in two of
|
|
504
|
+
regression, development, sealed, operational.
|
|
505
|
+
- **`economics.js` (QA-004.AC03).** `accountCosts` keeps model, tool, cache (USD) and human-review (MINUTES) apart, treats an entry with no amount as
|
|
506
|
+
unmeasured (never zero; the ratios become lower bounds), and divides ALL machine spend, failed and duplicate attempts included, by DISTINCT confirmed
|
|
507
|
+
root causes and validated fixes. A confirmed outcome must name its root cause so a duplicate cannot be counted twice.
|
|
508
|
+
- **`why-missed.js` (QA-005).** `classifyMiss` names the earliest failed stage for a labelled DEVELOPMENT miss (scan-execution, parser-ir,
|
|
509
|
+
source-modelling, propagation, sink-modelling, filters, suppression, deduplication, attribution) or `unknown` with a reason. A candidate observed
|
|
510
|
+
downstream (a reported finding, a dedupe loser, a ledger drop) is checked first because it proves the earlier stages worked. Evidence comes from
|
|
511
|
+
`scan-child.js` in `diagnose` mode (the suppression ledger incl. guard-recognized drops with finding id, CWE, family and the dominating guard line;
|
|
512
|
+
dedupe and rejected-guard stage evidence behind `AGENTIC_SECURITY_STAGE_EVIDENCE=1`, diagnostic only; sources, sinks, IR lowering).
|
|
513
|
+
`collectDiagnostics` refuses any sealed target. `compareMisses` gives before and after stage counts, the recovered defects with the stage that lost
|
|
514
|
+
each, and names new false positives instead of netting them. `dev-cases.js` builds the development case suite (`test/fixtures/engine-mechanisms/`,
|
|
515
|
+
every label flagged synthetic) that `scripts/dev-recovery.mjs` runs against ANY engine checkout, so a before and after is a measurement.
|
|
516
|
+
- **`release-gate.js` (QA-007).** `evaluateReleaseGates` wraps `gates.js` (which now computes the per-language F1 lower bound from the grouped bootstrap
|
|
517
|
+
and adds the integrity gates `answer-key-leaks` and `denominator-intact`, and `no-regression-vs-baseline`) and decides whether a NEW accuracy claim is
|
|
518
|
+
allowed. The baseline comes only from a verified ledger: `appendSealedEvaluation` / `verifyLedger` (hash chain) / `signLedger` (the custodian signs the
|
|
519
|
+
head, so a rebuilt chain does not verify). `assessReleaseClaim` blocks a different detector digest (`detectorDigestOf`, evaluation tooling excluded) on
|
|
520
|
+
already-consumed sealed targets (`fresh-sealed-population-required`); old results are retained as evaluation-only. `signGateReport` signs an AGGREGATE
|
|
521
|
+
verdict (no per-case outcome) with a local Ed25519 key, trust basis `self-issued-local-key`, never independent certification.
|
|
522
|
+
- **`bakeoff.js` (QA-008.AC02).** The bake-off manifest, generic comparator slots (no tool is named anywhere), adapters that mark an unavailable slot
|
|
523
|
+
`not-evaluated` with a typed reason, a comparability check (identical workload digest and verification standard) and a claim guard: accuracy and
|
|
524
|
+
cost claims only between evaluated, comparable participants; `superiority` is always `none claimed`. Scoring is `comparison.js`, unchanged.
|
|
525
|
+
Docs: `docs/guides/bakeoff-recipe.md`; the offline reproduction is `scripts/public-reproduction.mjs` (`npm run reproduce:mini`).
|
|
526
|
+
|
|
527
|
+
Tests: `test/evaluation/{protocol,grouping,labels,custody,runner,gates-scorecard,reporting,why-missed,guard-dominance,engine-mechanisms,release-gate,publication}.test.js` (`npm run test:evaluation`; the two engine files are also in `test:dataflow`). Guides: `docs/guides/{evaluation-reporting,miniature-reproduction,bakeoff-recipe,engine-mechanism-evidence}.md`.
|
|
528
|
+
|
|
529
|
+
## Release assurance, portable evidence and resumable portfolios: `portfolio/` (X-701 to X-708)
|
|
530
|
+
|
|
531
|
+
Same posture as the sections above: consumed, not wired into the default scan. The one place it touches shipped output is the verdict
|
|
532
|
+
wording, and that is additive behind the `portfolio-assurance` feature (`assurance/config.js`, default off, so flag-off output is
|
|
533
|
+
byte-identical and no pinned fixture changed). **It builds machinery, not evidence**: no real release or portfolio exists, every
|
|
534
|
+
fixture is flagged `synthetic: true`, and nothing here claims real-world assurance. Linux enforcement stays `unverified`; no Linux
|
|
535
|
+
outcome is asserted. Suite: `npm run test:portfolio` (`test/portfolio/`).
|
|
536
|
+
|
|
537
|
+
- **`manifest.js` (X-701).** `buildManifest` / `validateManifest`: the release assurance manifest on the assurance schema kit and
|
|
538
|
+
`identity.js` (id prefix `ram`, allowlisted fields, no clock). Binds subject repository and commit, dependency revisions, build
|
|
539
|
+
artifact digests, scope, graph snapshot digest, invariant versions and verification receipts. Every mandatory check is in exactly one
|
|
540
|
+
of completed / incomplete / unsupported / waived (incomplete and unsupported name their gaps, a waiver names its approver and reason),
|
|
541
|
+
and the blocking policy (id, digest, severity) is part of the record. Validation rejects evidence not bound to a declared receipt,
|
|
542
|
+
a receipt whose commit is not the bound commit of its repository, a mandatory check listed nowhere, and coverage or `complete` that
|
|
543
|
+
disagrees with the lists. Residual risks and coverage are machine-readable fields.
|
|
544
|
+
- **`bundle.js` (X-702).** The portable bundle: an index file (bundle.json) plus `blobs/<sha256 hex>`. `exportBundle` writes the sanitized
|
|
545
|
+
findings (closed field set, snippets stay behind, secret shapes redacted, then refused if any survive), provenance, replay manifests,
|
|
546
|
+
toolchain identities and the cited receipts, index last. `verifyBundle` is the OFFLINE verifier (fs and crypto only, executes nothing
|
|
547
|
+
from the bundle): re-hashes every blob, rejects symlinks, oversize, bad names, missing roles, a modified index, a manifest that fails
|
|
548
|
+
validation and a cited receipt that is absent, and DISCLOSES replay prerequisites without attempting a replay. `importBundle`
|
|
549
|
+
verifies first, confines every destination, never overwrites. Limits are `BUNDLE_LIMITS`.
|
|
550
|
+
- **`signing.js` (X-703).** `signAssuranceClaim` is a signer-domain act (`sandbox/trust-domains.js`, default deny: worker, target and
|
|
551
|
+
verifier are refused before a key is touched). The key lives in its own `assurance-signing/` directory (reusing `ensureKeyPair`) and may
|
|
552
|
+
be required to differ from the scan evidence key. The signed claim names manifest id and digest, bundle digest, commit, blocking policy,
|
|
553
|
+
coverage, the bounded headline, signer id, key id and the assurance policy. `verifyAssuranceClaim` is offline against an explicit
|
|
554
|
+
trust-root policy (`buildTrustPolicy`): unknown root, revoked root, a basis this build cannot verify, a tampered claim, a tampered or
|
|
555
|
+
missing bundle and a claim that does not match the bundle's manifest are each typed failures. The only verifiable basis is
|
|
556
|
+
`self-issued-local-key`, `independentlyCertified` is always false and inside the signed bytes. Private key material is never in an
|
|
557
|
+
envelope, policy, error or output.
|
|
558
|
+
- **`wording.js` (X-703.AC03).** `assuranceStatement` renders "No blocking findings in completed supported checks", then scope, then
|
|
559
|
+
gaps, then the not-a-guarantee line. `report/index.js` `toShipVerdict` and `integrations/index.js` digests use it only when
|
|
560
|
+
`portfolioAssuranceEnabled` (option, else the assurance configuration); otherwise the old "Safe to deploy" strings are untouched.
|
|
561
|
+
- **`work-units.js` (X-704).** `planPortfolio` (authorized repositories only, exact commits, stable `wu:` ids), a seven-state machine with
|
|
562
|
+
lease expiry, retry count and a hash-chained attempt record per unit (rewriting an attempt fails `verifyStore`), idempotent lease /
|
|
563
|
+
start / complete / fail by attempt id (a duplicate completion verifies once; a stale attempt, such as a zombie after lease expiry,
|
|
564
|
+
is refused), `progressOf` counting only verified units, locked atomic persistence (`openStore`, `mutateStore`; a corrupt store is an
|
|
565
|
+
error, never a reset) and `runPortfolio` (bounded concurrency, an executor that throws fails its unit). `fleet.js` `runFleet` accepts
|
|
566
|
+
`portfolioVerified` (from `fullyVerifiedRepositories`) and skips those repositories like a resumed completed one.
|
|
567
|
+
- **`resume.js` (X-705).** Every verified result records one digest per dimension (code, policy, graph, invariant, oracle, toolchain;
|
|
568
|
+
`scan-checkpoint.js` `computeRunKey` / `computeGlobalKey` supply the code and toolchain digests). `planResume` reuses a unit only on a
|
|
569
|
+
full match; any other change `applyResume` invalidates: the old result moves to `stale` (inspectable with `inspectStale`), the unit
|
|
570
|
+
returns to pending in a new generation, `assessClaims` marks dependent claims stale. A graph-only change is narrowed by the drift
|
|
571
|
+
module (`lineage/deployment/drift.js` `planRescan`): a unit bound to hypotheses is reused only if the plan is complete and all its
|
|
572
|
+
hypotheses are in `skipped`; anything else fails closed.
|
|
573
|
+
|
|
574
|
+
Tests: `test/portfolio/{manifest,bundle,signing,work-units,resume}.test.js` (`npm run test:portfolio`).
|
|
575
|
+
## Calibrated model routing: `routing/` (X-601 to X-608)
|
|
576
|
+
- **`scheduler.js` (X-706).** Bounded scheduling over the store. `scheduleNext` admits and leases ONE unit in a single locked transaction
|
|
577
|
+
(`mutateStore`), after checking five limits at two levels (portfolio and per repository: wall time, provider spend, requests, storage,
|
|
578
|
+
plus concurrency) against what is used PLUS what in-flight attempts have reserved PLUS the unit's own estimate. A unit with no
|
|
579
|
+
estimate is not leased (unknown is never zero). A refusal is `at-capacity` (a wait) or `exhausted` (will not recover), reported per
|
|
580
|
+
unit and per scope, and a refusal changes nothing. An attempt that ends with no usage report is charged its whole reservation; a
|
|
581
|
+
reported overrun is recorded (`overruns`) and shrinks what remains. Wall time is also enforced while a unit runs (the executor's
|
|
582
|
+
`signal` aborts at the unit's wall estimate). Fairness: priority is a WEIGHT (integer 1..8, default 1), never an order; the next lease
|
|
583
|
+
goes to the admissible repository with the lowest `leasesGranted / weight` (ties: repository name, then unit id), so a 120-unit
|
|
584
|
+
repository cannot starve a 1-unit one, weight 8 against 1 is served about eight times as often with the light one still served early,
|
|
585
|
+
and a repository that was blocked is owed service when it returns. A blocked or over-budget repository is skipped with a reason, never
|
|
586
|
+
waited on; an unaffordable unit does not block a cheaper sibling. `routingBudgetFor` hands the routing policy (`routing/decide.js`) the
|
|
587
|
+
smaller of the portfolio and repository dollar remainders, less reservations; `chargeModelSpend` charges a routing decision at its
|
|
588
|
+
upper cost bound and refuses a blocked or unbounded decision. No provider is called here. `createCancelScope().cancel` takes a scope
|
|
589
|
+
(`{unitId}`, `{repository}` or `{all}`; an empty scope is refused, never read as cancel-all), records it in the ledger so a process
|
|
590
|
+
that did not receive the call still honours it, aborts in-process attempts (work started with `ctx.spawn` runs under
|
|
591
|
+
`sandbox/supervise.js`, which ends the whole process tree and reports survivors), releases a lease ONLY when the attempt settled with no
|
|
592
|
+
survivor, otherwise leaves it to expire (so no other worker can start the unit while a descendant may live) and never re-leases it.
|
|
593
|
+
Verified units and receipts are untouched; the report is `incomplete` with the reason. `heartbeat` renews the lease and records a
|
|
594
|
+
heartbeat in one transaction; `runScheduled` heartbeats every `HEARTBEAT.intervalMs` (5 s) while an attempt runs and takes an optional
|
|
595
|
+
`backend`, probed before every lease (an unavailable one stops the run as `blocked`). Limits: the driver's heartbeat proves the
|
|
596
|
+
controlling process is alive, not that an in-process executor is making progress (the wall bound handles that); tree termination is
|
|
597
|
+
verified on the macOS userspace backend only and Linux enforcement stays `unverified`.
|
|
598
|
+
- **`progress.js` (X-707).** ONE projection (`buildProgressView`) for the CLI, the MCP tool and the fleet summary, in the pattern of
|
|
599
|
+
`lineage/deployment/projection.js`. It keeps apart verified units (the only progress), blocked / failed / canceled / stale units with
|
|
600
|
+
reasons, per-repository coverage (fully-verified, partial, incomplete, not-started), per-limit budget (limit, used, reserved,
|
|
601
|
+
remaining; an unenforced limit says so), pending human review (blocked units plus `reviewItemsFromInvariantCoverage` and
|
|
602
|
+
`reviewItemsFromBoundaryContexts`), and every worker with its age, `live` or `stale` (stale after `HEARTBEAT.staleAfterMs`, 15 s;
|
|
603
|
+
silent workers are measured from their lease). `aggregateFindings` deduplicates by `stableId` ONLY, keeps every occurrence
|
|
604
|
+
(repository, environment, release, commit, file, line, severity) and lists affected releases separately; a finding with no stable id
|
|
605
|
+
is listed apart, never merged. `completion` states that a finished controller is not every unit verified and not any repository
|
|
606
|
+
passing (`passAssessment: 'not-implied'`). Additive behind `portfolio-assurance`: `attachPortfolioProgress` returns the SAME rollup
|
|
607
|
+
object when the feature is off, `renderFleetSummary` adds a PORTFOLIO clause only when the field is present, and
|
|
608
|
+
`test/fixtures/portfolio/pre-change-pins.json` (generated by `test/portfolio/make-pins.mjs` from code before the change) pins the
|
|
609
|
+
fleet rollup, summary and HTML. CLI `agentic-security portfolio progress`; MCP `portfolio_progress` (read-only, `mcp/portfolio-tools.js`).
|
|
610
|
+
- **`backend.js` (X-708).** The storage backend interface (`probe`, `storeFile`, `withExclusive`, fenced `acquireLease` / `renewLease` /
|
|
611
|
+
`releaseLease`) and a file-lock reference implementation. `createLocalBackend` makes its directory; `createSharedBackend` requires a
|
|
612
|
+
marker written by an explicit `initSharedBackend`, so an unmounted path, an empty mount point, a symlink or a damaged marker is a typed
|
|
613
|
+
`blocked` (`openBackend` returns `{ ok: false, state: 'blocked', code }` and no backend; there is deliberately no fallback argument,
|
|
614
|
+
and a blocked open creates nothing). A backend that disappears mid-run stops `runScheduled` as `blocked` without recreating the
|
|
615
|
+
directory or writing anywhere else. Lease semantics, each tested with two REAL processes: mutual exclusion, monotonic fence, expiry
|
|
616
|
+
takeover, a replaced holder refused on renew and release. NOT claimed: network-filesystem safety (`describe()` reports it
|
|
617
|
+
`unverified`; the operator must verify exclusive-create atomicity on that mount), cross-host clock agreement, or race-free stale-lock
|
|
618
|
+
recovery (narrow window, documented). `exportState` / `verifyStateExport` / `importState`: one self-checking, secret-checked file for an
|
|
619
|
+
air-gapped machine; import verifies first and never overwrites.
|
|
620
|
+
- **`retention.js` (X-708.AC01).** Retention by class (`replay-evidence`, `metadata`, `model-trace`, `secret`; defaults 365 / 730 / 30 / 0
|
|
621
|
+
days, ceilings 1095 / 1825 / 90 / 7, an over-ceiling setting is clamped and disclosed). Order: a required current receipt (named in
|
|
622
|
+
`currentReceiptIds`, or `requiredBy` a unit that is `verified` in the store NOW) is never deleted and an expired one is reported as
|
|
623
|
+
`expiredButRequired`; then a legal hold (validated and checked with `legal-hold.js`'s `isUnderHold`; by id, class or repository; a
|
|
624
|
+
malformed hold refuses the whole plan); then expiry. An unclassified or malformed record is kept and reported. `applyRetention` re-plans
|
|
625
|
+
against the live store, confines paths to the root (traversal, absolute paths, symlinks and directories refused), and logs each deletion
|
|
626
|
+
to a hash-chained log BEFORE removing the file (no log, no deletion; a broken log blocks every deletion). The log holds digests and sizes,
|
|
627
|
+
never content. CLI `agentic-security portfolio retention plan|apply|verify-log`.
|
|
628
|
+
|
|
629
|
+
Tests: `test/portfolio/{manifest,bundle,signing,work-units,resume,scheduler,progress,retention,backend}.test.js` (`npm run test:portfolio`).
|
|
630
|
+
`test/portfolio/proc-worker.mjs` is the child process the multi-process consistency tests spawn; it is not a test file.
|
|
631
|
+
## Calibrated model routing: `routing/` (X-601 to X-604)
|
|
632
|
+
|
|
633
|
+
Extends `model-routing.js`, `model-trust.js`, the provider catalog and cache economics; consumed, not wired into the scan or the interactive advisor. Everything is behind the **`model-routing`** feature (`assurance/config.js`, model-network risk, off by default, kill switch `AGENTIC_SECURITY_NO_MODEL_ROUTING`). With it off, `routeModelWithPolicy` returns exactly what `routeModelWithTrust` returns, and `test/routing/unchanged-when-off.test.js` recomputes the routing, trust, cache-economics, catalog and advisor outputs against `test/fixtures/routing/pre-change-pins.json` (generated from the code before any routing edit). Suite: `npm run test:routing`. **Machinery, not evidence:** no real routing outcome or adjudicated task exists in this repository, every population in the tests is SYNTHETIC (`test/helpers/routing-fixtures.js`), and the section 5 routing gate reads `unmeasured` (synthetic) or `insufficient-population`, never `pass`. No cost or quality improvement is claimed anywhere.
|
|
634
|
+
|
|
635
|
+
- **`outcomes.js` (X-601).** Routing task record (kind: discovery, triage, verification-planning, repair; language; vulnerability class; context tokens and bucket; required capabilities; data class), the stratum key `kind|language|class|bucket` and its coarser parents, and the outcome record. `deriveRoutingLabel` is the only way to a decided label: independent adjudication (a human or verifier service that is not the model or its provider, with the adjudication version) or trusted execution (a `vrec:` verification record). Provider agreement, self-reported confidence, accepted suggestions, model judgement and majority vote are refused (`NOT_A_TRUTH_LABEL`) and the outcome is recorded `unknown`. Outcome records keep every status (failed, timeout, cancelled, provider-error, partial, blocked) and unknown or delayed correctness; an unreported count is `null`, never 0. `createOutcomeLedger` has no remove or overwrite.
|
|
636
|
+
- **`economics.js` (X-602).** `priceBookFromCatalog` builds the versioned price book from `provider-catalog.js` (the catalog's dated snapshot stays the single price table). `costOf` splits input, cached input, output, retries and tool execution, and names price version, currency and billing basis. Unknown is `null` or a bounded `[lower, upper]` estimate, never 0 (an unpriced model, a subscription basis, an unreported token count, an unknown cache split, an unmeasured tool cost). A prediction goes in `predicted` and never fills `measured`. `cache-economics.js` (which prices Claude Code's own transcript) and `hooks/model-cost-advisor.js` are deliberately unchanged and pinned.
|
|
637
|
+
- **`adapter.js` (X-602.AC03).** `createProviderAdapter(...).invoke` has no HTTP client: it goes through `guardedModelCall` (feature gate, egress policy before the prompt exists, redaction, request cap, deadline, bounded retries, audit chain) with an INJECTED transport. Telemetry is built from an allowlist (provider, model, endpoint host only, status, a fixed failure code, token counts, cache state, latency, byte counts); it never carries the prompt, the response, an error message or a credential. Provider failures, partial responses, cancellation, timeouts and cache hits each yield an outcome record.
|
|
638
|
+
- **`calibration.js` (X-603).** `buildCalibration` fits per (model, version, stratum) quality on HELD-OUT outcomes with a time split, a model-version split (versions are never pooled) and group disjointness (a group on both sides is removed from the held-out side and counted). Uncertainty is `groupedBootstrap` from `evaluation/interval.js` (`unmeasured` below 10 groups). The label `reliable` needs at least 200 paired adjudicated tasks overall, 30 per stratum (`ROUTING_MINIMUMS`, a config cannot lower them), a measured interval and a stable development-versus-held-out rate; a synthetic population only ever gets `reliable-synthetic`. Otherwise the stratum carries `fallback-parent` (a reliable coarser stratum, named), `shifted` or `insufficient-evidence`, and an explicit fallback (`parent-stratum` or `baseline-model`). The artifact records the dataset hash, config hash, adjudication versions, counts and every exclusion by reason, and `verifyCalibration` rebuilds it byte for byte. `routingPopulationGate` is the population half of the PRD section 5 routing row only; the paired quality, cost and latency comparison is `promotion.js` (X-605).
|
|
639
|
+
- **`decide.js` (X-604).** `routeConstrained` applies availability, capability, privacy (the egress policy, or a capability-manifest egress check), context window, reliable and fresh quality evidence whose LOWER bound meets the minimum, interval width and budget BEFORE minimising measured cost or latency. Every candidate lists all of its failing constraints. When nothing is acceptable the result is `fallback` (the existing capability route, only if it passes capability, privacy, context and budget, with quality marked UNVERIFIED) or `blocked` with a code; a constraint is never relaxed to find a route. `routeModelWithPolicy` in `model-routing.js` is the entry point; a blocked decision returns `model: null, blocked: true`.
|
|
640
|
+
- **Limits, not hidden.** The provider catalog carries prices and cache models but not capabilities or context windows, so `candidateFromCatalog` takes them from the operator and an unknown window is rejected. Catalog prices are a dated snapshot. Cost bounds cover model tokens only. Telemetry redaction depends on the egress redaction patterns, which are pattern checks, not proof against every secret shape.
|
|
641
|
+
- **`shadow.js` (X-605.AC01).** `createShadowRecorder().shadow()` records what `routeConstrained` WOULD pick and returns the production route it was given, the same object. It holds no transport or adapter (a test pins that the source cannot call a provider), so no paid call is possible by construction. A disabled routing control records nothing.
|
|
642
|
+
- **`promotion.js` (X-605.AC02, AC03).** `freezeTaskSet` fixes the evaluation tasks by hash; `replayPaired` pairs each frozen task's baseline arm with the arm the shadow decision chose, from recorded outcomes. It issues a provider call only with BOTH an injected `invoke` and a bounded authorization (`authorizedBy`, finite `maxCalls`, `maxUsd`, `perCallUsdCeiling`, all under `REPLAY_LIMITS`); an `invoke` without them is refused (`UNBOUNDED_REPLAY`), an unknown cost is charged at the ceiling, and a replay that hits its bound is `truncated`. `evaluatePromotion` judges the PRD section 5 routing row (200 paired adjudicated overall, 30 per promoted stratum, quality difference lower bound above -0.02, median cost 20% lower or quality +0.05 at no higher median cost, p95 latency 1.2x) with the grouped bootstrap. Verdict precedence: `invalid` > `unmeasured` (synthetic) > `insufficient-population` > `fail` > `pass`. Invalidations: `cherry-picked` (pairs differ from the frozen set), `dropped-tasks` (no pair: no shadow record, missing or ambiguous outcome, bound reached), `unbounded-replay`, `truncated-replay`, `replay-edited`, `frozen-set-edited`. A failure status, a timeout, a blocked proposal or an unserved task is not-correct in the comparison and is reported, never dropped; an unadjudicated pair is counted apart and never imputed; an unknown cost or latency leaves the criterion unmet. The quality-gain path is stricter than the PRD wording (the lower bound must clear zero too).
|
|
643
|
+
- **`drift.js`, `canary.js` (X-606).** `snapshotRoutingBasis` records what a policy was promoted on; `assessDrift` compares it with the present (model version, price entry, schema, recent decided quality) and returns a documented `fallback` or `shadow` state; `applyInvalidations` derives a calibration whose affected estimates are `invalidated` (quality, version, schema) or lose their measured cost (pricing), so `routeConstrained` refuses them. A cost-invalidated model ranks behind one with a valid measured cost, by the existing ranking rule. `createCanary` is finite by construction (no limits, no canary): task count, spend budget, error rate, consecutive-error outage, incorrect rate; a trip restores the known-good policy and only appends receipts. `activate` needs a `pass` promotion verdict. `routeUnderDrift` is the state-aware entry point.
|
|
644
|
+
- **`feedback.js` (X-607).** `createFeedbackStore`: actors (human or verifier-service) registered with a key sign each record (HMAC); the first accepted label is the immutable original, a change is a correction citing the entry it corrects; an allowlist of fields is kept and the names of dropped fields only; unauthorized, unverifiable, tampered, duplicate and poisoned feedback is quarantined with a count and reason, and `applyToOutcomes` is what a calibration should be built from. `authorizePayload` / `sendToProvider` need the task policy AND the egress policy (or a capability manifest check) to allow source or evidence, and send only through the guarded adapter. Limit: a trusted-execution label cannot be recomputed from the outcome alone, so a flip of one is not detected here.
|
|
645
|
+
- **`receipts.js`, `control.js`, `report.js` (X-608).** `createReceiptLog` is a hash-linked log of every shadow decision, replay, promotion, drift, canary, rollback and feedback decision (`exportReceipts`, `verifyReceiptChain`; tamper evidence, not a signature). `resolveRoutingControl` (option, then `AGENTIC_SECURITY_ROUTING`, then default; a bad value fails closed to `disabled`) is honoured by `routeModelWithPolicy` (disabled keeps the capability route, `pin:<model>` selects that model). `buildRoutingReplayReport` / `renderRoutingReport` / `buildPolicyCard` restate the verdict with exact denominators, intervals, measured cost, cache states and latency percentiles, restrict claims to the strata and model versions the gate passed, and keep unsupported strata and unmet gates listed. Reproduce offline with `npm run reproduce:routing` (`scripts/routing-replay.mjs`, controls, `--fault`); guide `docs/guides/routing-replay.md`. Everything in the tests and the script is generated and `synthetic`; the gate reads `unmeasured`, never `pass`, on it, and no advantage is claimed.
|
|
646
|
+
|
|
142
647
|
## Execution-proof tiers (R2)
|
|
143
648
|
|
|
144
649
|
`proof-tier.js` + `execution-proof.js` add a fourth axis to a finding's
|