@vibecheckai/cli 3.2.6 → 3.4.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 +306 -90
- 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 +136 -141
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/entitlements-v2.js +96 -505
- 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/scan-output.js +18 -19
- package/bin/runners/lib/ship-output.js +18 -25
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +105 -205
- 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 +77 -45
- 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 +103 -21
- 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 +1152 -856
- package/mcp-server/lib/api-client.cjs +13 -0
- 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 +194 -383
- package/mcp-server/tools-v3.js +495 -533
- 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/lib/api-client.js +0 -269
- 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
|
+
],
|
|
98
|
+
};
|
|
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
|
|
67
382
|
};
|
|
68
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
|
|
|
@@ -233,628 +650,23 @@ function checkTruthFirewallBlock(toolName, args, projectPath) {
|
|
|
233
650
|
// TOOL DEFINITIONS - Public Tools (Clean Product Surface)
|
|
234
651
|
// ============================================================================
|
|
235
652
|
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
653
|
+
// ============================================================================
|
|
654
|
+
// TOOL REGISTRATION - V3 Tools Only (2-tier: FREE / PRO)
|
|
655
|
+
// ============================================================================
|
|
656
|
+
// V3 tools are the canonical tool surface with 2-tier model:
|
|
657
|
+
// - FREE (10 tools): Inspect & Observe
|
|
658
|
+
// - PRO (15 tools): Fix, Prove & Enforce (includes Authority, Conductor, Firewall)
|
|
241
659
|
|
|
242
|
-
const TOOLS =
|
|
243
|
-
//
|
|
660
|
+
const TOOLS = [
|
|
661
|
+
// V3 tools include all FREE and PRO tools
|
|
662
|
+
// Authority, Conductor, and Agent Firewall are included in MCP_TOOLS_V3
|
|
244
663
|
...MCP_TOOLS_V3,
|
|
245
|
-
|
|
246
|
-
] : USE_CONSOLIDATED_TOOLS ? [
|
|
247
|
-
// Curated tools for agents (legacy)
|
|
248
|
-
...CONSOLIDATED_TOOLS,
|
|
249
|
-
AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
|
|
250
|
-
] : [
|
|
251
|
-
// Legacy: Full tool set (50+ tools) - for backward compatibility
|
|
252
|
-
// PRIORITY: Agent Firewall - intercepts ALL file writes
|
|
253
|
-
AGENT_FIREWALL_TOOL,
|
|
254
|
-
// PRIORITY: Truth Firewall tools (Hallucination Stopper) - agents MUST use these
|
|
255
|
-
...TRUTH_FIREWALL_TOOLS, // vibecheck.get_truthpack, vibecheck.validate_claim, vibecheck.compile_context, etc.
|
|
256
|
-
|
|
257
|
-
// Truth Context tools (Evidence-Backed AI)
|
|
258
|
-
...TRUTH_CONTEXT_TOOLS, // vibecheck.ctx, vibecheck.verify_claim, vibecheck.evidence
|
|
259
|
-
|
|
260
|
-
...INTELLIGENCE_TOOLS, // Add all intelligence suite tools
|
|
261
|
-
...VIBECHECK_TOOLS, // Add AI vibecheck tools (verify, quality, smells, etc.)
|
|
262
|
-
...AGENT_CHECKPOINT_TOOLS, // Add agent checkpoint tools
|
|
263
|
-
...ARCHITECT_TOOLS, // Add architect review/suggest tools
|
|
264
|
-
...CODEBASE_ARCHITECT_TOOLS, // Add codebase-aware architect tools
|
|
265
|
-
...VIBECHECK_2_TOOLS, // Add vibecheck 2.0 consolidated tools
|
|
266
|
-
...intentDriftTools, // Add intent drift guard tools
|
|
267
|
-
mdcGeneratorTool, // Add MDC generator tool
|
|
268
|
-
// 1. SHIP - Quick health check (vibe coder friendly)
|
|
269
|
-
{
|
|
270
|
-
name: "vibecheck.ship",
|
|
271
|
-
description:
|
|
272
|
-
"🚀 Quick health check — 'Is my app ready?' Plain English, traffic light score",
|
|
273
|
-
inputSchema: {
|
|
274
|
-
type: "object",
|
|
275
|
-
properties: {
|
|
276
|
-
projectPath: {
|
|
277
|
-
type: "string",
|
|
278
|
-
description: "Path to project root",
|
|
279
|
-
default: ".",
|
|
280
|
-
},
|
|
281
|
-
fix: {
|
|
282
|
-
type: "boolean",
|
|
283
|
-
description: "Auto-fix problems where possible",
|
|
284
|
-
default: false,
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
},
|
|
664
|
+
].filter(t => t !== null);
|
|
289
665
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
name: "vibecheck.scan",
|
|
293
|
-
description:
|
|
294
|
-
"🔍 Deep scan — technical analysis of secrets, auth, mocks, routes (detailed output)",
|
|
295
|
-
inputSchema: {
|
|
296
|
-
type: "object",
|
|
297
|
-
properties: {
|
|
298
|
-
projectPath: {
|
|
299
|
-
type: "string",
|
|
300
|
-
description: "Path to project root",
|
|
301
|
-
default: ".",
|
|
302
|
-
},
|
|
303
|
-
profile: {
|
|
304
|
-
type: "string",
|
|
305
|
-
enum: ["quick", "full", "ship", "ci", "security", "compliance", "ai"],
|
|
306
|
-
description:
|
|
307
|
-
"Check profile: quick, full, ship, ci, security, compliance, ai",
|
|
308
|
-
default: "quick",
|
|
309
|
-
},
|
|
310
|
-
only: {
|
|
311
|
-
type: "array",
|
|
312
|
-
items: { type: "string" },
|
|
313
|
-
description:
|
|
314
|
-
"Run only specific checks: integrity, security, hygiene, contracts, auth, routes, mocks, compliance, ai",
|
|
315
|
-
},
|
|
316
|
-
format: {
|
|
317
|
-
type: "string",
|
|
318
|
-
enum: ["text", "json", "html", "sarif"],
|
|
319
|
-
description: "Output format",
|
|
320
|
-
default: "text",
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
},
|
|
666
|
+
// Legacy tool definitions removed - V3 is the only supported mode
|
|
667
|
+
// All tools are now defined in tools-v3.js
|
|
325
668
|
|
|
326
|
-
// 3. VERIFY - Runtime verification with Playwright
|
|
327
|
-
{
|
|
328
|
-
name: "vibecheck.verify",
|
|
329
|
-
description:
|
|
330
|
-
"🧪 Runtime Verify — clicks buttons, fills forms, finds Dead UI with Playwright",
|
|
331
|
-
inputSchema: {
|
|
332
|
-
type: "object",
|
|
333
|
-
properties: {
|
|
334
|
-
url: {
|
|
335
|
-
type: "string",
|
|
336
|
-
description: "Target URL to test (required)",
|
|
337
|
-
},
|
|
338
|
-
auth: {
|
|
339
|
-
type: "string",
|
|
340
|
-
description: "Auth credentials (email:password)",
|
|
341
|
-
},
|
|
342
|
-
flows: {
|
|
343
|
-
type: "array",
|
|
344
|
-
items: { type: "string" },
|
|
345
|
-
description: "Flow packs to test: auth, ui, forms, billing",
|
|
346
|
-
},
|
|
347
|
-
headed: {
|
|
348
|
-
type: "boolean",
|
|
349
|
-
description: "Run browser in visible mode",
|
|
350
|
-
default: false,
|
|
351
|
-
},
|
|
352
|
-
record: {
|
|
353
|
-
type: "boolean",
|
|
354
|
-
description: "Record video of test run",
|
|
355
|
-
default: false,
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
required: ["url"],
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
669
|
|
|
362
|
-
// 3b. REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
|
|
363
|
-
{
|
|
364
|
-
name: "vibecheck.reality",
|
|
365
|
-
description:
|
|
366
|
-
"🧪 Reality Mode v2 — Two-pass auth verification: crawl anon, then auth. Finds Dead UI, HTTP errors, auth coverage gaps.",
|
|
367
|
-
inputSchema: {
|
|
368
|
-
type: "object",
|
|
369
|
-
properties: {
|
|
370
|
-
url: {
|
|
371
|
-
type: "string",
|
|
372
|
-
description: "Target URL to test (required)",
|
|
373
|
-
},
|
|
374
|
-
auth: {
|
|
375
|
-
type: "string",
|
|
376
|
-
description: "Auth credentials (email:password) for login attempt",
|
|
377
|
-
},
|
|
378
|
-
verifyAuth: {
|
|
379
|
-
type: "boolean",
|
|
380
|
-
description: "Enable two-pass auth verification (anon + auth)",
|
|
381
|
-
default: false,
|
|
382
|
-
},
|
|
383
|
-
storageState: {
|
|
384
|
-
type: "string",
|
|
385
|
-
description: "Path to Playwright storageState.json for pre-authenticated session",
|
|
386
|
-
},
|
|
387
|
-
saveStorageState: {
|
|
388
|
-
type: "string",
|
|
389
|
-
description: "Path to save storageState after successful login",
|
|
390
|
-
},
|
|
391
|
-
truthpack: {
|
|
392
|
-
type: "string",
|
|
393
|
-
description: "Path to truthpack.json for auth matcher verification",
|
|
394
|
-
},
|
|
395
|
-
headed: {
|
|
396
|
-
type: "boolean",
|
|
397
|
-
description: "Run browser in visible mode",
|
|
398
|
-
default: false,
|
|
399
|
-
},
|
|
400
|
-
maxPages: {
|
|
401
|
-
type: "number",
|
|
402
|
-
description: "Max pages to visit per pass (default: 18)",
|
|
403
|
-
default: 18,
|
|
404
|
-
},
|
|
405
|
-
maxDepth: {
|
|
406
|
-
type: "number",
|
|
407
|
-
description: "Max link depth to crawl (default: 2)",
|
|
408
|
-
default: 2,
|
|
409
|
-
},
|
|
410
|
-
danger: {
|
|
411
|
-
type: "boolean",
|
|
412
|
-
description: "Allow clicking risky buttons (delete, cancel, etc.)",
|
|
413
|
-
default: false,
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
required: ["url"],
|
|
417
|
-
},
|
|
418
|
-
},
|
|
419
|
-
|
|
420
|
-
// 4. AI-TEST - AI Agent testing
|
|
421
|
-
{
|
|
422
|
-
name: "vibecheckai.dev-test",
|
|
423
|
-
description:
|
|
424
|
-
"🤖 AI Agent — autonomous testing that explores your app and generates fix prompts",
|
|
425
|
-
inputSchema: {
|
|
426
|
-
type: "object",
|
|
427
|
-
properties: {
|
|
428
|
-
url: {
|
|
429
|
-
type: "string",
|
|
430
|
-
description: "Target URL to test (required)",
|
|
431
|
-
},
|
|
432
|
-
goal: {
|
|
433
|
-
type: "string",
|
|
434
|
-
description: "Natural language goal for the AI agent",
|
|
435
|
-
default: "Test all features and find issues",
|
|
436
|
-
},
|
|
437
|
-
headed: {
|
|
438
|
-
type: "boolean",
|
|
439
|
-
description: "Run browser in visible mode",
|
|
440
|
-
default: false,
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
required: ["url"],
|
|
444
|
-
},
|
|
445
|
-
},
|
|
446
|
-
|
|
447
|
-
// 3. GATE - Enforce truth in CI
|
|
448
|
-
{
|
|
449
|
-
name: "vibecheck.gate",
|
|
450
|
-
description: "🚦 Enforce truth in CI — fail builds on policy violations (STARTER tier)",
|
|
451
|
-
inputSchema: {
|
|
452
|
-
type: "object",
|
|
453
|
-
properties: {
|
|
454
|
-
projectPath: {
|
|
455
|
-
type: "string",
|
|
456
|
-
default: ".",
|
|
457
|
-
},
|
|
458
|
-
policy: {
|
|
459
|
-
type: "string",
|
|
460
|
-
enum: ["default", "strict", "ci"],
|
|
461
|
-
description: "Policy strictness level",
|
|
462
|
-
default: "strict",
|
|
463
|
-
},
|
|
464
|
-
sarif: {
|
|
465
|
-
type: "boolean",
|
|
466
|
-
description: "Generate SARIF for GitHub Code Scanning",
|
|
467
|
-
default: true,
|
|
468
|
-
},
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
|
-
},
|
|
472
|
-
|
|
473
|
-
// 5. FIX - Fix Missions v1
|
|
474
|
-
{
|
|
475
|
-
name: "vibecheck.fix",
|
|
476
|
-
description:
|
|
477
|
-
"🔧 Fix Missions v1 — AI-powered surgical fixes with proof verification loop. --apply and --autopilot require PRO tier ($99/mo).",
|
|
478
|
-
inputSchema: {
|
|
479
|
-
type: "object",
|
|
480
|
-
properties: {
|
|
481
|
-
projectPath: {
|
|
482
|
-
type: "string",
|
|
483
|
-
default: ".",
|
|
484
|
-
},
|
|
485
|
-
promptOnly: {
|
|
486
|
-
type: "boolean",
|
|
487
|
-
description: "Generate mission prompts only (no edits)",
|
|
488
|
-
default: false,
|
|
489
|
-
},
|
|
490
|
-
apply: {
|
|
491
|
-
type: "boolean",
|
|
492
|
-
description: "Apply patches returned by the model",
|
|
493
|
-
default: false,
|
|
494
|
-
},
|
|
495
|
-
autopilot: {
|
|
496
|
-
type: "boolean",
|
|
497
|
-
description: "Loop: fix → verify → fix until SHIP or stuck",
|
|
498
|
-
default: false,
|
|
499
|
-
},
|
|
500
|
-
share: {
|
|
501
|
-
type: "boolean",
|
|
502
|
-
description: "Generate share bundle for review",
|
|
503
|
-
default: false,
|
|
504
|
-
},
|
|
505
|
-
maxMissions: {
|
|
506
|
-
type: "number",
|
|
507
|
-
description: "Max missions to plan (default: 8)",
|
|
508
|
-
default: 8,
|
|
509
|
-
},
|
|
510
|
-
maxSteps: {
|
|
511
|
-
type: "number",
|
|
512
|
-
description: "Max autopilot steps (default: 10)",
|
|
513
|
-
default: 10,
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
// 6. SHARE - Generate share bundle from fix missions
|
|
520
|
-
{
|
|
521
|
-
name: "vibecheck.share",
|
|
522
|
-
description: "📦 Share Bundle — generate PR comment / review bundle from latest fix missions",
|
|
523
|
-
inputSchema: {
|
|
524
|
-
type: "object",
|
|
525
|
-
properties: {
|
|
526
|
-
projectPath: {
|
|
527
|
-
type: "string",
|
|
528
|
-
default: ".",
|
|
529
|
-
},
|
|
530
|
-
prComment: {
|
|
531
|
-
type: "boolean",
|
|
532
|
-
description: "Output GitHub PR comment format",
|
|
533
|
-
default: false,
|
|
534
|
-
},
|
|
535
|
-
out: {
|
|
536
|
-
type: "string",
|
|
537
|
-
description: "Write output to file path",
|
|
538
|
-
},
|
|
539
|
-
},
|
|
540
|
-
},
|
|
541
|
-
},
|
|
542
|
-
|
|
543
|
-
// 7. PROVE - One Command Reality Proof (orchestrates ctx → reality → ship → fix)
|
|
544
|
-
{
|
|
545
|
-
name: "vibecheck.prove",
|
|
546
|
-
description: "🔬 One Command Reality Proof — orchestrates ctx → reality → ship → fix loop until SHIP or stuck (PRO tier)",
|
|
547
|
-
inputSchema: {
|
|
548
|
-
type: "object",
|
|
549
|
-
properties: {
|
|
550
|
-
projectPath: {
|
|
551
|
-
type: "string",
|
|
552
|
-
default: ".",
|
|
553
|
-
},
|
|
554
|
-
url: {
|
|
555
|
-
type: "string",
|
|
556
|
-
description: "Base URL for runtime testing",
|
|
557
|
-
},
|
|
558
|
-
auth: {
|
|
559
|
-
type: "string",
|
|
560
|
-
description: "Auth credentials (email:password)",
|
|
561
|
-
},
|
|
562
|
-
storageState: {
|
|
563
|
-
type: "string",
|
|
564
|
-
description: "Path to Playwright storageState.json",
|
|
565
|
-
},
|
|
566
|
-
maxFixRounds: {
|
|
567
|
-
type: "number",
|
|
568
|
-
description: "Max auto-fix attempts (default: 3)",
|
|
569
|
-
default: 3,
|
|
570
|
-
},
|
|
571
|
-
skipReality: {
|
|
572
|
-
type: "boolean",
|
|
573
|
-
description: "Skip runtime crawling (static only)",
|
|
574
|
-
default: false,
|
|
575
|
-
},
|
|
576
|
-
skipFix: {
|
|
577
|
-
type: "boolean",
|
|
578
|
-
description: "Don't auto-fix, just diagnose",
|
|
579
|
-
default: false,
|
|
580
|
-
},
|
|
581
|
-
headed: {
|
|
582
|
-
type: "boolean",
|
|
583
|
-
description: "Run browser in visible mode",
|
|
584
|
-
default: false,
|
|
585
|
-
},
|
|
586
|
-
danger: {
|
|
587
|
-
type: "boolean",
|
|
588
|
-
description: "Allow clicking risky buttons",
|
|
589
|
-
default: false,
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
},
|
|
593
|
-
},
|
|
594
|
-
|
|
595
|
-
// 8. CTX - Truth Pack Generator
|
|
596
|
-
{
|
|
597
|
-
name: "vibecheck.ctx",
|
|
598
|
-
description: "📦 Truth Pack — generate ground truth for AI agents (routes, env, auth, billing)",
|
|
599
|
-
inputSchema: {
|
|
600
|
-
type: "object",
|
|
601
|
-
properties: {
|
|
602
|
-
projectPath: {
|
|
603
|
-
type: "string",
|
|
604
|
-
default: ".",
|
|
605
|
-
},
|
|
606
|
-
snapshot: {
|
|
607
|
-
type: "boolean",
|
|
608
|
-
description: "Save timestamped snapshot",
|
|
609
|
-
default: false,
|
|
610
|
-
},
|
|
611
|
-
json: {
|
|
612
|
-
type: "boolean",
|
|
613
|
-
description: "Output raw JSON",
|
|
614
|
-
default: false,
|
|
615
|
-
},
|
|
616
|
-
},
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
|
-
|
|
620
|
-
// 9. PROOF - Premium verification
|
|
621
|
-
{
|
|
622
|
-
name: "vibecheck.proof",
|
|
623
|
-
description:
|
|
624
|
-
"🎬 Premium verification — mocks (static) or reality (runtime with Playwright)",
|
|
625
|
-
inputSchema: {
|
|
626
|
-
type: "object",
|
|
627
|
-
properties: {
|
|
628
|
-
projectPath: {
|
|
629
|
-
type: "string",
|
|
630
|
-
default: ".",
|
|
631
|
-
},
|
|
632
|
-
mode: {
|
|
633
|
-
type: "string",
|
|
634
|
-
enum: ["mocks", "reality"],
|
|
635
|
-
description:
|
|
636
|
-
"Proof mode: mocks (import graph + fake domains) or reality (Playwright runtime)",
|
|
637
|
-
},
|
|
638
|
-
url: {
|
|
639
|
-
type: "string",
|
|
640
|
-
description: "Base URL for reality mode",
|
|
641
|
-
default: "http://localhost:3000",
|
|
642
|
-
},
|
|
643
|
-
flow: {
|
|
644
|
-
type: "string",
|
|
645
|
-
enum: ["auth", "checkout", "dashboard"],
|
|
646
|
-
description: "Flow to test in reality mode",
|
|
647
|
-
default: "auth",
|
|
648
|
-
},
|
|
649
|
-
},
|
|
650
|
-
required: ["mode"],
|
|
651
|
-
},
|
|
652
|
-
},
|
|
653
|
-
|
|
654
|
-
// 5. REPORT - Access artifacts
|
|
655
|
-
{
|
|
656
|
-
name: "vibecheck.validate",
|
|
657
|
-
description:
|
|
658
|
-
"🤖 Validate AI-generated code. Checks for hallucinations, intent mismatch, and quality issues.",
|
|
659
|
-
inputSchema: {
|
|
660
|
-
type: "object",
|
|
661
|
-
properties: {
|
|
662
|
-
code: { type: "string", description: "The code content to validate" },
|
|
663
|
-
intent: {
|
|
664
|
-
type: "string",
|
|
665
|
-
description: "The user's original request/intent",
|
|
666
|
-
},
|
|
667
|
-
projectPath: { type: "string", default: "." },
|
|
668
|
-
},
|
|
669
|
-
required: ["code"],
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
// 10. REPORT - Access scan artifacts
|
|
673
|
-
{
|
|
674
|
-
name: "vibecheck.report",
|
|
675
|
-
description:
|
|
676
|
-
"📄 Access scan artifacts — summary, full report, SARIF export",
|
|
677
|
-
inputSchema: {
|
|
678
|
-
type: "object",
|
|
679
|
-
properties: {
|
|
680
|
-
projectPath: {
|
|
681
|
-
type: "string",
|
|
682
|
-
default: ".",
|
|
683
|
-
},
|
|
684
|
-
type: {
|
|
685
|
-
type: "string",
|
|
686
|
-
enum: ["summary", "full", "sarif", "html"],
|
|
687
|
-
description: "Report type to retrieve",
|
|
688
|
-
default: "summary",
|
|
689
|
-
},
|
|
690
|
-
runId: {
|
|
691
|
-
type: "string",
|
|
692
|
-
description: "Specific run ID (defaults to last run)",
|
|
693
|
-
},
|
|
694
|
-
},
|
|
695
|
-
},
|
|
696
|
-
},
|
|
697
|
-
|
|
698
|
-
// 11. STATUS - Health and config
|
|
699
|
-
{
|
|
700
|
-
name: "vibecheck.status",
|
|
701
|
-
description: "📊 Server status — health, versions, config, last run info",
|
|
702
|
-
inputSchema: {
|
|
703
|
-
type: "object",
|
|
704
|
-
properties: {
|
|
705
|
-
projectPath: {
|
|
706
|
-
type: "string",
|
|
707
|
-
default: ".",
|
|
708
|
-
},
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
},
|
|
712
|
-
|
|
713
|
-
// 12. AUTOPILOT - Continuous protection
|
|
714
|
-
{
|
|
715
|
-
name: "vibecheck.autopilot",
|
|
716
|
-
description:
|
|
717
|
-
"🤖 Autopilot — continuous protection with weekly reports, auto-PRs, deploy blocking",
|
|
718
|
-
inputSchema: {
|
|
719
|
-
type: "object",
|
|
720
|
-
properties: {
|
|
721
|
-
projectPath: {
|
|
722
|
-
type: "string",
|
|
723
|
-
default: ".",
|
|
724
|
-
},
|
|
725
|
-
action: {
|
|
726
|
-
type: "string",
|
|
727
|
-
enum: ["status", "enable", "disable", "digest"],
|
|
728
|
-
description: "Autopilot action",
|
|
729
|
-
default: "status",
|
|
730
|
-
},
|
|
731
|
-
slack: {
|
|
732
|
-
type: "string",
|
|
733
|
-
description: "Slack webhook URL for notifications",
|
|
734
|
-
},
|
|
735
|
-
email: {
|
|
736
|
-
type: "string",
|
|
737
|
-
description: "Email for weekly digest",
|
|
738
|
-
},
|
|
739
|
-
},
|
|
740
|
-
},
|
|
741
|
-
},
|
|
742
|
-
|
|
743
|
-
// 13. AUTOPILOT PLAN - Generate fix plan (PRO tier)
|
|
744
|
-
{
|
|
745
|
-
name: "vibecheck.autopilot_plan",
|
|
746
|
-
description:
|
|
747
|
-
"🤖 Autopilot Plan — scan codebase, group issues into fix packs, estimate risk (PRO tier)",
|
|
748
|
-
inputSchema: {
|
|
749
|
-
type: "object",
|
|
750
|
-
properties: {
|
|
751
|
-
projectPath: {
|
|
752
|
-
type: "string",
|
|
753
|
-
default: ".",
|
|
754
|
-
},
|
|
755
|
-
profile: {
|
|
756
|
-
type: "string",
|
|
757
|
-
enum: ["quick", "full", "ship", "ci"],
|
|
758
|
-
description: "Scan profile",
|
|
759
|
-
default: "ship",
|
|
760
|
-
},
|
|
761
|
-
maxFixes: {
|
|
762
|
-
type: "number",
|
|
763
|
-
description: "Max fixes per category",
|
|
764
|
-
default: 10,
|
|
765
|
-
},
|
|
766
|
-
},
|
|
767
|
-
},
|
|
768
|
-
},
|
|
769
|
-
|
|
770
|
-
// 14. AUTOPILOT APPLY - Apply fixes (PRO tier)
|
|
771
|
-
{
|
|
772
|
-
name: "vibecheck.autopilot_apply",
|
|
773
|
-
description:
|
|
774
|
-
"🔧 Autopilot Apply — apply fix packs with verification, re-scan to confirm (PRO tier)",
|
|
775
|
-
inputSchema: {
|
|
776
|
-
type: "object",
|
|
777
|
-
properties: {
|
|
778
|
-
projectPath: {
|
|
779
|
-
type: "string",
|
|
780
|
-
default: ".",
|
|
781
|
-
},
|
|
782
|
-
profile: {
|
|
783
|
-
type: "string",
|
|
784
|
-
enum: ["quick", "full", "ship", "ci"],
|
|
785
|
-
description: "Scan profile",
|
|
786
|
-
default: "ship",
|
|
787
|
-
},
|
|
788
|
-
maxFixes: {
|
|
789
|
-
type: "number",
|
|
790
|
-
description: "Max fixes per category",
|
|
791
|
-
default: 10,
|
|
792
|
-
},
|
|
793
|
-
verify: {
|
|
794
|
-
type: "boolean",
|
|
795
|
-
description: "Run verification after apply",
|
|
796
|
-
default: true,
|
|
797
|
-
},
|
|
798
|
-
dryRun: {
|
|
799
|
-
type: "boolean",
|
|
800
|
-
description: "Preview changes without applying",
|
|
801
|
-
default: false,
|
|
802
|
-
},
|
|
803
|
-
},
|
|
804
|
-
},
|
|
805
|
-
},
|
|
806
|
-
|
|
807
|
-
// 15. BADGE - Generate ship badge
|
|
808
|
-
{
|
|
809
|
-
name: "vibecheck.badge",
|
|
810
|
-
description:
|
|
811
|
-
"🏅 Ship Badge — generate a badge for README/PR showing scan status (STARTER tier)",
|
|
812
|
-
inputSchema: {
|
|
813
|
-
type: "object",
|
|
814
|
-
properties: {
|
|
815
|
-
projectPath: {
|
|
816
|
-
type: "string",
|
|
817
|
-
default: ".",
|
|
818
|
-
},
|
|
819
|
-
format: {
|
|
820
|
-
type: "string",
|
|
821
|
-
enum: ["svg", "md", "html"],
|
|
822
|
-
description: "Badge format",
|
|
823
|
-
default: "svg",
|
|
824
|
-
},
|
|
825
|
-
style: {
|
|
826
|
-
type: "string",
|
|
827
|
-
enum: ["flat", "flat-square"],
|
|
828
|
-
description: "Badge style",
|
|
829
|
-
default: "flat",
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
|
-
},
|
|
833
|
-
},
|
|
834
|
-
|
|
835
|
-
// 16. CONTEXT - AI Rules Generator
|
|
836
|
-
{
|
|
837
|
-
name: "vibecheck.context",
|
|
838
|
-
description:
|
|
839
|
-
"🧠 AI Context — generate rules files for Cursor, Windsurf, Copilot to understand your codebase",
|
|
840
|
-
inputSchema: {
|
|
841
|
-
type: "object",
|
|
842
|
-
properties: {
|
|
843
|
-
projectPath: {
|
|
844
|
-
type: "string",
|
|
845
|
-
description: "Path to project root",
|
|
846
|
-
default: ".",
|
|
847
|
-
},
|
|
848
|
-
platform: {
|
|
849
|
-
type: "string",
|
|
850
|
-
enum: ["all", "cursor", "windsurf", "copilot", "claude"],
|
|
851
|
-
description: "Target platform (default: all)",
|
|
852
|
-
default: "all",
|
|
853
|
-
},
|
|
854
|
-
},
|
|
855
|
-
},
|
|
856
|
-
},
|
|
857
|
-
];
|
|
858
670
|
|
|
859
671
|
// ============================================================================
|
|
860
672
|
// SERVER IMPLEMENTATION
|
|
@@ -872,32 +684,56 @@ class VibecheckMCP {
|
|
|
872
684
|
|
|
873
685
|
// ============================================================================
|
|
874
686
|
// TOOL REGISTRY - Maps tool names to handlers for cleaner dispatch
|
|
687
|
+
// HARDENING: Validates all handlers are functions
|
|
875
688
|
// ============================================================================
|
|
876
689
|
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),
|
|
690
|
+
const registry = {};
|
|
691
|
+
|
|
692
|
+
// Helper to safely add handler with validation
|
|
693
|
+
const addHandler = (name, handler) => {
|
|
694
|
+
if (typeof handler !== 'function') {
|
|
695
|
+
console.error(`[MCP] Warning: Tool ${name} handler is not a function`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
registry[name] = handler;
|
|
900
699
|
};
|
|
700
|
+
|
|
701
|
+
// Agent Firewall - intercepts file writes (if available)
|
|
702
|
+
if (handleAgentFirewallIntercept && typeof handleAgentFirewallIntercept === 'function') {
|
|
703
|
+
addHandler("vibecheck_agent_firewall_intercept", handleAgentFirewallIntercept);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Conductor - multi-agent coordination tools
|
|
707
|
+
addHandler("vibecheck_conductor_register", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_register", args, projectPath));
|
|
708
|
+
addHandler("vibecheck_conductor_acquire_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_acquire_lock", args, projectPath));
|
|
709
|
+
addHandler("vibecheck_conductor_release_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_release_lock", args, projectPath));
|
|
710
|
+
addHandler("vibecheck_conductor_propose", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_propose", args, projectPath));
|
|
711
|
+
addHandler("vibecheck_conductor_status", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_status", args, projectPath));
|
|
712
|
+
addHandler("vibecheck_conductor_terminate", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_terminate", args, projectPath));
|
|
713
|
+
|
|
714
|
+
// Core CLI tools
|
|
715
|
+
addHandler("vibecheck.ship", this.handleShip.bind(this));
|
|
716
|
+
addHandler("vibecheck.scan", this.handleScan.bind(this));
|
|
717
|
+
addHandler("vibecheck.verify", this.handleVerify.bind(this));
|
|
718
|
+
addHandler("vibecheck.reality", this.handleReality.bind(this));
|
|
719
|
+
addHandler("vibecheckai.dev-test", this.handleAITest.bind(this));
|
|
720
|
+
addHandler("vibecheck.gate", this.handleGate.bind(this));
|
|
721
|
+
addHandler("vibecheck.fix", this.handleFix.bind(this));
|
|
722
|
+
addHandler("vibecheck.share", this.handleShare.bind(this));
|
|
723
|
+
addHandler("vibecheck.ctx", this.handleCtx.bind(this));
|
|
724
|
+
addHandler("vibecheck.prove", this.handleProve.bind(this));
|
|
725
|
+
addHandler("vibecheck.proof", this.handleProof.bind(this));
|
|
726
|
+
addHandler("vibecheck.validate", this.handleValidate.bind(this));
|
|
727
|
+
addHandler("vibecheck.report", this.handleReport.bind(this));
|
|
728
|
+
addHandler("vibecheck.status", this.handleStatus.bind(this));
|
|
729
|
+
addHandler("vibecheck.autopilot", this.handleAutopilot.bind(this));
|
|
730
|
+
addHandler("vibecheck.autopilot_plan", this.handleAutopilotPlan.bind(this));
|
|
731
|
+
addHandler("vibecheck.autopilot_apply", this.handleAutopilotApply.bind(this));
|
|
732
|
+
addHandler("vibecheck.badge", this.handleBadge.bind(this));
|
|
733
|
+
addHandler("vibecheck.context", this.handleContext.bind(this));
|
|
734
|
+
|
|
735
|
+
console.error(`[MCP] Tool registry built with ${Object.keys(registry).length} handlers`);
|
|
736
|
+
return registry;
|
|
901
737
|
}
|
|
902
738
|
|
|
903
739
|
// ============================================================================
|
|
@@ -910,70 +746,208 @@ class VibecheckMCP {
|
|
|
910
746
|
skipAuth = true,
|
|
911
747
|
} = options;
|
|
912
748
|
|
|
749
|
+
// ========================================================================
|
|
750
|
+
// HARDENING: Validate command
|
|
751
|
+
// ========================================================================
|
|
752
|
+
const sanitizedCommand = sanitizeString(command, 50);
|
|
753
|
+
if (!sanitizedCommand || !/^[a-z0-9_-]+$/i.test(sanitizedCommand)) {
|
|
754
|
+
throw new Error(`Invalid CLI command: ${sanitizedCommand}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ========================================================================
|
|
758
|
+
// HARDENING: Validate and sanitize arguments
|
|
759
|
+
// ========================================================================
|
|
760
|
+
const sanitizedArgs = sanitizeArray(args, 50).map(arg => {
|
|
761
|
+
const str = String(arg);
|
|
762
|
+
// Validate argument format (must be simple flags or values)
|
|
763
|
+
if (str.length > 1000) {
|
|
764
|
+
return str.slice(0, 1000);
|
|
765
|
+
}
|
|
766
|
+
return str;
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// ========================================================================
|
|
770
|
+
// HARDENING: Validate working directory
|
|
771
|
+
// ========================================================================
|
|
772
|
+
const resolvedCwd = path.resolve(cwd || process.cwd());
|
|
773
|
+
if (!fsSync.existsSync(resolvedCwd)) {
|
|
774
|
+
throw new Error(`Working directory does not exist: ${resolvedCwd}`);
|
|
775
|
+
}
|
|
776
|
+
|
|
913
777
|
// Build argument array - this prevents command injection
|
|
914
|
-
const finalArgs = [CONFIG.BIN_PATH,
|
|
778
|
+
const finalArgs = [CONFIG.BIN_PATH, sanitizedCommand, ...sanitizedArgs];
|
|
779
|
+
|
|
780
|
+
// ========================================================================
|
|
781
|
+
// HARDENING: Clean environment - don't leak sensitive vars
|
|
782
|
+
// ========================================================================
|
|
783
|
+
const safeEnv = { ...process.env };
|
|
784
|
+
// Remove potentially sensitive env vars from being passed through
|
|
785
|
+
const sensitiveEnvKeys = ['AWS_SECRET_ACCESS_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL'];
|
|
786
|
+
for (const key of sensitiveEnvKeys) {
|
|
787
|
+
if (safeEnv[key] && !env[key]) {
|
|
788
|
+
// Only remove if not explicitly set in options
|
|
789
|
+
delete safeEnv[key];
|
|
790
|
+
}
|
|
791
|
+
}
|
|
915
792
|
|
|
916
793
|
const execEnv = {
|
|
917
|
-
...
|
|
794
|
+
...safeEnv,
|
|
918
795
|
...CONFIG.ENV_DEFAULTS,
|
|
919
796
|
...(skipAuth ? { VIBECHECK_SKIP_AUTH: "1" } : {}),
|
|
920
797
|
...env,
|
|
798
|
+
// Ensure Node.js doesn't prompt for anything
|
|
799
|
+
NODE_NO_READLINE: "1",
|
|
800
|
+
FORCE_COLOR: "0",
|
|
921
801
|
};
|
|
922
802
|
|
|
803
|
+
// ========================================================================
|
|
804
|
+
// HARDENING: Bounded timeout
|
|
805
|
+
// ========================================================================
|
|
806
|
+
const boundedTimeout = sanitizeNumber(timeout, 1000, 900000, CONFIG.TIMEOUTS.DEFAULT);
|
|
807
|
+
|
|
923
808
|
try {
|
|
924
809
|
const { stdout, stderr } = await execFileAsync(process.execPath, finalArgs, {
|
|
925
|
-
cwd,
|
|
810
|
+
cwd: resolvedCwd,
|
|
926
811
|
encoding: "utf8",
|
|
927
812
|
maxBuffer: CONFIG.MAX_BUFFER,
|
|
928
|
-
timeout,
|
|
813
|
+
timeout: boundedTimeout,
|
|
929
814
|
env: execEnv,
|
|
815
|
+
// Don't inherit stdin - prevents hanging
|
|
816
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
930
817
|
});
|
|
931
|
-
|
|
818
|
+
|
|
819
|
+
// Sanitize output before returning
|
|
820
|
+
return {
|
|
821
|
+
stdout: redactSensitive(truncateOutput(stdout || '')),
|
|
822
|
+
stderr: redactSensitive(truncateOutput(stderr || '')),
|
|
823
|
+
success: true
|
|
824
|
+
};
|
|
932
825
|
} catch (error) {
|
|
933
|
-
// Attach partial output for graceful degradation
|
|
934
|
-
error.partialOutput = error.stdout ||
|
|
935
|
-
error.partialStderr = error.stderr ||
|
|
826
|
+
// Attach partial output for graceful degradation (sanitized)
|
|
827
|
+
error.partialOutput = redactSensitive(truncateOutput(error.stdout || ''));
|
|
828
|
+
error.partialStderr = redactSensitive(truncateOutput(error.stderr || ''));
|
|
829
|
+
|
|
830
|
+
// Add helpful error code for timeout
|
|
831
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
832
|
+
error.code = 'TIMEOUT';
|
|
833
|
+
error.message = `Command timed out after ${boundedTimeout}ms`;
|
|
834
|
+
}
|
|
835
|
+
|
|
936
836
|
throw error;
|
|
937
837
|
}
|
|
938
838
|
}
|
|
939
839
|
|
|
940
840
|
// ============================================================================
|
|
941
|
-
// UTILITY HELPERS
|
|
841
|
+
// HARDENED UTILITY HELPERS
|
|
942
842
|
// ============================================================================
|
|
943
843
|
|
|
944
|
-
|
|
844
|
+
/**
|
|
845
|
+
* Strip ANSI escape codes from output with length validation
|
|
846
|
+
* @param {string} str - String to strip
|
|
847
|
+
* @returns {string}
|
|
848
|
+
*/
|
|
945
849
|
stripAnsi(str) {
|
|
946
|
-
|
|
850
|
+
if (!str || typeof str !== 'string') {
|
|
851
|
+
return '';
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Truncate first to prevent DoS on very long strings
|
|
855
|
+
const truncated = str.length > CONFIG.LIMITS.MAX_OUTPUT_LENGTH
|
|
856
|
+
? str.slice(0, CONFIG.LIMITS.MAX_OUTPUT_LENGTH)
|
|
857
|
+
: str;
|
|
858
|
+
|
|
859
|
+
return truncated.replace(/\x1b\[[0-9;]*m/g, "");
|
|
947
860
|
}
|
|
948
861
|
|
|
949
|
-
|
|
862
|
+
/**
|
|
863
|
+
* Parse summary from disk with size limits and validation
|
|
864
|
+
* @param {string} projectPath - Project root path
|
|
865
|
+
* @returns {Promise<object|null>} Parsed summary or null
|
|
866
|
+
*/
|
|
950
867
|
async parseSummaryFromDisk(projectPath) {
|
|
951
868
|
const summaryPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "summary.json");
|
|
869
|
+
|
|
952
870
|
try {
|
|
871
|
+
// Check file size first
|
|
872
|
+
const stats = await fs.stat(summaryPath);
|
|
873
|
+
if (stats.size > 5 * 1024 * 1024) { // 5MB limit
|
|
874
|
+
console.error(`[MCP] Summary file too large: ${stats.size} bytes`);
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
953
878
|
const content = await fs.readFile(summaryPath, "utf-8");
|
|
954
|
-
|
|
955
|
-
|
|
879
|
+
const parsed = safeJsonParse(content);
|
|
880
|
+
|
|
881
|
+
if (!parsed.success) {
|
|
882
|
+
console.error(`[MCP] Invalid summary JSON: ${parsed.error}`);
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return parsed.data;
|
|
887
|
+
} catch (err) {
|
|
888
|
+
// Silent fail - this is for graceful degradation
|
|
956
889
|
return null;
|
|
957
890
|
}
|
|
958
891
|
}
|
|
959
892
|
|
|
960
|
-
|
|
893
|
+
/**
|
|
894
|
+
* Format scan output from summary object with validation
|
|
895
|
+
* @param {object} summary - Summary object
|
|
896
|
+
* @param {string} projectPath - Project root path
|
|
897
|
+
* @returns {string}
|
|
898
|
+
*/
|
|
961
899
|
formatScanOutput(summary, projectPath) {
|
|
962
|
-
|
|
963
|
-
|
|
900
|
+
if (!summary || typeof summary !== 'object') {
|
|
901
|
+
return '## Error: Invalid summary data\n';
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Safely extract values with defaults
|
|
905
|
+
const score = sanitizeNumber(summary.score, 0, 100, 0);
|
|
906
|
+
const grade = sanitizeString(summary.grade, 10) || 'N/A';
|
|
907
|
+
const canShip = Boolean(summary.canShip);
|
|
908
|
+
|
|
909
|
+
let output = `## Score: ${score}/100 (${grade})\n\n`;
|
|
910
|
+
output += `**Verdict:** ${canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n\n`;
|
|
964
911
|
|
|
965
|
-
if (summary.counts) {
|
|
912
|
+
if (summary.counts && typeof summary.counts === 'object') {
|
|
966
913
|
output += "### Checks\n\n";
|
|
967
914
|
output += "| Category | Issues |\n|----------|--------|\n";
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
915
|
+
|
|
916
|
+
// Limit to 50 categories to prevent output bloat
|
|
917
|
+
const entries = Object.entries(summary.counts).slice(0, 50);
|
|
918
|
+
for (const [key, count] of entries) {
|
|
919
|
+
const safeKey = sanitizeString(key, 50);
|
|
920
|
+
const safeCount = sanitizeNumber(count, 0, 999999, 0);
|
|
921
|
+
const icon = safeCount === 0 ? "✅" : "⚠️";
|
|
922
|
+
output += `| ${icon} ${safeKey} | ${safeCount} |\n`;
|
|
971
923
|
}
|
|
972
924
|
}
|
|
973
925
|
|
|
974
926
|
output += `\n📄 **Report:** ${CONFIG.OUTPUT_DIR}/report.html\n`;
|
|
975
927
|
return output;
|
|
976
928
|
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Safely read a file with size limits
|
|
932
|
+
* @param {string} filePath - Path to file
|
|
933
|
+
* @param {number} maxSize - Maximum file size in bytes
|
|
934
|
+
* @returns {Promise<string|null>} File contents or null
|
|
935
|
+
*/
|
|
936
|
+
async safeReadFile(filePath, maxSize = 10 * 1024 * 1024) {
|
|
937
|
+
try {
|
|
938
|
+
const stats = await fs.stat(filePath);
|
|
939
|
+
|
|
940
|
+
if (stats.size > maxSize) {
|
|
941
|
+
console.error(`[MCP] File too large: ${filePath} (${stats.size} bytes)`);
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
946
|
+
return content;
|
|
947
|
+
} catch (err) {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
977
951
|
|
|
978
952
|
setupHandlers() {
|
|
979
953
|
// List tools
|
|
@@ -983,24 +957,95 @@ class VibecheckMCP {
|
|
|
983
957
|
|
|
984
958
|
// Call tool - main dispatch handler
|
|
985
959
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
986
|
-
const { name, arguments: args } = request.params;
|
|
987
|
-
const projectPath = path.resolve(args?.projectPath || ".");
|
|
988
960
|
const startTime = Date.now();
|
|
961
|
+
let toolName = 'unknown';
|
|
962
|
+
let projectPath = '.';
|
|
963
|
+
|
|
964
|
+
try {
|
|
965
|
+
// ====================================================================
|
|
966
|
+
// HARDENING: Input extraction with validation
|
|
967
|
+
// ====================================================================
|
|
968
|
+
const params = request?.params;
|
|
969
|
+
if (!params || typeof params !== 'object') {
|
|
970
|
+
return this.error('Invalid request: missing params', { code: 'INVALID_REQUEST' });
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
toolName = sanitizeString(params.name, 100);
|
|
974
|
+
const args = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
975
|
+
|
|
976
|
+
// Validate tool name
|
|
977
|
+
if (!toolName || toolName.length < 2) {
|
|
978
|
+
return this.error('Invalid tool name', { code: 'INVALID_TOOL_NAME' });
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// ====================================================================
|
|
982
|
+
// HARDENING: Rate limiting (per-API-key)
|
|
983
|
+
// ====================================================================
|
|
984
|
+
const apiKey = args?.apiKey || null;
|
|
985
|
+
const rateCheck = checkRateLimit(apiKey);
|
|
986
|
+
if (!rateCheck.allowed) {
|
|
987
|
+
return this.error(`Rate limit exceeded. Try again in ${Math.ceil(rateCheck.resetIn / 1000)} seconds`, {
|
|
988
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
989
|
+
suggestion: 'Reduce the frequency of tool calls',
|
|
990
|
+
nextSteps: [`Wait ${Math.ceil(rateCheck.resetIn / 1000)} seconds before retrying`],
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ====================================================================
|
|
995
|
+
// HARDENING: Project path validation
|
|
996
|
+
// ====================================================================
|
|
997
|
+
const rawProjectPath = args?.projectPath || '.';
|
|
998
|
+
const pathValidation = sanitizePath(rawProjectPath, process.cwd());
|
|
999
|
+
|
|
1000
|
+
if (!pathValidation.valid) {
|
|
1001
|
+
return this.error(pathValidation.error, {
|
|
1002
|
+
code: 'INVALID_PATH',
|
|
1003
|
+
suggestion: 'Provide a valid path within the current working directory',
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
projectPath = pathValidation.path;
|
|
989
1007
|
|
|
990
|
-
|
|
991
|
-
|
|
1008
|
+
// Emit audit event for tool invocation start
|
|
1009
|
+
// SECURITY: Include apiKey hash for audit trail (never log raw key)
|
|
1010
|
+
try {
|
|
1011
|
+
const crypto = require('crypto');
|
|
1012
|
+
const apiKeyHash = apiKey
|
|
1013
|
+
? crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16)
|
|
1014
|
+
: 'anonymous';
|
|
1015
|
+
|
|
1016
|
+
emitToolInvoke(toolName, args, "success", {
|
|
1017
|
+
projectPath,
|
|
1018
|
+
apiKeyHash,
|
|
1019
|
+
rateLimit: rateCheck,
|
|
1020
|
+
timestamp: new Date().toISOString(),
|
|
1021
|
+
});
|
|
1022
|
+
} catch {
|
|
1023
|
+
// Audit logging should never break the tool
|
|
1024
|
+
}
|
|
992
1025
|
|
|
993
|
-
|
|
994
|
-
//
|
|
995
|
-
|
|
1026
|
+
// ====================================================================
|
|
1027
|
+
// HARDENING: Truth firewall check with error handling
|
|
1028
|
+
// ====================================================================
|
|
1029
|
+
let firewallCheck = { blocked: false };
|
|
1030
|
+
try {
|
|
1031
|
+
firewallCheck = checkTruthFirewallBlock(toolName, args, projectPath);
|
|
1032
|
+
} catch (firewallError) {
|
|
1033
|
+
console.error(`[MCP] Firewall check error: ${firewallError.message}`);
|
|
1034
|
+
// Continue - don't block on firewall errors
|
|
1035
|
+
}
|
|
1036
|
+
|
|
996
1037
|
if (firewallCheck.blocked) {
|
|
997
1038
|
const policy = getTruthPolicy(args);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1039
|
+
try {
|
|
1040
|
+
await emitGuardrailMetric(projectPath, {
|
|
1041
|
+
event: "truth_firewall_block",
|
|
1042
|
+
tool: toolName,
|
|
1043
|
+
policy,
|
|
1044
|
+
reason: firewallCheck.code || "no_recent_claim_validation",
|
|
1045
|
+
});
|
|
1046
|
+
} catch {
|
|
1047
|
+
// Metrics should never break the tool
|
|
1048
|
+
}
|
|
1004
1049
|
return this.error(firewallCheck.reason, {
|
|
1005
1050
|
code: firewallCheck.code,
|
|
1006
1051
|
suggestion: firewallCheck.suggestion,
|
|
@@ -1009,98 +1054,142 @@ class VibecheckMCP {
|
|
|
1009
1054
|
}
|
|
1010
1055
|
|
|
1011
1056
|
// Handle v3 tools (10 consolidated tools, STARTER+ only)
|
|
1012
|
-
if (USE_V3_TOOLS && V3_TOOL_TIERS[
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1057
|
+
if (USE_V3_TOOLS && V3_TOOL_TIERS[toolName]) {
|
|
1058
|
+
// SECURITY FIX: Never trust client-provided tier - validate from API key
|
|
1059
|
+
// Previous: const userTier = sanitizeString(args?.tier, 20) || ...
|
|
1060
|
+
// This allowed privilege escalation via args.tier = "pro"
|
|
1061
|
+
const { getMcpToolAccess } = await import('./tier-auth.js');
|
|
1062
|
+
const access = await getMcpToolAccess(toolName, apiKey);
|
|
1063
|
+
const userTier = access.tier || 'free';
|
|
1064
|
+
const result = await handleToolV3(toolName, args, { tier: userTier });
|
|
1015
1065
|
|
|
1016
1066
|
if (result.error) {
|
|
1017
1067
|
return this.error(result.error, { tier: result.tier, required: result.required });
|
|
1018
1068
|
}
|
|
1019
1069
|
|
|
1070
|
+
// Sanitize and truncate output
|
|
1071
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1020
1072
|
return {
|
|
1021
|
-
content: [{ type: "text", text:
|
|
1073
|
+
content: [{ type: "text", text: outputText }],
|
|
1022
1074
|
};
|
|
1023
1075
|
}
|
|
1024
1076
|
|
|
1025
1077
|
// 1. Check tool registry first (local CLI handlers)
|
|
1026
|
-
if (this.toolRegistry[
|
|
1027
|
-
return await this.toolRegistry[
|
|
1078
|
+
if (this.toolRegistry[toolName]) {
|
|
1079
|
+
return await this.toolRegistry[toolName](projectPath, args);
|
|
1028
1080
|
}
|
|
1029
1081
|
|
|
1030
1082
|
// 2. Handle external module tools by prefix/pattern
|
|
1031
|
-
if (
|
|
1032
|
-
return await handleIntelligenceTool(
|
|
1083
|
+
if (toolName.startsWith("vibecheck.intelligence.")) {
|
|
1084
|
+
return await handleIntelligenceTool(toolName, args, __dirname);
|
|
1033
1085
|
}
|
|
1034
1086
|
|
|
1035
1087
|
// Handle AI vibecheck tools
|
|
1036
1088
|
if (["vibecheck.verify", "vibecheck.quality", "vibecheck.smells",
|
|
1037
1089
|
"vibecheck.hallucination", "vibecheck.breaking", "vibecheck.mdc",
|
|
1038
|
-
"vibecheck.coverage"].includes(
|
|
1039
|
-
const result = await handleVibecheckTool(
|
|
1090
|
+
"vibecheck.coverage"].includes(toolName)) {
|
|
1091
|
+
const result = await handleVibecheckTool(toolName, args);
|
|
1092
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1040
1093
|
return {
|
|
1041
|
-
content: [{ type: "text", text:
|
|
1094
|
+
content: [{ type: "text", text: outputText }],
|
|
1042
1095
|
};
|
|
1043
1096
|
}
|
|
1044
1097
|
|
|
1045
1098
|
// Handle agent checkpoint tools
|
|
1046
|
-
if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(
|
|
1047
|
-
return await handleCheckpointTool(
|
|
1099
|
+
if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(toolName)) {
|
|
1100
|
+
return await handleCheckpointTool(toolName, args);
|
|
1048
1101
|
}
|
|
1049
1102
|
|
|
1050
1103
|
// Handle architect tools
|
|
1051
1104
|
if (["vibecheck_architect_review", "vibecheck_architect_suggest",
|
|
1052
|
-
"vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(
|
|
1053
|
-
return await handleArchitectTool(
|
|
1105
|
+
"vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(toolName)) {
|
|
1106
|
+
return await handleArchitectTool(toolName, args);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Handle authority system tools
|
|
1110
|
+
if (["authority.classify", "authority.approve", "authority.list"].includes(toolName)) {
|
|
1111
|
+
const result = await handleAuthorityTool(toolName, args, userTier || "free");
|
|
1112
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1113
|
+
return {
|
|
1114
|
+
content: [{ type: "text", text: outputText }],
|
|
1115
|
+
};
|
|
1054
1116
|
}
|
|
1055
1117
|
|
|
1056
1118
|
// Handle codebase architect tools
|
|
1057
1119
|
if (["vibecheck_architect_context", "vibecheck_architect_guide",
|
|
1058
1120
|
"vibecheck_architect_validate", "vibecheck_architect_patterns",
|
|
1059
|
-
"vibecheck_architect_dependencies"].includes(
|
|
1060
|
-
return await handleCodebaseArchitectTool(
|
|
1121
|
+
"vibecheck_architect_dependencies"].includes(toolName)) {
|
|
1122
|
+
return await handleCodebaseArchitectTool(toolName, args);
|
|
1061
1123
|
}
|
|
1062
1124
|
|
|
1063
1125
|
// Handle vibecheck 2.0 tools
|
|
1064
|
-
if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(
|
|
1065
|
-
return await handleVibecheck2Tool(
|
|
1126
|
+
if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(toolName)) {
|
|
1127
|
+
return await handleVibecheck2Tool(toolName, args, __dirname);
|
|
1066
1128
|
}
|
|
1067
1129
|
|
|
1068
1130
|
// Handle intent drift tools
|
|
1069
|
-
if (
|
|
1070
|
-
const tool = intentDriftTools.find(t => t.name ===
|
|
1131
|
+
if (toolName.startsWith("vibecheck_intent_")) {
|
|
1132
|
+
const tool = intentDriftTools.find(t => t.name === toolName);
|
|
1071
1133
|
if (tool && tool.handler) {
|
|
1072
1134
|
const result = await tool.handler(args);
|
|
1135
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1073
1136
|
return {
|
|
1074
|
-
content: [{ type: "text", text:
|
|
1137
|
+
content: [{ type: "text", text: outputText }],
|
|
1075
1138
|
};
|
|
1076
1139
|
}
|
|
1077
1140
|
}
|
|
1078
1141
|
|
|
1079
1142
|
// Handle MDC generator
|
|
1080
|
-
if (
|
|
1143
|
+
if (toolName === "generate_mdc") {
|
|
1081
1144
|
return await handleMDCGeneration(args);
|
|
1082
1145
|
}
|
|
1083
1146
|
|
|
1084
1147
|
// Handle Truth Context tools (Evidence Pack / Truth Pack)
|
|
1085
|
-
if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(
|
|
1086
|
-
return await handleTruthContextTool(
|
|
1148
|
+
if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(toolName)) {
|
|
1149
|
+
return await handleTruthContextTool(toolName, args);
|
|
1087
1150
|
}
|
|
1088
1151
|
|
|
1089
1152
|
// Handle Truth Firewall tools (Hallucination Stopper)
|
|
1090
1153
|
if (["vibecheck.get_truthpack", "vibecheck.validate_claim", "vibecheck.compile_context",
|
|
1091
1154
|
"vibecheck.search_evidence", "vibecheck.find_counterexamples", "vibecheck.propose_patch",
|
|
1092
|
-
"vibecheck.check_invariants", "vibecheck.add_assumption"].includes(
|
|
1093
|
-
return await handleTruthFirewallTool(
|
|
1155
|
+
"vibecheck.check_invariants", "vibecheck.add_assumption"].includes(toolName)) {
|
|
1156
|
+
return await handleTruthFirewallTool(toolName, args, projectPath);
|
|
1094
1157
|
}
|
|
1095
1158
|
|
|
1096
|
-
return this.error(`Unknown tool: ${
|
|
1159
|
+
return this.error(`Unknown tool: ${toolName}`, {
|
|
1160
|
+
code: 'UNKNOWN_TOOL',
|
|
1161
|
+
suggestion: 'Check the tool name and try again',
|
|
1162
|
+
nextSteps: ['Use vibecheck.status to see available tools'],
|
|
1163
|
+
});
|
|
1097
1164
|
} catch (err) {
|
|
1098
|
-
//
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1165
|
+
// ====================================================================
|
|
1166
|
+
// HARDENING: Enhanced error handling with sanitization
|
|
1167
|
+
// ====================================================================
|
|
1168
|
+
const durationMs = Date.now() - startTime;
|
|
1169
|
+
const errorMessage = sanitizeString(err?.message || 'Unknown error', 500);
|
|
1170
|
+
|
|
1171
|
+
// Emit audit event for tool error (safely)
|
|
1172
|
+
try {
|
|
1173
|
+
emitToolComplete(toolName, "error", {
|
|
1174
|
+
errorMessage: redactSensitive(errorMessage),
|
|
1175
|
+
durationMs,
|
|
1176
|
+
});
|
|
1177
|
+
} catch {
|
|
1178
|
+
// Audit logging should never break the response
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Log error details to stderr (not stdout - preserves MCP protocol)
|
|
1182
|
+
console.error(`[MCP] Tool ${toolName} failed after ${durationMs}ms: ${errorMessage}`);
|
|
1183
|
+
|
|
1184
|
+
return this.error(`${toolName} failed: ${redactSensitive(errorMessage)}`, {
|
|
1185
|
+
code: err?.code || 'TOOL_ERROR',
|
|
1186
|
+
suggestion: 'Check the error message and try again',
|
|
1187
|
+
nextSteps: [
|
|
1188
|
+
'Verify the tool arguments are correct',
|
|
1189
|
+
'Check that the project path is valid',
|
|
1190
|
+
'Try running with simpler arguments first',
|
|
1191
|
+
],
|
|
1102
1192
|
});
|
|
1103
|
-
return this.error(`${name} failed: ${err.message}`);
|
|
1104
1193
|
}
|
|
1105
1194
|
});
|
|
1106
1195
|
|
|
@@ -1161,15 +1250,41 @@ class VibecheckMCP {
|
|
|
1161
1250
|
this.server.setRequestHandler(
|
|
1162
1251
|
ReadResourceRequestSchema,
|
|
1163
1252
|
async (request) => {
|
|
1164
|
-
|
|
1253
|
+
// ====================================================================
|
|
1254
|
+
// HARDENING: Resource request validation
|
|
1255
|
+
// ====================================================================
|
|
1256
|
+
const uri = sanitizeString(request?.params?.uri, 200);
|
|
1257
|
+
if (!uri || !uri.startsWith('vibecheck://')) {
|
|
1258
|
+
return { contents: [{ uri: uri || '', mimeType: "application/json", text: '{"error": "Invalid resource URI"}' }] };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1165
1261
|
const projectPath = process.cwd();
|
|
1262
|
+
|
|
1263
|
+
// Helper to safely read and return JSON resource
|
|
1264
|
+
const safeReadResource = async (filePath, defaultMessage) => {
|
|
1265
|
+
try {
|
|
1266
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1267
|
+
// Validate JSON and sanitize
|
|
1268
|
+
const parsed = safeJsonParse(content);
|
|
1269
|
+
if (!parsed.success) {
|
|
1270
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "Invalid JSON in resource file" }) }] };
|
|
1271
|
+
}
|
|
1272
|
+
// Redact any sensitive data and truncate
|
|
1273
|
+
const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
1274
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1275
|
+
} catch {
|
|
1276
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ message: defaultMessage }) }] };
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1166
1279
|
|
|
1167
1280
|
if (uri === "vibecheck://config") {
|
|
1168
1281
|
const configPath = path.join(projectPath, "vibecheck.config.json");
|
|
1169
1282
|
try {
|
|
1170
1283
|
const content = await fs.readFile(configPath, "utf-8");
|
|
1284
|
+
// Redact sensitive config values
|
|
1285
|
+
const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
1171
1286
|
return {
|
|
1172
|
-
contents: [{ uri, mimeType: "application/json", text:
|
|
1287
|
+
contents: [{ uri, mimeType: "application/json", text: sanitized }],
|
|
1173
1288
|
};
|
|
1174
1289
|
} catch {
|
|
1175
1290
|
return {
|
|
@@ -1179,141 +1294,161 @@ class VibecheckMCP {
|
|
|
1179
1294
|
}
|
|
1180
1295
|
|
|
1181
1296
|
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
|
-
}
|
|
1297
|
+
const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
|
|
1298
|
+
return await safeReadResource(summaryPath, "No scan found. Run vibecheck.scan first.");
|
|
1203
1299
|
}
|
|
1204
1300
|
|
|
1205
1301
|
if (uri === "vibecheck://truthpack") {
|
|
1206
1302
|
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
|
-
}
|
|
1303
|
+
return await safeReadResource(truthpackPath, "No truthpack. Run vibecheck.ctx first.");
|
|
1213
1304
|
}
|
|
1214
1305
|
|
|
1215
1306
|
if (uri === "vibecheck://missions") {
|
|
1216
1307
|
const missionsDir = path.join(projectPath, ".vibecheck", "missions");
|
|
1217
1308
|
try {
|
|
1309
|
+
// HARDENING: Validate directory read
|
|
1218
1310
|
const dirs = await fs.readdir(missionsDir);
|
|
1219
|
-
const
|
|
1311
|
+
const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
|
|
1312
|
+
const latest = safeDirs.sort().reverse()[0];
|
|
1313
|
+
|
|
1220
1314
|
if (latest) {
|
|
1221
1315
|
const missionPath = path.join(missionsDir, latest, "missions.json");
|
|
1222
|
-
|
|
1223
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1316
|
+
return await safeReadResource(missionPath, "No missions found in latest directory.");
|
|
1224
1317
|
}
|
|
1225
|
-
} catch {
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
console.error(`[MCP] Error reading missions: ${err.message}`);
|
|
1320
|
+
}
|
|
1226
1321
|
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No missions. Run vibecheck.fix first."}' }] };
|
|
1227
1322
|
}
|
|
1228
1323
|
|
|
1229
1324
|
if (uri === "vibecheck://reality") {
|
|
1230
1325
|
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
|
-
}
|
|
1326
|
+
return await safeReadResource(realityPath, "No reality results. Run vibecheck verify first.");
|
|
1237
1327
|
}
|
|
1238
1328
|
|
|
1239
1329
|
if (uri === "vibecheck://findings") {
|
|
1240
1330
|
const findingsPath = path.join(projectPath, ".vibecheck", "findings.json");
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1331
|
+
|
|
1332
|
+
// Try primary findings file
|
|
1333
|
+
const content = await this.safeReadFile(findingsPath, 10 * 1024 * 1024);
|
|
1334
|
+
if (content) {
|
|
1335
|
+
const parsed = safeJsonParse(content);
|
|
1336
|
+
if (parsed.success) {
|
|
1337
|
+
const sanitized = redactSensitive(truncateOutput(content));
|
|
1338
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// HARDENING: Try summary.json as fallback with size limits
|
|
1343
|
+
const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
|
|
1344
|
+
const summaryContent = await this.safeReadFile(summaryPath, 10 * 1024 * 1024);
|
|
1345
|
+
|
|
1346
|
+
if (summaryContent) {
|
|
1347
|
+
const parsed = safeJsonParse(summaryContent);
|
|
1348
|
+
if (parsed.success && parsed.data.findings) {
|
|
1349
|
+
const findings = sanitizeArray(parsed.data.findings, 1000);
|
|
1250
1350
|
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
1351
|
}
|
|
1254
1352
|
}
|
|
1353
|
+
|
|
1354
|
+
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
|
|
1255
1355
|
}
|
|
1256
1356
|
|
|
1257
1357
|
if (uri === "vibecheck://share") {
|
|
1258
1358
|
const missionsDir = path.join(projectPath, ".vibecheck", "missions");
|
|
1259
1359
|
try {
|
|
1360
|
+
// HARDENING: Safe directory read
|
|
1260
1361
|
const dirs = await fs.readdir(missionsDir);
|
|
1261
|
-
const
|
|
1362
|
+
const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
|
|
1363
|
+
const latest = safeDirs.sort().reverse()[0];
|
|
1364
|
+
|
|
1262
1365
|
if (latest) {
|
|
1263
1366
|
const sharePath = path.join(missionsDir, latest, "share", "share.json");
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1367
|
+
|
|
1368
|
+
// Try share.json first
|
|
1369
|
+
const shareContent = await this.safeReadFile(sharePath, 10 * 1024 * 1024);
|
|
1370
|
+
if (shareContent) {
|
|
1371
|
+
const parsed = safeJsonParse(shareContent);
|
|
1372
|
+
if (parsed.success) {
|
|
1373
|
+
const sanitized = redactSensitive(truncateOutput(shareContent));
|
|
1374
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1375
|
+
}
|
|
1272
1376
|
}
|
|
1377
|
+
|
|
1378
|
+
// HARDENING: Fallback to missions.json with safe read
|
|
1379
|
+
const missionPath = path.join(missionsDir, latest, "missions.json");
|
|
1380
|
+
return await safeReadResource(missionPath, "No share data available in latest mission.");
|
|
1273
1381
|
}
|
|
1274
|
-
} catch {
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
console.error(`[MCP] Error reading share pack: ${err.message}`);
|
|
1384
|
+
}
|
|
1275
1385
|
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No share pack. Run vibecheck.fix --share first."}' }] };
|
|
1276
1386
|
}
|
|
1277
1387
|
|
|
1278
1388
|
if (uri === "vibecheck://prove") {
|
|
1279
1389
|
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
|
-
}
|
|
1390
|
+
return await safeReadResource(provePath, "No prove results. Run vibecheck.prove first.");
|
|
1286
1391
|
}
|
|
1287
1392
|
|
|
1288
|
-
return {
|
|
1393
|
+
return {
|
|
1394
|
+
contents: [{
|
|
1395
|
+
uri,
|
|
1396
|
+
mimeType: "application/json",
|
|
1397
|
+
text: JSON.stringify({ error: "Unknown resource URI" })
|
|
1398
|
+
}]
|
|
1399
|
+
};
|
|
1289
1400
|
},
|
|
1290
1401
|
);
|
|
1291
1402
|
}
|
|
1292
1403
|
|
|
1293
|
-
//
|
|
1404
|
+
// ============================================================================
|
|
1405
|
+
// HARDENED HELPERS
|
|
1406
|
+
// ============================================================================
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Return a successful response with sanitization
|
|
1410
|
+
* @param {string} text - Response text
|
|
1411
|
+
* @param {boolean} includeAttribution - Include vibecheck attribution
|
|
1412
|
+
* @returns {object} MCP response
|
|
1413
|
+
*/
|
|
1294
1414
|
success(text, includeAttribution = true) {
|
|
1415
|
+
// Sanitize output: redact secrets and truncate
|
|
1416
|
+
let sanitized = redactSensitive(sanitizeString(text, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
1417
|
+
|
|
1295
1418
|
const finalText = includeAttribution
|
|
1296
|
-
? `${
|
|
1297
|
-
:
|
|
1419
|
+
? `${sanitized}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
|
|
1420
|
+
: sanitized;
|
|
1421
|
+
|
|
1298
1422
|
return { content: [{ type: "text", text: finalText }] };
|
|
1299
1423
|
}
|
|
1300
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* Return an error response with sanitization
|
|
1427
|
+
* @param {string} text - Error message
|
|
1428
|
+
* @param {object} options - Additional options
|
|
1429
|
+
* @returns {object} MCP error response
|
|
1430
|
+
*/
|
|
1301
1431
|
error(text, options = {}) {
|
|
1302
1432
|
const { code, suggestion, nextSteps = [] } = options;
|
|
1303
1433
|
|
|
1304
|
-
|
|
1434
|
+
// Sanitize all text inputs
|
|
1435
|
+
const sanitizedText = redactSensitive(sanitizeString(text, 1000));
|
|
1436
|
+
const sanitizedSuggestion = suggestion ? redactSensitive(sanitizeString(suggestion, 500)) : null;
|
|
1437
|
+
const sanitizedSteps = sanitizeArray(nextSteps, 10).map(s => sanitizeString(s, 200));
|
|
1438
|
+
|
|
1439
|
+
let errorText = `❌ ${sanitizedText}`;
|
|
1305
1440
|
|
|
1306
1441
|
if (code) {
|
|
1307
|
-
errorText += `\n\n**Error Code:** \`${code}\``;
|
|
1442
|
+
errorText += `\n\n**Error Code:** \`${sanitizeString(code, 50)}\``;
|
|
1308
1443
|
}
|
|
1309
1444
|
|
|
1310
|
-
if (
|
|
1311
|
-
errorText += `\n\n💡 **Suggestion:** ${
|
|
1445
|
+
if (sanitizedSuggestion) {
|
|
1446
|
+
errorText += `\n\n💡 **Suggestion:** ${sanitizedSuggestion}`;
|
|
1312
1447
|
}
|
|
1313
1448
|
|
|
1314
|
-
if (
|
|
1449
|
+
if (sanitizedSteps.length > 0) {
|
|
1315
1450
|
errorText += `\n\n**Next Steps:**\n`;
|
|
1316
|
-
|
|
1451
|
+
sanitizedSteps.forEach((step, i) => {
|
|
1317
1452
|
errorText += `${i + 1}. ${step}\n`;
|
|
1318
1453
|
});
|
|
1319
1454
|
}
|
|
@@ -1366,28 +1501,53 @@ class VibecheckMCP {
|
|
|
1366
1501
|
});
|
|
1367
1502
|
}
|
|
1368
1503
|
|
|
1369
|
-
|
|
1370
|
-
const
|
|
1504
|
+
// HARDENING: Validate and sanitize profile
|
|
1505
|
+
const validProfiles = ["quick", "full", "ship", "ci", "security", "compliance", "ai"];
|
|
1506
|
+
const profile = validProfiles.includes(args?.profile) ? args?.profile : "quick";
|
|
1507
|
+
|
|
1508
|
+
// HARDENING: Sanitize only array
|
|
1509
|
+
const only = sanitizeArray(args?.only, 20).map(item => sanitizeString(item, 50));
|
|
1371
1510
|
|
|
1372
|
-
// Initialize API integration
|
|
1511
|
+
// Initialize API integration with timeout and circuit breaker
|
|
1373
1512
|
let apiScan = null;
|
|
1374
1513
|
let apiConnected = false;
|
|
1375
1514
|
|
|
1515
|
+
// HARDENING: Check circuit breaker before attempting API calls
|
|
1516
|
+
const circuitCheck = checkCircuitBreaker();
|
|
1517
|
+
|
|
1376
1518
|
// Try to connect to API for dashboard integration
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1519
|
+
if (circuitCheck.allowed) {
|
|
1520
|
+
try {
|
|
1521
|
+
// HARDENING: Add timeout to API availability check
|
|
1522
|
+
const apiCheckPromise = isApiAvailable();
|
|
1523
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
1524
|
+
setTimeout(() => reject(new Error('API check timeout')), 5000)
|
|
1525
|
+
);
|
|
1526
|
+
|
|
1527
|
+
apiConnected = await Promise.race([apiCheckPromise, timeoutPromise]);
|
|
1528
|
+
|
|
1529
|
+
if (apiConnected) {
|
|
1530
|
+
// Create scan record in dashboard
|
|
1531
|
+
const createScanPromise = createScan({
|
|
1532
|
+
localPath: sanitizeString(projectPath, 500),
|
|
1533
|
+
branch: sanitizeString(args?.branch, 100) || 'main',
|
|
1534
|
+
enableLLM: false,
|
|
1535
|
+
});
|
|
1536
|
+
const scanTimeoutPromise = new Promise((_, reject) =>
|
|
1537
|
+
setTimeout(() => reject(new Error('Create scan timeout')), 10000)
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
apiScan = await Promise.race([createScanPromise, scanTimeoutPromise]);
|
|
1541
|
+
console.error(`[MCP] Connected to dashboard (Scan ID: ${apiScan.scanId})`);
|
|
1542
|
+
recordApiResult(true); // Record success
|
|
1543
|
+
}
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
// API connection is optional, continue without it
|
|
1546
|
+
console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
|
|
1547
|
+
recordApiResult(false); // Record failure
|
|
1387
1548
|
}
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
|
|
1549
|
+
} else {
|
|
1550
|
+
console.error(`[MCP] ${circuitCheck.reason}`);
|
|
1391
1551
|
}
|
|
1392
1552
|
|
|
1393
1553
|
let output = "# 🔍 vibecheck Scan\n\n";
|
|
@@ -1396,7 +1556,9 @@ class VibecheckMCP {
|
|
|
1396
1556
|
|
|
1397
1557
|
// Build CLI arguments array (secure - no injection possible)
|
|
1398
1558
|
const cliArgs = [`--profile=${profile}`, "--json"];
|
|
1399
|
-
if (only
|
|
1559
|
+
if (only.length > 0) {
|
|
1560
|
+
cliArgs.push(`--only=${only.join(",")}`);
|
|
1561
|
+
}
|
|
1400
1562
|
|
|
1401
1563
|
try {
|
|
1402
1564
|
await this.runCLI("scan", cliArgs, projectPath, { timeout: CONFIG.TIMEOUTS.SCAN });
|
|
@@ -1409,19 +1571,25 @@ class VibecheckMCP {
|
|
|
1409
1571
|
// Submit results to dashboard if connected
|
|
1410
1572
|
if (apiConnected && apiScan) {
|
|
1411
1573
|
try {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1574
|
+
// HARDENING: Add timeout to result submission
|
|
1575
|
+
const submitPromise = submitScanResults(apiScan.scanId, {
|
|
1576
|
+
verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
|
|
1577
|
+
score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
|
|
1578
|
+
findings: sanitizeArray(summary.findings, 1000) || [],
|
|
1579
|
+
filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
|
|
1580
|
+
linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
|
|
1581
|
+
durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
|
|
1419
1582
|
metadata: {
|
|
1420
1583
|
profile,
|
|
1421
1584
|
source: 'mcp-server',
|
|
1422
1585
|
version: CONFIG.VERSION,
|
|
1423
1586
|
},
|
|
1424
1587
|
});
|
|
1588
|
+
const submitTimeout = new Promise((_, reject) =>
|
|
1589
|
+
setTimeout(() => reject(new Error('Submit timeout')), 10000)
|
|
1590
|
+
);
|
|
1591
|
+
|
|
1592
|
+
await Promise.race([submitPromise, submitTimeout]);
|
|
1425
1593
|
console.error(`[MCP] Results sent to dashboard`);
|
|
1426
1594
|
} catch (err) {
|
|
1427
1595
|
console.error(`[MCP] Failed to send results to dashboard: ${err.message}`);
|
|
@@ -1439,23 +1607,29 @@ class VibecheckMCP {
|
|
|
1439
1607
|
// Submit results to dashboard if connected
|
|
1440
1608
|
if (apiConnected && apiScan) {
|
|
1441
1609
|
try {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1610
|
+
// HARDENING: Add timeout to error case submission
|
|
1611
|
+
const submitPromise = submitScanResults(apiScan.scanId, {
|
|
1612
|
+
verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
|
|
1613
|
+
score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
|
|
1614
|
+
findings: sanitizeArray(summary.findings, 1000) || [],
|
|
1615
|
+
filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
|
|
1616
|
+
linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
|
|
1617
|
+
durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
|
|
1449
1618
|
metadata: {
|
|
1450
1619
|
profile,
|
|
1451
1620
|
source: 'mcp-server',
|
|
1452
1621
|
version: CONFIG.VERSION,
|
|
1453
|
-
error: err.message,
|
|
1622
|
+
error: sanitizeString(err.message, 500),
|
|
1454
1623
|
},
|
|
1455
1624
|
});
|
|
1625
|
+
const submitTimeout = new Promise((_, reject) =>
|
|
1626
|
+
setTimeout(() => reject(new Error('Submit timeout')), 10000)
|
|
1627
|
+
);
|
|
1628
|
+
|
|
1629
|
+
await Promise.race([submitPromise, submitTimeout]);
|
|
1456
1630
|
console.error(`[MCP] Results sent to dashboard (with error)`);
|
|
1457
|
-
} catch (
|
|
1458
|
-
console.error(`[MCP] Failed to send results to dashboard: ${
|
|
1631
|
+
} catch (apiErr) {
|
|
1632
|
+
console.error(`[MCP] Failed to send results to dashboard: ${apiErr.message}`);
|
|
1459
1633
|
}
|
|
1460
1634
|
}
|
|
1461
1635
|
output += `\n⚠️ Scan completed with findings (exit code ${err.code || 1})\n`;
|
|
@@ -1465,10 +1639,16 @@ class VibecheckMCP {
|
|
|
1465
1639
|
// Report error to dashboard if connected
|
|
1466
1640
|
if (apiConnected && apiScan) {
|
|
1467
1641
|
try {
|
|
1468
|
-
|
|
1642
|
+
// HARDENING: Add timeout to error reporting
|
|
1643
|
+
const reportPromise = reportScanError(apiScan.scanId, err);
|
|
1644
|
+
const reportTimeout = new Promise((_, reject) =>
|
|
1645
|
+
setTimeout(() => reject(new Error('Report timeout')), 10000)
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
await Promise.race([reportPromise, reportTimeout]);
|
|
1469
1649
|
console.error(`[MCP] Error reported to dashboard`);
|
|
1470
|
-
} catch (
|
|
1471
|
-
console.error(`[MCP] Failed to report error to dashboard: ${
|
|
1650
|
+
} catch (apiErr) {
|
|
1651
|
+
console.error(`[MCP] Failed to report error to dashboard: ${apiErr.message}`);
|
|
1472
1652
|
}
|
|
1473
1653
|
}
|
|
1474
1654
|
|
|
@@ -1490,7 +1670,7 @@ class VibecheckMCP {
|
|
|
1490
1670
|
// ============================================================================
|
|
1491
1671
|
async handleGate(projectPath, args) {
|
|
1492
1672
|
// Check tier access (STARTER tier required)
|
|
1493
|
-
const access = await
|
|
1673
|
+
const access = await getFeatureAccessStatus("gate", args?.apiKey);
|
|
1494
1674
|
if (!access.hasAccess) {
|
|
1495
1675
|
return {
|
|
1496
1676
|
content: [{
|
|
@@ -1530,7 +1710,7 @@ class VibecheckMCP {
|
|
|
1530
1710
|
async handleFix(projectPath, args) {
|
|
1531
1711
|
// Check tier access for --apply and --autopilot (PRO tier required)
|
|
1532
1712
|
if (args?.apply || args?.autopilot) {
|
|
1533
|
-
const access = await
|
|
1713
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
1534
1714
|
if (!access.hasAccess) {
|
|
1535
1715
|
return {
|
|
1536
1716
|
content: [{
|
|
@@ -1705,7 +1885,7 @@ class VibecheckMCP {
|
|
|
1705
1885
|
// ============================================================================
|
|
1706
1886
|
async handleProve(projectPath, args) {
|
|
1707
1887
|
// Check tier access (PRO tier required)
|
|
1708
|
-
const access = await
|
|
1888
|
+
const access = await getFeatureAccessStatus("prove", args?.apiKey);
|
|
1709
1889
|
if (!access.hasAccess) {
|
|
1710
1890
|
return {
|
|
1711
1891
|
content: [{
|
|
@@ -1908,6 +2088,16 @@ class VibecheckMCP {
|
|
|
1908
2088
|
// SHIP - Quick health check
|
|
1909
2089
|
// ============================================================================
|
|
1910
2090
|
async handleShip(projectPath, args) {
|
|
2091
|
+
// HARDENING: Validate project path
|
|
2092
|
+
const validation = this.validateProjectPath(projectPath);
|
|
2093
|
+
if (!validation.valid) {
|
|
2094
|
+
return this.error(validation.error, {
|
|
2095
|
+
code: validation.code || "INVALID_PATH",
|
|
2096
|
+
suggestion: validation.suggestion,
|
|
2097
|
+
nextSteps: validation.nextSteps || [],
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1911
2101
|
let output = "# 🚀 vibecheck Ship\n\n";
|
|
1912
2102
|
output += `**Path:** ${projectPath}\n\n`;
|
|
1913
2103
|
|
|
@@ -1932,20 +2122,34 @@ class VibecheckMCP {
|
|
|
1932
2122
|
// VERIFY - Runtime browser testing
|
|
1933
2123
|
// ============================================================================
|
|
1934
2124
|
async handleVerify(projectPath, args) {
|
|
1935
|
-
|
|
1936
|
-
|
|
2125
|
+
// HARDENING: Validate URL
|
|
2126
|
+
const urlValidation = validateUrl(args?.url);
|
|
2127
|
+
if (!urlValidation.valid) {
|
|
2128
|
+
return this.error(urlValidation.error, {
|
|
2129
|
+
code: 'INVALID_URL',
|
|
2130
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2131
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
const url = urlValidation.url;
|
|
1937
2135
|
|
|
1938
2136
|
let output = "# 🧪 vibecheck Verify\n\n";
|
|
1939
2137
|
output += `**URL:** ${url}\n`;
|
|
1940
|
-
|
|
2138
|
+
|
|
2139
|
+
// HARDENING: Sanitize array inputs
|
|
2140
|
+
const flows = sanitizeArray(args?.flows, 10);
|
|
2141
|
+
if (flows.length) output += `**Flows:** ${flows.join(", ")}\n`;
|
|
1941
2142
|
if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
|
|
1942
2143
|
if (args?.record) output += `**Recording:** Enabled\n`;
|
|
1943
2144
|
output += "\n";
|
|
1944
2145
|
|
|
1945
2146
|
// Build CLI arguments array (secure)
|
|
1946
2147
|
const cliArgs = ["--url", url];
|
|
1947
|
-
|
|
1948
|
-
if (args?.
|
|
2148
|
+
// HARDENING: Sanitize auth - don't log full credentials
|
|
2149
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2150
|
+
cliArgs.push("--auth", sanitizeString(args.auth, 200));
|
|
2151
|
+
}
|
|
2152
|
+
if (flows.length) cliArgs.push("--flows", flows.join(","));
|
|
1949
2153
|
if (args?.headed) cliArgs.push("--headed");
|
|
1950
2154
|
if (args?.record) cliArgs.push("--record");
|
|
1951
2155
|
|
|
@@ -1995,28 +2199,67 @@ class VibecheckMCP {
|
|
|
1995
2199
|
// REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
|
|
1996
2200
|
// ============================================================================
|
|
1997
2201
|
async handleReality(projectPath, args) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2202
|
+
// HARDENING: Validate URL
|
|
2203
|
+
const urlValidation = validateUrl(args?.url);
|
|
2204
|
+
if (!urlValidation.valid) {
|
|
2205
|
+
return this.error(urlValidation.error, {
|
|
2206
|
+
code: 'INVALID_URL',
|
|
2207
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2208
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
const url = urlValidation.url;
|
|
2000
2212
|
|
|
2001
2213
|
let output = "# 🧪 vibecheck Reality v2\n\n";
|
|
2002
2214
|
output += `**URL:** ${url}\n`;
|
|
2003
2215
|
output += `**Two-Pass Auth:** ${args?.verifyAuth ? "Yes" : "No"}\n`;
|
|
2004
|
-
|
|
2005
|
-
|
|
2216
|
+
|
|
2217
|
+
// HARDENING: Safely display auth info (mask password)
|
|
2218
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2219
|
+
const authParts = args.auth.split(":");
|
|
2220
|
+
const maskedAuth = authParts[0] ? `${authParts[0].slice(0, 20)}:***` : '***';
|
|
2221
|
+
output += `**Auth:** ${maskedAuth}\n`;
|
|
2222
|
+
}
|
|
2223
|
+
if (args?.storageState) output += `**Storage State:** ${sanitizeString(args.storageState, 100)}\n`;
|
|
2006
2224
|
if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
|
|
2007
2225
|
if (args?.danger) output += `**Danger Mode:** Enabled (risky clicks allowed)\n`;
|
|
2008
2226
|
output += "\n";
|
|
2009
2227
|
|
|
2010
2228
|
// Build CLI arguments array (secure)
|
|
2011
2229
|
const cliArgs = ["--url", url];
|
|
2012
|
-
if (args?.auth
|
|
2230
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2231
|
+
cliArgs.push("--auth", sanitizeString(args.auth, 200));
|
|
2232
|
+
}
|
|
2013
2233
|
if (args?.verifyAuth) cliArgs.push("--verify-auth");
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
if (args?.
|
|
2234
|
+
|
|
2235
|
+
// HARDENING: Validate path arguments
|
|
2236
|
+
if (args?.storageState) {
|
|
2237
|
+
const pathCheck = sanitizePath(args.storageState, projectPath);
|
|
2238
|
+
if (pathCheck.valid) {
|
|
2239
|
+
cliArgs.push("--storage-state", pathCheck.path);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (args?.saveStorageState) {
|
|
2243
|
+
const pathCheck = sanitizePath(args.saveStorageState, projectPath);
|
|
2244
|
+
if (pathCheck.valid) {
|
|
2245
|
+
cliArgs.push("--save-storage-state", pathCheck.path);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
if (args?.truthpack) {
|
|
2249
|
+
const pathCheck = sanitizePath(args.truthpack, projectPath);
|
|
2250
|
+
if (pathCheck.valid) {
|
|
2251
|
+
cliArgs.push("--truthpack", pathCheck.path);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2017
2254
|
if (args?.headed) cliArgs.push("--headed");
|
|
2018
|
-
|
|
2019
|
-
|
|
2255
|
+
|
|
2256
|
+
// HARDENING: Bound numeric arguments
|
|
2257
|
+
if (args?.maxPages) {
|
|
2258
|
+
cliArgs.push("--max-pages", String(sanitizeNumber(args.maxPages, 1, 100, 18)));
|
|
2259
|
+
}
|
|
2260
|
+
if (args?.maxDepth) {
|
|
2261
|
+
cliArgs.push("--max-depth", String(sanitizeNumber(args.maxDepth, 1, 10, 2)));
|
|
2262
|
+
}
|
|
2020
2263
|
if (args?.danger) cliArgs.push("--danger");
|
|
2021
2264
|
|
|
2022
2265
|
try {
|
|
@@ -2097,16 +2340,27 @@ class VibecheckMCP {
|
|
|
2097
2340
|
// AI-TEST - AI Agent testing
|
|
2098
2341
|
// ============================================================================
|
|
2099
2342
|
async handleAITest(projectPath, args) {
|
|
2100
|
-
|
|
2101
|
-
|
|
2343
|
+
// HARDENING: Validate URL
|
|
2344
|
+
const urlValidation = validateUrl(args?.url);
|
|
2345
|
+
if (!urlValidation.valid) {
|
|
2346
|
+
return this.error(urlValidation.error, {
|
|
2347
|
+
code: 'INVALID_URL',
|
|
2348
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2349
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
const url = urlValidation.url;
|
|
2353
|
+
|
|
2354
|
+
// HARDENING: Sanitize goal string
|
|
2355
|
+
const goal = sanitizeString(args?.goal, 500) || "Test all features";
|
|
2102
2356
|
|
|
2103
2357
|
let output = "# 🤖 vibecheck AI Agent\n\n";
|
|
2104
2358
|
output += `**URL:** ${url}\n`;
|
|
2105
|
-
output += `**Goal:** ${
|
|
2359
|
+
output += `**Goal:** ${goal}\n\n`;
|
|
2106
2360
|
|
|
2107
2361
|
// Build CLI arguments array (secure)
|
|
2108
2362
|
const cliArgs = ["--url", url];
|
|
2109
|
-
if (
|
|
2363
|
+
if (goal) cliArgs.push("--goal", goal);
|
|
2110
2364
|
if (args?.headed) cliArgs.push("--headed");
|
|
2111
2365
|
|
|
2112
2366
|
try {
|
|
@@ -2162,7 +2416,7 @@ class VibecheckMCP {
|
|
|
2162
2416
|
// ============================================================================
|
|
2163
2417
|
async handleAutopilotPlan(projectPath, args) {
|
|
2164
2418
|
// Check tier access (PRO tier required)
|
|
2165
|
-
const access = await
|
|
2419
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
2166
2420
|
if (!access.hasAccess) {
|
|
2167
2421
|
return {
|
|
2168
2422
|
content: [{
|
|
@@ -2249,7 +2503,7 @@ class VibecheckMCP {
|
|
|
2249
2503
|
// ============================================================================
|
|
2250
2504
|
async handleAutopilotApply(projectPath, args) {
|
|
2251
2505
|
// Check tier access (PRO tier required)
|
|
2252
|
-
const access = await
|
|
2506
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
2253
2507
|
if (!access.hasAccess) {
|
|
2254
2508
|
return {
|
|
2255
2509
|
content: [{
|
|
@@ -2310,7 +2564,7 @@ class VibecheckMCP {
|
|
|
2310
2564
|
// ============================================================================
|
|
2311
2565
|
async handleBadge(projectPath, args) {
|
|
2312
2566
|
// Check tier access (STARTER tier required)
|
|
2313
|
-
const access = await
|
|
2567
|
+
const access = await getFeatureAccessStatus("badge", args?.apiKey);
|
|
2314
2568
|
if (!access.hasAccess) {
|
|
2315
2569
|
return {
|
|
2316
2570
|
content: [{
|
|
@@ -2466,15 +2720,57 @@ class VibecheckMCP {
|
|
|
2466
2720
|
}
|
|
2467
2721
|
|
|
2468
2722
|
// ============================================================================
|
|
2469
|
-
// RUN
|
|
2723
|
+
// RUN - with graceful shutdown handling
|
|
2470
2724
|
// ============================================================================
|
|
2471
2725
|
async run() {
|
|
2472
2726
|
const transport = new StdioServerTransport();
|
|
2727
|
+
|
|
2728
|
+
// ========================================================================
|
|
2729
|
+
// HARDENING: Graceful shutdown handling
|
|
2730
|
+
// ========================================================================
|
|
2731
|
+
const shutdown = async (signal) => {
|
|
2732
|
+
console.error(`\n[MCP] Received ${signal}, shutting down gracefully...`);
|
|
2733
|
+
try {
|
|
2734
|
+
// Clear rate limit state to prevent memory leaks
|
|
2735
|
+
rateLimitState.calls = [];
|
|
2736
|
+
|
|
2737
|
+
// Close server connection
|
|
2738
|
+
await this.server.close();
|
|
2739
|
+
console.error('[MCP] Server closed successfully');
|
|
2740
|
+
} catch (err) {
|
|
2741
|
+
console.error(`[MCP] Error during shutdown: ${err.message}`);
|
|
2742
|
+
}
|
|
2743
|
+
process.exit(0);
|
|
2744
|
+
};
|
|
2745
|
+
|
|
2746
|
+
// Handle termination signals
|
|
2747
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
2748
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
2749
|
+
|
|
2750
|
+
// Handle uncaught errors gracefully
|
|
2751
|
+
process.on('uncaughtException', (err) => {
|
|
2752
|
+
console.error(`[MCP] Uncaught exception: ${err.message}`);
|
|
2753
|
+
console.error(err.stack);
|
|
2754
|
+
// Don't exit - try to keep running
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
2758
|
+
console.error(`[MCP] Unhandled rejection at:`, promise);
|
|
2759
|
+
console.error(`[MCP] Reason:`, reason);
|
|
2760
|
+
// Don't exit - try to keep running
|
|
2761
|
+
});
|
|
2762
|
+
|
|
2473
2763
|
await this.server.connect(transport);
|
|
2474
|
-
console.error(
|
|
2764
|
+
console.error(`vibecheck MCP Server v${VERSION} running on stdio (hardened)`);
|
|
2475
2765
|
}
|
|
2476
2766
|
}
|
|
2477
2767
|
|
|
2478
|
-
//
|
|
2768
|
+
// ============================================================================
|
|
2769
|
+
// MAIN - with error handling
|
|
2770
|
+
// ============================================================================
|
|
2479
2771
|
const server = new VibecheckMCP();
|
|
2480
|
-
server.run().catch(
|
|
2772
|
+
server.run().catch((err) => {
|
|
2773
|
+
console.error(`[MCP] Fatal error starting server: ${err.message}`);
|
|
2774
|
+
console.error(err.stack);
|
|
2775
|
+
process.exit(1);
|
|
2776
|
+
});
|