@vibecheckai/cli 3.2.6 → 3.3.0

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