@vibecheckai/cli 3.4.0 → 3.5.1
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/bin/registry.js +154 -338
- package/bin/runners/context/generators/mcp.js +13 -15
- package/bin/runners/context/proof-context.js +1 -248
- package/bin/runners/lib/analysis-core.js +180 -198
- package/bin/runners/lib/analyzers.js +223 -1669
- package/bin/runners/lib/cli-output.js +210 -242
- package/bin/runners/lib/detectors-v2.js +785 -547
- package/bin/runners/lib/entitlements-v2.js +458 -96
- package/bin/runners/lib/error-handler.js +9 -16
- package/bin/runners/lib/global-flags.js +0 -37
- package/bin/runners/lib/route-truth.js +322 -1167
- package/bin/runners/lib/scan-output.js +469 -448
- package/bin/runners/lib/ship-output.js +27 -280
- package/bin/runners/lib/terminal-ui.js +733 -231
- package/bin/runners/lib/truth.js +321 -1004
- package/bin/runners/lib/unified-output.js +158 -162
- package/bin/runners/lib/upsell.js +204 -104
- package/bin/runners/runAllowlist.js +324 -0
- package/bin/runners/runAuth.js +95 -324
- package/bin/runners/runCheckpoint.js +21 -39
- package/bin/runners/runContext.js +24 -136
- package/bin/runners/runDoctor.js +67 -115
- package/bin/runners/runEvidencePack.js +219 -0
- package/bin/runners/runFix.js +5 -6
- package/bin/runners/runGuard.js +118 -212
- package/bin/runners/runInit.js +2 -14
- package/bin/runners/runInstall.js +281 -0
- package/bin/runners/runLabs.js +341 -0
- package/bin/runners/runMcp.js +52 -130
- package/bin/runners/runPolish.js +20 -43
- package/bin/runners/runProve.js +3 -13
- package/bin/runners/runReality.js +0 -14
- package/bin/runners/runReport.js +2 -3
- package/bin/runners/runScan.js +44 -511
- package/bin/runners/runShip.js +14 -28
- package/bin/runners/runValidate.js +2 -19
- package/bin/runners/runWatch.js +54 -118
- package/bin/vibecheck.js +41 -148
- package/mcp-server/ARCHITECTURE.md +339 -0
- package/mcp-server/__tests__/cache.test.ts +313 -0
- package/mcp-server/__tests__/executor.test.ts +239 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
- package/mcp-server/__tests__/ids.test.ts +345 -0
- package/mcp-server/__tests__/integration/tools.test.ts +410 -0
- package/mcp-server/__tests__/registry.test.ts +365 -0
- package/mcp-server/__tests__/sandbox.test.ts +323 -0
- package/mcp-server/__tests__/schemas.test.ts +372 -0
- package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
- package/mcp-server/examples/doctor.request.json +14 -0
- package/mcp-server/examples/doctor.response.json +53 -0
- package/mcp-server/examples/error.response.json +15 -0
- package/mcp-server/examples/scan.request.json +14 -0
- package/mcp-server/examples/scan.response.json +108 -0
- package/mcp-server/handlers/tool-handler.ts +671 -0
- package/mcp-server/index-v3.ts +293 -0
- package/mcp-server/index.js +1072 -1573
- package/mcp-server/index.old.js +4137 -0
- package/mcp-server/lib/cache.ts +341 -0
- package/mcp-server/lib/errors.ts +346 -0
- package/mcp-server/lib/executor.ts +792 -0
- package/mcp-server/lib/ids.ts +238 -0
- package/mcp-server/lib/logger.ts +368 -0
- package/mcp-server/lib/metrics.ts +365 -0
- package/mcp-server/lib/sandbox.ts +337 -0
- package/mcp-server/lib/validator.ts +229 -0
- package/mcp-server/package-lock.json +165 -0
- package/mcp-server/package.json +32 -7
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/registry/tools.json +476 -0
- package/mcp-server/schemas/error-envelope.schema.json +125 -0
- package/mcp-server/schemas/finding.schema.json +167 -0
- package/mcp-server/schemas/report-artifact.schema.json +88 -0
- package/mcp-server/schemas/run-request.schema.json +75 -0
- package/mcp-server/schemas/verdict.schema.json +168 -0
- package/mcp-server/tier-auth.d.ts +71 -0
- package/mcp-server/tier-auth.js +371 -183
- package/mcp-server/truth-context.js +90 -131
- package/mcp-server/truth-firewall-tools.js +1000 -1611
- package/mcp-server/tsconfig.json +34 -0
- package/mcp-server/vibecheck-tools.js +2 -2
- package/mcp-server/vitest.config.ts +16 -0
- package/package.json +3 -4
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
- package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
- package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
- package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
- package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
- package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
- package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
- package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
- package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
- package/bin/runners/lib/agent-firewall/logger.js +0 -141
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
- package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
- package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
- package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
- package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
- package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
- package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
- package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
- package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
- package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
- package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
- package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
- package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
- package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
- package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
- package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
- package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
- package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
- package/bin/runners/lib/api-client.js +0 -269
- package/bin/runners/lib/authority-badge.js +0 -425
- package/bin/runners/lib/engines/accessibility-engine.js +0 -190
- package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
- package/bin/runners/lib/engines/ast-cache.js +0 -99
- package/bin/runners/lib/engines/code-quality-engine.js +0 -255
- package/bin/runners/lib/engines/console-logs-engine.js +0 -115
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
- package/bin/runners/lib/engines/dead-code-engine.js +0 -198
- package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
- package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
- package/bin/runners/lib/engines/file-filter.js +0 -131
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
- package/bin/runners/lib/engines/mock-data-engine.js +0 -272
- package/bin/runners/lib/engines/parallel-processor.js +0 -71
- package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
- package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
- package/bin/runners/lib/engines/type-aware-engine.js +0 -152
- package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
- package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
- package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
- package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
- package/bin/runners/lib/exit-codes.js +0 -275
- package/bin/runners/lib/fingerprint.js +0 -377
- package/bin/runners/lib/help-formatter.js +0 -413
- package/bin/runners/lib/logger.js +0 -38
- package/bin/runners/lib/ship-output-enterprise.js +0 -239
- package/bin/runners/lib/unified-cli-output.js +0 -604
- package/bin/runners/runAgent.d.ts +0 -5
- package/bin/runners/runAgent.js +0 -161
- package/bin/runners/runApprove.js +0 -1200
- package/bin/runners/runClassify.js +0 -859
- package/bin/runners/runContext.d.ts +0 -4
- package/bin/runners/runFirewall.d.ts +0 -5
- package/bin/runners/runFirewall.js +0 -134
- package/bin/runners/runFirewallHook.d.ts +0 -5
- package/bin/runners/runFirewallHook.js +0 -56
- package/bin/runners/runPolish.d.ts +0 -4
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runTruth.d.ts +0 -5
- package/bin/runners/runTruth.js +0 -101
- package/mcp-server/HARDENING_SUMMARY.md +0 -299
- package/mcp-server/agent-firewall-interceptor.js +0 -500
- package/mcp-server/authority-tools.js +0 -569
- package/mcp-server/conductor/conflict-resolver.js +0 -588
- package/mcp-server/conductor/execution-planner.js +0 -544
- package/mcp-server/conductor/index.js +0 -377
- package/mcp-server/conductor/lock-manager.js +0 -615
- package/mcp-server/conductor/request-queue.js +0 -550
- package/mcp-server/conductor/session-manager.js +0 -500
- package/mcp-server/conductor/tools.js +0 -510
- package/mcp-server/lib/api-client.cjs +0 -13
- package/mcp-server/lib/logger.cjs +0 -30
- package/mcp-server/logger.js +0 -173
- package/mcp-server/tools-v3.js +0 -706
- package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collection for MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Provides structured metrics for:
|
|
5
|
+
* - Tool execution duration
|
|
6
|
+
* - Error rates by type
|
|
7
|
+
* - Cache hit rates
|
|
8
|
+
* - Findings counts by severity
|
|
9
|
+
*
|
|
10
|
+
* Metrics are aggregated in memory and can be flushed to storage.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Metric types
|
|
17
|
+
*/
|
|
18
|
+
export type MetricType = 'counter' | 'gauge' | 'histogram';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Metric entry structure
|
|
22
|
+
*/
|
|
23
|
+
export interface MetricEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
type: MetricType;
|
|
26
|
+
value: number;
|
|
27
|
+
unit: 'ms' | 'count' | 'bytes' | 'percent' | 'ratio';
|
|
28
|
+
timestamp: number;
|
|
29
|
+
tags?: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Histogram statistics result type
|
|
34
|
+
*/
|
|
35
|
+
export interface HistogramStats {
|
|
36
|
+
count: number;
|
|
37
|
+
min: number;
|
|
38
|
+
max: number;
|
|
39
|
+
avg: number;
|
|
40
|
+
p50: number;
|
|
41
|
+
p95: number;
|
|
42
|
+
p99: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Histogram buckets for duration metrics
|
|
47
|
+
*/
|
|
48
|
+
const DURATION_BUCKETS_MS = [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Metrics aggregator
|
|
52
|
+
*/
|
|
53
|
+
export class MetricsCollector {
|
|
54
|
+
private counters = new Map<string, number>();
|
|
55
|
+
private gauges = new Map<string, number>();
|
|
56
|
+
private histograms = new Map<string, number[]>();
|
|
57
|
+
private entries: MetricEntry[] = [];
|
|
58
|
+
private flushInterval?: NodeJS.Timeout;
|
|
59
|
+
private maxEntries: number;
|
|
60
|
+
|
|
61
|
+
constructor(options: { maxEntries?: number; flushIntervalMs?: number } = {}) {
|
|
62
|
+
this.maxEntries = options.maxEntries ?? 10000;
|
|
63
|
+
|
|
64
|
+
if (options.flushIntervalMs) {
|
|
65
|
+
this.flushInterval = setInterval(() => {
|
|
66
|
+
this.trimEntries();
|
|
67
|
+
}, options.flushIntervalMs);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Increment a counter
|
|
73
|
+
*/
|
|
74
|
+
incCounter(name: string, value = 1, tags?: Record<string, string>): void {
|
|
75
|
+
const key = this.makeKey(name, tags);
|
|
76
|
+
const current = this.counters.get(key) ?? 0;
|
|
77
|
+
this.counters.set(key, current + value);
|
|
78
|
+
|
|
79
|
+
this.record({
|
|
80
|
+
name,
|
|
81
|
+
type: 'counter',
|
|
82
|
+
value: current + value,
|
|
83
|
+
unit: 'count',
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
tags,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set a gauge value
|
|
91
|
+
*/
|
|
92
|
+
setGauge(name: string, value: number, tags?: Record<string, string>): void {
|
|
93
|
+
const key = this.makeKey(name, tags);
|
|
94
|
+
this.gauges.set(key, value);
|
|
95
|
+
|
|
96
|
+
this.record({
|
|
97
|
+
name,
|
|
98
|
+
type: 'gauge',
|
|
99
|
+
value,
|
|
100
|
+
unit: 'count',
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
tags,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Record a histogram value (e.g., duration)
|
|
108
|
+
*/
|
|
109
|
+
recordHistogram(
|
|
110
|
+
name: string,
|
|
111
|
+
value: number,
|
|
112
|
+
unit: 'ms' | 'bytes' = 'ms',
|
|
113
|
+
tags?: Record<string, string>
|
|
114
|
+
): void {
|
|
115
|
+
const key = this.makeKey(name, tags);
|
|
116
|
+
const values = this.histograms.get(key) ?? [];
|
|
117
|
+
values.push(value);
|
|
118
|
+
this.histograms.set(key, values);
|
|
119
|
+
|
|
120
|
+
this.record({
|
|
121
|
+
name,
|
|
122
|
+
type: 'histogram',
|
|
123
|
+
value,
|
|
124
|
+
unit,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
tags,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Record tool execution metrics
|
|
132
|
+
*/
|
|
133
|
+
recordToolExecution(
|
|
134
|
+
tool: string,
|
|
135
|
+
durationMs: number,
|
|
136
|
+
success: boolean,
|
|
137
|
+
cached: boolean
|
|
138
|
+
): void {
|
|
139
|
+
const tags = { tool, status: success ? 'success' : 'error', cached: String(cached) };
|
|
140
|
+
|
|
141
|
+
this.recordHistogram('tool_duration_ms', durationMs, 'ms', tags);
|
|
142
|
+
this.incCounter('tool_executions_total', 1, tags);
|
|
143
|
+
|
|
144
|
+
if (!success) {
|
|
145
|
+
this.incCounter('tool_errors_total', 1, { tool });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (cached) {
|
|
149
|
+
this.incCounter('cache_hits_total', 1, { tool });
|
|
150
|
+
} else {
|
|
151
|
+
this.incCounter('cache_misses_total', 1, { tool });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Record findings by severity
|
|
157
|
+
*/
|
|
158
|
+
recordFindings(
|
|
159
|
+
tool: string,
|
|
160
|
+
findings: { severity?: string }[]
|
|
161
|
+
): void {
|
|
162
|
+
const bySeverity: Record<string, number> = {};
|
|
163
|
+
|
|
164
|
+
for (const finding of findings) {
|
|
165
|
+
const severity = finding.severity || 'INFO';
|
|
166
|
+
bySeverity[severity] = (bySeverity[severity] || 0) + 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const [severity, count] of Object.entries(bySeverity)) {
|
|
170
|
+
this.incCounter('findings_total', count, { tool, severity });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.setGauge('findings_current', findings.length, { tool });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get cache hit rate
|
|
178
|
+
*/
|
|
179
|
+
getCacheHitRate(tool?: string): number {
|
|
180
|
+
const tags = tool ? { tool } : undefined;
|
|
181
|
+
const hitsKey = this.makeKey('cache_hits_total', tags);
|
|
182
|
+
const missesKey = this.makeKey('cache_misses_total', tags);
|
|
183
|
+
|
|
184
|
+
const hits = this.counters.get(hitsKey) ?? 0;
|
|
185
|
+
const misses = this.counters.get(missesKey) ?? 0;
|
|
186
|
+
const total = hits + misses;
|
|
187
|
+
|
|
188
|
+
return total > 0 ? hits / total : 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get histogram statistics
|
|
193
|
+
*/
|
|
194
|
+
getHistogramStats(name: string, tags?: Record<string, string>): HistogramStats | null {
|
|
195
|
+
const key = this.makeKey(name, tags);
|
|
196
|
+
const values = this.histograms.get(key);
|
|
197
|
+
|
|
198
|
+
if (!values || values.length === 0) return null;
|
|
199
|
+
|
|
200
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
201
|
+
const count = sorted.length;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
count,
|
|
205
|
+
min: sorted[0],
|
|
206
|
+
max: sorted[count - 1],
|
|
207
|
+
avg: sorted.reduce((a, b) => a + b, 0) / count,
|
|
208
|
+
p50: sorted[Math.floor(count * 0.5)],
|
|
209
|
+
p95: sorted[Math.floor(count * 0.95)],
|
|
210
|
+
p99: sorted[Math.floor(count * 0.99)],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get all metrics as summary
|
|
216
|
+
*/
|
|
217
|
+
getSummary(): {
|
|
218
|
+
counters: Record<string, number>;
|
|
219
|
+
gauges: Record<string, number>;
|
|
220
|
+
histograms: Record<string, HistogramStats | null>;
|
|
221
|
+
cacheHitRate: number;
|
|
222
|
+
} {
|
|
223
|
+
const counters: Record<string, number> = {};
|
|
224
|
+
const gauges: Record<string, number> = {};
|
|
225
|
+
const histograms: Record<string, HistogramStats | null> = {};
|
|
226
|
+
|
|
227
|
+
for (const [key, value] of this.counters.entries()) {
|
|
228
|
+
counters[key] = value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const [key, value] of this.gauges.entries()) {
|
|
232
|
+
gauges[key] = value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const key of this.histograms.keys()) {
|
|
236
|
+
histograms[key] = this.getHistogramStats(key);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
counters,
|
|
241
|
+
gauges,
|
|
242
|
+
histograms,
|
|
243
|
+
cacheHitRate: this.getCacheHitRate(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get raw entries (for export)
|
|
249
|
+
*/
|
|
250
|
+
getEntries(): MetricEntry[] {
|
|
251
|
+
return [...this.entries];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clear all metrics
|
|
256
|
+
*/
|
|
257
|
+
clear(): void {
|
|
258
|
+
this.counters.clear();
|
|
259
|
+
this.gauges.clear();
|
|
260
|
+
this.histograms.clear();
|
|
261
|
+
this.entries = [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Stop the collector
|
|
266
|
+
*/
|
|
267
|
+
stop(): void {
|
|
268
|
+
if (this.flushInterval) {
|
|
269
|
+
clearInterval(this.flushInterval);
|
|
270
|
+
this.flushInterval = undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Internal: record a metric entry
|
|
276
|
+
*/
|
|
277
|
+
private record(entry: MetricEntry): void {
|
|
278
|
+
this.entries.push(entry);
|
|
279
|
+
this.trimEntries();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Internal: trim entries if over limit
|
|
284
|
+
*/
|
|
285
|
+
private trimEntries(): void {
|
|
286
|
+
if (this.entries.length > this.maxEntries) {
|
|
287
|
+
this.entries = this.entries.slice(-this.maxEntries);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Internal: make a unique key from name and tags
|
|
293
|
+
*/
|
|
294
|
+
private makeKey(name: string, tags?: Record<string, string>): string {
|
|
295
|
+
if (!tags || Object.keys(tags).length === 0) {
|
|
296
|
+
return name;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const sortedTags = Object.entries(tags)
|
|
300
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
301
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
302
|
+
.join(',');
|
|
303
|
+
|
|
304
|
+
return `${name}{${sortedTags}}`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Singleton instance
|
|
309
|
+
let globalCollector: MetricsCollector | null = null;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get global metrics collector
|
|
313
|
+
*/
|
|
314
|
+
export function getMetricsCollector(): MetricsCollector {
|
|
315
|
+
if (!globalCollector) {
|
|
316
|
+
globalCollector = new MetricsCollector({
|
|
317
|
+
maxEntries: 10000,
|
|
318
|
+
flushIntervalMs: 60000, // 1 minute
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return globalCollector;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Initialize global metrics collector
|
|
326
|
+
*/
|
|
327
|
+
export function initMetricsCollector(options?: {
|
|
328
|
+
maxEntries?: number;
|
|
329
|
+
flushIntervalMs?: number;
|
|
330
|
+
}): void {
|
|
331
|
+
if (globalCollector) {
|
|
332
|
+
globalCollector.stop();
|
|
333
|
+
}
|
|
334
|
+
globalCollector = new MetricsCollector(options);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Convenience: record tool execution
|
|
339
|
+
*/
|
|
340
|
+
export function recordToolExecution(
|
|
341
|
+
tool: string,
|
|
342
|
+
durationMs: number,
|
|
343
|
+
success: boolean,
|
|
344
|
+
cached: boolean
|
|
345
|
+
): void {
|
|
346
|
+
getMetricsCollector().recordToolExecution(tool, durationMs, success, cached);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Convenience: record findings
|
|
351
|
+
*/
|
|
352
|
+
export function recordFindings(
|
|
353
|
+
tool: string,
|
|
354
|
+
findings: { severity?: string }[]
|
|
355
|
+
): void {
|
|
356
|
+
getMetricsCollector().recordFindings(tool, findings);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export default {
|
|
360
|
+
MetricsCollector,
|
|
361
|
+
getMetricsCollector,
|
|
362
|
+
initMetricsCollector,
|
|
363
|
+
recordToolExecution,
|
|
364
|
+
recordFindings,
|
|
365
|
+
};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path Sandboxing & Security
|
|
3
|
+
*
|
|
4
|
+
* Ensures all file operations stay within allowed workspace.
|
|
5
|
+
* Prevents path traversal and access to sensitive files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolve, normalize, relative, isAbsolute } from 'path';
|
|
9
|
+
import { existsSync, statSync } from 'fs';
|
|
10
|
+
import { Errors } from './errors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sensitive paths that should never be accessed
|
|
14
|
+
*/
|
|
15
|
+
const SENSITIVE_PATTERNS = [
|
|
16
|
+
/^\/etc\//,
|
|
17
|
+
/^\/root\//,
|
|
18
|
+
/^\/home\/[^/]+\/\.ssh\//,
|
|
19
|
+
/^\/home\/[^/]+\/\.gnupg\//,
|
|
20
|
+
/^\/home\/[^/]+\/\.aws\//,
|
|
21
|
+
/^\/home\/[^/]+\/\.config\//,
|
|
22
|
+
/^C:\\Windows\\/i,
|
|
23
|
+
/^C:\\Users\\[^\\]+\\\.ssh\\/i,
|
|
24
|
+
/^C:\\Users\\[^\\]+\\\.aws\\/i,
|
|
25
|
+
/^C:\\Users\\[^\\]+\\AppData\\/i,
|
|
26
|
+
/\.env$/,
|
|
27
|
+
/\.env\.\w+$/,
|
|
28
|
+
/credentials\.json$/,
|
|
29
|
+
/secrets\.json$/,
|
|
30
|
+
/\.pem$/,
|
|
31
|
+
/\.key$/,
|
|
32
|
+
/id_rsa/,
|
|
33
|
+
/id_ed25519/,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Paths that should be excluded from scanning (but not blocked)
|
|
38
|
+
*
|
|
39
|
+
* CRITICAL: Default exclusions enforced at MCP layer even if CLI forgets.
|
|
40
|
+
* These are the canonical exclusions matching the architecture spec.
|
|
41
|
+
*/
|
|
42
|
+
const EXCLUDE_PATTERNS = [
|
|
43
|
+
// Package managers
|
|
44
|
+
/node_modules\//,
|
|
45
|
+
/node_modules\\/,
|
|
46
|
+
/\.npm\//,
|
|
47
|
+
/\.yarn\//,
|
|
48
|
+
/\.pnpm-store\//,
|
|
49
|
+
|
|
50
|
+
// Python environments
|
|
51
|
+
/venv\//,
|
|
52
|
+
/venv\\/,
|
|
53
|
+
/\.venv\//,
|
|
54
|
+
/\.venv\\/,
|
|
55
|
+
/env\//,
|
|
56
|
+
/\.env\//,
|
|
57
|
+
/__pycache__\//,
|
|
58
|
+
/\.pytest_cache\//,
|
|
59
|
+
/\.mypy_cache\//,
|
|
60
|
+
|
|
61
|
+
// Version control
|
|
62
|
+
/\.git\//,
|
|
63
|
+
/\.git\\/,
|
|
64
|
+
/\.svn\//,
|
|
65
|
+
/\.hg\//,
|
|
66
|
+
|
|
67
|
+
// Build outputs
|
|
68
|
+
/dist\//,
|
|
69
|
+
/dist\\/,
|
|
70
|
+
/build\//,
|
|
71
|
+
/build\\/,
|
|
72
|
+
/out\//,
|
|
73
|
+
/out\\/,
|
|
74
|
+
/\.next\//,
|
|
75
|
+
/\.next\\/,
|
|
76
|
+
/\.nuxt\//,
|
|
77
|
+
/\.output\//,
|
|
78
|
+
|
|
79
|
+
// Cache directories
|
|
80
|
+
/\.cache\//,
|
|
81
|
+
/\.cache\\/,
|
|
82
|
+
/\.turbo\//,
|
|
83
|
+
/\.turbo\\/,
|
|
84
|
+
/\.parcel-cache\//,
|
|
85
|
+
/\.webpack\//,
|
|
86
|
+
|
|
87
|
+
// Coverage
|
|
88
|
+
/coverage\//,
|
|
89
|
+
/coverage\\/,
|
|
90
|
+
/\.nyc_output\//,
|
|
91
|
+
|
|
92
|
+
// Vendor directories
|
|
93
|
+
/vendor\//,
|
|
94
|
+
/\.bundle\//,
|
|
95
|
+
/bower_components\//,
|
|
96
|
+
|
|
97
|
+
// IDE/Editor
|
|
98
|
+
/\.idea\//,
|
|
99
|
+
/\.vscode\//,
|
|
100
|
+
/\.vs\//,
|
|
101
|
+
|
|
102
|
+
// Temp files
|
|
103
|
+
/\.tmp\//,
|
|
104
|
+
/\.temp\//,
|
|
105
|
+
/tmp\//,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sandbox configuration
|
|
110
|
+
*/
|
|
111
|
+
export interface SandboxConfig {
|
|
112
|
+
workspace: string;
|
|
113
|
+
allowSymlinks?: boolean;
|
|
114
|
+
maxDepth?: number;
|
|
115
|
+
excludePatterns?: RegExp[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sandbox result
|
|
120
|
+
*/
|
|
121
|
+
export interface SandboxResult {
|
|
122
|
+
allowed: boolean;
|
|
123
|
+
path: string;
|
|
124
|
+
relativePath: string;
|
|
125
|
+
reason?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create a sandbox validator for a workspace
|
|
130
|
+
*/
|
|
131
|
+
export function createSandbox(config: SandboxConfig) {
|
|
132
|
+
const workspace = resolve(normalize(config.workspace));
|
|
133
|
+
const maxDepth = config.maxDepth ?? 20;
|
|
134
|
+
const excludePatterns = [...EXCLUDE_PATTERNS, ...(config.excludePatterns ?? [])];
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
/**
|
|
138
|
+
* Validate a path is within the sandbox
|
|
139
|
+
*/
|
|
140
|
+
validate(inputPath: string): SandboxResult {
|
|
141
|
+
// Normalize the input path
|
|
142
|
+
const normalizedInput = normalize(inputPath);
|
|
143
|
+
|
|
144
|
+
// Resolve to absolute
|
|
145
|
+
const absolutePath = isAbsolute(normalizedInput)
|
|
146
|
+
? normalizedInput
|
|
147
|
+
: resolve(workspace, normalizedInput);
|
|
148
|
+
|
|
149
|
+
// Re-normalize after resolution
|
|
150
|
+
const normalized = normalize(absolutePath);
|
|
151
|
+
|
|
152
|
+
// Get relative path
|
|
153
|
+
const relativePath = relative(workspace, normalized);
|
|
154
|
+
|
|
155
|
+
// Check for path traversal (relative path should not start with ..)
|
|
156
|
+
if (relativePath.startsWith('..') || relativePath.startsWith('/..')) {
|
|
157
|
+
return {
|
|
158
|
+
allowed: false,
|
|
159
|
+
path: normalized,
|
|
160
|
+
relativePath,
|
|
161
|
+
reason: 'Path traversal detected',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check depth
|
|
166
|
+
const depth = relativePath.split(/[/\\]/).filter(Boolean).length;
|
|
167
|
+
if (depth > maxDepth) {
|
|
168
|
+
return {
|
|
169
|
+
allowed: false,
|
|
170
|
+
path: normalized,
|
|
171
|
+
relativePath,
|
|
172
|
+
reason: `Path depth exceeds maximum (${maxDepth})`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check sensitive patterns
|
|
177
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
178
|
+
if (pattern.test(normalized) || pattern.test(relativePath)) {
|
|
179
|
+
return {
|
|
180
|
+
allowed: false,
|
|
181
|
+
path: normalized,
|
|
182
|
+
relativePath,
|
|
183
|
+
reason: 'Sensitive file access not allowed',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if path is within workspace
|
|
189
|
+
if (!normalized.startsWith(workspace)) {
|
|
190
|
+
return {
|
|
191
|
+
allowed: false,
|
|
192
|
+
path: normalized,
|
|
193
|
+
relativePath,
|
|
194
|
+
reason: 'Path outside workspace',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
allowed: true,
|
|
200
|
+
path: normalized,
|
|
201
|
+
relativePath,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check if path should be excluded from scanning
|
|
207
|
+
*/
|
|
208
|
+
shouldExclude(inputPath: string): boolean {
|
|
209
|
+
const normalized = normalize(inputPath);
|
|
210
|
+
const relativePath = relative(workspace, normalized);
|
|
211
|
+
|
|
212
|
+
for (const pattern of excludePatterns) {
|
|
213
|
+
if (pattern.test(relativePath) || pattern.test(normalized)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return false;
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Validate and return path or throw
|
|
223
|
+
*/
|
|
224
|
+
requireValid(inputPath: string): string {
|
|
225
|
+
const result = this.validate(inputPath);
|
|
226
|
+
|
|
227
|
+
if (!result.allowed) {
|
|
228
|
+
throw Errors.pathOutsideSandbox(inputPath, workspace);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result.path;
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if path exists within sandbox
|
|
236
|
+
*/
|
|
237
|
+
exists(inputPath: string): boolean {
|
|
238
|
+
const result = this.validate(inputPath);
|
|
239
|
+
if (!result.allowed) return false;
|
|
240
|
+
return existsSync(result.path);
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get workspace root
|
|
245
|
+
*/
|
|
246
|
+
getWorkspace(): string {
|
|
247
|
+
return workspace;
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Convert absolute path to relative
|
|
252
|
+
*/
|
|
253
|
+
toRelative(absolutePath: string): string {
|
|
254
|
+
return relative(workspace, absolutePath);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Convert relative path to absolute
|
|
259
|
+
*/
|
|
260
|
+
toAbsolute(relativePath: string): string {
|
|
261
|
+
return resolve(workspace, relativePath);
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Default sandbox for current working directory
|
|
268
|
+
*/
|
|
269
|
+
let defaultSandbox: ReturnType<typeof createSandbox> | null = null;
|
|
270
|
+
|
|
271
|
+
export function getDefaultSandbox(): ReturnType<typeof createSandbox> {
|
|
272
|
+
if (!defaultSandbox) {
|
|
273
|
+
defaultSandbox = createSandbox({ workspace: process.cwd() });
|
|
274
|
+
}
|
|
275
|
+
return defaultSandbox;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function setDefaultSandbox(workspace: string): void {
|
|
279
|
+
defaultSandbox = createSandbox({ workspace });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Redact sensitive information from paths in output
|
|
284
|
+
*/
|
|
285
|
+
export function redactPath(path: string, workspace: string): string {
|
|
286
|
+
// Convert to relative path
|
|
287
|
+
const relativePath = relative(workspace, path);
|
|
288
|
+
|
|
289
|
+
// If it's outside workspace, redact completely
|
|
290
|
+
if (relativePath.startsWith('..')) {
|
|
291
|
+
return '[REDACTED]';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return relativePath;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Redact sensitive values from objects
|
|
299
|
+
*/
|
|
300
|
+
export function redactSensitive(obj: Record<string, unknown>): Record<string, unknown> {
|
|
301
|
+
const sensitiveKeys = [
|
|
302
|
+
'password', 'secret', 'token', 'apiKey', 'api_key', 'auth',
|
|
303
|
+
'credential', 'private', 'key', 'cert', 'cookie', 'session',
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const result: Record<string, unknown> = {};
|
|
307
|
+
|
|
308
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
309
|
+
const lowerKey = key.toLowerCase();
|
|
310
|
+
|
|
311
|
+
if (sensitiveKeys.some(s => lowerKey.includes(s))) {
|
|
312
|
+
result[key] = '[REDACTED]';
|
|
313
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
314
|
+
result[key] = redactSensitive(value as Record<string, unknown>);
|
|
315
|
+
} else if (Array.isArray(value)) {
|
|
316
|
+
result[key] = value.map(item =>
|
|
317
|
+
typeof item === 'object' && item !== null
|
|
318
|
+
? redactSensitive(item as Record<string, unknown>)
|
|
319
|
+
: item
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
result[key] = value;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export default {
|
|
330
|
+
createSandbox,
|
|
331
|
+
getDefaultSandbox,
|
|
332
|
+
setDefaultSandbox,
|
|
333
|
+
redactPath,
|
|
334
|
+
redactSensitive,
|
|
335
|
+
SENSITIVE_PATTERNS,
|
|
336
|
+
EXCLUDE_PATTERNS,
|
|
337
|
+
};
|