@clear-capabilities/agentic-security-scanner 0.150.1 → 0.150.2

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 (86) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/bin/agentic-security.js +73 -10
  3. package/dist/1122.index.js +16 -16
  4. package/dist/1208.index.js +23 -0
  5. package/dist/1301.index.js +3 -3
  6. package/dist/1379.index.js +3 -3
  7. package/dist/1444.index.js +3 -3
  8. package/dist/1660.index.js +1 -1
  9. package/dist/1700.index.js +5 -5
  10. package/dist/1905.index.js +12 -12
  11. package/dist/1920.index.js +10 -10
  12. package/dist/2238.index.js +4 -4
  13. package/dist/2271.index.js +3 -3
  14. package/dist/2432.index.js +8 -8
  15. package/dist/2659.index.js +1 -1
  16. package/dist/2826.index.js +2 -2
  17. package/dist/2830.index.js +3 -3
  18. package/dist/2923.index.js +1 -1
  19. package/dist/3001.index.js +4 -4
  20. package/dist/3117.index.js +4 -4
  21. package/dist/3180.index.js +9 -9
  22. package/dist/3276.index.js +1 -1
  23. package/dist/3415.index.js +1 -1
  24. package/dist/3499.index.js +1 -1
  25. package/dist/3518.index.js +4 -4
  26. package/dist/3736.index.js +7 -7
  27. package/dist/3839.index.js +4 -4
  28. package/dist/4113.index.js +441 -418
  29. package/dist/4265.index.js +4 -4
  30. package/dist/4384.index.js +8 -3
  31. package/dist/4399.index.js +29 -3
  32. package/dist/4547.index.js +2 -2
  33. package/dist/4863.index.js +6 -6
  34. package/dist/4970.index.js +2 -2
  35. package/dist/5051.index.js +15 -15
  36. package/dist/5144.index.js +4 -4
  37. package/dist/5333.index.js +8 -8
  38. package/dist/5343.index.js +2 -2
  39. package/dist/5350.index.js +6 -6
  40. package/dist/5561.index.js +1 -1
  41. package/dist/5637.index.js +10 -5
  42. package/dist/5724.index.js +21642 -0
  43. package/dist/5756.index.js +99 -489
  44. package/dist/5830.index.js +3 -3
  45. package/dist/6257.index.js +21 -8
  46. package/dist/6626.index.js +5 -5
  47. package/dist/6662.index.js +1 -1
  48. package/dist/6675.index.js +4 -4
  49. package/dist/6730.index.js +10 -10
  50. package/dist/6829.index.js +4 -4
  51. package/dist/6944.index.js +2 -2
  52. package/dist/6994.index.js +5 -5
  53. package/dist/7039.index.js +83 -19
  54. package/dist/7178.index.js +11 -6
  55. package/dist/7227.index.js +3 -3
  56. package/dist/7552.index.js +2 -2
  57. package/dist/7709.index.js +2 -2
  58. package/dist/7838.index.js +1037 -0
  59. package/dist/8218.index.js +4 -4
  60. package/dist/8476.index.js +4 -4
  61. package/dist/8513.index.js +8 -8
  62. package/dist/8520.index.js +1 -1
  63. package/dist/8752.index.js +4 -4
  64. package/dist/9207.index.js +2 -2
  65. package/dist/9220.index.js +5 -5
  66. package/dist/9390.index.js +6 -6
  67. package/dist/9503.index.js +2 -2
  68. package/dist/9560.index.js +3221 -0
  69. package/dist/957.index.js +4 -4
  70. package/dist/9801.index.js +1 -1
  71. package/dist/9824.index.js +5 -5
  72. package/dist/agentic-security.mjs +23 -23
  73. package/dist/agentic-security.mjs.sha256 +1 -1
  74. package/package.json +3 -2
  75. package/src/egress/redact.js +1 -1
  76. package/src/engine.js +13 -2
  77. package/src/llm-validator/agent-loop.js +85 -5
  78. package/src/llm-validator/fix-proposal.js +16 -3
  79. package/src/llm-validator/model-capabilities.js +26 -1
  80. package/src/llm-validator/model-probe.js +46 -8
  81. package/src/llm-validator/ollama-provider.js +26 -1
  82. package/src/llm-validator/oom-feedback.js +69 -0
  83. package/src/llm-validator/providers.js +50 -0
  84. package/src/llm-validator/redact.js +177 -11
  85. package/src/report/index.js +14 -3
  86. package/src/sast/java-ast-folding.js +18 -2
@@ -1 +1 @@
1
- 188a70d75f1a823e67422f8af02d42d14bc8dd476bf66136e02d6e59abc51f4d agentic-security.mjs
1
+ 54183e4ed75d70aeec00ba53f44450879c52d23fc1bd3ccc834719531f18a017 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.150.1",
3
+ "version": "0.150.2",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, pipeline integrity, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -66,9 +66,10 @@
66
66
  "test": "node ../scripts/run-unit-tests.mjs && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js && node --experimental-test-module-mocks --test test/fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection-k8s-admission.test.js && node --experimental-test-module-mocks --test test/lineage-fault-injection.test.js && npm run test:python",
67
67
  "test:python": "node ../scripts/run-python-tests.mjs",
68
68
  "test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
69
+ "test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
69
70
  "test:glob": "node --test test/glob-compat.test.js",
70
71
  "test:sast": "node --test test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/zip-slip-containment-guard.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/mcp-audit-scan.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/guard-recognition.test.js test/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js",
71
- "test:posture": "node --test test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/release-workflow.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js",
72
+ "test:posture": "node --test test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/release-workflow.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js",
72
73
  "test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js",
73
74
  "test:mcp": "node --test test/mcp.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js",
74
75
  "test:report": "node --test test/report-scan-invariants.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
@@ -156,7 +156,7 @@ export function redactPayload({ text, filePath = null, scanRoot = null, taxonomy
156
156
 
157
157
  let out = text;
158
158
 
159
- const secretResult = redactSecrets(out);
159
+ const secretResult = redactSecrets(out, { filePath });
160
160
  out = secretResult.text;
161
161
  categories.secrets = secretResult.redactions;
162
162
 
package/src/engine.js CHANGED
@@ -191,7 +191,7 @@ import { annotateExploitability, detectProjectContext } from './posture/exploita
191
191
  import { applyFeedback as applyLearnedFeedback } from './posture/learning.js';
192
192
  import { validateMany as llmValidateMany, applyValidatorVerdicts } from './llm-validator/index.js';
193
193
  import { MODEL_STATUS, stageSummaryFromModelStatus } from './llm-validator/model-status.js';
194
- import { resolveProvider as resolveLlmProvider } from './llm-validator/providers.js';
194
+ import { resolveProvider as resolveLlmProvider, otherRemoteRoles as llmOtherRemoteRoles, NO_CLOUD_FALLBACK } from './llm-validator/providers.js';
195
195
  import { scanCrossLangOpenAPI } from './posture/cross-lang-openapi.js';
196
196
  import { scanCrossLangGrpc } from './posture/cross-lang-grpc.js';
197
197
  import { scanCrossLangGraphql } from './posture/cross-lang-graphql.js';
@@ -9993,12 +9993,23 @@ function _deterministicFileTimings(timings) {
9993
9993
  const status = finalFindings.llmValidatorStatus;
9994
9994
  if (status && status.counts[MODEL_STATUS.DISABLED] !== status.total) {
9995
9995
  const resolved = resolveLlmProvider({ role: 'validate' });
9996
+ // Adversarial-review finding (Ollama offline PRD premortem, 2026-09):
9997
+ // this block only ever checked the `validate` role, but printed an
9998
+ // unqualified "LLM inference was loopback-only" claim a reader could
9999
+ // reasonably take as describing the WHOLE scan — while `verify`/
10000
+ // `explain`/`fix`/`poc`/`logic`/`hunt` each have their own
10001
+ // independent per-role provider override (providers.js's `_forRole`)
10002
+ // and could genuinely be pointed at a cloud vendor at the same time.
10003
+ // `otherRolesRemote` makes that visible instead of silent.
10004
+ const otherRolesRemote = llmOtherRemoteRoles('validate');
9996
10005
  _aiAssistance = {
10006
+ scopeRole: 'validate',
9997
10007
  provider: resolved.ok ? resolved.config.provider : null,
9998
10008
  model: resolved.ok ? resolved.config.model : null,
9999
10009
  endpoint: resolved.ok ? resolved.config.endpoint : null,
10000
10010
  egress: resolved.ok ? resolved.config.egress : null,
10001
- cloudFallback: false,
10011
+ cloudFallback: !NO_CLOUD_FALLBACK,
10012
+ otherRolesRemote,
10002
10013
  stages: { validate: stageSummaryFromModelStatus(status) },
10003
10014
  };
10004
10015
  }
@@ -26,6 +26,7 @@ import { evaluateEgress } from '../egress/policy.js';
26
26
  import { TOOL_DEFINITIONS, TOOL_ERROR, runTool } from './agent-tools.js';
27
27
  import { getModelCapabilities } from './model-probe.js';
28
28
  import { statePath as defaultStatePath } from '../posture/state-dir.js';
29
+ import { priorOOMFor } from './oom-feedback.js';
29
30
 
30
31
  export const AGENT_LOOP_ERROR = Object.freeze({
31
32
  NOT_CONFIGURED: 'agent-loop-not-configured',
@@ -37,6 +38,35 @@ export const AGENT_LOOP_ERROR = Object.freeze({
37
38
  export const DEFAULT_MAX_TOOL_ITERATIONS = 12;
38
39
  const DEFAULT_WALL_CLOCK_TIMEOUT_MS = 5 * 60 * 1000;
39
40
 
41
+ // Adversarial-review finding (2026-09), confirmed against a real, slow
42
+ // (CPU-only) Ollama server: the wall-clock deadline used to be checked ONLY
43
+ // at the top of each iteration, never around the in-flight callOllamaChat
44
+ // itself. `docs/guides/ollama.md` tells users to raise
45
+ // AGENTIC_SECURITY_LLM_TIMEOUT_MS for a cold-loading model — that value
46
+ // flows into the PER-CALL requestTimeoutMs, which used to be entirely
47
+ // independent of this loop's own wall-clock budget. A single call already
48
+ // running when the wall clock expires would complete anyway (its own
49
+ // timeout could be minutes longer), and only the NEXT iteration's top-of-
50
+ // loop check would notice — reporting `wall-clock-timeout` after exactly
51
+ // one useful call, no matter how high the per-call timeout was raised,
52
+ // which made the documented remedy for slow models actively useless.
53
+ //
54
+ // Fix: cap the PER-CALL requestTimeoutMs at whatever wall-clock budget
55
+ // actually remains, every iteration. A slow call now gets cut off by ITS
56
+ // OWN timeout mechanism (producing the same clean `{ok:false,
57
+ // code:'ollama-timeout'}` every other caller already handles) at exactly
58
+ // the moment the wall clock would have run out anyway — never later. This
59
+ // also makes the two settings coherent for the first time: raising
60
+ // AGENTIC_SECURITY_LLM_TIMEOUT_MS now genuinely helps, as long as the loop's
61
+ // OWN budget (wallClockTimeoutMs / AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS)
62
+ // is raised enough to give it room.
63
+ function _cappedTimeouts(baseTimeouts, remainingMs) {
64
+ if (!baseTimeouts) return { requestTimeoutMs: Math.max(1, remainingMs) };
65
+ const base = Number(baseTimeouts.requestTimeoutMs);
66
+ const capped = Number.isFinite(base) ? Math.min(base, remainingMs) : remainingMs;
67
+ return { ...baseTimeouts, requestTimeoutMs: Math.max(1, capped) };
68
+ }
69
+
40
70
  function systemPrompt(scanRoot) {
41
71
  return [
42
72
  'You are a security-scan assistant with READ-ONLY access to the scanned',
@@ -54,14 +84,49 @@ function systemPrompt(scanRoot) {
54
84
  * maxToolIterations?:number, wallClockTimeoutMs?:number}} opts
55
85
  * `statePath` defaults to posture/state-dir.js's real implementation;
56
86
  * overridable only for tests that need a fixture-scoped state dir.
87
+ * `wallClockTimeoutMs`, when not passed explicitly, falls back to
88
+ * `AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS` — a SEPARATE setting from
89
+ * `AGENTIC_SECURITY_LLM_TIMEOUT_MS` (the per-call timeout) on purpose: the
90
+ * two used to be incoherent (raising the per-call setting alone did nothing
91
+ * for a loop that could still time out after one call), so a caller who
92
+ * genuinely needs a longer overall budget for a slow model must raise BOTH.
57
93
  * @returns {{ok:true, finalText, iterations, toolCalls, stopReason} |
58
94
  * {ok:false, code, reason}}
59
95
  */
60
- export async function runAgentLoop({
96
+ export async function runAgentLoop(opts = {}) {
97
+ const result = await _runAgentLoopCore(opts);
98
+ // Adversarial-review fix (2026-09, second pass): Round 1's original OOM-
99
+ // feedback fix only surfaced `priorOOMWarning` in `models doctor`'s
100
+ // advisory output — a user who never happens to run `doctor` would OOM
101
+ // again on the exact same model via `ask` with no warning at all, since
102
+ // `recommendAdmission` (where the warning lives) is never consulted on
103
+ // this real call path. Surface it here too, on any outcome where a real
104
+ // call was actually attempted (a pure config/capability refusal before
105
+ // any call has nothing useful to warn about).
106
+ const attemptedARealCall = result.ok || result.code === AGENT_LOOP_ERROR.FAILED;
107
+ if (attemptedARealCall) {
108
+ const resolved = resolveProvider({ role: 'hunt', env: opts.env || process.env });
109
+ const prior = resolved.ok ? priorOOMFor(resolved.config.model) : null;
110
+ if (prior) {
111
+ return {
112
+ ...result,
113
+ priorOOMWarning: `'${resolved.config.model}' has previously failed with an out-of-memory error on this machine ` +
114
+ `(${prior.count} time${prior.count === 1 ? '' : 's'}, most recently ${new Date(prior.lastAt).toISOString()}).`,
115
+ };
116
+ }
117
+ }
118
+ return result;
119
+ }
120
+
121
+ async function _runAgentLoopCore({
61
122
  goal, scanRoot, env = process.env, statePath = defaultStatePath,
62
- maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, wallClockTimeoutMs = DEFAULT_WALL_CLOCK_TIMEOUT_MS,
123
+ maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, wallClockTimeoutMs,
63
124
  } = {}) {
64
125
  const boundedIterations = Math.max(1, Math.min(maxToolIterations, DEFAULT_MAX_TOOL_ITERATIONS));
126
+ if (wallClockTimeoutMs === undefined) {
127
+ const fromEnv = Number(env.AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS);
128
+ wallClockTimeoutMs = Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_WALL_CLOCK_TIMEOUT_MS;
129
+ }
65
130
 
66
131
  const resolved = resolveProvider({ role: 'hunt', env });
67
132
  if (!resolved.ok || resolved.config.provider !== 'ollama') {
@@ -97,15 +162,30 @@ export async function runAgentLoop({
97
162
  const deadline = Date.now() + boundedTimeoutMs;
98
163
 
99
164
  for (let iteration = 0; iteration < boundedIterations; iteration++) {
100
- if (Date.now() >= deadline) {
165
+ const remainingMs = deadline - Date.now();
166
+ if (remainingMs <= 0) {
101
167
  return { ok: true, finalText: null, iterations: iteration, toolCalls: toolCallLog, stopReason: 'wall-clock-timeout' };
102
168
  }
103
169
 
170
+ // Cap this call's own timeout at whatever wall-clock budget remains, so
171
+ // a slow call can never silently outlive the loop's overall bound (see
172
+ // the header comment above _cappedTimeouts for the incident this fixes).
173
+ const callTimeouts = _cappedTimeouts(timeouts, remainingMs);
174
+ const deadlineWasBinding = timeouts && Number(timeouts.requestTimeoutMs) > remainingMs;
104
175
  const r = await callOllamaChat({
105
176
  host: resolved.config.endpoint, model: resolved.config.model, messages,
106
- tools: TOOL_DEFINITIONS, keepAlive: oc?.keepAlive, timeouts,
177
+ tools: TOOL_DEFINITIONS, keepAlive: oc?.keepAlive, timeouts: callTimeouts,
107
178
  });
108
- if (!r.ok) return { ok: false, code: AGENT_LOOP_ERROR.FAILED, reason: r.reason || r.code };
179
+ if (!r.ok) {
180
+ // A timeout caused by the WALL CLOCK (not the operator's own per-call
181
+ // setting) is this loop doing exactly what it's supposed to, not an
182
+ // unexpected error — report it the same way the pre-flight check
183
+ // above does, rather than as a hard failure.
184
+ if (r.code === 'ollama-timeout' && deadlineWasBinding) {
185
+ return { ok: true, finalText: null, iterations: iteration, toolCalls: toolCallLog, stopReason: 'wall-clock-timeout' };
186
+ }
187
+ return { ok: false, code: AGENT_LOOP_ERROR.FAILED, reason: r.reason || r.code };
188
+ }
109
189
 
110
190
  const toolCalls = r.result.toolCalls || [];
111
191
  if (toolCalls.length === 0) {
@@ -52,7 +52,7 @@ export const FIX_PROPOSAL_ERROR = Object.freeze({
52
52
  * through the same redaction pipeline llm-validator/index.js's renderPrompt
53
53
  * uses, and is framed as data the model must never treat as instructions.
54
54
  */
55
- export function buildFixPrompt(finding, fileContent, scanRoot) {
55
+ export function buildFixPrompt(finding, fileContent, scanRoot, rejectionFeedback) {
56
56
  const sterileContent = redactPayload({ text: String(fileContent || ''), filePath: finding.file, scanRoot }).text;
57
57
  return [
58
58
  'You are a security patch-synthesis component. You PROPOSE a fix; a separate',
@@ -71,6 +71,19 @@ export function buildFixPrompt(finding, fileContent, scanRoot) {
71
71
  sterileContent,
72
72
  '--- END-UNTRUSTED-FILE-CONTENT ---',
73
73
  '',
74
+ // Adversarial-review fix (2026-09): at temperature 0 with the SAME
75
+ // prompt, a rejected patch would very likely just reproduce itself on
76
+ // retry — this is the one place cmdFix's bounded one-time retry
77
+ // (bin/agentic-security.js) feeds the deterministic gate's OWN rejection
78
+ // reason back in, so the second attempt has an actual reason to differ
79
+ // rather than repeating the first attempt's exact mistake.
80
+ ...(rejectionFeedback ? [
81
+ 'Your previous proposal for this exact finding was REJECTED by the',
82
+ 'deterministic verification gate below. Propose a DIFFERENT fix that',
83
+ 'avoids this specific problem — do not repeat the same patch:',
84
+ ` ${String(rejectionFeedback).slice(0, 500)}`,
85
+ '',
86
+ ] : []),
74
87
  'Propose a minimal, targeted fix for the finding above. Reply with ONLY a',
75
88
  'single JSON object, no other text:',
76
89
  '{"target_file": "<must exactly equal the File given above>", ' +
@@ -94,7 +107,7 @@ function validateFixResponse(obj, { file }) {
94
107
  * @returns {{ok:true, replacement, rationale, expectedSecurityEffect,
95
108
  * testsToRun, model} | {ok:false, code, reason}}
96
109
  */
97
- export async function proposeOllamaFix({ finding, fileContent, scanRoot, env = process.env }) {
110
+ export async function proposeOllamaFix({ finding, fileContent, scanRoot, env = process.env, rejectionFeedback } = {}) {
98
111
  const resolved = resolveProvider({ role: 'fix', env });
99
112
  if (!resolved.ok || resolved.config.provider !== 'ollama') {
100
113
  return {
@@ -112,7 +125,7 @@ export async function proposeOllamaFix({ finding, fileContent, scanRoot, env = p
112
125
  return { ok: false, code: FIX_PROPOSAL_ERROR.POLICY_BLOCKED, reason: decision.reason, egressDecision: decision };
113
126
  }
114
127
 
115
- const prompt = buildFixPrompt(finding, fileContent, scanRoot);
128
+ const prompt = buildFixPrompt(finding, fileContent, scanRoot, rejectionFeedback);
116
129
  const oc = resolved.config.ollama;
117
130
  const r = await callOllamaStructured({
118
131
  host: resolved.config.endpoint,
@@ -18,6 +18,7 @@
18
18
  // memory to the byte.
19
19
 
20
20
  import * as os from 'node:os';
21
+ import { priorOOMFor } from './oom-feedback.js';
21
22
 
22
23
  // PRD §12/§13 FR-1203 — non-authoritative family hint from a model name.
23
24
  // Longest/most-specific pattern first so `qwen3.5:4b` doesn't fall through to
@@ -200,7 +201,31 @@ export function evaluateMemoryAdmission({
200
201
  * `{admitted:false}` with a human-readable explanation, which callers treat
201
202
  * as "run deterministic-only" (PRD §23.4).
202
203
  */
203
- export function recommendAdmission({ profile, freeBytes, requestedContextTokens, requestedModel } = {}) {
204
+ export function recommendAdmission(opts = {}) {
205
+ const result = _recommendAdmissionCore(opts);
206
+ // Adversarial-review fix (2026-09): a memory-admission ESTIMATE that
207
+ // actually caused a real OOM (ollama-provider.js's callOllamaChat now
208
+ // records this via oom-feedback.js) used to have no way to affect a
209
+ // future admission decision for the SAME model on the SAME machine — the
210
+ // exact same "admitted: true" would repeat forever. This does not
211
+ // recalibrate the underlying estimate (that needs real hardware variety
212
+ // one machine's observed failures can't substitute for); it attaches an
213
+ // honest warning so the decision is no longer presented with unqualified
214
+ // confidence.
215
+ const chosenModel = result.model || opts.requestedModel;
216
+ const prior = chosenModel ? priorOOMFor(chosenModel) : null;
217
+ if (prior) {
218
+ return {
219
+ ...result,
220
+ priorOOMWarning: `'${chosenModel}' has previously failed with an out-of-memory error on this machine ` +
221
+ `(${prior.count} time${prior.count === 1 ? '' : 's'}, most recently ${new Date(prior.lastAt).toISOString()}). ` +
222
+ 'The memory estimate below may be optimistic for your hardware.',
223
+ };
224
+ }
225
+ return result;
226
+ }
227
+
228
+ function _recommendAdmissionCore({ profile, freeBytes, requestedContextTokens, requestedModel } = {}) {
204
229
  const p = MEMORY_PROFILES[profile];
205
230
  if (!p) return { admitted: false, reason: `unknown memory profile '${profile}'` };
206
231
 
@@ -18,10 +18,21 @@
18
18
  // CACHE KEY = Ollama version + model digest + model name (PRD §13.2 exactly).
19
19
  // Digest is load-bearing: `ollama pull` replacing a tag's underlying weights
20
20
  // must invalidate the cache even though the name/tag string is unchanged.
21
- // Persisted forever (no TTL) because the key itself is what expires the
22
- // entry — a version/digest bump makes a new key, not a stale hit on the old
23
- // one. Same disk-cache directory convention as sca/sigstore-verify.js and
21
+ // Same disk-cache directory convention as sca/sigstore-verify.js and
24
22
  // engine.js's OSV cache (`~/.claude/agentic-security/<name>/`).
23
+ //
24
+ // TTL + force-reprobe (adversarial-review fix, 2026-09). The key-based
25
+ // invalidation above is real but not complete: this module's own comment
26
+ // used to claim the entry is safe "forever" because the key changes when
27
+ // the model does — but `/api/show` doesn't expose a digest on every Ollama
28
+ // version (falls back to model NAME alone then, a few lines below), so a
29
+ // same-tag re-pull, or simply an unlucky single-trial probe the first time
30
+ // (see probeStructuredOutput/probeToolCalling's own single-call design),
31
+ // had no way to ever self-correct short of a user manually deleting a file
32
+ // under `~/.claude/agentic-security/`. Two independent fixes, since either
33
+ // alone leaves a real gap: a default TTL as a safety net for the case
34
+ // nobody notices, and an explicit `force` option (`models test --force`)
35
+ // for the case someone DOES suspect a stale answer and wants it right now.
25
36
 
26
37
  import * as fs from 'node:fs';
27
38
  import * as path from 'node:path';
@@ -32,18 +43,38 @@ import { capabilitiesFromFamilyHint } from './model-capabilities.js';
32
43
 
33
44
  const CACHE_DIR = path.join(os.homedir(), '.claude', 'agentic-security', 'ollama-capability-cache');
34
45
 
46
+ // Default safety-net TTL: 30 days. Not the primary invalidation mechanism
47
+ // (the key is) — a backstop for the cases the key can't see: a same-tag
48
+ // re-pull on an Ollama version that doesn't expose a digest, or a single
49
+ // unlucky probe trial that happened to pass/fail against the model's true
50
+ // behavior. Overridable for anyone who wants a tighter or looser bound.
51
+ export const DEFAULT_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
52
+
35
53
  function _ensureCacheDir() { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); } catch {} }
36
54
  function _cacheKey(ollamaVersion, modelDigest, modelName) {
37
55
  return crypto.createHash('sha256').update(`${ollamaVersion}::${modelDigest}::${modelName}`).digest('hex');
38
56
  }
39
57
  function _cachePath(key) { return path.join(CACHE_DIR, key + '.json'); }
40
58
 
41
- function _readProbeCache(key) {
42
- try { return JSON.parse(fs.readFileSync(_cachePath(key), 'utf8')); } catch { return null; }
59
+ /**
60
+ * @returns {object|null} the cached probe RESULT (not the envelope), or
61
+ * `null` on a miss, a parse failure, OR an entry older than `ttlMs`.
62
+ */
63
+ function _readProbeCache(key, ttlMs) {
64
+ let envelope;
65
+ try { envelope = JSON.parse(fs.readFileSync(_cachePath(key), 'utf8')); } catch { return null; }
66
+ // Backward-compatible with a pre-TTL cache file that was just the bare
67
+ // result object (no `probedAt`) — treat an entry with no timestamp as
68
+ // fresh rather than discarding every cache written before this fix.
69
+ if (envelope && typeof envelope === 'object' && 'probedAt' in envelope && 'result' in envelope) {
70
+ if (Number.isFinite(ttlMs) && ttlMs > 0 && Date.now() - envelope.probedAt > ttlMs) return null;
71
+ return envelope.result;
72
+ }
73
+ return envelope;
43
74
  }
44
75
  function _writeProbeCache(key, value) {
45
76
  _ensureCacheDir();
46
- try { fs.writeFileSync(_cachePath(key), JSON.stringify(value)); } catch {}
77
+ try { fs.writeFileSync(_cachePath(key), JSON.stringify({ probedAt: Date.now(), result: value })); } catch {}
47
78
  }
48
79
 
49
80
  /**
@@ -146,9 +177,16 @@ function _mergeLayer(base, overlay, sourceFlag) {
146
177
  * should use, since Layer C spends real inference time on the user's
147
178
  * machine.
148
179
  *
180
+ * `force: true` (adversarial-review fix, 2026-09 — `models test --force`)
181
+ * skips reading the cache — always runs a fresh probe and overwrites
182
+ * whatever was there. `ttlMs` (default 30 days, `DEFAULT_CACHE_TTL_MS`)
183
+ * bounds how long a cached entry is trusted without either; pass `0`/
184
+ * `Infinity` to disable the TTL safety net entirely and rely on the key
185
+ * alone, matching this module's original design intent.
186
+ *
149
187
  * @returns {{ok:true, capabilities:object, cached:boolean} | {ok:false, code, reason}}
150
188
  */
151
- export async function getModelCapabilities({ host, model, env = process.env, probe = false, timeouts, keepAlive } = {}) {
189
+ export async function getModelCapabilities({ host, model, env = process.env, probe = false, force = false, ttlMs = DEFAULT_CACHE_TTL_MS, timeouts, keepAlive } = {}) {
152
190
  let capabilities = capabilitiesFromFamilyHint(model);
153
191
 
154
192
  const show = await showOllamaModel({ host, model, timeouts });
@@ -169,7 +207,7 @@ export async function getModelCapabilities({ host, model, env = process.env, pro
169
207
  const modelDigest = show.ok && show.details?.digest ? show.details.digest : 'unknown-digest';
170
208
  const cacheKey = _cacheKey(ollamaVersion, modelDigest, model);
171
209
 
172
- const cached = _readProbeCache(cacheKey);
210
+ const cached = force ? null : _readProbeCache(cacheKey, ttlMs);
173
211
  if (cached) {
174
212
  return { ok: true, capabilities: _mergeLayer(capabilities, cached, 'runtimeProbe'), cached: true };
175
213
  }
@@ -32,6 +32,7 @@
32
32
  // never to this module's error path. See ollama-offline-egress.test.js.
33
33
 
34
34
  import { isLoopbackUrl } from './local-endpoint.js';
35
+ import { recordOOMEvent } from './oom-feedback.js';
35
36
 
36
37
  export const DEFAULT_OLLAMA_HOST = 'http://127.0.0.1:11434';
37
38
  export const DEFAULT_OLLAMA_MODEL = 'qwen3.5:4b';
@@ -44,6 +45,7 @@ const DEFAULT_MAX_CONCURRENCY = 1;
44
45
  // exactly one of these, never an ad-hoc string, so a caller (and a report) can
45
46
  // react on `code` instead of parsing prose.
46
47
  export const OLLAMA_ERROR_CODES = Object.freeze([
48
+ 'ollama-disabled',
47
49
  'ollama-not-running',
48
50
  'ollama-unreachable',
49
51
  'ollama-non-loopback-refused',
@@ -69,6 +71,18 @@ function _err(code, reason) {
69
71
  * @returns {{ok:true, config:object} | {ok:false, code:string, reason:string}}
70
72
  */
71
73
  export function ollamaEndpointConfig(env = process.env) {
74
+ // Adversarial-review fix (2026-09): no kill switch existed for this whole
75
+ // provider, unlike AGENTIC_SECURITY_MCP_DISABLED for the MCP server —
76
+ // worse, a per-role AGENTIC_SECURITY_LLM_PRESET_<ROLE>=ollama override
77
+ // (providers.js's _forRole) can keep a role calling Ollama even after an
78
+ // operator unsets the GLOBAL preset during an incident, so "just unset
79
+ // the preset" is not reliably enough. This check is here, in the one
80
+ // function every Ollama call path resolves through (resolveProvider's
81
+ // ollama branch, and models/setup's direct callers), so it can never be
82
+ // bypassed by a role-specific override the operator forgot about.
83
+ if (env.AGENTIC_SECURITY_OLLAMA_DISABLED === '1') {
84
+ return _err('ollama-disabled', 'Ollama is disabled (AGENTIC_SECURITY_OLLAMA_DISABLED=1). Unset it to re-enable.');
85
+ }
72
86
  const rawHost = env.AGENTIC_SECURITY_OLLAMA_HOST || DEFAULT_OLLAMA_HOST;
73
87
  const host = String(rawHost).replace(/\/+$/, '');
74
88
  const allowRemote = env.AGENTIC_SECURITY_OLLAMA_ALLOW_REMOTE === '1';
@@ -220,7 +234,18 @@ export async function callOllamaChat({ host, model, messages, maxTokens, schema,
220
234
  if (res.status === 404 || /not found/i.test(detail)) {
221
235
  return _err('ollama-model-not-installed', `Model '${model}' is not installed. ${detail || ''}`.trim());
222
236
  }
223
- if (/memory|oom/i.test(detail)) return _err('ollama-model-out-of-memory', detail || `HTTP ${res.status}`);
237
+ if (/memory|oom/i.test(detail)) {
238
+ // Adversarial-review fix (2026-09): this was a real, defined error
239
+ // code with no reactive call site anywhere — a wrong memory-admission
240
+ // estimate that caused a genuine OOM would repeat the identical wrong
241
+ // decision forever. Record it so recommendAdmission (model-
242
+ // capabilities.js) can warn on the NEXT admission check for this
243
+ // model on this machine, even though the underlying size/KV-cache
244
+ // ESTIMATES themselves stay uncalibrated (that needs real hardware
245
+ // variety a single machine's observed failures can't substitute for).
246
+ recordOOMEvent(model);
247
+ return _err('ollama-model-out-of-memory', detail || `HTTP ${res.status}`);
248
+ }
224
249
  if (/context/i.test(detail)) return _err('ollama-context-overflow', detail || `HTTP ${res.status}`);
225
250
  return _err('ollama-model-load-failed', detail || `HTTP ${res.status}`);
226
251
  }
@@ -0,0 +1,69 @@
1
+ // Adversarial-review fix (2026-09): `ollama-model-out-of-memory` was a real,
2
+ // defined error code (ollama-provider.js's OLLAMA_ERROR_CODES) with ZERO
3
+ // call sites reacting to it anywhere in this codebase — a wrong memory-
4
+ // admission estimate that caused a genuine OOM would silently repeat the
5
+ // identical wrong "admitted: true" decision forever, since nothing recorded
6
+ // the failure for next time.
7
+ //
8
+ // NOT a replacement for real calibration. model-capabilities.js's
9
+ // KNOWN_MODEL_SIZE_GB / ESTIMATED_KV_CACHE_MB_PER_1K_TOKENS /
10
+ // RUNTIME_OVERHEAD_MB remain asserted, unmeasured "best-effort" constants —
11
+ // properly calibrating them needs real hardware variety this session cannot
12
+ // manufacture. This module is the cheapest thing that CAN improve after a
13
+ // wrong estimate without that: a per-machine, per-model OBSERVED-FAILURE
14
+ // ledger. A model that has already OOM'd on THIS machine gets an explicit
15
+ // warning attached to the next admission decision, rather than the same
16
+ // unqualified confidence a first-time estimate gets.
17
+ //
18
+ // Same disk-cache directory convention as model-probe.js's capability
19
+ // cache and sca/sigstore-verify.js's Rekor cache
20
+ // (`~/.claude/agentic-security/<name>/`).
21
+
22
+ import * as fs from 'node:fs';
23
+ import * as path from 'node:path';
24
+ import * as os from 'node:os';
25
+
26
+ const LOG_PATH = path.join(os.homedir(), '.claude', 'agentic-security', 'ollama-oom-log.json');
27
+
28
+ function _readLog() {
29
+ try {
30
+ const parsed = JSON.parse(fs.readFileSync(LOG_PATH, 'utf8'));
31
+ return parsed && typeof parsed === 'object' ? parsed : {};
32
+ } catch { return {}; }
33
+ }
34
+
35
+ function _writeLog(log) {
36
+ try {
37
+ fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
38
+ fs.writeFileSync(LOG_PATH, JSON.stringify(log));
39
+ } catch { /* best-effort; a failure here must never break the caller's real request */ }
40
+ }
41
+
42
+ /**
43
+ * Record a real, observed OOM failure for `model` on this machine. Called
44
+ * from ollama-provider.js's callOllamaChat — the single choke point every
45
+ * Ollama HTTP call in this codebase goes through, so every role's OOM
46
+ * failures land in the same ledger regardless of which one hit it.
47
+ */
48
+ export function recordOOMEvent(model) {
49
+ if (typeof model !== 'string' || !model) return;
50
+ const log = _readLog();
51
+ const entry = log[model] || { count: 0, firstAt: Date.now() };
52
+ entry.count += 1;
53
+ entry.lastAt = Date.now();
54
+ log[model] = entry;
55
+ _writeLog(log);
56
+ }
57
+
58
+ /**
59
+ * @returns {{count:number, firstAt:number, lastAt:number} | null} prior OOM
60
+ * history for `model` on this machine, or null if it has never failed
61
+ * this way here before.
62
+ */
63
+ export function priorOOMFor(model) {
64
+ if (typeof model !== 'string' || !model) return null;
65
+ const log = _readLog();
66
+ return log[model] || null;
67
+ }
68
+
69
+ export const _internals = { LOG_PATH };
@@ -237,6 +237,18 @@ export function buildProviderRequest(config, prompt, maxTokens) {
237
237
  };
238
238
  }
239
239
 
240
+ // Architectural invariant, not a per-scan observation: nothing in this
241
+ // module (or any caller of resolveProvider) ever reacts to a failed/refused
242
+ // resolution by silently trying a DIFFERENT provider — a refusal is refused,
243
+ // full stop (see the local/ollama loopback-refusal branches above, both of
244
+ // which return `{ok:false}` rather than falling through to a vendor preset).
245
+ // Declared once, HERE, so every place in the codebase that asserts "no cloud
246
+ // fallback" (currently only engine.js's AI-Assistance report) reads the same
247
+ // single source of truth instead of each hardcoding its own `false` literal
248
+ // — if this invariant ever needs to become conditional, there is exactly one
249
+ // place to change it and everything downstream updates with it.
250
+ export const NO_CLOUD_FALLBACK = true;
251
+
240
252
  /** Which provider each role would use, for reporting. Never includes keys. */
241
253
  export function providerMatrix(env = process.env) {
242
254
  const out = {};
@@ -249,4 +261,42 @@ export function providerMatrix(env = process.env) {
249
261
  return out;
250
262
  }
251
263
 
264
+ // `hunt` is deliberately NOT one of `ROLES` above (discovery/hunter.js's own
265
+ // comment: it has no per-role override of its own, always falling back to
266
+ // whatever the GLOBAL preset resolves to) — which means `providerMatrix()`,
267
+ // built by iterating `ROLES`, structurally never sees it. Adversarial-review
268
+ // fix (2026-09, second pass): that made `otherRemoteRoles` blind to hunt by
269
+ // construction, not by an oversight in its filter — and hunt is exactly the
270
+ // role most likely to diverge from `validate` in the scenario this function
271
+ // exists to catch (a global cloud preset with `validate` given its own
272
+ // `ollama` override: hunt then silently follows the global cloud preset).
273
+ // Checked explicitly, alongside the `ROLES`-iterated matrix, rather than
274
+ // added to `ROLES` itself — doing that would wrongly imply hunt supports a
275
+ // `_HUNT`-suffixed per-role override, which it does not and this fix does
276
+ // not add.
277
+ const ROLES_WITH_NO_PER_ROLE_OVERRIDE = Object.freeze(['hunt']);
278
+
279
+ /**
280
+ * Which roles OTHER than `excludeRole` resolve to a remote (non-loopback)
281
+ * provider right now. Exists so a report scoped to one role (the AI
282
+ * Assistance block's `validate`-only measurement) can disclose when a
283
+ * DIFFERENT role is genuinely configured for a cloud vendor at the same
284
+ * time, instead of leaving that role invisible to a reader who reasonably
285
+ * generalizes a single-role loopback claim onto the whole scan.
286
+ *
287
+ * @returns {{role:string, provider:string}[]}
288
+ */
289
+ export function otherRemoteRoles(excludeRole, env = process.env) {
290
+ const matrix = providerMatrix(env);
291
+ const out = Object.entries(matrix)
292
+ .filter(([role, r]) => role !== excludeRole && r.provider && r.egress === 'remote')
293
+ .map(([role, r]) => ({ role, provider: r.provider }));
294
+ for (const role of ROLES_WITH_NO_PER_ROLE_OVERRIDE) {
295
+ if (role === excludeRole) continue;
296
+ const r = resolveProvider({ role, env });
297
+ if (r.ok && r.config.egress === 'remote') out.push({ role, provider: r.config.provider });
298
+ }
299
+ return out;
300
+ }
301
+
252
302
  export const _internals = { SHAPES, DEFAULT_MODEL, _forRole };