@clear-capabilities/agentic-security-scanner 0.161.0 → 0.163.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/bin/agentic-security.js +98 -2
- package/dist/1340.index.js +1871 -0
- package/dist/3477.index.js +727 -0
- package/dist/3499.index.js +23 -3
- package/dist/4113.index.js +59 -8
- package/dist/5014.index.js +833 -0
- package/dist/5756.index.js +2 -2
- package/dist/6257.index.js +1 -1
- package/dist/6907.index.js +248 -0
- package/dist/6994.index.js +1 -1
- package/dist/7033.index.js +33 -5
- package/dist/7439.index.js +1822 -0
- package/dist/7838.index.js +38 -2
- package/dist/8798.index.js +1 -1
- package/dist/9207.index.js +5 -0
- package/dist/9560.index.js +2336 -27
- package/dist/957.index.js +1 -1
- package/dist/{1905.index.js → 9999.index.js} +306 -13
- package/dist/agentic-security.mjs +12 -12
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +39 -7
- package/src/capabilities/CLAUDE.md +114 -0
- package/src/capabilities/address.js +73 -0
- package/src/capabilities/attack-coverage.js +93 -0
- package/src/capabilities/decide.js +256 -0
- package/src/capabilities/delegate.js +69 -0
- package/src/capabilities/egress.js +20 -0
- package/src/capabilities/hook-advice.js +50 -0
- package/src/capabilities/index.js +32 -0
- package/src/capabilities/manifest.js +344 -0
- package/src/capabilities/outbound.js +189 -0
- package/src/capabilities/paths.js +57 -0
- package/src/capabilities/probes.js +211 -0
- package/src/capabilities/proxy.js +182 -0
- package/src/capabilities/reasons.js +67 -0
- package/src/capabilities/receipts.js +356 -0
- package/src/capabilities/records.js +58 -0
- package/src/capabilities/recovery.js +367 -0
- package/src/capabilities/report.js +81 -0
- package/src/capabilities/runner.js +236 -0
- package/src/capabilities/secrets.js +26 -0
- package/src/capabilities/tool-gate.js +119 -0
- package/src/capabilities/tool-registry.js +60 -0
- package/src/dataflow/CLAUDE.md +29 -0
- package/src/dataflow/catalog.js +21 -0
- package/src/dataflow/engine.js +20 -0
- package/src/dataflow/guard-dominance.js +352 -0
- package/src/discovery/CLAUDE.md +18 -0
- package/src/discovery/index.js +43 -1
- package/src/discovery/lenses.js +18 -0
- package/src/discovery/memory.js +7 -5
- package/src/engine.js +80 -15
- package/src/fix/apply-fix-service.js +33 -3
- package/src/integrations/index.js +5 -2
- package/src/ir/CLAUDE.md +14 -0
- package/src/ir/implicit-handler-params.js +74 -0
- package/src/ir/parser-cs.js +20 -0
- package/src/ir/parser-go.js +30 -13
- package/src/ir/parser-java.js +11 -0
- package/src/language/nix-eval-isolation.js +33 -5
- package/src/lineage/CLAUDE.md +31 -0
- package/src/lineage/deployment/adapter-cloud.js +202 -0
- package/src/lineage/deployment/adapter-compose.js +90 -0
- package/src/lineage/deployment/adapter-kit.js +142 -0
- package/src/lineage/deployment/adapter-kubernetes.js +247 -0
- package/src/lineage/deployment/attack-paths.js +443 -0
- package/src/lineage/deployment/boundaries-run.js +223 -0
- package/src/lineage/deployment/boundary-graph.js +567 -0
- package/src/lineage/deployment/cross-repo.js +253 -0
- package/src/lineage/deployment/drift.js +383 -0
- package/src/lineage/deployment/ingest.js +221 -0
- package/src/lineage/deployment/projection.js +244 -0
- package/src/lineage/deployment/trace-correlation.js +541 -0
- package/src/lineage/source-registry.js +19 -0
- package/src/mcp/CLAUDE.md +14 -3
- package/src/mcp/invariant-tools.js +64 -0
- package/src/mcp/portfolio-tools.js +79 -0
- package/src/mcp/redact.js +15 -0
- package/src/mcp/server.js +24 -3
- package/src/mcp/tools.js +30 -5
- package/src/posture/CLAUDE.md +505 -0
- package/src/posture/accuracy-scorecard.js +95 -4
- package/src/posture/assurance/baseline-inventory.js +218 -0
- package/src/posture/assurance/baseline.js +322 -0
- package/src/posture/assurance/bounded-io.js +145 -0
- package/src/posture/assurance/config.js +299 -0
- package/src/posture/assurance/contracts.js +315 -0
- package/src/posture/assurance/identity.js +62 -0
- package/src/posture/assurance/migrations.js +259 -0
- package/src/posture/assurance/rollback.js +214 -0
- package/src/posture/assurance/rollout.js +200 -0
- package/src/posture/assurance/schema-kit.js +111 -0
- package/src/posture/assurance/verification-record.js +258 -0
- package/src/posture/attestation.js +5 -0
- package/src/posture/autopilot.js +28 -1
- package/src/posture/business-logic.js +26 -4
- package/src/posture/evaluation/bakeoff.js +148 -0
- package/src/posture/evaluation/custody.js +200 -0
- package/src/posture/evaluation/deployment-ablation.js +416 -0
- package/src/posture/evaluation/dev-cases.js +65 -0
- package/src/posture/evaluation/economics.js +85 -0
- package/src/posture/evaluation/gates.js +161 -0
- package/src/posture/evaluation/grouping.js +101 -0
- package/src/posture/evaluation/interval.js +132 -0
- package/src/posture/evaluation/invariant-ablation.js +325 -0
- package/src/posture/evaluation/labels.js +200 -0
- package/src/posture/evaluation/protocol.js +308 -0
- package/src/posture/evaluation/release-gate.js +209 -0
- package/src/posture/evaluation/report.js +158 -0
- package/src/posture/evaluation/runner.js +273 -0
- package/src/posture/evaluation/scan-child.js +80 -0
- package/src/posture/evaluation/score.js +174 -0
- package/src/posture/evaluation/synthetic.js +90 -0
- package/src/posture/evaluation/why-missed.js +219 -0
- package/src/posture/evidence-bundle.js +93 -7
- package/src/posture/execution-proof.js +11 -1
- package/src/posture/fix-verify-loop.js +20 -6
- package/src/posture/fix-verify.js +37 -2
- package/src/posture/fleet.js +11 -2
- package/src/posture/invariants/cli.js +95 -0
- package/src/posture/invariants/coverage.js +164 -0
- package/src/posture/invariants/export.js +178 -0
- package/src/posture/invariants/expressions.js +46 -0
- package/src/posture/invariants/lifecycle.js +289 -0
- package/src/posture/invariants/project-input.js +55 -0
- package/src/posture/invariants/repair.js +340 -0
- package/src/posture/invariants/run.js +45 -0
- package/src/posture/invariants/scenarios.js +311 -0
- package/src/posture/invariants/schema.js +167 -0
- package/src/posture/invariants/shrink.js +407 -0
- package/src/posture/invariants/state-assertions.js +165 -0
- package/src/posture/logic-claims.js +12 -0
- package/src/posture/model-routing.js +40 -0
- package/src/posture/oracles/adapters.js +531 -0
- package/src/posture/oracles/conformance.js +276 -0
- package/src/posture/oracles/oracle.js +453 -0
- package/src/posture/oracles/registry.js +76 -0
- package/src/posture/oracles/scenario-classes.js +223 -0
- package/src/posture/portfolio/backend.js +279 -0
- package/src/posture/portfolio/bundle.js +314 -0
- package/src/posture/portfolio/cli.js +142 -0
- package/src/posture/portfolio/manifest.js +263 -0
- package/src/posture/portfolio/progress.js +267 -0
- package/src/posture/portfolio/resume.js +130 -0
- package/src/posture/portfolio/retention.js +207 -0
- package/src/posture/portfolio/scheduler.js +488 -0
- package/src/posture/portfolio/signing.js +155 -0
- package/src/posture/portfolio/wording.js +59 -0
- package/src/posture/portfolio/work-units.js +405 -0
- package/src/posture/replay/replay.js +250 -0
- package/src/posture/routing/adapter.js +140 -0
- package/src/posture/routing/calibration.js +278 -0
- package/src/posture/routing/canary.js +141 -0
- package/src/posture/routing/control.js +35 -0
- package/src/posture/routing/decide.js +211 -0
- package/src/posture/routing/drift.js +137 -0
- package/src/posture/routing/economics.js +185 -0
- package/src/posture/routing/feedback.js +207 -0
- package/src/posture/routing/index.js +31 -0
- package/src/posture/routing/outcomes.js +281 -0
- package/src/posture/routing/promotion.js +291 -0
- package/src/posture/routing/receipts.js +59 -0
- package/src/posture/routing/report.js +142 -0
- package/src/posture/routing/shadow.js +53 -0
- package/src/posture/specification-mining.js +26 -0
- package/src/posture/verification/advisory-state.js +71 -0
- package/src/posture/verification/emit.js +257 -0
- package/src/posture/verification/hypothesis-promotion.js +211 -0
- package/src/posture/verification/patch-negative.js +226 -0
- package/src/posture/verification/patch-promotion.js +113 -0
- package/src/posture/verification/projection.js +183 -0
- package/src/posture/verification/repair-records.js +124 -0
- package/src/posture/verifier.js +16 -0
- package/src/report/index.js +100 -1
- package/src/sandbox/CLAUDE.md +51 -0
- package/src/sandbox/backend-namespace.js +7 -0
- package/src/sandbox/backend-userspace.js +151 -34
- package/src/sandbox/control-probes.js +215 -0
- package/src/sandbox/supervise.js +201 -0
- package/src/sandbox/trust-boundary.js +113 -0
- package/src/sandbox/trust-domains.js +171 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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];
|