@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.
Files changed (182) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/bin/agentic-security.js +98 -2
  3. package/dist/1340.index.js +1871 -0
  4. package/dist/3477.index.js +727 -0
  5. package/dist/3499.index.js +23 -3
  6. package/dist/4113.index.js +59 -8
  7. package/dist/5014.index.js +833 -0
  8. package/dist/5756.index.js +2 -2
  9. package/dist/6257.index.js +1 -1
  10. package/dist/6907.index.js +248 -0
  11. package/dist/6994.index.js +1 -1
  12. package/dist/7033.index.js +33 -5
  13. package/dist/7439.index.js +1822 -0
  14. package/dist/7838.index.js +38 -2
  15. package/dist/8798.index.js +1 -1
  16. package/dist/9207.index.js +5 -0
  17. package/dist/9560.index.js +2336 -27
  18. package/dist/957.index.js +1 -1
  19. package/dist/{1905.index.js → 9999.index.js} +306 -13
  20. package/dist/agentic-security.mjs +12 -12
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/package.json +39 -7
  23. package/src/capabilities/CLAUDE.md +114 -0
  24. package/src/capabilities/address.js +73 -0
  25. package/src/capabilities/attack-coverage.js +93 -0
  26. package/src/capabilities/decide.js +256 -0
  27. package/src/capabilities/delegate.js +69 -0
  28. package/src/capabilities/egress.js +20 -0
  29. package/src/capabilities/hook-advice.js +50 -0
  30. package/src/capabilities/index.js +32 -0
  31. package/src/capabilities/manifest.js +344 -0
  32. package/src/capabilities/outbound.js +189 -0
  33. package/src/capabilities/paths.js +57 -0
  34. package/src/capabilities/probes.js +211 -0
  35. package/src/capabilities/proxy.js +182 -0
  36. package/src/capabilities/reasons.js +67 -0
  37. package/src/capabilities/receipts.js +356 -0
  38. package/src/capabilities/records.js +58 -0
  39. package/src/capabilities/recovery.js +367 -0
  40. package/src/capabilities/report.js +81 -0
  41. package/src/capabilities/runner.js +236 -0
  42. package/src/capabilities/secrets.js +26 -0
  43. package/src/capabilities/tool-gate.js +119 -0
  44. package/src/capabilities/tool-registry.js +60 -0
  45. package/src/dataflow/CLAUDE.md +29 -0
  46. package/src/dataflow/catalog.js +21 -0
  47. package/src/dataflow/engine.js +20 -0
  48. package/src/dataflow/guard-dominance.js +352 -0
  49. package/src/discovery/CLAUDE.md +18 -0
  50. package/src/discovery/index.js +43 -1
  51. package/src/discovery/lenses.js +18 -0
  52. package/src/discovery/memory.js +7 -5
  53. package/src/engine.js +80 -15
  54. package/src/fix/apply-fix-service.js +33 -3
  55. package/src/integrations/index.js +5 -2
  56. package/src/ir/CLAUDE.md +14 -0
  57. package/src/ir/implicit-handler-params.js +74 -0
  58. package/src/ir/parser-cs.js +20 -0
  59. package/src/ir/parser-go.js +30 -13
  60. package/src/ir/parser-java.js +11 -0
  61. package/src/language/nix-eval-isolation.js +33 -5
  62. package/src/lineage/CLAUDE.md +31 -0
  63. package/src/lineage/deployment/adapter-cloud.js +202 -0
  64. package/src/lineage/deployment/adapter-compose.js +90 -0
  65. package/src/lineage/deployment/adapter-kit.js +142 -0
  66. package/src/lineage/deployment/adapter-kubernetes.js +247 -0
  67. package/src/lineage/deployment/attack-paths.js +443 -0
  68. package/src/lineage/deployment/boundaries-run.js +223 -0
  69. package/src/lineage/deployment/boundary-graph.js +567 -0
  70. package/src/lineage/deployment/cross-repo.js +253 -0
  71. package/src/lineage/deployment/drift.js +383 -0
  72. package/src/lineage/deployment/ingest.js +221 -0
  73. package/src/lineage/deployment/projection.js +244 -0
  74. package/src/lineage/deployment/trace-correlation.js +541 -0
  75. package/src/lineage/source-registry.js +19 -0
  76. package/src/mcp/CLAUDE.md +14 -3
  77. package/src/mcp/invariant-tools.js +64 -0
  78. package/src/mcp/portfolio-tools.js +79 -0
  79. package/src/mcp/redact.js +15 -0
  80. package/src/mcp/server.js +24 -3
  81. package/src/mcp/tools.js +30 -5
  82. package/src/posture/CLAUDE.md +505 -0
  83. package/src/posture/accuracy-scorecard.js +95 -4
  84. package/src/posture/assurance/baseline-inventory.js +218 -0
  85. package/src/posture/assurance/baseline.js +322 -0
  86. package/src/posture/assurance/bounded-io.js +145 -0
  87. package/src/posture/assurance/config.js +299 -0
  88. package/src/posture/assurance/contracts.js +315 -0
  89. package/src/posture/assurance/identity.js +62 -0
  90. package/src/posture/assurance/migrations.js +259 -0
  91. package/src/posture/assurance/rollback.js +214 -0
  92. package/src/posture/assurance/rollout.js +200 -0
  93. package/src/posture/assurance/schema-kit.js +111 -0
  94. package/src/posture/assurance/verification-record.js +258 -0
  95. package/src/posture/attestation.js +5 -0
  96. package/src/posture/autopilot.js +28 -1
  97. package/src/posture/business-logic.js +26 -4
  98. package/src/posture/evaluation/bakeoff.js +148 -0
  99. package/src/posture/evaluation/custody.js +200 -0
  100. package/src/posture/evaluation/deployment-ablation.js +416 -0
  101. package/src/posture/evaluation/dev-cases.js +65 -0
  102. package/src/posture/evaluation/economics.js +85 -0
  103. package/src/posture/evaluation/gates.js +161 -0
  104. package/src/posture/evaluation/grouping.js +101 -0
  105. package/src/posture/evaluation/interval.js +132 -0
  106. package/src/posture/evaluation/invariant-ablation.js +325 -0
  107. package/src/posture/evaluation/labels.js +200 -0
  108. package/src/posture/evaluation/protocol.js +308 -0
  109. package/src/posture/evaluation/release-gate.js +209 -0
  110. package/src/posture/evaluation/report.js +158 -0
  111. package/src/posture/evaluation/runner.js +273 -0
  112. package/src/posture/evaluation/scan-child.js +80 -0
  113. package/src/posture/evaluation/score.js +174 -0
  114. package/src/posture/evaluation/synthetic.js +90 -0
  115. package/src/posture/evaluation/why-missed.js +219 -0
  116. package/src/posture/evidence-bundle.js +93 -7
  117. package/src/posture/execution-proof.js +11 -1
  118. package/src/posture/fix-verify-loop.js +20 -6
  119. package/src/posture/fix-verify.js +37 -2
  120. package/src/posture/fleet.js +11 -2
  121. package/src/posture/invariants/cli.js +95 -0
  122. package/src/posture/invariants/coverage.js +164 -0
  123. package/src/posture/invariants/export.js +178 -0
  124. package/src/posture/invariants/expressions.js +46 -0
  125. package/src/posture/invariants/lifecycle.js +289 -0
  126. package/src/posture/invariants/project-input.js +55 -0
  127. package/src/posture/invariants/repair.js +340 -0
  128. package/src/posture/invariants/run.js +45 -0
  129. package/src/posture/invariants/scenarios.js +311 -0
  130. package/src/posture/invariants/schema.js +167 -0
  131. package/src/posture/invariants/shrink.js +407 -0
  132. package/src/posture/invariants/state-assertions.js +165 -0
  133. package/src/posture/logic-claims.js +12 -0
  134. package/src/posture/model-routing.js +40 -0
  135. package/src/posture/oracles/adapters.js +531 -0
  136. package/src/posture/oracles/conformance.js +276 -0
  137. package/src/posture/oracles/oracle.js +453 -0
  138. package/src/posture/oracles/registry.js +76 -0
  139. package/src/posture/oracles/scenario-classes.js +223 -0
  140. package/src/posture/portfolio/backend.js +279 -0
  141. package/src/posture/portfolio/bundle.js +314 -0
  142. package/src/posture/portfolio/cli.js +142 -0
  143. package/src/posture/portfolio/manifest.js +263 -0
  144. package/src/posture/portfolio/progress.js +267 -0
  145. package/src/posture/portfolio/resume.js +130 -0
  146. package/src/posture/portfolio/retention.js +207 -0
  147. package/src/posture/portfolio/scheduler.js +488 -0
  148. package/src/posture/portfolio/signing.js +155 -0
  149. package/src/posture/portfolio/wording.js +59 -0
  150. package/src/posture/portfolio/work-units.js +405 -0
  151. package/src/posture/replay/replay.js +250 -0
  152. package/src/posture/routing/adapter.js +140 -0
  153. package/src/posture/routing/calibration.js +278 -0
  154. package/src/posture/routing/canary.js +141 -0
  155. package/src/posture/routing/control.js +35 -0
  156. package/src/posture/routing/decide.js +211 -0
  157. package/src/posture/routing/drift.js +137 -0
  158. package/src/posture/routing/economics.js +185 -0
  159. package/src/posture/routing/feedback.js +207 -0
  160. package/src/posture/routing/index.js +31 -0
  161. package/src/posture/routing/outcomes.js +281 -0
  162. package/src/posture/routing/promotion.js +291 -0
  163. package/src/posture/routing/receipts.js +59 -0
  164. package/src/posture/routing/report.js +142 -0
  165. package/src/posture/routing/shadow.js +53 -0
  166. package/src/posture/specification-mining.js +26 -0
  167. package/src/posture/verification/advisory-state.js +71 -0
  168. package/src/posture/verification/emit.js +257 -0
  169. package/src/posture/verification/hypothesis-promotion.js +211 -0
  170. package/src/posture/verification/patch-negative.js +226 -0
  171. package/src/posture/verification/patch-promotion.js +113 -0
  172. package/src/posture/verification/projection.js +183 -0
  173. package/src/posture/verification/repair-records.js +124 -0
  174. package/src/posture/verifier.js +16 -0
  175. package/src/report/index.js +100 -1
  176. package/src/sandbox/CLAUDE.md +51 -0
  177. package/src/sandbox/backend-namespace.js +7 -0
  178. package/src/sandbox/backend-userspace.js +151 -34
  179. package/src/sandbox/control-probes.js +215 -0
  180. package/src/sandbox/supervise.js +201 -0
  181. package/src/sandbox/trust-boundary.js +113 -0
  182. package/src/sandbox/trust-domains.js +171 -0
@@ -0,0 +1,64 @@
1
+ // invariant-tools.js: the MCP surface for bounded business-logic scenarios (X-407.AC03).
2
+ //
3
+ // One READ-ONLY tool, `invariant_scenario_export`: build the reproducible scenario package for a reviewer-supplied contract and a
4
+ // disposable fixture directory that both live inside the session root. It runs nothing and writes nothing. The same function
5
+ // the CLI calls (`posture/invariants/export.js` `exportFromFiles`) does the work, so the two surfaces cannot disagree.
6
+ //
7
+ // Guarantees, each with a test:
8
+ // - paths are confined to the session root by the server's own `_confine` (passed in; this file never resolves a path itself);
9
+ // - the export carries no tenant secret: scenarios holding secret-looking content are withheld, the fixture source is never
10
+ // included, and the serialized result is checked once more with the server's own secret-shape redactor, which BLOCKS the
11
+ // result rather than editing it (an edited scenario is a different scenario);
12
+ // - it never claims exhaustive correctness: every export carries the bounded-sample statement;
13
+ // - gated behind the `invariant-scenarios` feature (off by default, operator-only): with it off the tool reports it is
14
+ // disabled and builds nothing.
15
+ // The factory shape (rather than importing `_confine` from tools.js) avoids a cycle between this file and the registry.
16
+ import { exportFromFiles } from '../posture/invariants/export.js';
17
+ import { resolveAssuranceConfig } from '../posture/assurance/config.js';
18
+ import { redactSecretShapes } from './redact.js';
19
+
20
+ const MAX_RESPONSE_BYTES = 1_500_000;
21
+
22
+ export function makeInvariantTools({ confine, META }) {
23
+ const invariant_scenario_export = {
24
+ name: 'invariant_scenario_export',
25
+ description: 'Export the bounded stateful scenarios for one executable business-logic invariant as a reproducible package: scenario documents with content digests, the pinned fixture digest, replay manifests (when a commit is given), the bounds applied and the scenario families that could not be built. Read-only: runs nothing. The fixture source and any secret-looking content are never included, and the package states that it is a bounded sample, not a proof of correctness. Requires the invariant-scenarios feature (off by default).',
26
+ inputSchema: {
27
+ type: 'object',
28
+ additionalProperties: false,
29
+ properties: {
30
+ invariant_file: { type: 'string', minLength: 1, maxLength: 512 },
31
+ fixture_dir: { type: 'string', minLength: 1, maxLength: 512 },
32
+ ledger_file: { type: 'string', minLength: 1, maxLength: 512 },
33
+ commit: { type: 'string', pattern: '^[0-9a-f]{40}([0-9a-f]{24})?$' },
34
+ seed: { type: 'integer', minimum: 0, maximum: 4294967295 },
35
+ bounds: {
36
+ type: 'object', additionalProperties: false,
37
+ properties: {
38
+ actors: { type: 'integer', minimum: 1, maximum: 4 }, depth: { type: 'integer', minimum: 1, maximum: 12 }, requests: { type: 'integer', minimum: 1, maximum: 16 },
39
+ scenarios: { type: 'integer', minimum: 1, maximum: 6 }, timeBudgetMs: { type: 'integer', minimum: 1, maximum: 8000 },
40
+ },
41
+ },
42
+ },
43
+ required: ['invariant_file', 'fixture_dir'],
44
+ },
45
+ async handler(args, ctx) {
46
+ let invariantPath; let fixturePath; let ledgerPath;
47
+ try {
48
+ invariantPath = confine(ctx.sessionRoot, args.invariant_file, 'invariant_file');
49
+ fixturePath = confine(ctx.sessionRoot, args.fixture_dir, 'fixture_dir');
50
+ if (args.ledger_file) ledgerPath = confine(ctx.sessionRoot, args.ledger_file, 'ledger_file');
51
+ } catch (e) {
52
+ return { _meta: META, ok: false, status: 'rejected', reason: `path refused: ${String(e.message).replace(ctx.sessionRoot, '<root>')}` };
53
+ }
54
+ const config = resolveAssuranceConfig({ scanRoot: ctx.sessionRoot, env: process.env });
55
+ const r = exportFromFiles({ invariantPath, fixturePath, ledgerPath, commit: args.commit, seed: args.seed, bounds: args.bounds, config });
56
+ if (r.status !== 'ok') return { _meta: META, ok: false, status: r.status, reason: String(r.reason || '').replace(ctx.sessionRoot, '<root>').slice(0, 400), withheld: r.withheld ?? [] };
57
+ const text = JSON.stringify(r.export);
58
+ if (redactSecretShapes(text).redactions > 0) return { _meta: META, ok: false, status: 'blocked', reason: 'the export contains secret-shaped content and was not returned; an edited scenario would be a different scenario' };
59
+ if (text.length > MAX_RESPONSE_BYTES) return { _meta: META, ok: false, status: 'blocked', reason: 'the export is too large for one response; lower the bounds' };
60
+ return { _meta: META, ok: true, status: 'ok', export: r.export, withheld: r.withheld };
61
+ },
62
+ };
63
+ return { invariant_scenario_export };
64
+ }
@@ -0,0 +1,79 @@
1
+ // portfolio-tools.js: the MCP surface for portfolio progress and review queues (X-707).
2
+ //
3
+ // One READ-ONLY tool, `portfolio_progress`: the coverage-aware progress view of a durable portfolio store that lives inside the session
4
+ // root. It reads the store (verified; a corrupt store is an error, never a reset), the scheduler ledger beside it, and optionally a
5
+ // budgets file and a findings file, and writes nothing. The same function the CLI calls (`posture/portfolio/progress.js`
6
+ // `buildProgressView`) does the work, so the two surfaces cannot disagree.
7
+ //
8
+ // Guarantees, each with a test:
9
+ // - paths are confined to the session root by the server's own `_confine` (passed in; this file never resolves a path itself);
10
+ // - the view separates verified, blocked/failed/stale, coverage, budget and pending review, keeps stale workers visible, and never
11
+ // states that the portfolio passed (a finished controller is reported as finished);
12
+ // - the serialized result is checked with the server's secret-shape redactor and BLOCKED rather than edited if anything would change;
13
+ // - gated behind the `portfolio-assurance` feature (off by default): with it off the tool answers `status: 'disabled'` and reads nothing.
14
+ // The factory shape (rather than importing `_confine` from tools.js) avoids a cycle between this file and the registry.
15
+ import * as fs from 'node:fs';
16
+ import { resolveAssuranceConfig, featureStatus } from '../posture/assurance/config.js';
17
+ import { readStore } from '../posture/portfolio/work-units.js';
18
+ import { readLedger } from '../posture/portfolio/scheduler.js';
19
+ import { buildProgressView } from '../posture/portfolio/progress.js';
20
+ import { FEATURE_ID } from '../posture/portfolio/wording.js';
21
+ import { redactSecretShapes } from './redact.js';
22
+
23
+ const MAX_RESPONSE_BYTES = 1_500_000;
24
+ const MAX_INPUT_BYTES = 8 * 1024 * 1024;
25
+
26
+ function readJson(file) {
27
+ const st = fs.lstatSync(file);
28
+ if (st.isSymbolicLink() || !st.isFile() || st.size > MAX_INPUT_BYTES) throw new Error('not a regular JSON file within the size limit');
29
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
30
+ }
31
+
32
+ export function makePortfolioTools({ confine, META }) {
33
+ const portfolio_progress = {
34
+ name: 'portfolio_progress',
35
+ description: 'Show the progress of a resumable multi-repository portfolio audit as a coverage-aware view: verified units, blocked/failed/stale units, per-repository coverage, remaining budget, pending human review, worker liveness (stale workers stay visible) and, when a findings file is given, aggregate findings deduplicated by stable identity with per-repository/environment evidence and separate affected releases. A finished controller is reported as finished, never as passed. Read-only; paths must be inside the session root. Disabled unless the portfolio-assurance feature is on.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ properties: {
40
+ store_file: { type: 'string', minLength: 1, maxLength: 512 },
41
+ budgets_file: { type: 'string', minLength: 1, maxLength: 512 },
42
+ findings_file: { type: 'string', minLength: 1, maxLength: 512 },
43
+ blocking_severity: { type: 'string', enum: ['info', 'low', 'medium', 'high', 'critical'] },
44
+ now: { type: 'integer', minimum: 0 },
45
+ },
46
+ required: ['store_file'],
47
+ },
48
+ async handler(args, ctx) {
49
+ const config = resolveAssuranceConfig({ scanRoot: ctx.sessionRoot, env: process.env });
50
+ const gate = featureStatus(config, FEATURE_ID);
51
+ if (gate.status !== 'ok') return { _meta: META, ok: false, status: gate.status, reason: `portfolio progress is not available: ${gate.reason ?? gate.code}` };
52
+ let storePath; let budgetsPath; let findingsPath;
53
+ try {
54
+ storePath = confine(ctx.sessionRoot, args.store_file, 'store_file');
55
+ if (args.budgets_file) budgetsPath = confine(ctx.sessionRoot, args.budgets_file, 'budgets_file');
56
+ if (args.findings_file) findingsPath = confine(ctx.sessionRoot, args.findings_file, 'findings_file');
57
+ } catch (e) {
58
+ return { _meta: META, ok: false, status: 'rejected', reason: `path refused: ${String(e.message).replace(ctx.sessionRoot, '<root>')}` };
59
+ }
60
+ let store; let ledger; let budgets = null; let findings = null;
61
+ try {
62
+ store = readStore(storePath);
63
+ if (!store) return { _meta: META, ok: false, status: 'rejected', reason: 'no portfolio store at that path' };
64
+ ledger = readLedger(storePath);
65
+ if (budgetsPath) budgets = readJson(budgetsPath);
66
+ if (findingsPath) findings = readJson(findingsPath);
67
+ } catch (e) {
68
+ return { _meta: META, ok: false, status: 'blocked', reason: `the store or an input failed verification: ${String(e.code ?? e.message).slice(0, 200)}` };
69
+ }
70
+ const r = buildProgressView({ store, ledger, budgets, now: Number.isInteger(args.now) ? args.now : Date.now(), findings, blockingSeverity: args.blocking_severity });
71
+ if (!r.ok) return { _meta: META, ok: false, status: 'rejected', reason: r.errors.map((e) => e.message).join('; ').slice(0, 400) };
72
+ const text = JSON.stringify(r.view);
73
+ if (redactSecretShapes(text).redactions > 0) return { _meta: META, ok: false, status: 'blocked', reason: 'the view contains secret-shaped content and was not returned' };
74
+ if (text.length > MAX_RESPONSE_BYTES) return { _meta: META, ok: false, status: 'blocked', reason: 'the view is too large for one response' };
75
+ return { _meta: META, ok: true, status: 'ok', view: r.view };
76
+ },
77
+ };
78
+ return { portfolio_progress };
79
+ }
package/src/mcp/redact.js CHANGED
@@ -81,6 +81,21 @@ export function redactString(s) {
81
81
  return out;
82
82
  }
83
83
 
84
+ /**
85
+ * The same provider-shape redaction as `redactString`, without its size caps:
86
+ * for a caller that must keep the whole payload and only remove secrets from it
87
+ * (the capability layer's outbound filtering). Returns the text and a count.
88
+ */
89
+ export function redactSecretShapes(s) {
90
+ if (typeof s !== 'string' || !s) return { text: typeof s === 'string' ? s : '', redactions: 0 };
91
+ let out = s;
92
+ let redactions = 0;
93
+ for (const [re, kind] of PATTERNS) {
94
+ out = out.replace(re, () => { redactions++; return `[REDACTED:${kind}]`; });
95
+ }
96
+ return { text: out, redactions };
97
+ }
98
+
84
99
  // Deep-redact every string in a finding-like object (mutates returned copy).
85
100
  export function redactFinding(f) {
86
101
  if (!f || typeof f !== 'object') return f;
package/src/mcp/server.js CHANGED
@@ -16,6 +16,8 @@ import { fileURLToPath } from 'node:url';
16
16
  import { ALL_TOOLS } from './tools.js';
17
17
  import { validate } from './validate.js';
18
18
  import { auditCall } from './audit.js';
19
+ // X-505: tool calls are checked against the capability policy before a handler runs.
20
+ import { createToolGate } from '../capabilities/tool-gate.js';
19
21
 
20
22
  const PROTOCOL_VERSION = '2025-03-26';
21
23
  const SERVER_NAME = 'agentic-security';
@@ -43,7 +45,7 @@ const TOOLS_BY_NAME = Object.fromEntries(ALL_TOOLS.map(t => [t.name, t]));
43
45
  function _codeFingerprint() {
44
46
  try {
45
47
  const here = path.dirname(fileURLToPath(import.meta.url));
46
- const files = ['server.js', 'tools.js', 'dataflow-tools.js', 'stdio.js', 'audit.js', 'validate.js', 'redact.js'];
48
+ const files = ['server.js', 'tools.js', 'dataflow-tools.js', 'invariant-tools.js', 'portfolio-tools.js', 'stdio.js', 'audit.js', 'validate.js', 'redact.js'];
47
49
  const h = crypto.createHash('sha256');
48
50
  for (const f of files) {
49
51
  try { h.update(f); h.update(fs.readFileSync(path.join(here, f))); } catch {}
@@ -63,8 +65,12 @@ function _ok(id, result) {
63
65
  return { jsonrpc: '2.0', id, result };
64
66
  }
65
67
 
66
- export function createServer({ sessionRoot = process.cwd() } = {}) {
68
+ // `capabilityPolicy` is `{ bound, binding }`, supplied by the operator from the
69
+ // capability manifest facility. Absent, and with the `capability-enforcement`
70
+ // feature off, the gate is inactive and behaviour is unchanged.
71
+ export function createServer({ sessionRoot = process.cwd(), capabilityPolicy = null } = {}) {
67
72
  const ctx = { sessionRoot };
73
+ const gate = createToolGate({ sessionRoot, policy: capabilityPolicy });
68
74
 
69
75
  async function handleRequest(msg) {
70
76
  if (!msg || typeof msg !== 'object') return _err(null, -32600, 'Invalid Request');
@@ -117,6 +123,21 @@ export function createServer({ sessionRoot = process.cwd() } = {}) {
117
123
  auditCall({ sessionRoot, tool: name, args, outcome: 'rejected', reason: 'unknown-tool' });
118
124
  return _err(id, -32602, `Unknown tool: ${name}`);
119
125
  }
126
+ // X-505: the capability check, for the task identity bound to this server.
127
+ // An identity named in the request metadata can only be compared with it.
128
+ const gated = gate.check(name, { claimedTaskId: msg.params?._meta?.taskId });
129
+ if (!gated.allowed) {
130
+ auditCall({ sessionRoot, tool: name, args, outcome: 'rejected', reason: `capability-denied: ${gated.code}` });
131
+ const p = gated.proposal;
132
+ return _ok(id, {
133
+ content: [{ type: 'text', text: JSON.stringify({
134
+ blocked: true, code: gated.code, reason: gated.reason, missing: gated.missing,
135
+ proposedChange: p && p.proposable ? { review: p.review, change: p.change, nextPolicyVersion: p.nextPolicyVersion, requiresOperatorGrant: true } : null,
136
+ attemptsRemaining: gated.attemptsRemaining, exhausted: gated.exhausted,
137
+ }, null, 2) }],
138
+ isError: true,
139
+ });
140
+ }
120
141
  try { validate(tool.inputSchema, args); }
121
142
  catch (e) {
122
143
  auditCall({ sessionRoot, tool: name, args, outcome: 'rejected', reason: `invalid-args: ${e.message}` });
@@ -147,7 +168,7 @@ export function createServer({ sessionRoot = process.cwd() } = {}) {
147
168
  }
148
169
  }
149
170
 
150
- return { handleRequest, sessionRoot };
171
+ return { handleRequest, sessionRoot, capabilityGate: gate };
151
172
  }
152
173
 
153
174
  // NOTE: no default-singleton export. Callers must use createServer({...})
package/src/mcp/tools.js CHANGED
@@ -26,11 +26,16 @@ import { withStateWritesDisabled } from '../posture/state-dir.js';
26
26
  import { analyzeTranscript, formatCacheReport, renderCacheStatusLine } from '../posture/cache-economics.js';
27
27
  import { redactString, redactFinding } from './redact.js';
28
28
  import { _remediationOf, normalizeFindings } from '../report/index.js';
29
+ // X-206: the one projection of the verification record, shared with the JSON report, the text report and the autopilot response.
30
+ import { verificationFields } from '../posture/verification/projection.js';
31
+ import { boundaryFields } from '../lineage/deployment/projection.js';
29
32
  // Git-origin provenance (Finding Provenance M0/M1). Distinct from
30
33
  // `finding.provenance` (AI-authorship) and from an SCA entry's `provenance`
31
34
  // (Sigstore/SLSA attestation) — see report/index.js's import comment.
32
35
  import { redactFindingProvenance } from '../posture/provenance/schema.js';
33
36
  import { dataflow_get_graph, dataflow_get_node, dataflow_get_edge, dataflow_get_flow } from './dataflow-tools.js';
37
+ import { makeInvariantTools } from './invariant-tools.js';
38
+ import { makePortfolioTools } from './portfolio-tools.js';
34
39
 
35
40
  // Lazy-loaded: these transitively pull in npm packages (@babel/core and
36
41
  // friends) that aren't available in the plugin-cache install path
@@ -599,6 +604,14 @@ export const explain_finding = {
599
604
  findingProvenance: f.findingProvenance ? redactFindingProvenance(f.findingProvenance, {
600
605
  pseudonymize: process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS === '1',
601
606
  }) : null,
607
+ // X-206: the same verification projection the JSON and text reports carry; absent when the finding has no record.
608
+ ...(f.verificationRecord && typeof f.verificationRecord === 'object'
609
+ ? { verificationRecord: f.verificationRecord, ...verificationFields(f.verificationRecord, { replay: f.verificationReplay }) }
610
+ : {}),
611
+ // X-307: the same boundary projection the JSON and text reports carry; absent when the finding has no context.
612
+ ...(f.boundaryContext && typeof f.boundaryContext === 'object'
613
+ ? boundaryFields(f.boundaryContext)
614
+ : {}),
602
615
  };
603
616
  },
604
617
  };
@@ -875,7 +888,7 @@ export const apply_fix = {
875
888
  // default. Normalized to the scan+lint verdict shape used below.
876
889
  const { verifyFixWithTests } = await import('../posture/fix-verify-loop.js');
877
890
  const t = await verifyFixWithTests({ scanRoot: ctx.sessionRoot, originalFindingStableId: f.stableId, files: _files });
878
- verdict = { ok: t.ok, summary: t.summary, rescan: t.legs?.scan?.detail, lint: t.legs?.lint?.detail, tests: t.legs?.tests, testVerdict: t.verdict };
891
+ verdict = { ok: t.ok, summary: t.summary, rescan: t.legs?.scan?.detail, lint: t.legs?.lint?.detail, tests: t.legs?.tests, testVerdict: t.verdict, verificationRecord: t.verificationRecord ?? null };
879
892
  } else {
880
893
  const verifyFixCore = await getVerifyFixCore();
881
894
  verdict = await verifyFixCore({
@@ -893,6 +906,9 @@ export const apply_fix = {
893
906
  _meta: META, applied: false,
894
907
  reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
895
908
  verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
909
+ // X-201: the same version-1 verification record every surface emits (additive; null when none could be formed).
910
+ verificationRecord: verdict.verificationRecord ?? null,
911
+ ...verificationFields(verdict.verificationRecord),
896
912
  };
897
913
  }
898
914
  // FR-307/FR-1002/D-0024: this caller-supplied-patch branch writes via
@@ -913,7 +929,7 @@ export const apply_fix = {
913
929
  }
914
930
  const materialClassification = classifyFixMaterialRisk(filesForMaterialClassification);
915
931
  if (dry_run) {
916
- return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
932
+ return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, verificationRecord: verdict.verificationRecord ?? null, ...verificationFields(verdict.verificationRecord), materialClassification };
917
933
  }
918
934
  const approvalRefusal = _highImpactApprovalRefusal(ctx, materialClassification, fixMeta);
919
935
  if (approvalRefusal) return approvalRefusal;
@@ -944,7 +960,7 @@ export const apply_fix = {
944
960
  }
945
961
  let acceptance = null;
946
962
  try { acceptance = fixAcceptanceRate(ctx.sessionRoot); } catch { /* best-effort */ }
947
- return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
963
+ return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary, verificationRecord: verdict.verificationRecord ?? null, ...verificationFields(verdict.verificationRecord) }, acceptance, materialClassification };
948
964
  }
949
965
 
950
966
  if (typeof f.fix?.replacement !== 'string') {
@@ -1033,7 +1049,7 @@ export const apply_fix = {
1033
1049
  // proceed with apply_fix.
1034
1050
  export const verify_fix = {
1035
1051
  name: 'verify_fix',
1036
- description: 'Verify a proposed patch before applying. Re-scans the patched files in memory, runs the project linter, runs the project test suite, checks fix honesty (FULL/MITIGATION/WORKAROUND) when fixMeta is supplied, and re-runs the PoC when one exists. Returns { ok, rescan, lint, tests, honesty, poc, summary }. Does not write to the target project’s own files, but DOES append one record per attempt to .agentic-security/fix-metrics.jsonl for the measured fix-loop.',
1052
+ description: 'Verify a proposed patch before applying. Re-scans the patched files in memory, runs the project linter, runs the project test suite, checks fix honesty (FULL/MITIGATION/WORKAROUND) when fixMeta is supplied, and re-runs the PoC when one exists. Returns { ok, rescan, lint, tests, honesty, poc, summary, verificationRecord } (verificationRecord is the shared version-1 record: outcome is one of not-run, unsupported, inconclusive, error, refuted, confirmed, and a static re-scan alone is never reported as confirmed; verificationView is the shared projection every interface shows for it, with state, scope, evidence ids, replay prerequisites and what was and was not verified). Does not write to the target project’s own files, but DOES append one record per attempt to .agentic-security/fix-metrics.jsonl for the measured fix-loop.',
1037
1053
  inputSchema: {
1038
1054
  type: 'object',
1039
1055
  additionalProperties: false,
@@ -1134,6 +1150,10 @@ export const verify_fix = {
1134
1150
  honesty: r.honesty,
1135
1151
  poc: r.poc ? { ...r.poc, reason: r.poc.reason ? redactString(r.poc.reason) : r.poc.reason } : r.poc,
1136
1152
  summary: r.summary,
1153
+ // X-201: the one version-1 verification record (additive). It states what was actually established, e.g.
1154
+ // `not-run` when no exploit oracle executed against the patch, never a bare pass/fail.
1155
+ verificationRecord: r.verificationRecord ?? null,
1156
+ ...verificationFields(r.verificationRecord),
1137
1157
  };
1138
1158
  } catch (e) {
1139
1159
  return { _meta: META, ok: false, reason: `verify_fix failed: ${e.message}` };
@@ -1699,4 +1719,9 @@ export const apply_sca_upgrade = {
1699
1719
  },
1700
1720
  };
1701
1721
 
1702
- export const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory, query_cache_telemetry, dataflow_get_graph, dataflow_get_node, dataflow_get_edge, dataflow_get_flow];
1722
+ // X-407: read-only scenario export; the factory takes this file's confinement so the two modules do not import each other.
1723
+ const { invariant_scenario_export } = makeInvariantTools({ confine: _confine, META });
1724
+ // X-707: read-only portfolio progress view, same factory shape.
1725
+ const { portfolio_progress } = makePortfolioTools({ confine: _confine, META });
1726
+
1727
+ export const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory, query_cache_telemetry, dataflow_get_graph, dataflow_get_node, dataflow_get_edge, dataflow_get_flow, invariant_scenario_export, portfolio_progress];