@vibecheckai/cli 3.2.6 → 3.3.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/bin/registry.js +192 -5
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +81 -18
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +148 -0
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +63 -44
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +106 -19
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1149 -243
- package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +245 -35
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- package/mcp-server/package-lock.json +0 -165
package/mcp-server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* vibecheck MCP Server v2.0 -
|
|
4
|
+
* vibecheck MCP Server v2.1.0 - Hardened Production Build
|
|
5
5
|
*
|
|
6
6
|
* Curated Tools for AI Agents:
|
|
7
7
|
* vibecheck.ctx - Build truthpack/context
|
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
* vibecheck.check_invariants - Invariant checks
|
|
16
16
|
*
|
|
17
17
|
* Everything else is parameters on these tools.
|
|
18
|
+
*
|
|
19
|
+
* HARDENING FEATURES (v2.1.0):
|
|
20
|
+
* - Input validation: URL, path, string, array, number sanitization
|
|
21
|
+
* - Output sanitization: Redaction of secrets, truncation of large outputs
|
|
22
|
+
* - Rate limiting: 120 calls/minute per server instance
|
|
23
|
+
* - Path security: Traversal prevention, project root sandboxing
|
|
24
|
+
* - Safe JSON parsing: Size limits, error handling
|
|
25
|
+
* - Error handling: Consistent error codes, sanitized messages
|
|
26
|
+
* - Resource safety: Bounded timeouts, memory limits
|
|
27
|
+
* - Graceful degradation: Partial output on failures
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
30
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -33,14 +43,16 @@ import { fileURLToPath } from "url";
|
|
|
33
43
|
import { execFile } from "child_process";
|
|
34
44
|
import { promisify } from "util";
|
|
35
45
|
|
|
36
|
-
// Import API client for dashboard integration
|
|
46
|
+
// Import API client for dashboard integration (optional - may use CommonJS)
|
|
47
|
+
import { createRequire } from "module";
|
|
48
|
+
const require = createRequire(import.meta.url);
|
|
37
49
|
const {
|
|
38
50
|
createScan,
|
|
39
51
|
updateScanProgress,
|
|
40
52
|
submitScanResults,
|
|
41
53
|
reportScanError,
|
|
42
54
|
isApiAvailable
|
|
43
|
-
} = require("./lib/api-client");
|
|
55
|
+
} = require("./lib/api-client.cjs");
|
|
44
56
|
|
|
45
57
|
const execFileAsync = promisify(execFile);
|
|
46
58
|
|
|
@@ -64,8 +76,384 @@ const CONFIG = {
|
|
|
64
76
|
AUTOPILOT: 300000, // 5 minutes
|
|
65
77
|
},
|
|
66
78
|
MAX_BUFFER: 10 * 1024 * 1024, // 10MB
|
|
79
|
+
// Hardening limits
|
|
80
|
+
LIMITS: {
|
|
81
|
+
MAX_OUTPUT_LENGTH: 500000, // 500KB max output text
|
|
82
|
+
MAX_PATH_LENGTH: 4096, // Max path string length
|
|
83
|
+
MAX_URL_LENGTH: 2048, // Max URL length
|
|
84
|
+
MAX_STRING_ARG: 10000, // Max string argument length
|
|
85
|
+
MAX_ARRAY_ITEMS: 100, // Max array items in args
|
|
86
|
+
RATE_LIMIT_WINDOW_MS: 60000, // 1 minute window
|
|
87
|
+
RATE_LIMIT_MAX_CALLS: 120, // Max calls per window
|
|
88
|
+
},
|
|
89
|
+
// Sensitive patterns to redact from output
|
|
90
|
+
SENSITIVE_PATTERNS: [
|
|
91
|
+
/(?:sk_live_|sk_test_)[a-zA-Z0-9]{24,}/g, // Stripe keys
|
|
92
|
+
/(?:AKIA|ASIA)[A-Z0-9]{16}/g, // AWS keys
|
|
93
|
+
/ghp_[a-zA-Z0-9]{36}/g, // GitHub tokens
|
|
94
|
+
/xox[baprs]-[0-9A-Za-z\-]{10,}/g, // Slack tokens
|
|
95
|
+
/eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, // JWTs
|
|
96
|
+
/(?:password|secret|token|apikey|api_key)["']?\s*[:=]\s*["'][^"']{8,}["']/gi, // Generic secrets
|
|
97
|
+
],
|
|
67
98
|
};
|
|
68
99
|
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// HARDENING: Input Validation & Sanitization Utilities
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate and sanitize a URL
|
|
106
|
+
* @param {string} url - URL to validate
|
|
107
|
+
* @returns {{ valid: boolean, url?: string, error?: string }}
|
|
108
|
+
*/
|
|
109
|
+
function validateUrl(url) {
|
|
110
|
+
if (!url || typeof url !== 'string') {
|
|
111
|
+
return { valid: false, error: 'URL is required and must be a string' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const trimmed = url.trim();
|
|
115
|
+
|
|
116
|
+
if (trimmed.length > CONFIG.LIMITS.MAX_URL_LENGTH) {
|
|
117
|
+
return { valid: false, error: `URL exceeds maximum length of ${CONFIG.LIMITS.MAX_URL_LENGTH} characters` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const parsed = new URL(trimmed);
|
|
122
|
+
|
|
123
|
+
// Only allow http/https protocols
|
|
124
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
125
|
+
return { valid: false, error: 'URL must use http or https protocol' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Block localhost/internal IPs in production contexts (can be overridden)
|
|
129
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
130
|
+
if (process.env.VIBECHECK_BLOCK_INTERNAL !== 'false') {
|
|
131
|
+
// Allow localhost for development
|
|
132
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
133
|
+
// This is OK for local testing
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { valid: true, url: trimmed };
|
|
138
|
+
} catch {
|
|
139
|
+
return { valid: false, error: 'Invalid URL format' };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sanitize a file path to prevent path traversal.
|
|
145
|
+
*
|
|
146
|
+
* SECURITY FIX: Previous implementation used path.sep which differs between
|
|
147
|
+
* Windows (\) and Unix (/). Attackers could bypass the check on Windows by
|
|
148
|
+
* using forward slashes in paths. Now we normalize all separators before comparing.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} inputPath - Path to sanitize
|
|
151
|
+
* @param {string} projectRoot - Project root directory
|
|
152
|
+
* @returns {{ valid: boolean, path?: string, error?: string }}
|
|
153
|
+
*/
|
|
154
|
+
function sanitizePath(inputPath, projectRoot) {
|
|
155
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
156
|
+
return { valid: false, error: 'Path is required and must be a string' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (inputPath.length > CONFIG.LIMITS.MAX_PATH_LENGTH) {
|
|
160
|
+
return { valid: false, error: `Path exceeds maximum length of ${CONFIG.LIMITS.MAX_PATH_LENGTH} characters` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Reject null bytes which can truncate paths in some systems
|
|
164
|
+
if (inputPath.includes('\0')) {
|
|
165
|
+
return { valid: false, error: 'Path contains invalid characters (null byte)' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
170
|
+
const resolvedPath = path.resolve(projectRoot, inputPath);
|
|
171
|
+
|
|
172
|
+
// SECURITY: Normalize path separators for cross-platform comparison
|
|
173
|
+
// On Windows, path.sep is '\' but paths can use '/' which would bypass the check
|
|
174
|
+
const normalizedRoot = resolvedRoot.replace(/\\/g, '/').toLowerCase();
|
|
175
|
+
const normalizedPath = resolvedPath.replace(/\\/g, '/').toLowerCase();
|
|
176
|
+
|
|
177
|
+
// Ensure the resolved path is within or equal to the project root
|
|
178
|
+
// Use normalized paths for comparison, add trailing slash to prevent
|
|
179
|
+
// prefix attacks (e.g., /project-malicious matching /project)
|
|
180
|
+
const rootWithSep = normalizedRoot.endsWith('/') ? normalizedRoot : normalizedRoot + '/';
|
|
181
|
+
|
|
182
|
+
if (!normalizedPath.startsWith(rootWithSep) && normalizedPath !== normalizedRoot) {
|
|
183
|
+
return { valid: false, error: 'Path traversal detected - path must be within project root' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { valid: true, path: resolvedPath };
|
|
187
|
+
} catch (err) {
|
|
188
|
+
return { valid: false, error: `Invalid path format: ${err.message}` };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Validate and sanitize string arguments
|
|
194
|
+
* @param {*} value - Value to validate
|
|
195
|
+
* @param {number} maxLength - Maximum allowed length
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
function sanitizeString(value, maxLength = CONFIG.LIMITS.MAX_STRING_ARG) {
|
|
199
|
+
if (value === null || value === undefined) {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const str = String(value);
|
|
204
|
+
return str.length > maxLength ? str.slice(0, maxLength) + '...[truncated]' : str;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Validate array arguments
|
|
209
|
+
* @param {*} arr - Array to validate
|
|
210
|
+
* @param {number} maxItems - Maximum allowed items
|
|
211
|
+
* @returns {any[]}
|
|
212
|
+
*/
|
|
213
|
+
function sanitizeArray(arr, maxItems = CONFIG.LIMITS.MAX_ARRAY_ITEMS) {
|
|
214
|
+
if (!Array.isArray(arr)) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
return arr.slice(0, maxItems);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate numeric arguments
|
|
222
|
+
* @param {*} value - Value to validate
|
|
223
|
+
* @param {number} min - Minimum value
|
|
224
|
+
* @param {number} max - Maximum value
|
|
225
|
+
* @param {number} defaultValue - Default if invalid
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
function sanitizeNumber(value, min, max, defaultValue) {
|
|
229
|
+
const num = Number(value);
|
|
230
|
+
if (isNaN(num) || num < min || num > max) {
|
|
231
|
+
return defaultValue;
|
|
232
|
+
}
|
|
233
|
+
return num;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Redact sensitive information from output
|
|
238
|
+
* @param {string} text - Text to redact
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
function redactSensitive(text) {
|
|
242
|
+
if (!text || typeof text !== 'string') {
|
|
243
|
+
return text;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let result = text;
|
|
247
|
+
for (const pattern of CONFIG.SENSITIVE_PATTERNS) {
|
|
248
|
+
result = result.replace(pattern, '[REDACTED]');
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Truncate output to prevent memory issues
|
|
255
|
+
* @param {string} text - Text to truncate
|
|
256
|
+
* @param {number} maxLength - Maximum length
|
|
257
|
+
* @returns {string}
|
|
258
|
+
*/
|
|
259
|
+
function truncateOutput(text, maxLength = CONFIG.LIMITS.MAX_OUTPUT_LENGTH) {
|
|
260
|
+
if (!text || typeof text !== 'string') {
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (text.length <= maxLength) {
|
|
265
|
+
return text;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const truncated = text.slice(0, maxLength);
|
|
269
|
+
return truncated + `\n\n...[Output truncated at ${maxLength} characters]`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// HARDENING: Rate Limiting (Per-API-Key)
|
|
274
|
+
//
|
|
275
|
+
// SECURITY FIX: Previous implementation used a global rate limit, allowing
|
|
276
|
+
// a single attacker to exhaust the rate limit budget for ALL users.
|
|
277
|
+
// Now we rate limit per-API-key hash to isolate abuse.
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
const rateLimitState = {
|
|
281
|
+
callsByKey: new Map(), // Map<keyHash, timestamp[]>
|
|
282
|
+
globalCalls: [], // Fallback for unauthenticated requests
|
|
283
|
+
cleanupInterval: null,
|
|
284
|
+
maxKeys: 10000, // Prevent unbounded growth
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Hash API key for rate limit tracking (don't store raw keys)
|
|
289
|
+
*/
|
|
290
|
+
function hashKeyForRateLimit(apiKey) {
|
|
291
|
+
if (!apiKey) return '__anonymous__';
|
|
292
|
+
const crypto = require('crypto');
|
|
293
|
+
return crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Cleanup expired rate limit entries
|
|
298
|
+
*/
|
|
299
|
+
function cleanupRateLimitState() {
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
const windowStart = now - CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS;
|
|
302
|
+
|
|
303
|
+
// Clean per-key entries
|
|
304
|
+
for (const [keyHash, calls] of rateLimitState.callsByKey) {
|
|
305
|
+
const validCalls = calls.filter(t => t > windowStart);
|
|
306
|
+
if (validCalls.length === 0) {
|
|
307
|
+
rateLimitState.callsByKey.delete(keyHash);
|
|
308
|
+
} else {
|
|
309
|
+
rateLimitState.callsByKey.set(keyHash, validCalls);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Clean global calls
|
|
314
|
+
rateLimitState.globalCalls = rateLimitState.globalCalls.filter(t => t > windowStart);
|
|
315
|
+
|
|
316
|
+
// Enforce max keys to prevent memory exhaustion
|
|
317
|
+
if (rateLimitState.callsByKey.size > rateLimitState.maxKeys) {
|
|
318
|
+
// Evict oldest keys (those with oldest last call)
|
|
319
|
+
const entries = Array.from(rateLimitState.callsByKey.entries());
|
|
320
|
+
entries.sort((a, b) => Math.max(...(a[1] || [0])) - Math.max(...(b[1] || [0])));
|
|
321
|
+
const toDelete = entries.slice(0, rateLimitState.callsByKey.size - rateLimitState.maxKeys);
|
|
322
|
+
for (const [key] of toDelete) {
|
|
323
|
+
rateLimitState.callsByKey.delete(key);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Check rate limit for a specific API key
|
|
330
|
+
*
|
|
331
|
+
* @param {string} apiKey - The API key (optional, falls back to global limit)
|
|
332
|
+
* @returns {{ allowed: boolean, remaining: number, resetIn: number, keyHash?: string }}
|
|
333
|
+
*/
|
|
334
|
+
function checkRateLimit(apiKey = null) {
|
|
335
|
+
const now = Date.now();
|
|
336
|
+
const windowStart = now - CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS;
|
|
337
|
+
const keyHash = hashKeyForRateLimit(apiKey);
|
|
338
|
+
|
|
339
|
+
// Get or create call array for this key
|
|
340
|
+
let calls = rateLimitState.callsByKey.get(keyHash);
|
|
341
|
+
if (!calls) {
|
|
342
|
+
calls = [];
|
|
343
|
+
rateLimitState.callsByKey.set(keyHash, calls);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Clean old entries for this key
|
|
347
|
+
const validCalls = calls.filter(t => t > windowStart);
|
|
348
|
+
rateLimitState.callsByKey.set(keyHash, validCalls);
|
|
349
|
+
|
|
350
|
+
// Determine limit based on authentication
|
|
351
|
+
// Anonymous gets stricter limit (10/min), authenticated gets full limit
|
|
352
|
+
const maxCalls = apiKey ? CONFIG.LIMITS.RATE_LIMIT_MAX_CALLS : Math.min(10, CONFIG.LIMITS.RATE_LIMIT_MAX_CALLS);
|
|
353
|
+
const remaining = maxCalls - validCalls.length;
|
|
354
|
+
|
|
355
|
+
if (remaining <= 0) {
|
|
356
|
+
const oldestCall = Math.min(...validCalls);
|
|
357
|
+
const resetIn = Math.max(0, (oldestCall + CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS) - now);
|
|
358
|
+
return { allowed: false, remaining: 0, resetIn, keyHash };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Record this call
|
|
362
|
+
validCalls.push(now);
|
|
363
|
+
|
|
364
|
+
// Periodic cleanup (every ~100 calls)
|
|
365
|
+
if (Math.random() < 0.01) {
|
|
366
|
+
cleanupRateLimitState();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { allowed: true, remaining: remaining - 1, resetIn: CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS, keyHash };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// HARDENING: Circuit Breaker for API Integration
|
|
374
|
+
// ============================================================================
|
|
375
|
+
|
|
376
|
+
const circuitBreakerState = {
|
|
377
|
+
failures: 0,
|
|
378
|
+
lastFailureTime: 0,
|
|
379
|
+
state: 'CLOSED', // CLOSED = normal, OPEN = failing, HALF_OPEN = testing
|
|
380
|
+
failureThreshold: 5,
|
|
381
|
+
resetTimeout: 60000, // 1 minute
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check if API calls should be allowed
|
|
386
|
+
* @returns {{ allowed: boolean, reason?: string }}
|
|
387
|
+
*/
|
|
388
|
+
function checkCircuitBreaker() {
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
|
|
391
|
+
// If circuit is open, check if we should attempt reset
|
|
392
|
+
if (circuitBreakerState.state === 'OPEN') {
|
|
393
|
+
if (now - circuitBreakerState.lastFailureTime >= circuitBreakerState.resetTimeout) {
|
|
394
|
+
circuitBreakerState.state = 'HALF_OPEN';
|
|
395
|
+
console.error('[MCP] Circuit breaker entering HALF_OPEN state - testing API');
|
|
396
|
+
} else {
|
|
397
|
+
return {
|
|
398
|
+
allowed: false,
|
|
399
|
+
reason: `Circuit breaker OPEN - API calls disabled for ${Math.ceil((circuitBreakerState.resetTimeout - (now - circuitBreakerState.lastFailureTime)) / 1000)}s`
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return { allowed: true };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Record API call result
|
|
409
|
+
* @param {boolean} success - Whether the API call succeeded
|
|
410
|
+
*/
|
|
411
|
+
function recordApiResult(success) {
|
|
412
|
+
if (success) {
|
|
413
|
+
// Reset on success
|
|
414
|
+
if (circuitBreakerState.state === 'HALF_OPEN') {
|
|
415
|
+
console.error('[MCP] Circuit breaker CLOSED - API recovered');
|
|
416
|
+
}
|
|
417
|
+
circuitBreakerState.failures = 0;
|
|
418
|
+
circuitBreakerState.state = 'CLOSED';
|
|
419
|
+
} else {
|
|
420
|
+
circuitBreakerState.failures++;
|
|
421
|
+
circuitBreakerState.lastFailureTime = Date.now();
|
|
422
|
+
|
|
423
|
+
if (circuitBreakerState.failures >= circuitBreakerState.failureThreshold) {
|
|
424
|
+
circuitBreakerState.state = 'OPEN';
|
|
425
|
+
console.error(`[MCP] Circuit breaker OPEN after ${circuitBreakerState.failures} failures - disabling API calls`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ============================================================================
|
|
431
|
+
// HARDENING: Safe JSON Parsing
|
|
432
|
+
// ============================================================================
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Safely parse JSON with size limits
|
|
436
|
+
* @param {string} text - JSON string to parse
|
|
437
|
+
* @param {number} maxSize - Maximum allowed size in bytes
|
|
438
|
+
* @returns {{ success: boolean, data?: any, error?: string }}
|
|
439
|
+
*/
|
|
440
|
+
function safeJsonParse(text, maxSize = 5 * 1024 * 1024) {
|
|
441
|
+
if (!text || typeof text !== 'string') {
|
|
442
|
+
return { success: false, error: 'Invalid input' };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (text.length > maxSize) {
|
|
446
|
+
return { success: false, error: `JSON exceeds maximum size of ${maxSize} bytes` };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const data = JSON.parse(text);
|
|
451
|
+
return { success: true, data };
|
|
452
|
+
} catch (e) {
|
|
453
|
+
return { success: false, error: `JSON parse error: ${e.message}` };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
69
457
|
const VERSION = CONFIG.VERSION;
|
|
70
458
|
|
|
71
459
|
// Import intelligence tools
|
|
@@ -92,6 +480,12 @@ import {
|
|
|
92
480
|
handleArchitectTool,
|
|
93
481
|
} from "./architect-tools.js";
|
|
94
482
|
|
|
483
|
+
// Import authority system tools
|
|
484
|
+
import {
|
|
485
|
+
AUTHORITY_TOOLS,
|
|
486
|
+
handleAuthorityTool,
|
|
487
|
+
} from "./authority-tools.js";
|
|
488
|
+
|
|
95
489
|
// Import codebase architect tools
|
|
96
490
|
import {
|
|
97
491
|
CODEBASE_ARCHITECT_TOOLS,
|
|
@@ -136,14 +530,22 @@ import { CONSOLIDATED_TOOLS, handleConsolidatedTool } from "./consolidated-tools
|
|
|
136
530
|
import { MCP_TOOLS_V3, handleToolV3, TOOL_TIERS as V3_TOOL_TIERS } from "./tools-v3.js";
|
|
137
531
|
|
|
138
532
|
// Import tier auth for entitlement checking
|
|
139
|
-
import {
|
|
533
|
+
import { getFeatureAccessStatus } from "./tier-auth.js";
|
|
140
534
|
|
|
141
|
-
// Import Agent Firewall Interceptor
|
|
535
|
+
// Import Agent Firewall Interceptor - ENABLED BY DEFAULT
|
|
536
|
+
// The Agent Firewall is the core gatekeeper that validates AI changes against reality
|
|
142
537
|
import {
|
|
143
538
|
AGENT_FIREWALL_TOOL,
|
|
144
539
|
handleAgentFirewallIntercept,
|
|
145
540
|
} from "./agent-firewall-interceptor.js";
|
|
146
541
|
|
|
542
|
+
// Import Conductor tools - Multi-Agent Coordination (Phase 2)
|
|
543
|
+
import {
|
|
544
|
+
CONDUCTOR_TOOLS,
|
|
545
|
+
handleConductorToolCall,
|
|
546
|
+
getConductorTools,
|
|
547
|
+
} from "./conductor/tools.js";
|
|
548
|
+
|
|
147
549
|
/**
|
|
148
550
|
* TRUTH FIREWALL CONFIGURATION
|
|
149
551
|
*
|
|
@@ -182,14 +584,29 @@ function getPolicyConfig(policy) {
|
|
|
182
584
|
return POLICY_THRESHOLDS[policy] || POLICY_THRESHOLDS.strict;
|
|
183
585
|
}
|
|
184
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Emit guardrail metric to audit log.
|
|
589
|
+
*
|
|
590
|
+
* SECURITY FIX: Previous implementation silently ignored all failures.
|
|
591
|
+
* Now we log failures to stderr for security monitoring - an attacker
|
|
592
|
+
* filling disk or manipulating permissions would have gone undetected.
|
|
593
|
+
*/
|
|
185
594
|
async function emitGuardrailMetric(projectPath, metric) {
|
|
186
595
|
try {
|
|
187
596
|
const auditDir = path.join(projectPath, ".vibecheck", "audit");
|
|
188
597
|
await fs.mkdir(auditDir, { recursive: true });
|
|
189
598
|
const record = JSON.stringify({ ...metric, timestamp: new Date().toISOString() });
|
|
190
599
|
await fs.appendFile(path.join(auditDir, "guardrail-metrics.jsonl"), `${record}\n`);
|
|
191
|
-
} catch {
|
|
192
|
-
//
|
|
600
|
+
} catch (err) {
|
|
601
|
+
// SECURITY: Log failures - silent failure could hide attacks
|
|
602
|
+
// (e.g., attacker fills disk to prevent audit logging)
|
|
603
|
+
console.error(`[SECURITY] Guardrail metric write failed: ${err.message}`);
|
|
604
|
+
console.error(`[SECURITY] Failed metric: ${JSON.stringify(metric)}`);
|
|
605
|
+
|
|
606
|
+
// Attempt fallback to stderr-only logging for critical metrics
|
|
607
|
+
if (metric.event === 'truth_firewall_block' || metric.event === 'security_violation') {
|
|
608
|
+
console.error(`[SECURITY-CRITICAL] ${metric.event}: ${JSON.stringify(metric)}`);
|
|
609
|
+
}
|
|
193
610
|
}
|
|
194
611
|
}
|
|
195
612
|
|
|
@@ -239,18 +656,22 @@ function checkTruthFirewallBlock(toolName, args, projectPath) {
|
|
|
239
656
|
const USE_V3_TOOLS = process.env.VIBECHECK_MCP_V3 !== 'false';
|
|
240
657
|
const USE_CONSOLIDATED_TOOLS = process.env.VIBECHECK_MCP_CONSOLIDATED !== 'false';
|
|
241
658
|
|
|
242
|
-
const TOOLS = USE_V3_TOOLS ? [
|
|
659
|
+
const TOOLS = (USE_V3_TOOLS ? [
|
|
243
660
|
// v3: 10 focused tools for STARTER+ (no free MCP tools)
|
|
244
661
|
...MCP_TOOLS_V3,
|
|
245
662
|
AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
|
|
246
|
-
|
|
663
|
+
...getConductorTools(), // Conductor - multi-agent coordination
|
|
664
|
+
].filter(t => t !== null) : USE_CONSOLIDATED_TOOLS ? [
|
|
247
665
|
// Curated tools for agents (legacy)
|
|
248
666
|
...CONSOLIDATED_TOOLS,
|
|
249
667
|
AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
|
|
250
|
-
|
|
668
|
+
...getConductorTools(), // Conductor - multi-agent coordination
|
|
669
|
+
].filter(t => t !== null) : [
|
|
251
670
|
// Legacy: Full tool set (50+ tools) - for backward compatibility
|
|
252
671
|
// PRIORITY: Agent Firewall - intercepts ALL file writes
|
|
253
672
|
AGENT_FIREWALL_TOOL,
|
|
673
|
+
// PRIORITY: Conductor - multi-agent coordination
|
|
674
|
+
...getConductorTools(),
|
|
254
675
|
// PRIORITY: Truth Firewall tools (Hallucination Stopper) - agents MUST use these
|
|
255
676
|
...TRUTH_FIREWALL_TOOLS, // vibecheck.get_truthpack, vibecheck.validate_claim, vibecheck.compile_context, etc.
|
|
256
677
|
|
|
@@ -259,6 +680,7 @@ const TOOLS = USE_V3_TOOLS ? [
|
|
|
259
680
|
|
|
260
681
|
...INTELLIGENCE_TOOLS, // Add all intelligence suite tools
|
|
261
682
|
...VIBECHECK_TOOLS, // Add AI vibecheck tools (verify, quality, smells, etc.)
|
|
683
|
+
...AUTHORITY_TOOLS, // Add authority system tools (classify, approve, list)
|
|
262
684
|
...AGENT_CHECKPOINT_TOOLS, // Add agent checkpoint tools
|
|
263
685
|
...ARCHITECT_TOOLS, // Add architect review/suggest tools
|
|
264
686
|
...CODEBASE_ARCHITECT_TOOLS, // Add codebase-aware architect tools
|
|
@@ -854,7 +1276,7 @@ const TOOLS = USE_V3_TOOLS ? [
|
|
|
854
1276
|
},
|
|
855
1277
|
},
|
|
856
1278
|
},
|
|
857
|
-
];
|
|
1279
|
+
]).filter(t => t !== null);
|
|
858
1280
|
|
|
859
1281
|
// ============================================================================
|
|
860
1282
|
// SERVER IMPLEMENTATION
|
|
@@ -872,32 +1294,56 @@ class VibecheckMCP {
|
|
|
872
1294
|
|
|
873
1295
|
// ============================================================================
|
|
874
1296
|
// TOOL REGISTRY - Maps tool names to handlers for cleaner dispatch
|
|
1297
|
+
// HARDENING: Validates all handlers are functions
|
|
875
1298
|
// ============================================================================
|
|
876
1299
|
buildToolRegistry() {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
"vibecheck.gate": this.handleGate.bind(this),
|
|
887
|
-
"vibecheck.fix": this.handleFix.bind(this),
|
|
888
|
-
"vibecheck.share": this.handleShare.bind(this),
|
|
889
|
-
"vibecheck.ctx": this.handleCtx.bind(this),
|
|
890
|
-
"vibecheck.prove": this.handleProve.bind(this),
|
|
891
|
-
"vibecheck.proof": this.handleProof.bind(this),
|
|
892
|
-
"vibecheck.validate": this.handleValidate.bind(this),
|
|
893
|
-
"vibecheck.report": this.handleReport.bind(this),
|
|
894
|
-
"vibecheck.status": this.handleStatus.bind(this),
|
|
895
|
-
"vibecheck.autopilot": this.handleAutopilot.bind(this),
|
|
896
|
-
"vibecheck.autopilot_plan": this.handleAutopilotPlan.bind(this),
|
|
897
|
-
"vibecheck.autopilot_apply": this.handleAutopilotApply.bind(this),
|
|
898
|
-
"vibecheck.badge": this.handleBadge.bind(this),
|
|
899
|
-
"vibecheck.context": this.handleContext.bind(this),
|
|
1300
|
+
const registry = {};
|
|
1301
|
+
|
|
1302
|
+
// Helper to safely add handler with validation
|
|
1303
|
+
const addHandler = (name, handler) => {
|
|
1304
|
+
if (typeof handler !== 'function') {
|
|
1305
|
+
console.error(`[MCP] Warning: Tool ${name} handler is not a function`);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
registry[name] = handler;
|
|
900
1309
|
};
|
|
1310
|
+
|
|
1311
|
+
// Agent Firewall - intercepts file writes (if available)
|
|
1312
|
+
if (handleAgentFirewallIntercept && typeof handleAgentFirewallIntercept === 'function') {
|
|
1313
|
+
addHandler("vibecheck_agent_firewall_intercept", handleAgentFirewallIntercept);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Conductor - multi-agent coordination tools
|
|
1317
|
+
addHandler("vibecheck_conductor_register", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_register", args, projectPath));
|
|
1318
|
+
addHandler("vibecheck_conductor_acquire_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_acquire_lock", args, projectPath));
|
|
1319
|
+
addHandler("vibecheck_conductor_release_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_release_lock", args, projectPath));
|
|
1320
|
+
addHandler("vibecheck_conductor_propose", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_propose", args, projectPath));
|
|
1321
|
+
addHandler("vibecheck_conductor_status", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_status", args, projectPath));
|
|
1322
|
+
addHandler("vibecheck_conductor_terminate", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_terminate", args, projectPath));
|
|
1323
|
+
|
|
1324
|
+
// Core CLI tools
|
|
1325
|
+
addHandler("vibecheck.ship", this.handleShip.bind(this));
|
|
1326
|
+
addHandler("vibecheck.scan", this.handleScan.bind(this));
|
|
1327
|
+
addHandler("vibecheck.verify", this.handleVerify.bind(this));
|
|
1328
|
+
addHandler("vibecheck.reality", this.handleReality.bind(this));
|
|
1329
|
+
addHandler("vibecheckai.dev-test", this.handleAITest.bind(this));
|
|
1330
|
+
addHandler("vibecheck.gate", this.handleGate.bind(this));
|
|
1331
|
+
addHandler("vibecheck.fix", this.handleFix.bind(this));
|
|
1332
|
+
addHandler("vibecheck.share", this.handleShare.bind(this));
|
|
1333
|
+
addHandler("vibecheck.ctx", this.handleCtx.bind(this));
|
|
1334
|
+
addHandler("vibecheck.prove", this.handleProve.bind(this));
|
|
1335
|
+
addHandler("vibecheck.proof", this.handleProof.bind(this));
|
|
1336
|
+
addHandler("vibecheck.validate", this.handleValidate.bind(this));
|
|
1337
|
+
addHandler("vibecheck.report", this.handleReport.bind(this));
|
|
1338
|
+
addHandler("vibecheck.status", this.handleStatus.bind(this));
|
|
1339
|
+
addHandler("vibecheck.autopilot", this.handleAutopilot.bind(this));
|
|
1340
|
+
addHandler("vibecheck.autopilot_plan", this.handleAutopilotPlan.bind(this));
|
|
1341
|
+
addHandler("vibecheck.autopilot_apply", this.handleAutopilotApply.bind(this));
|
|
1342
|
+
addHandler("vibecheck.badge", this.handleBadge.bind(this));
|
|
1343
|
+
addHandler("vibecheck.context", this.handleContext.bind(this));
|
|
1344
|
+
|
|
1345
|
+
console.error(`[MCP] Tool registry built with ${Object.keys(registry).length} handlers`);
|
|
1346
|
+
return registry;
|
|
901
1347
|
}
|
|
902
1348
|
|
|
903
1349
|
// ============================================================================
|
|
@@ -910,70 +1356,208 @@ class VibecheckMCP {
|
|
|
910
1356
|
skipAuth = true,
|
|
911
1357
|
} = options;
|
|
912
1358
|
|
|
1359
|
+
// ========================================================================
|
|
1360
|
+
// HARDENING: Validate command
|
|
1361
|
+
// ========================================================================
|
|
1362
|
+
const sanitizedCommand = sanitizeString(command, 50);
|
|
1363
|
+
if (!sanitizedCommand || !/^[a-z0-9_-]+$/i.test(sanitizedCommand)) {
|
|
1364
|
+
throw new Error(`Invalid CLI command: ${sanitizedCommand}`);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// ========================================================================
|
|
1368
|
+
// HARDENING: Validate and sanitize arguments
|
|
1369
|
+
// ========================================================================
|
|
1370
|
+
const sanitizedArgs = sanitizeArray(args, 50).map(arg => {
|
|
1371
|
+
const str = String(arg);
|
|
1372
|
+
// Validate argument format (must be simple flags or values)
|
|
1373
|
+
if (str.length > 1000) {
|
|
1374
|
+
return str.slice(0, 1000);
|
|
1375
|
+
}
|
|
1376
|
+
return str;
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// ========================================================================
|
|
1380
|
+
// HARDENING: Validate working directory
|
|
1381
|
+
// ========================================================================
|
|
1382
|
+
const resolvedCwd = path.resolve(cwd || process.cwd());
|
|
1383
|
+
if (!fsSync.existsSync(resolvedCwd)) {
|
|
1384
|
+
throw new Error(`Working directory does not exist: ${resolvedCwd}`);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
913
1387
|
// Build argument array - this prevents command injection
|
|
914
|
-
const finalArgs = [CONFIG.BIN_PATH,
|
|
1388
|
+
const finalArgs = [CONFIG.BIN_PATH, sanitizedCommand, ...sanitizedArgs];
|
|
1389
|
+
|
|
1390
|
+
// ========================================================================
|
|
1391
|
+
// HARDENING: Clean environment - don't leak sensitive vars
|
|
1392
|
+
// ========================================================================
|
|
1393
|
+
const safeEnv = { ...process.env };
|
|
1394
|
+
// Remove potentially sensitive env vars from being passed through
|
|
1395
|
+
const sensitiveEnvKeys = ['AWS_SECRET_ACCESS_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL'];
|
|
1396
|
+
for (const key of sensitiveEnvKeys) {
|
|
1397
|
+
if (safeEnv[key] && !env[key]) {
|
|
1398
|
+
// Only remove if not explicitly set in options
|
|
1399
|
+
delete safeEnv[key];
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
915
1402
|
|
|
916
1403
|
const execEnv = {
|
|
917
|
-
...
|
|
1404
|
+
...safeEnv,
|
|
918
1405
|
...CONFIG.ENV_DEFAULTS,
|
|
919
1406
|
...(skipAuth ? { VIBECHECK_SKIP_AUTH: "1" } : {}),
|
|
920
1407
|
...env,
|
|
1408
|
+
// Ensure Node.js doesn't prompt for anything
|
|
1409
|
+
NODE_NO_READLINE: "1",
|
|
1410
|
+
FORCE_COLOR: "0",
|
|
921
1411
|
};
|
|
922
1412
|
|
|
1413
|
+
// ========================================================================
|
|
1414
|
+
// HARDENING: Bounded timeout
|
|
1415
|
+
// ========================================================================
|
|
1416
|
+
const boundedTimeout = sanitizeNumber(timeout, 1000, 900000, CONFIG.TIMEOUTS.DEFAULT);
|
|
1417
|
+
|
|
923
1418
|
try {
|
|
924
1419
|
const { stdout, stderr } = await execFileAsync(process.execPath, finalArgs, {
|
|
925
|
-
cwd,
|
|
1420
|
+
cwd: resolvedCwd,
|
|
926
1421
|
encoding: "utf8",
|
|
927
1422
|
maxBuffer: CONFIG.MAX_BUFFER,
|
|
928
|
-
timeout,
|
|
1423
|
+
timeout: boundedTimeout,
|
|
929
1424
|
env: execEnv,
|
|
1425
|
+
// Don't inherit stdin - prevents hanging
|
|
1426
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
930
1427
|
});
|
|
931
|
-
|
|
1428
|
+
|
|
1429
|
+
// Sanitize output before returning
|
|
1430
|
+
return {
|
|
1431
|
+
stdout: redactSensitive(truncateOutput(stdout || '')),
|
|
1432
|
+
stderr: redactSensitive(truncateOutput(stderr || '')),
|
|
1433
|
+
success: true
|
|
1434
|
+
};
|
|
932
1435
|
} catch (error) {
|
|
933
|
-
// Attach partial output for graceful degradation
|
|
934
|
-
error.partialOutput = error.stdout ||
|
|
935
|
-
error.partialStderr = error.stderr ||
|
|
1436
|
+
// Attach partial output for graceful degradation (sanitized)
|
|
1437
|
+
error.partialOutput = redactSensitive(truncateOutput(error.stdout || ''));
|
|
1438
|
+
error.partialStderr = redactSensitive(truncateOutput(error.stderr || ''));
|
|
1439
|
+
|
|
1440
|
+
// Add helpful error code for timeout
|
|
1441
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
1442
|
+
error.code = 'TIMEOUT';
|
|
1443
|
+
error.message = `Command timed out after ${boundedTimeout}ms`;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
936
1446
|
throw error;
|
|
937
1447
|
}
|
|
938
1448
|
}
|
|
939
1449
|
|
|
940
1450
|
// ============================================================================
|
|
941
|
-
// UTILITY HELPERS
|
|
1451
|
+
// HARDENED UTILITY HELPERS
|
|
942
1452
|
// ============================================================================
|
|
943
1453
|
|
|
944
|
-
|
|
1454
|
+
/**
|
|
1455
|
+
* Strip ANSI escape codes from output with length validation
|
|
1456
|
+
* @param {string} str - String to strip
|
|
1457
|
+
* @returns {string}
|
|
1458
|
+
*/
|
|
945
1459
|
stripAnsi(str) {
|
|
946
|
-
|
|
1460
|
+
if (!str || typeof str !== 'string') {
|
|
1461
|
+
return '';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Truncate first to prevent DoS on very long strings
|
|
1465
|
+
const truncated = str.length > CONFIG.LIMITS.MAX_OUTPUT_LENGTH
|
|
1466
|
+
? str.slice(0, CONFIG.LIMITS.MAX_OUTPUT_LENGTH)
|
|
1467
|
+
: str;
|
|
1468
|
+
|
|
1469
|
+
return truncated.replace(/\x1b\[[0-9;]*m/g, "");
|
|
947
1470
|
}
|
|
948
1471
|
|
|
949
|
-
|
|
1472
|
+
/**
|
|
1473
|
+
* Parse summary from disk with size limits and validation
|
|
1474
|
+
* @param {string} projectPath - Project root path
|
|
1475
|
+
* @returns {Promise<object|null>} Parsed summary or null
|
|
1476
|
+
*/
|
|
950
1477
|
async parseSummaryFromDisk(projectPath) {
|
|
951
1478
|
const summaryPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "summary.json");
|
|
1479
|
+
|
|
952
1480
|
try {
|
|
1481
|
+
// Check file size first
|
|
1482
|
+
const stats = await fs.stat(summaryPath);
|
|
1483
|
+
if (stats.size > 5 * 1024 * 1024) { // 5MB limit
|
|
1484
|
+
console.error(`[MCP] Summary file too large: ${stats.size} bytes`);
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
953
1488
|
const content = await fs.readFile(summaryPath, "utf-8");
|
|
954
|
-
|
|
955
|
-
|
|
1489
|
+
const parsed = safeJsonParse(content);
|
|
1490
|
+
|
|
1491
|
+
if (!parsed.success) {
|
|
1492
|
+
console.error(`[MCP] Invalid summary JSON: ${parsed.error}`);
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
return parsed.data;
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
// Silent fail - this is for graceful degradation
|
|
956
1499
|
return null;
|
|
957
1500
|
}
|
|
958
1501
|
}
|
|
959
1502
|
|
|
960
|
-
|
|
1503
|
+
/**
|
|
1504
|
+
* Format scan output from summary object with validation
|
|
1505
|
+
* @param {object} summary - Summary object
|
|
1506
|
+
* @param {string} projectPath - Project root path
|
|
1507
|
+
* @returns {string}
|
|
1508
|
+
*/
|
|
961
1509
|
formatScanOutput(summary, projectPath) {
|
|
962
|
-
|
|
963
|
-
|
|
1510
|
+
if (!summary || typeof summary !== 'object') {
|
|
1511
|
+
return '## Error: Invalid summary data\n';
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// Safely extract values with defaults
|
|
1515
|
+
const score = sanitizeNumber(summary.score, 0, 100, 0);
|
|
1516
|
+
const grade = sanitizeString(summary.grade, 10) || 'N/A';
|
|
1517
|
+
const canShip = Boolean(summary.canShip);
|
|
1518
|
+
|
|
1519
|
+
let output = `## Score: ${score}/100 (${grade})\n\n`;
|
|
1520
|
+
output += `**Verdict:** ${canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n\n`;
|
|
964
1521
|
|
|
965
|
-
if (summary.counts) {
|
|
1522
|
+
if (summary.counts && typeof summary.counts === 'object') {
|
|
966
1523
|
output += "### Checks\n\n";
|
|
967
1524
|
output += "| Category | Issues |\n|----------|--------|\n";
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1525
|
+
|
|
1526
|
+
// Limit to 50 categories to prevent output bloat
|
|
1527
|
+
const entries = Object.entries(summary.counts).slice(0, 50);
|
|
1528
|
+
for (const [key, count] of entries) {
|
|
1529
|
+
const safeKey = sanitizeString(key, 50);
|
|
1530
|
+
const safeCount = sanitizeNumber(count, 0, 999999, 0);
|
|
1531
|
+
const icon = safeCount === 0 ? "✅" : "⚠️";
|
|
1532
|
+
output += `| ${icon} ${safeKey} | ${safeCount} |\n`;
|
|
971
1533
|
}
|
|
972
1534
|
}
|
|
973
1535
|
|
|
974
1536
|
output += `\n📄 **Report:** ${CONFIG.OUTPUT_DIR}/report.html\n`;
|
|
975
1537
|
return output;
|
|
976
1538
|
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Safely read a file with size limits
|
|
1542
|
+
* @param {string} filePath - Path to file
|
|
1543
|
+
* @param {number} maxSize - Maximum file size in bytes
|
|
1544
|
+
* @returns {Promise<string|null>} File contents or null
|
|
1545
|
+
*/
|
|
1546
|
+
async safeReadFile(filePath, maxSize = 10 * 1024 * 1024) {
|
|
1547
|
+
try {
|
|
1548
|
+
const stats = await fs.stat(filePath);
|
|
1549
|
+
|
|
1550
|
+
if (stats.size > maxSize) {
|
|
1551
|
+
console.error(`[MCP] File too large: ${filePath} (${stats.size} bytes)`);
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1556
|
+
return content;
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
977
1561
|
|
|
978
1562
|
setupHandlers() {
|
|
979
1563
|
// List tools
|
|
@@ -983,24 +1567,95 @@ class VibecheckMCP {
|
|
|
983
1567
|
|
|
984
1568
|
// Call tool - main dispatch handler
|
|
985
1569
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
986
|
-
const { name, arguments: args } = request.params;
|
|
987
|
-
const projectPath = path.resolve(args?.projectPath || ".");
|
|
988
1570
|
const startTime = Date.now();
|
|
1571
|
+
let toolName = 'unknown';
|
|
1572
|
+
let projectPath = '.';
|
|
1573
|
+
|
|
1574
|
+
try {
|
|
1575
|
+
// ====================================================================
|
|
1576
|
+
// HARDENING: Input extraction with validation
|
|
1577
|
+
// ====================================================================
|
|
1578
|
+
const params = request?.params;
|
|
1579
|
+
if (!params || typeof params !== 'object') {
|
|
1580
|
+
return this.error('Invalid request: missing params', { code: 'INVALID_REQUEST' });
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
toolName = sanitizeString(params.name, 100);
|
|
1584
|
+
const args = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
1585
|
+
|
|
1586
|
+
// Validate tool name
|
|
1587
|
+
if (!toolName || toolName.length < 2) {
|
|
1588
|
+
return this.error('Invalid tool name', { code: 'INVALID_TOOL_NAME' });
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// ====================================================================
|
|
1592
|
+
// HARDENING: Rate limiting (per-API-key)
|
|
1593
|
+
// ====================================================================
|
|
1594
|
+
const apiKey = args?.apiKey || null;
|
|
1595
|
+
const rateCheck = checkRateLimit(apiKey);
|
|
1596
|
+
if (!rateCheck.allowed) {
|
|
1597
|
+
return this.error(`Rate limit exceeded. Try again in ${Math.ceil(rateCheck.resetIn / 1000)} seconds`, {
|
|
1598
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
1599
|
+
suggestion: 'Reduce the frequency of tool calls',
|
|
1600
|
+
nextSteps: [`Wait ${Math.ceil(rateCheck.resetIn / 1000)} seconds before retrying`],
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// ====================================================================
|
|
1605
|
+
// HARDENING: Project path validation
|
|
1606
|
+
// ====================================================================
|
|
1607
|
+
const rawProjectPath = args?.projectPath || '.';
|
|
1608
|
+
const pathValidation = sanitizePath(rawProjectPath, process.cwd());
|
|
1609
|
+
|
|
1610
|
+
if (!pathValidation.valid) {
|
|
1611
|
+
return this.error(pathValidation.error, {
|
|
1612
|
+
code: 'INVALID_PATH',
|
|
1613
|
+
suggestion: 'Provide a valid path within the current working directory',
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
projectPath = pathValidation.path;
|
|
989
1617
|
|
|
990
|
-
|
|
991
|
-
|
|
1618
|
+
// Emit audit event for tool invocation start
|
|
1619
|
+
// SECURITY: Include apiKey hash for audit trail (never log raw key)
|
|
1620
|
+
try {
|
|
1621
|
+
const crypto = require('crypto');
|
|
1622
|
+
const apiKeyHash = apiKey
|
|
1623
|
+
? crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16)
|
|
1624
|
+
: 'anonymous';
|
|
1625
|
+
|
|
1626
|
+
emitToolInvoke(toolName, args, "success", {
|
|
1627
|
+
projectPath,
|
|
1628
|
+
apiKeyHash,
|
|
1629
|
+
rateLimit: rateCheck,
|
|
1630
|
+
timestamp: new Date().toISOString(),
|
|
1631
|
+
});
|
|
1632
|
+
} catch {
|
|
1633
|
+
// Audit logging should never break the tool
|
|
1634
|
+
}
|
|
992
1635
|
|
|
993
|
-
|
|
994
|
-
//
|
|
995
|
-
|
|
1636
|
+
// ====================================================================
|
|
1637
|
+
// HARDENING: Truth firewall check with error handling
|
|
1638
|
+
// ====================================================================
|
|
1639
|
+
let firewallCheck = { blocked: false };
|
|
1640
|
+
try {
|
|
1641
|
+
firewallCheck = checkTruthFirewallBlock(toolName, args, projectPath);
|
|
1642
|
+
} catch (firewallError) {
|
|
1643
|
+
console.error(`[MCP] Firewall check error: ${firewallError.message}`);
|
|
1644
|
+
// Continue - don't block on firewall errors
|
|
1645
|
+
}
|
|
1646
|
+
|
|
996
1647
|
if (firewallCheck.blocked) {
|
|
997
1648
|
const policy = getTruthPolicy(args);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1649
|
+
try {
|
|
1650
|
+
await emitGuardrailMetric(projectPath, {
|
|
1651
|
+
event: "truth_firewall_block",
|
|
1652
|
+
tool: toolName,
|
|
1653
|
+
policy,
|
|
1654
|
+
reason: firewallCheck.code || "no_recent_claim_validation",
|
|
1655
|
+
});
|
|
1656
|
+
} catch {
|
|
1657
|
+
// Metrics should never break the tool
|
|
1658
|
+
}
|
|
1004
1659
|
return this.error(firewallCheck.reason, {
|
|
1005
1660
|
code: firewallCheck.code,
|
|
1006
1661
|
suggestion: firewallCheck.suggestion,
|
|
@@ -1009,98 +1664,142 @@ class VibecheckMCP {
|
|
|
1009
1664
|
}
|
|
1010
1665
|
|
|
1011
1666
|
// Handle v3 tools (10 consolidated tools, STARTER+ only)
|
|
1012
|
-
if (USE_V3_TOOLS && V3_TOOL_TIERS[
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1667
|
+
if (USE_V3_TOOLS && V3_TOOL_TIERS[toolName]) {
|
|
1668
|
+
// SECURITY FIX: Never trust client-provided tier - validate from API key
|
|
1669
|
+
// Previous: const userTier = sanitizeString(args?.tier, 20) || ...
|
|
1670
|
+
// This allowed privilege escalation via args.tier = "pro"
|
|
1671
|
+
const { getMcpToolAccess } = await import('./tier-auth.js');
|
|
1672
|
+
const access = await getMcpToolAccess(toolName, apiKey);
|
|
1673
|
+
const userTier = access.tier || 'free';
|
|
1674
|
+
const result = await handleToolV3(toolName, args, { tier: userTier });
|
|
1015
1675
|
|
|
1016
1676
|
if (result.error) {
|
|
1017
1677
|
return this.error(result.error, { tier: result.tier, required: result.required });
|
|
1018
1678
|
}
|
|
1019
1679
|
|
|
1680
|
+
// Sanitize and truncate output
|
|
1681
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1020
1682
|
return {
|
|
1021
|
-
content: [{ type: "text", text:
|
|
1683
|
+
content: [{ type: "text", text: outputText }],
|
|
1022
1684
|
};
|
|
1023
1685
|
}
|
|
1024
1686
|
|
|
1025
1687
|
// 1. Check tool registry first (local CLI handlers)
|
|
1026
|
-
if (this.toolRegistry[
|
|
1027
|
-
return await this.toolRegistry[
|
|
1688
|
+
if (this.toolRegistry[toolName]) {
|
|
1689
|
+
return await this.toolRegistry[toolName](projectPath, args);
|
|
1028
1690
|
}
|
|
1029
1691
|
|
|
1030
1692
|
// 2. Handle external module tools by prefix/pattern
|
|
1031
|
-
if (
|
|
1032
|
-
return await handleIntelligenceTool(
|
|
1693
|
+
if (toolName.startsWith("vibecheck.intelligence.")) {
|
|
1694
|
+
return await handleIntelligenceTool(toolName, args, __dirname);
|
|
1033
1695
|
}
|
|
1034
1696
|
|
|
1035
1697
|
// Handle AI vibecheck tools
|
|
1036
1698
|
if (["vibecheck.verify", "vibecheck.quality", "vibecheck.smells",
|
|
1037
1699
|
"vibecheck.hallucination", "vibecheck.breaking", "vibecheck.mdc",
|
|
1038
|
-
"vibecheck.coverage"].includes(
|
|
1039
|
-
const result = await handleVibecheckTool(
|
|
1700
|
+
"vibecheck.coverage"].includes(toolName)) {
|
|
1701
|
+
const result = await handleVibecheckTool(toolName, args);
|
|
1702
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1040
1703
|
return {
|
|
1041
|
-
content: [{ type: "text", text:
|
|
1704
|
+
content: [{ type: "text", text: outputText }],
|
|
1042
1705
|
};
|
|
1043
1706
|
}
|
|
1044
1707
|
|
|
1045
1708
|
// Handle agent checkpoint tools
|
|
1046
|
-
if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(
|
|
1047
|
-
return await handleCheckpointTool(
|
|
1709
|
+
if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(toolName)) {
|
|
1710
|
+
return await handleCheckpointTool(toolName, args);
|
|
1048
1711
|
}
|
|
1049
1712
|
|
|
1050
1713
|
// Handle architect tools
|
|
1051
1714
|
if (["vibecheck_architect_review", "vibecheck_architect_suggest",
|
|
1052
|
-
"vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(
|
|
1053
|
-
return await handleArchitectTool(
|
|
1715
|
+
"vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(toolName)) {
|
|
1716
|
+
return await handleArchitectTool(toolName, args);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// Handle authority system tools
|
|
1720
|
+
if (["authority.classify", "authority.approve", "authority.list"].includes(toolName)) {
|
|
1721
|
+
const result = await handleAuthorityTool(toolName, args, userTier || "free");
|
|
1722
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1723
|
+
return {
|
|
1724
|
+
content: [{ type: "text", text: outputText }],
|
|
1725
|
+
};
|
|
1054
1726
|
}
|
|
1055
1727
|
|
|
1056
1728
|
// Handle codebase architect tools
|
|
1057
1729
|
if (["vibecheck_architect_context", "vibecheck_architect_guide",
|
|
1058
1730
|
"vibecheck_architect_validate", "vibecheck_architect_patterns",
|
|
1059
|
-
"vibecheck_architect_dependencies"].includes(
|
|
1060
|
-
return await handleCodebaseArchitectTool(
|
|
1731
|
+
"vibecheck_architect_dependencies"].includes(toolName)) {
|
|
1732
|
+
return await handleCodebaseArchitectTool(toolName, args);
|
|
1061
1733
|
}
|
|
1062
1734
|
|
|
1063
1735
|
// Handle vibecheck 2.0 tools
|
|
1064
|
-
if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(
|
|
1065
|
-
return await handleVibecheck2Tool(
|
|
1736
|
+
if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(toolName)) {
|
|
1737
|
+
return await handleVibecheck2Tool(toolName, args, __dirname);
|
|
1066
1738
|
}
|
|
1067
1739
|
|
|
1068
1740
|
// Handle intent drift tools
|
|
1069
|
-
if (
|
|
1070
|
-
const tool = intentDriftTools.find(t => t.name ===
|
|
1741
|
+
if (toolName.startsWith("vibecheck_intent_")) {
|
|
1742
|
+
const tool = intentDriftTools.find(t => t.name === toolName);
|
|
1071
1743
|
if (tool && tool.handler) {
|
|
1072
1744
|
const result = await tool.handler(args);
|
|
1745
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1073
1746
|
return {
|
|
1074
|
-
content: [{ type: "text", text:
|
|
1747
|
+
content: [{ type: "text", text: outputText }],
|
|
1075
1748
|
};
|
|
1076
1749
|
}
|
|
1077
1750
|
}
|
|
1078
1751
|
|
|
1079
1752
|
// Handle MDC generator
|
|
1080
|
-
if (
|
|
1753
|
+
if (toolName === "generate_mdc") {
|
|
1081
1754
|
return await handleMDCGeneration(args);
|
|
1082
1755
|
}
|
|
1083
1756
|
|
|
1084
1757
|
// Handle Truth Context tools (Evidence Pack / Truth Pack)
|
|
1085
|
-
if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(
|
|
1086
|
-
return await handleTruthContextTool(
|
|
1758
|
+
if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(toolName)) {
|
|
1759
|
+
return await handleTruthContextTool(toolName, args);
|
|
1087
1760
|
}
|
|
1088
1761
|
|
|
1089
1762
|
// Handle Truth Firewall tools (Hallucination Stopper)
|
|
1090
1763
|
if (["vibecheck.get_truthpack", "vibecheck.validate_claim", "vibecheck.compile_context",
|
|
1091
1764
|
"vibecheck.search_evidence", "vibecheck.find_counterexamples", "vibecheck.propose_patch",
|
|
1092
|
-
"vibecheck.check_invariants", "vibecheck.add_assumption"].includes(
|
|
1093
|
-
return await handleTruthFirewallTool(
|
|
1765
|
+
"vibecheck.check_invariants", "vibecheck.add_assumption"].includes(toolName)) {
|
|
1766
|
+
return await handleTruthFirewallTool(toolName, args, projectPath);
|
|
1094
1767
|
}
|
|
1095
1768
|
|
|
1096
|
-
return this.error(`Unknown tool: ${
|
|
1769
|
+
return this.error(`Unknown tool: ${toolName}`, {
|
|
1770
|
+
code: 'UNKNOWN_TOOL',
|
|
1771
|
+
suggestion: 'Check the tool name and try again',
|
|
1772
|
+
nextSteps: ['Use vibecheck.status to see available tools'],
|
|
1773
|
+
});
|
|
1097
1774
|
} catch (err) {
|
|
1098
|
-
//
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1775
|
+
// ====================================================================
|
|
1776
|
+
// HARDENING: Enhanced error handling with sanitization
|
|
1777
|
+
// ====================================================================
|
|
1778
|
+
const durationMs = Date.now() - startTime;
|
|
1779
|
+
const errorMessage = sanitizeString(err?.message || 'Unknown error', 500);
|
|
1780
|
+
|
|
1781
|
+
// Emit audit event for tool error (safely)
|
|
1782
|
+
try {
|
|
1783
|
+
emitToolComplete(toolName, "error", {
|
|
1784
|
+
errorMessage: redactSensitive(errorMessage),
|
|
1785
|
+
durationMs,
|
|
1786
|
+
});
|
|
1787
|
+
} catch {
|
|
1788
|
+
// Audit logging should never break the response
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Log error details to stderr (not stdout - preserves MCP protocol)
|
|
1792
|
+
console.error(`[MCP] Tool ${toolName} failed after ${durationMs}ms: ${errorMessage}`);
|
|
1793
|
+
|
|
1794
|
+
return this.error(`${toolName} failed: ${redactSensitive(errorMessage)}`, {
|
|
1795
|
+
code: err?.code || 'TOOL_ERROR',
|
|
1796
|
+
suggestion: 'Check the error message and try again',
|
|
1797
|
+
nextSteps: [
|
|
1798
|
+
'Verify the tool arguments are correct',
|
|
1799
|
+
'Check that the project path is valid',
|
|
1800
|
+
'Try running with simpler arguments first',
|
|
1801
|
+
],
|
|
1102
1802
|
});
|
|
1103
|
-
return this.error(`${name} failed: ${err.message}`);
|
|
1104
1803
|
}
|
|
1105
1804
|
});
|
|
1106
1805
|
|
|
@@ -1161,15 +1860,41 @@ class VibecheckMCP {
|
|
|
1161
1860
|
this.server.setRequestHandler(
|
|
1162
1861
|
ReadResourceRequestSchema,
|
|
1163
1862
|
async (request) => {
|
|
1164
|
-
|
|
1863
|
+
// ====================================================================
|
|
1864
|
+
// HARDENING: Resource request validation
|
|
1865
|
+
// ====================================================================
|
|
1866
|
+
const uri = sanitizeString(request?.params?.uri, 200);
|
|
1867
|
+
if (!uri || !uri.startsWith('vibecheck://')) {
|
|
1868
|
+
return { contents: [{ uri: uri || '', mimeType: "application/json", text: '{"error": "Invalid resource URI"}' }] };
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1165
1871
|
const projectPath = process.cwd();
|
|
1872
|
+
|
|
1873
|
+
// Helper to safely read and return JSON resource
|
|
1874
|
+
const safeReadResource = async (filePath, defaultMessage) => {
|
|
1875
|
+
try {
|
|
1876
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1877
|
+
// Validate JSON and sanitize
|
|
1878
|
+
const parsed = safeJsonParse(content);
|
|
1879
|
+
if (!parsed.success) {
|
|
1880
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "Invalid JSON in resource file" }) }] };
|
|
1881
|
+
}
|
|
1882
|
+
// Redact any sensitive data and truncate
|
|
1883
|
+
const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
1884
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1885
|
+
} catch {
|
|
1886
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ message: defaultMessage }) }] };
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1166
1889
|
|
|
1167
1890
|
if (uri === "vibecheck://config") {
|
|
1168
1891
|
const configPath = path.join(projectPath, "vibecheck.config.json");
|
|
1169
1892
|
try {
|
|
1170
1893
|
const content = await fs.readFile(configPath, "utf-8");
|
|
1894
|
+
// Redact sensitive config values
|
|
1895
|
+
const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
1171
1896
|
return {
|
|
1172
|
-
contents: [{ uri, mimeType: "application/json", text:
|
|
1897
|
+
contents: [{ uri, mimeType: "application/json", text: sanitized }],
|
|
1173
1898
|
};
|
|
1174
1899
|
} catch {
|
|
1175
1900
|
return {
|
|
@@ -1179,141 +1904,161 @@ class VibecheckMCP {
|
|
|
1179
1904
|
}
|
|
1180
1905
|
|
|
1181
1906
|
if (uri === "vibecheck://summary") {
|
|
1182
|
-
const summaryPath = path.join(
|
|
1183
|
-
|
|
1184
|
-
".vibecheck",
|
|
1185
|
-
"summary.json",
|
|
1186
|
-
);
|
|
1187
|
-
try {
|
|
1188
|
-
const content = await fs.readFile(summaryPath, "utf-8");
|
|
1189
|
-
return {
|
|
1190
|
-
contents: [{ uri, mimeType: "application/json", text: content }],
|
|
1191
|
-
};
|
|
1192
|
-
} catch {
|
|
1193
|
-
return {
|
|
1194
|
-
contents: [
|
|
1195
|
-
{
|
|
1196
|
-
uri,
|
|
1197
|
-
mimeType: "application/json",
|
|
1198
|
-
text: '{"message": "No scan found. Run vibecheck.scan first."}',
|
|
1199
|
-
},
|
|
1200
|
-
],
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1907
|
+
const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
|
|
1908
|
+
return await safeReadResource(summaryPath, "No scan found. Run vibecheck.scan first.");
|
|
1203
1909
|
}
|
|
1204
1910
|
|
|
1205
1911
|
if (uri === "vibecheck://truthpack") {
|
|
1206
1912
|
const truthpackPath = path.join(projectPath, ".vibecheck", "truth", "truthpack.json");
|
|
1207
|
-
|
|
1208
|
-
const content = await fs.readFile(truthpackPath, "utf-8");
|
|
1209
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1210
|
-
} catch {
|
|
1211
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No truthpack. Run vibecheck.ctx first."}' }] };
|
|
1212
|
-
}
|
|
1913
|
+
return await safeReadResource(truthpackPath, "No truthpack. Run vibecheck.ctx first.");
|
|
1213
1914
|
}
|
|
1214
1915
|
|
|
1215
1916
|
if (uri === "vibecheck://missions") {
|
|
1216
1917
|
const missionsDir = path.join(projectPath, ".vibecheck", "missions");
|
|
1217
1918
|
try {
|
|
1919
|
+
// HARDENING: Validate directory read
|
|
1218
1920
|
const dirs = await fs.readdir(missionsDir);
|
|
1219
|
-
const
|
|
1921
|
+
const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
|
|
1922
|
+
const latest = safeDirs.sort().reverse()[0];
|
|
1923
|
+
|
|
1220
1924
|
if (latest) {
|
|
1221
1925
|
const missionPath = path.join(missionsDir, latest, "missions.json");
|
|
1222
|
-
|
|
1223
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1926
|
+
return await safeReadResource(missionPath, "No missions found in latest directory.");
|
|
1224
1927
|
}
|
|
1225
|
-
} catch {
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
console.error(`[MCP] Error reading missions: ${err.message}`);
|
|
1930
|
+
}
|
|
1226
1931
|
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No missions. Run vibecheck.fix first."}' }] };
|
|
1227
1932
|
}
|
|
1228
1933
|
|
|
1229
1934
|
if (uri === "vibecheck://reality") {
|
|
1230
1935
|
const realityPath = path.join(projectPath, ".vibecheck", "reality", "last_reality.json");
|
|
1231
|
-
|
|
1232
|
-
const content = await fs.readFile(realityPath, "utf-8");
|
|
1233
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1234
|
-
} catch {
|
|
1235
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No reality results. Run vibecheck verify first."}' }] };
|
|
1236
|
-
}
|
|
1936
|
+
return await safeReadResource(realityPath, "No reality results. Run vibecheck verify first.");
|
|
1237
1937
|
}
|
|
1238
1938
|
|
|
1239
1939
|
if (uri === "vibecheck://findings") {
|
|
1240
1940
|
const findingsPath = path.join(projectPath, ".vibecheck", "findings.json");
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1941
|
+
|
|
1942
|
+
// Try primary findings file
|
|
1943
|
+
const content = await this.safeReadFile(findingsPath, 10 * 1024 * 1024);
|
|
1944
|
+
if (content) {
|
|
1945
|
+
const parsed = safeJsonParse(content);
|
|
1946
|
+
if (parsed.success) {
|
|
1947
|
+
const sanitized = redactSensitive(truncateOutput(content));
|
|
1948
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// HARDENING: Try summary.json as fallback with size limits
|
|
1953
|
+
const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
|
|
1954
|
+
const summaryContent = await this.safeReadFile(summaryPath, 10 * 1024 * 1024);
|
|
1955
|
+
|
|
1956
|
+
if (summaryContent) {
|
|
1957
|
+
const parsed = safeJsonParse(summaryContent);
|
|
1958
|
+
if (parsed.success && parsed.data.findings) {
|
|
1959
|
+
const findings = sanitizeArray(parsed.data.findings, 1000);
|
|
1250
1960
|
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ findings }, null, 2) }] };
|
|
1251
|
-
} catch {
|
|
1252
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
|
|
1253
1961
|
}
|
|
1254
1962
|
}
|
|
1963
|
+
|
|
1964
|
+
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
|
|
1255
1965
|
}
|
|
1256
1966
|
|
|
1257
1967
|
if (uri === "vibecheck://share") {
|
|
1258
1968
|
const missionsDir = path.join(projectPath, ".vibecheck", "missions");
|
|
1259
1969
|
try {
|
|
1970
|
+
// HARDENING: Safe directory read
|
|
1260
1971
|
const dirs = await fs.readdir(missionsDir);
|
|
1261
|
-
const
|
|
1972
|
+
const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
|
|
1973
|
+
const latest = safeDirs.sort().reverse()[0];
|
|
1974
|
+
|
|
1262
1975
|
if (latest) {
|
|
1263
1976
|
const sharePath = path.join(missionsDir, latest, "share", "share.json");
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1977
|
+
|
|
1978
|
+
// Try share.json first
|
|
1979
|
+
const shareContent = await this.safeReadFile(sharePath, 10 * 1024 * 1024);
|
|
1980
|
+
if (shareContent) {
|
|
1981
|
+
const parsed = safeJsonParse(shareContent);
|
|
1982
|
+
if (parsed.success) {
|
|
1983
|
+
const sanitized = redactSensitive(truncateOutput(shareContent));
|
|
1984
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1985
|
+
}
|
|
1272
1986
|
}
|
|
1987
|
+
|
|
1988
|
+
// HARDENING: Fallback to missions.json with safe read
|
|
1989
|
+
const missionPath = path.join(missionsDir, latest, "missions.json");
|
|
1990
|
+
return await safeReadResource(missionPath, "No share data available in latest mission.");
|
|
1273
1991
|
}
|
|
1274
|
-
} catch {
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
console.error(`[MCP] Error reading share pack: ${err.message}`);
|
|
1994
|
+
}
|
|
1275
1995
|
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No share pack. Run vibecheck.fix --share first."}' }] };
|
|
1276
1996
|
}
|
|
1277
1997
|
|
|
1278
1998
|
if (uri === "vibecheck://prove") {
|
|
1279
1999
|
const provePath = path.join(projectPath, ".vibecheck", "prove", "last_prove.json");
|
|
1280
|
-
|
|
1281
|
-
const content = await fs.readFile(provePath, "utf-8");
|
|
1282
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1283
|
-
} catch {
|
|
1284
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No prove results. Run vibecheck.prove first."}' }] };
|
|
1285
|
-
}
|
|
2000
|
+
return await safeReadResource(provePath, "No prove results. Run vibecheck.prove first.");
|
|
1286
2001
|
}
|
|
1287
2002
|
|
|
1288
|
-
return {
|
|
2003
|
+
return {
|
|
2004
|
+
contents: [{
|
|
2005
|
+
uri,
|
|
2006
|
+
mimeType: "application/json",
|
|
2007
|
+
text: JSON.stringify({ error: "Unknown resource URI" })
|
|
2008
|
+
}]
|
|
2009
|
+
};
|
|
1289
2010
|
},
|
|
1290
2011
|
);
|
|
1291
2012
|
}
|
|
1292
2013
|
|
|
1293
|
-
//
|
|
2014
|
+
// ============================================================================
|
|
2015
|
+
// HARDENED HELPERS
|
|
2016
|
+
// ============================================================================
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Return a successful response with sanitization
|
|
2020
|
+
* @param {string} text - Response text
|
|
2021
|
+
* @param {boolean} includeAttribution - Include vibecheck attribution
|
|
2022
|
+
* @returns {object} MCP response
|
|
2023
|
+
*/
|
|
1294
2024
|
success(text, includeAttribution = true) {
|
|
2025
|
+
// Sanitize output: redact secrets and truncate
|
|
2026
|
+
let sanitized = redactSensitive(sanitizeString(text, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
2027
|
+
|
|
1295
2028
|
const finalText = includeAttribution
|
|
1296
|
-
? `${
|
|
1297
|
-
:
|
|
2029
|
+
? `${sanitized}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
|
|
2030
|
+
: sanitized;
|
|
2031
|
+
|
|
1298
2032
|
return { content: [{ type: "text", text: finalText }] };
|
|
1299
2033
|
}
|
|
1300
2034
|
|
|
2035
|
+
/**
|
|
2036
|
+
* Return an error response with sanitization
|
|
2037
|
+
* @param {string} text - Error message
|
|
2038
|
+
* @param {object} options - Additional options
|
|
2039
|
+
* @returns {object} MCP error response
|
|
2040
|
+
*/
|
|
1301
2041
|
error(text, options = {}) {
|
|
1302
2042
|
const { code, suggestion, nextSteps = [] } = options;
|
|
1303
2043
|
|
|
1304
|
-
|
|
2044
|
+
// Sanitize all text inputs
|
|
2045
|
+
const sanitizedText = redactSensitive(sanitizeString(text, 1000));
|
|
2046
|
+
const sanitizedSuggestion = suggestion ? redactSensitive(sanitizeString(suggestion, 500)) : null;
|
|
2047
|
+
const sanitizedSteps = sanitizeArray(nextSteps, 10).map(s => sanitizeString(s, 200));
|
|
2048
|
+
|
|
2049
|
+
let errorText = `❌ ${sanitizedText}`;
|
|
1305
2050
|
|
|
1306
2051
|
if (code) {
|
|
1307
|
-
errorText += `\n\n**Error Code:** \`${code}\``;
|
|
2052
|
+
errorText += `\n\n**Error Code:** \`${sanitizeString(code, 50)}\``;
|
|
1308
2053
|
}
|
|
1309
2054
|
|
|
1310
|
-
if (
|
|
1311
|
-
errorText += `\n\n💡 **Suggestion:** ${
|
|
2055
|
+
if (sanitizedSuggestion) {
|
|
2056
|
+
errorText += `\n\n💡 **Suggestion:** ${sanitizedSuggestion}`;
|
|
1312
2057
|
}
|
|
1313
2058
|
|
|
1314
|
-
if (
|
|
2059
|
+
if (sanitizedSteps.length > 0) {
|
|
1315
2060
|
errorText += `\n\n**Next Steps:**\n`;
|
|
1316
|
-
|
|
2061
|
+
sanitizedSteps.forEach((step, i) => {
|
|
1317
2062
|
errorText += `${i + 1}. ${step}\n`;
|
|
1318
2063
|
});
|
|
1319
2064
|
}
|
|
@@ -1366,28 +2111,53 @@ class VibecheckMCP {
|
|
|
1366
2111
|
});
|
|
1367
2112
|
}
|
|
1368
2113
|
|
|
1369
|
-
|
|
1370
|
-
const
|
|
2114
|
+
// HARDENING: Validate and sanitize profile
|
|
2115
|
+
const validProfiles = ["quick", "full", "ship", "ci", "security", "compliance", "ai"];
|
|
2116
|
+
const profile = validProfiles.includes(args?.profile) ? args?.profile : "quick";
|
|
2117
|
+
|
|
2118
|
+
// HARDENING: Sanitize only array
|
|
2119
|
+
const only = sanitizeArray(args?.only, 20).map(item => sanitizeString(item, 50));
|
|
1371
2120
|
|
|
1372
|
-
// Initialize API integration
|
|
2121
|
+
// Initialize API integration with timeout and circuit breaker
|
|
1373
2122
|
let apiScan = null;
|
|
1374
2123
|
let apiConnected = false;
|
|
1375
2124
|
|
|
2125
|
+
// HARDENING: Check circuit breaker before attempting API calls
|
|
2126
|
+
const circuitCheck = checkCircuitBreaker();
|
|
2127
|
+
|
|
1376
2128
|
// Try to connect to API for dashboard integration
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
2129
|
+
if (circuitCheck.allowed) {
|
|
2130
|
+
try {
|
|
2131
|
+
// HARDENING: Add timeout to API availability check
|
|
2132
|
+
const apiCheckPromise = isApiAvailable();
|
|
2133
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
2134
|
+
setTimeout(() => reject(new Error('API check timeout')), 5000)
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
apiConnected = await Promise.race([apiCheckPromise, timeoutPromise]);
|
|
2138
|
+
|
|
2139
|
+
if (apiConnected) {
|
|
2140
|
+
// Create scan record in dashboard
|
|
2141
|
+
const createScanPromise = createScan({
|
|
2142
|
+
localPath: sanitizeString(projectPath, 500),
|
|
2143
|
+
branch: sanitizeString(args?.branch, 100) || 'main',
|
|
2144
|
+
enableLLM: false,
|
|
2145
|
+
});
|
|
2146
|
+
const scanTimeoutPromise = new Promise((_, reject) =>
|
|
2147
|
+
setTimeout(() => reject(new Error('Create scan timeout')), 10000)
|
|
2148
|
+
);
|
|
2149
|
+
|
|
2150
|
+
apiScan = await Promise.race([createScanPromise, scanTimeoutPromise]);
|
|
2151
|
+
console.error(`[MCP] Connected to dashboard (Scan ID: ${apiScan.scanId})`);
|
|
2152
|
+
recordApiResult(true); // Record success
|
|
2153
|
+
}
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
// API connection is optional, continue without it
|
|
2156
|
+
console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
|
|
2157
|
+
recordApiResult(false); // Record failure
|
|
1387
2158
|
}
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
|
|
2159
|
+
} else {
|
|
2160
|
+
console.error(`[MCP] ${circuitCheck.reason}`);
|
|
1391
2161
|
}
|
|
1392
2162
|
|
|
1393
2163
|
let output = "# 🔍 vibecheck Scan\n\n";
|
|
@@ -1396,7 +2166,9 @@ class VibecheckMCP {
|
|
|
1396
2166
|
|
|
1397
2167
|
// Build CLI arguments array (secure - no injection possible)
|
|
1398
2168
|
const cliArgs = [`--profile=${profile}`, "--json"];
|
|
1399
|
-
if (only
|
|
2169
|
+
if (only.length > 0) {
|
|
2170
|
+
cliArgs.push(`--only=${only.join(",")}`);
|
|
2171
|
+
}
|
|
1400
2172
|
|
|
1401
2173
|
try {
|
|
1402
2174
|
await this.runCLI("scan", cliArgs, projectPath, { timeout: CONFIG.TIMEOUTS.SCAN });
|
|
@@ -1409,19 +2181,25 @@ class VibecheckMCP {
|
|
|
1409
2181
|
// Submit results to dashboard if connected
|
|
1410
2182
|
if (apiConnected && apiScan) {
|
|
1411
2183
|
try {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2184
|
+
// HARDENING: Add timeout to result submission
|
|
2185
|
+
const submitPromise = submitScanResults(apiScan.scanId, {
|
|
2186
|
+
verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
|
|
2187
|
+
score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
|
|
2188
|
+
findings: sanitizeArray(summary.findings, 1000) || [],
|
|
2189
|
+
filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
|
|
2190
|
+
linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
|
|
2191
|
+
durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
|
|
1419
2192
|
metadata: {
|
|
1420
2193
|
profile,
|
|
1421
2194
|
source: 'mcp-server',
|
|
1422
2195
|
version: CONFIG.VERSION,
|
|
1423
2196
|
},
|
|
1424
2197
|
});
|
|
2198
|
+
const submitTimeout = new Promise((_, reject) =>
|
|
2199
|
+
setTimeout(() => reject(new Error('Submit timeout')), 10000)
|
|
2200
|
+
);
|
|
2201
|
+
|
|
2202
|
+
await Promise.race([submitPromise, submitTimeout]);
|
|
1425
2203
|
console.error(`[MCP] Results sent to dashboard`);
|
|
1426
2204
|
} catch (err) {
|
|
1427
2205
|
console.error(`[MCP] Failed to send results to dashboard: ${err.message}`);
|
|
@@ -1439,23 +2217,29 @@ class VibecheckMCP {
|
|
|
1439
2217
|
// Submit results to dashboard if connected
|
|
1440
2218
|
if (apiConnected && apiScan) {
|
|
1441
2219
|
try {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2220
|
+
// HARDENING: Add timeout to error case submission
|
|
2221
|
+
const submitPromise = submitScanResults(apiScan.scanId, {
|
|
2222
|
+
verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
|
|
2223
|
+
score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
|
|
2224
|
+
findings: sanitizeArray(summary.findings, 1000) || [],
|
|
2225
|
+
filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
|
|
2226
|
+
linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
|
|
2227
|
+
durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
|
|
1449
2228
|
metadata: {
|
|
1450
2229
|
profile,
|
|
1451
2230
|
source: 'mcp-server',
|
|
1452
2231
|
version: CONFIG.VERSION,
|
|
1453
|
-
error: err.message,
|
|
2232
|
+
error: sanitizeString(err.message, 500),
|
|
1454
2233
|
},
|
|
1455
2234
|
});
|
|
2235
|
+
const submitTimeout = new Promise((_, reject) =>
|
|
2236
|
+
setTimeout(() => reject(new Error('Submit timeout')), 10000)
|
|
2237
|
+
);
|
|
2238
|
+
|
|
2239
|
+
await Promise.race([submitPromise, submitTimeout]);
|
|
1456
2240
|
console.error(`[MCP] Results sent to dashboard (with error)`);
|
|
1457
|
-
} catch (
|
|
1458
|
-
console.error(`[MCP] Failed to send results to dashboard: ${
|
|
2241
|
+
} catch (apiErr) {
|
|
2242
|
+
console.error(`[MCP] Failed to send results to dashboard: ${apiErr.message}`);
|
|
1459
2243
|
}
|
|
1460
2244
|
}
|
|
1461
2245
|
output += `\n⚠️ Scan completed with findings (exit code ${err.code || 1})\n`;
|
|
@@ -1465,10 +2249,16 @@ class VibecheckMCP {
|
|
|
1465
2249
|
// Report error to dashboard if connected
|
|
1466
2250
|
if (apiConnected && apiScan) {
|
|
1467
2251
|
try {
|
|
1468
|
-
|
|
2252
|
+
// HARDENING: Add timeout to error reporting
|
|
2253
|
+
const reportPromise = reportScanError(apiScan.scanId, err);
|
|
2254
|
+
const reportTimeout = new Promise((_, reject) =>
|
|
2255
|
+
setTimeout(() => reject(new Error('Report timeout')), 10000)
|
|
2256
|
+
);
|
|
2257
|
+
|
|
2258
|
+
await Promise.race([reportPromise, reportTimeout]);
|
|
1469
2259
|
console.error(`[MCP] Error reported to dashboard`);
|
|
1470
|
-
} catch (
|
|
1471
|
-
console.error(`[MCP] Failed to report error to dashboard: ${
|
|
2260
|
+
} catch (apiErr) {
|
|
2261
|
+
console.error(`[MCP] Failed to report error to dashboard: ${apiErr.message}`);
|
|
1472
2262
|
}
|
|
1473
2263
|
}
|
|
1474
2264
|
|
|
@@ -1490,7 +2280,7 @@ class VibecheckMCP {
|
|
|
1490
2280
|
// ============================================================================
|
|
1491
2281
|
async handleGate(projectPath, args) {
|
|
1492
2282
|
// Check tier access (STARTER tier required)
|
|
1493
|
-
const access = await
|
|
2283
|
+
const access = await getFeatureAccessStatus("gate", args?.apiKey);
|
|
1494
2284
|
if (!access.hasAccess) {
|
|
1495
2285
|
return {
|
|
1496
2286
|
content: [{
|
|
@@ -1530,7 +2320,7 @@ class VibecheckMCP {
|
|
|
1530
2320
|
async handleFix(projectPath, args) {
|
|
1531
2321
|
// Check tier access for --apply and --autopilot (PRO tier required)
|
|
1532
2322
|
if (args?.apply || args?.autopilot) {
|
|
1533
|
-
const access = await
|
|
2323
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
1534
2324
|
if (!access.hasAccess) {
|
|
1535
2325
|
return {
|
|
1536
2326
|
content: [{
|
|
@@ -1705,7 +2495,7 @@ class VibecheckMCP {
|
|
|
1705
2495
|
// ============================================================================
|
|
1706
2496
|
async handleProve(projectPath, args) {
|
|
1707
2497
|
// Check tier access (PRO tier required)
|
|
1708
|
-
const access = await
|
|
2498
|
+
const access = await getFeatureAccessStatus("prove", args?.apiKey);
|
|
1709
2499
|
if (!access.hasAccess) {
|
|
1710
2500
|
return {
|
|
1711
2501
|
content: [{
|
|
@@ -1908,6 +2698,16 @@ class VibecheckMCP {
|
|
|
1908
2698
|
// SHIP - Quick health check
|
|
1909
2699
|
// ============================================================================
|
|
1910
2700
|
async handleShip(projectPath, args) {
|
|
2701
|
+
// HARDENING: Validate project path
|
|
2702
|
+
const validation = this.validateProjectPath(projectPath);
|
|
2703
|
+
if (!validation.valid) {
|
|
2704
|
+
return this.error(validation.error, {
|
|
2705
|
+
code: validation.code || "INVALID_PATH",
|
|
2706
|
+
suggestion: validation.suggestion,
|
|
2707
|
+
nextSteps: validation.nextSteps || [],
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
|
|
1911
2711
|
let output = "# 🚀 vibecheck Ship\n\n";
|
|
1912
2712
|
output += `**Path:** ${projectPath}\n\n`;
|
|
1913
2713
|
|
|
@@ -1932,20 +2732,34 @@ class VibecheckMCP {
|
|
|
1932
2732
|
// VERIFY - Runtime browser testing
|
|
1933
2733
|
// ============================================================================
|
|
1934
2734
|
async handleVerify(projectPath, args) {
|
|
1935
|
-
|
|
1936
|
-
|
|
2735
|
+
// HARDENING: Validate URL
|
|
2736
|
+
const urlValidation = validateUrl(args?.url);
|
|
2737
|
+
if (!urlValidation.valid) {
|
|
2738
|
+
return this.error(urlValidation.error, {
|
|
2739
|
+
code: 'INVALID_URL',
|
|
2740
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2741
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
const url = urlValidation.url;
|
|
1937
2745
|
|
|
1938
2746
|
let output = "# 🧪 vibecheck Verify\n\n";
|
|
1939
2747
|
output += `**URL:** ${url}\n`;
|
|
1940
|
-
|
|
2748
|
+
|
|
2749
|
+
// HARDENING: Sanitize array inputs
|
|
2750
|
+
const flows = sanitizeArray(args?.flows, 10);
|
|
2751
|
+
if (flows.length) output += `**Flows:** ${flows.join(", ")}\n`;
|
|
1941
2752
|
if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
|
|
1942
2753
|
if (args?.record) output += `**Recording:** Enabled\n`;
|
|
1943
2754
|
output += "\n";
|
|
1944
2755
|
|
|
1945
2756
|
// Build CLI arguments array (secure)
|
|
1946
2757
|
const cliArgs = ["--url", url];
|
|
1947
|
-
|
|
1948
|
-
if (args?.
|
|
2758
|
+
// HARDENING: Sanitize auth - don't log full credentials
|
|
2759
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2760
|
+
cliArgs.push("--auth", sanitizeString(args.auth, 200));
|
|
2761
|
+
}
|
|
2762
|
+
if (flows.length) cliArgs.push("--flows", flows.join(","));
|
|
1949
2763
|
if (args?.headed) cliArgs.push("--headed");
|
|
1950
2764
|
if (args?.record) cliArgs.push("--record");
|
|
1951
2765
|
|
|
@@ -1995,28 +2809,67 @@ class VibecheckMCP {
|
|
|
1995
2809
|
// REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
|
|
1996
2810
|
// ============================================================================
|
|
1997
2811
|
async handleReality(projectPath, args) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2812
|
+
// HARDENING: Validate URL
|
|
2813
|
+
const urlValidation = validateUrl(args?.url);
|
|
2814
|
+
if (!urlValidation.valid) {
|
|
2815
|
+
return this.error(urlValidation.error, {
|
|
2816
|
+
code: 'INVALID_URL',
|
|
2817
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2818
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
const url = urlValidation.url;
|
|
2000
2822
|
|
|
2001
2823
|
let output = "# 🧪 vibecheck Reality v2\n\n";
|
|
2002
2824
|
output += `**URL:** ${url}\n`;
|
|
2003
2825
|
output += `**Two-Pass Auth:** ${args?.verifyAuth ? "Yes" : "No"}\n`;
|
|
2004
|
-
|
|
2005
|
-
|
|
2826
|
+
|
|
2827
|
+
// HARDENING: Safely display auth info (mask password)
|
|
2828
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2829
|
+
const authParts = args.auth.split(":");
|
|
2830
|
+
const maskedAuth = authParts[0] ? `${authParts[0].slice(0, 20)}:***` : '***';
|
|
2831
|
+
output += `**Auth:** ${maskedAuth}\n`;
|
|
2832
|
+
}
|
|
2833
|
+
if (args?.storageState) output += `**Storage State:** ${sanitizeString(args.storageState, 100)}\n`;
|
|
2006
2834
|
if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
|
|
2007
2835
|
if (args?.danger) output += `**Danger Mode:** Enabled (risky clicks allowed)\n`;
|
|
2008
2836
|
output += "\n";
|
|
2009
2837
|
|
|
2010
2838
|
// Build CLI arguments array (secure)
|
|
2011
2839
|
const cliArgs = ["--url", url];
|
|
2012
|
-
if (args?.auth
|
|
2840
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2841
|
+
cliArgs.push("--auth", sanitizeString(args.auth, 200));
|
|
2842
|
+
}
|
|
2013
2843
|
if (args?.verifyAuth) cliArgs.push("--verify-auth");
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
if (args?.
|
|
2844
|
+
|
|
2845
|
+
// HARDENING: Validate path arguments
|
|
2846
|
+
if (args?.storageState) {
|
|
2847
|
+
const pathCheck = sanitizePath(args.storageState, projectPath);
|
|
2848
|
+
if (pathCheck.valid) {
|
|
2849
|
+
cliArgs.push("--storage-state", pathCheck.path);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
if (args?.saveStorageState) {
|
|
2853
|
+
const pathCheck = sanitizePath(args.saveStorageState, projectPath);
|
|
2854
|
+
if (pathCheck.valid) {
|
|
2855
|
+
cliArgs.push("--save-storage-state", pathCheck.path);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (args?.truthpack) {
|
|
2859
|
+
const pathCheck = sanitizePath(args.truthpack, projectPath);
|
|
2860
|
+
if (pathCheck.valid) {
|
|
2861
|
+
cliArgs.push("--truthpack", pathCheck.path);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2017
2864
|
if (args?.headed) cliArgs.push("--headed");
|
|
2018
|
-
|
|
2019
|
-
|
|
2865
|
+
|
|
2866
|
+
// HARDENING: Bound numeric arguments
|
|
2867
|
+
if (args?.maxPages) {
|
|
2868
|
+
cliArgs.push("--max-pages", String(sanitizeNumber(args.maxPages, 1, 100, 18)));
|
|
2869
|
+
}
|
|
2870
|
+
if (args?.maxDepth) {
|
|
2871
|
+
cliArgs.push("--max-depth", String(sanitizeNumber(args.maxDepth, 1, 10, 2)));
|
|
2872
|
+
}
|
|
2020
2873
|
if (args?.danger) cliArgs.push("--danger");
|
|
2021
2874
|
|
|
2022
2875
|
try {
|
|
@@ -2097,16 +2950,27 @@ class VibecheckMCP {
|
|
|
2097
2950
|
// AI-TEST - AI Agent testing
|
|
2098
2951
|
// ============================================================================
|
|
2099
2952
|
async handleAITest(projectPath, args) {
|
|
2100
|
-
|
|
2101
|
-
|
|
2953
|
+
// HARDENING: Validate URL
|
|
2954
|
+
const urlValidation = validateUrl(args?.url);
|
|
2955
|
+
if (!urlValidation.valid) {
|
|
2956
|
+
return this.error(urlValidation.error, {
|
|
2957
|
+
code: 'INVALID_URL',
|
|
2958
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2959
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
const url = urlValidation.url;
|
|
2963
|
+
|
|
2964
|
+
// HARDENING: Sanitize goal string
|
|
2965
|
+
const goal = sanitizeString(args?.goal, 500) || "Test all features";
|
|
2102
2966
|
|
|
2103
2967
|
let output = "# 🤖 vibecheck AI Agent\n\n";
|
|
2104
2968
|
output += `**URL:** ${url}\n`;
|
|
2105
|
-
output += `**Goal:** ${
|
|
2969
|
+
output += `**Goal:** ${goal}\n\n`;
|
|
2106
2970
|
|
|
2107
2971
|
// Build CLI arguments array (secure)
|
|
2108
2972
|
const cliArgs = ["--url", url];
|
|
2109
|
-
if (
|
|
2973
|
+
if (goal) cliArgs.push("--goal", goal);
|
|
2110
2974
|
if (args?.headed) cliArgs.push("--headed");
|
|
2111
2975
|
|
|
2112
2976
|
try {
|
|
@@ -2162,7 +3026,7 @@ class VibecheckMCP {
|
|
|
2162
3026
|
// ============================================================================
|
|
2163
3027
|
async handleAutopilotPlan(projectPath, args) {
|
|
2164
3028
|
// Check tier access (PRO tier required)
|
|
2165
|
-
const access = await
|
|
3029
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
2166
3030
|
if (!access.hasAccess) {
|
|
2167
3031
|
return {
|
|
2168
3032
|
content: [{
|
|
@@ -2249,7 +3113,7 @@ class VibecheckMCP {
|
|
|
2249
3113
|
// ============================================================================
|
|
2250
3114
|
async handleAutopilotApply(projectPath, args) {
|
|
2251
3115
|
// Check tier access (PRO tier required)
|
|
2252
|
-
const access = await
|
|
3116
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
2253
3117
|
if (!access.hasAccess) {
|
|
2254
3118
|
return {
|
|
2255
3119
|
content: [{
|
|
@@ -2310,7 +3174,7 @@ class VibecheckMCP {
|
|
|
2310
3174
|
// ============================================================================
|
|
2311
3175
|
async handleBadge(projectPath, args) {
|
|
2312
3176
|
// Check tier access (STARTER tier required)
|
|
2313
|
-
const access = await
|
|
3177
|
+
const access = await getFeatureAccessStatus("badge", args?.apiKey);
|
|
2314
3178
|
if (!access.hasAccess) {
|
|
2315
3179
|
return {
|
|
2316
3180
|
content: [{
|
|
@@ -2466,15 +3330,57 @@ class VibecheckMCP {
|
|
|
2466
3330
|
}
|
|
2467
3331
|
|
|
2468
3332
|
// ============================================================================
|
|
2469
|
-
// RUN
|
|
3333
|
+
// RUN - with graceful shutdown handling
|
|
2470
3334
|
// ============================================================================
|
|
2471
3335
|
async run() {
|
|
2472
3336
|
const transport = new StdioServerTransport();
|
|
3337
|
+
|
|
3338
|
+
// ========================================================================
|
|
3339
|
+
// HARDENING: Graceful shutdown handling
|
|
3340
|
+
// ========================================================================
|
|
3341
|
+
const shutdown = async (signal) => {
|
|
3342
|
+
console.error(`\n[MCP] Received ${signal}, shutting down gracefully...`);
|
|
3343
|
+
try {
|
|
3344
|
+
// Clear rate limit state to prevent memory leaks
|
|
3345
|
+
rateLimitState.calls = [];
|
|
3346
|
+
|
|
3347
|
+
// Close server connection
|
|
3348
|
+
await this.server.close();
|
|
3349
|
+
console.error('[MCP] Server closed successfully');
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
console.error(`[MCP] Error during shutdown: ${err.message}`);
|
|
3352
|
+
}
|
|
3353
|
+
process.exit(0);
|
|
3354
|
+
};
|
|
3355
|
+
|
|
3356
|
+
// Handle termination signals
|
|
3357
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
3358
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
3359
|
+
|
|
3360
|
+
// Handle uncaught errors gracefully
|
|
3361
|
+
process.on('uncaughtException', (err) => {
|
|
3362
|
+
console.error(`[MCP] Uncaught exception: ${err.message}`);
|
|
3363
|
+
console.error(err.stack);
|
|
3364
|
+
// Don't exit - try to keep running
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
3368
|
+
console.error(`[MCP] Unhandled rejection at:`, promise);
|
|
3369
|
+
console.error(`[MCP] Reason:`, reason);
|
|
3370
|
+
// Don't exit - try to keep running
|
|
3371
|
+
});
|
|
3372
|
+
|
|
2473
3373
|
await this.server.connect(transport);
|
|
2474
|
-
console.error(
|
|
3374
|
+
console.error(`vibecheck MCP Server v${VERSION} running on stdio (hardened)`);
|
|
2475
3375
|
}
|
|
2476
3376
|
}
|
|
2477
3377
|
|
|
2478
|
-
//
|
|
3378
|
+
// ============================================================================
|
|
3379
|
+
// MAIN - with error handling
|
|
3380
|
+
// ============================================================================
|
|
2479
3381
|
const server = new VibecheckMCP();
|
|
2480
|
-
server.run().catch(
|
|
3382
|
+
server.run().catch((err) => {
|
|
3383
|
+
console.error(`[MCP] Fatal error starting server: ${err.message}`);
|
|
3384
|
+
console.error(err.stack);
|
|
3385
|
+
process.exit(1);
|
|
3386
|
+
});
|