@vibecheckai/cli 3.2.5 → 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 (197) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +192 -5
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  6. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  7. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  8. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  11. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  12. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  13. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  14. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  15. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  16. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  17. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  18. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  19. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  20. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  21. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  22. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  23. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  24. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  25. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  26. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  27. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  28. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  29. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  30. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  31. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  32. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  35. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  36. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  37. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  38. package/bin/runners/lib/analyzers.js +81 -18
  39. package/bin/runners/lib/api-client.js +269 -0
  40. package/bin/runners/lib/auth-truth.js +193 -193
  41. package/bin/runners/lib/authority-badge.js +425 -0
  42. package/bin/runners/lib/backup.js +62 -62
  43. package/bin/runners/lib/billing.js +107 -107
  44. package/bin/runners/lib/claims.js +118 -118
  45. package/bin/runners/lib/cli-output.js +7 -1
  46. package/bin/runners/lib/cli-ui.js +540 -540
  47. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  48. package/bin/runners/lib/contracts/env-contract.js +181 -181
  49. package/bin/runners/lib/contracts/external-contract.js +206 -206
  50. package/bin/runners/lib/contracts/guard.js +168 -168
  51. package/bin/runners/lib/contracts/index.js +89 -89
  52. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  53. package/bin/runners/lib/contracts/route-contract.js +199 -199
  54. package/bin/runners/lib/contracts.js +804 -804
  55. package/bin/runners/lib/detect.js +89 -89
  56. package/bin/runners/lib/doctor/autofix.js +254 -254
  57. package/bin/runners/lib/doctor/index.js +37 -37
  58. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  59. package/bin/runners/lib/doctor/modules/index.js +46 -46
  60. package/bin/runners/lib/doctor/modules/network.js +250 -250
  61. package/bin/runners/lib/doctor/modules/project.js +312 -312
  62. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  63. package/bin/runners/lib/doctor/modules/security.js +348 -348
  64. package/bin/runners/lib/doctor/modules/system.js +213 -213
  65. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  66. package/bin/runners/lib/doctor/reporter.js +262 -262
  67. package/bin/runners/lib/doctor/service.js +262 -262
  68. package/bin/runners/lib/doctor/types.js +113 -113
  69. package/bin/runners/lib/doctor/ui.js +263 -263
  70. package/bin/runners/lib/doctor-v2.js +608 -608
  71. package/bin/runners/lib/drift.js +425 -425
  72. package/bin/runners/lib/enforcement.js +72 -72
  73. package/bin/runners/lib/enterprise-detect.js +603 -603
  74. package/bin/runners/lib/enterprise-init.js +942 -942
  75. package/bin/runners/lib/env-resolver.js +417 -417
  76. package/bin/runners/lib/env-template.js +66 -66
  77. package/bin/runners/lib/env.js +189 -189
  78. package/bin/runners/lib/error-handler.js +16 -9
  79. package/bin/runners/lib/exit-codes.js +275 -0
  80. package/bin/runners/lib/extractors/client-calls.js +990 -990
  81. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  82. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  83. package/bin/runners/lib/extractors/index.js +363 -363
  84. package/bin/runners/lib/extractors/next-routes.js +524 -524
  85. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  86. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  87. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  88. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  89. package/bin/runners/lib/findings-schema.js +281 -281
  90. package/bin/runners/lib/firewall-prompt.js +50 -50
  91. package/bin/runners/lib/global-flags.js +37 -0
  92. package/bin/runners/lib/graph/graph-builder.js +265 -265
  93. package/bin/runners/lib/graph/html-renderer.js +413 -413
  94. package/bin/runners/lib/graph/index.js +32 -32
  95. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  96. package/bin/runners/lib/graph/static-extractor.js +518 -518
  97. package/bin/runners/lib/help-formatter.js +413 -0
  98. package/bin/runners/lib/html-report.js +650 -650
  99. package/bin/runners/lib/llm.js +75 -75
  100. package/bin/runners/lib/logger.js +38 -0
  101. package/bin/runners/lib/meter.js +61 -61
  102. package/bin/runners/lib/missions/evidence.js +126 -126
  103. package/bin/runners/lib/patch.js +40 -40
  104. package/bin/runners/lib/permissions/auth-model.js +213 -213
  105. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  106. package/bin/runners/lib/permissions/index.js +45 -45
  107. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  108. package/bin/runners/lib/pkgjson.js +28 -28
  109. package/bin/runners/lib/policy.js +295 -295
  110. package/bin/runners/lib/preflight.js +142 -142
  111. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  112. package/bin/runners/lib/reality/index.js +318 -318
  113. package/bin/runners/lib/reality/request-hashing.js +416 -416
  114. package/bin/runners/lib/reality/request-mapper.js +453 -453
  115. package/bin/runners/lib/reality/safety-rails.js +463 -463
  116. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  117. package/bin/runners/lib/reality/toast-detector.js +393 -393
  118. package/bin/runners/lib/reality-findings.js +84 -84
  119. package/bin/runners/lib/receipts.js +179 -179
  120. package/bin/runners/lib/redact.js +29 -29
  121. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  122. package/bin/runners/lib/replay/index.js +263 -263
  123. package/bin/runners/lib/replay/player.js +348 -348
  124. package/bin/runners/lib/replay/recorder.js +331 -331
  125. package/bin/runners/lib/report.js +135 -135
  126. package/bin/runners/lib/route-detection.js +1140 -1140
  127. package/bin/runners/lib/sandbox/index.js +59 -59
  128. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  129. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  130. package/bin/runners/lib/sandbox/worktree.js +174 -174
  131. package/bin/runners/lib/schema-validator.js +350 -350
  132. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  133. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  134. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  135. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  136. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  137. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  138. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  139. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  140. package/bin/runners/lib/schemas/validator.js +438 -438
  141. package/bin/runners/lib/score-history.js +282 -282
  142. package/bin/runners/lib/share-pack.js +239 -239
  143. package/bin/runners/lib/snippets.js +67 -67
  144. package/bin/runners/lib/unified-cli-output.js +604 -0
  145. package/bin/runners/lib/upsell.js +658 -510
  146. package/bin/runners/lib/usage.js +153 -153
  147. package/bin/runners/lib/validate-patch.js +156 -156
  148. package/bin/runners/lib/verdict-engine.js +628 -628
  149. package/bin/runners/reality/engine.js +917 -917
  150. package/bin/runners/reality/flows.js +122 -122
  151. package/bin/runners/reality/report.js +378 -378
  152. package/bin/runners/reality/session.js +193 -193
  153. package/bin/runners/runAgent.d.ts +5 -0
  154. package/bin/runners/runApprove.js +1200 -0
  155. package/bin/runners/runAuth.js +324 -95
  156. package/bin/runners/runCheckpoint.js +39 -21
  157. package/bin/runners/runClassify.js +859 -0
  158. package/bin/runners/runContext.js +136 -24
  159. package/bin/runners/runDoctor.js +108 -68
  160. package/bin/runners/runFirewall.d.ts +5 -0
  161. package/bin/runners/runFirewallHook.d.ts +5 -0
  162. package/bin/runners/runFix.js +6 -5
  163. package/bin/runners/runGuard.js +262 -168
  164. package/bin/runners/runInit.js +3 -2
  165. package/bin/runners/runMcp.js +130 -52
  166. package/bin/runners/runPolish.js +43 -20
  167. package/bin/runners/runProve.js +1 -2
  168. package/bin/runners/runReport.js +3 -2
  169. package/bin/runners/runScan.js +145 -44
  170. package/bin/runners/runShip.js +3 -4
  171. package/bin/runners/runTruth.d.ts +5 -0
  172. package/bin/runners/runValidate.js +19 -2
  173. package/bin/runners/runWatch.js +104 -53
  174. package/bin/vibecheck.js +106 -19
  175. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  176. package/mcp-server/agent-firewall-interceptor.js +367 -31
  177. package/mcp-server/authority-tools.js +569 -0
  178. package/mcp-server/conductor/conflict-resolver.js +588 -0
  179. package/mcp-server/conductor/execution-planner.js +544 -0
  180. package/mcp-server/conductor/index.js +377 -0
  181. package/mcp-server/conductor/lock-manager.js +615 -0
  182. package/mcp-server/conductor/request-queue.js +550 -0
  183. package/mcp-server/conductor/session-manager.js +500 -0
  184. package/mcp-server/conductor/tools.js +510 -0
  185. package/mcp-server/index.js +1199 -208
  186. package/mcp-server/lib/api-client.cjs +305 -0
  187. package/mcp-server/lib/logger.cjs +30 -0
  188. package/mcp-server/logger.js +173 -0
  189. package/mcp-server/package.json +2 -2
  190. package/mcp-server/premium-tools.js +2 -2
  191. package/mcp-server/tier-auth.js +351 -136
  192. package/mcp-server/tools/index.js +72 -72
  193. package/mcp-server/truth-firewall-tools.js +145 -15
  194. package/mcp-server/vibecheck-tools.js +2 -2
  195. package/package.json +2 -3
  196. package/mcp-server/index.old.js +0 -4137
  197. 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,6 +43,17 @@ import { fileURLToPath } from "url";
33
43
  import { execFile } from "child_process";
34
44
  import { promisify } from "util";
35
45
 
46
+ // Import API client for dashboard integration (optional - may use CommonJS)
47
+ import { createRequire } from "module";
48
+ const require = createRequire(import.meta.url);
49
+ const {
50
+ createScan,
51
+ updateScanProgress,
52
+ submitScanResults,
53
+ reportScanError,
54
+ isApiAvailable
55
+ } = require("./lib/api-client.cjs");
56
+
36
57
  const execFileAsync = promisify(execFile);
37
58
 
38
59
  const __filename = fileURLToPath(import.meta.url);
@@ -55,8 +76,384 @@ const CONFIG = {
55
76
  AUTOPILOT: 300000, // 5 minutes
56
77
  },
57
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
58
382
  };
59
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
+
60
457
  const VERSION = CONFIG.VERSION;
61
458
 
62
459
  // Import intelligence tools
@@ -83,6 +480,12 @@ import {
83
480
  handleArchitectTool,
84
481
  } from "./architect-tools.js";
85
482
 
483
+ // Import authority system tools
484
+ import {
485
+ AUTHORITY_TOOLS,
486
+ handleAuthorityTool,
487
+ } from "./authority-tools.js";
488
+
86
489
  // Import codebase architect tools
87
490
  import {
88
491
  CODEBASE_ARCHITECT_TOOLS,
@@ -127,14 +530,22 @@ import { CONSOLIDATED_TOOLS, handleConsolidatedTool } from "./consolidated-tools
127
530
  import { MCP_TOOLS_V3, handleToolV3, TOOL_TIERS as V3_TOOL_TIERS } from "./tools-v3.js";
128
531
 
129
532
  // Import tier auth for entitlement checking
130
- import { checkFeatureAccess } from "./tier-auth.js";
533
+ import { getFeatureAccessStatus } from "./tier-auth.js";
131
534
 
132
- // 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
133
537
  import {
134
538
  AGENT_FIREWALL_TOOL,
135
539
  handleAgentFirewallIntercept,
136
540
  } from "./agent-firewall-interceptor.js";
137
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
+
138
549
  /**
139
550
  * TRUTH FIREWALL CONFIGURATION
140
551
  *
@@ -173,14 +584,29 @@ function getPolicyConfig(policy) {
173
584
  return POLICY_THRESHOLDS[policy] || POLICY_THRESHOLDS.strict;
174
585
  }
175
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
+ */
176
594
  async function emitGuardrailMetric(projectPath, metric) {
177
595
  try {
178
596
  const auditDir = path.join(projectPath, ".vibecheck", "audit");
179
597
  await fs.mkdir(auditDir, { recursive: true });
180
598
  const record = JSON.stringify({ ...metric, timestamp: new Date().toISOString() });
181
599
  await fs.appendFile(path.join(auditDir, "guardrail-metrics.jsonl"), `${record}\n`);
182
- } catch {
183
- // 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
+ }
184
610
  }
185
611
  }
186
612
 
@@ -230,18 +656,22 @@ function checkTruthFirewallBlock(toolName, args, projectPath) {
230
656
  const USE_V3_TOOLS = process.env.VIBECHECK_MCP_V3 !== 'false';
231
657
  const USE_CONSOLIDATED_TOOLS = process.env.VIBECHECK_MCP_CONSOLIDATED !== 'false';
232
658
 
233
- const TOOLS = USE_V3_TOOLS ? [
659
+ const TOOLS = (USE_V3_TOOLS ? [
234
660
  // v3: 10 focused tools for STARTER+ (no free MCP tools)
235
661
  ...MCP_TOOLS_V3,
236
662
  AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
237
- ] : USE_CONSOLIDATED_TOOLS ? [
663
+ ...getConductorTools(), // Conductor - multi-agent coordination
664
+ ].filter(t => t !== null) : USE_CONSOLIDATED_TOOLS ? [
238
665
  // Curated tools for agents (legacy)
239
666
  ...CONSOLIDATED_TOOLS,
240
667
  AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
241
- ] : [
668
+ ...getConductorTools(), // Conductor - multi-agent coordination
669
+ ].filter(t => t !== null) : [
242
670
  // Legacy: Full tool set (50+ tools) - for backward compatibility
243
671
  // PRIORITY: Agent Firewall - intercepts ALL file writes
244
672
  AGENT_FIREWALL_TOOL,
673
+ // PRIORITY: Conductor - multi-agent coordination
674
+ ...getConductorTools(),
245
675
  // PRIORITY: Truth Firewall tools (Hallucination Stopper) - agents MUST use these
246
676
  ...TRUTH_FIREWALL_TOOLS, // vibecheck.get_truthpack, vibecheck.validate_claim, vibecheck.compile_context, etc.
247
677
 
@@ -250,6 +680,7 @@ const TOOLS = USE_V3_TOOLS ? [
250
680
 
251
681
  ...INTELLIGENCE_TOOLS, // Add all intelligence suite tools
252
682
  ...VIBECHECK_TOOLS, // Add AI vibecheck tools (verify, quality, smells, etc.)
683
+ ...AUTHORITY_TOOLS, // Add authority system tools (classify, approve, list)
253
684
  ...AGENT_CHECKPOINT_TOOLS, // Add agent checkpoint tools
254
685
  ...ARCHITECT_TOOLS, // Add architect review/suggest tools
255
686
  ...CODEBASE_ARCHITECT_TOOLS, // Add codebase-aware architect tools
@@ -845,7 +1276,7 @@ const TOOLS = USE_V3_TOOLS ? [
845
1276
  },
846
1277
  },
847
1278
  },
848
- ];
1279
+ ]).filter(t => t !== null);
849
1280
 
850
1281
  // ============================================================================
851
1282
  // SERVER IMPLEMENTATION
@@ -863,32 +1294,56 @@ class VibecheckMCP {
863
1294
 
864
1295
  // ============================================================================
865
1296
  // TOOL REGISTRY - Maps tool names to handlers for cleaner dispatch
1297
+ // HARDENING: Validates all handlers are functions
866
1298
  // ============================================================================
867
1299
  buildToolRegistry() {
868
- return {
869
- // Agent Firewall - intercepts file writes
870
- "vibecheck_agent_firewall_intercept": handleAgentFirewallIntercept,
871
- // Core CLI tools
872
- "vibecheck.ship": this.handleShip.bind(this),
873
- "vibecheck.scan": this.handleScan.bind(this),
874
- "vibecheck.verify": this.handleVerify.bind(this),
875
- "vibecheck.reality": this.handleReality.bind(this),
876
- "vibecheckai.dev-test": this.handleAITest.bind(this),
877
- "vibecheck.gate": this.handleGate.bind(this),
878
- "vibecheck.fix": this.handleFix.bind(this),
879
- "vibecheck.share": this.handleShare.bind(this),
880
- "vibecheck.ctx": this.handleCtx.bind(this),
881
- "vibecheck.prove": this.handleProve.bind(this),
882
- "vibecheck.proof": this.handleProof.bind(this),
883
- "vibecheck.validate": this.handleValidate.bind(this),
884
- "vibecheck.report": this.handleReport.bind(this),
885
- "vibecheck.status": this.handleStatus.bind(this),
886
- "vibecheck.autopilot": this.handleAutopilot.bind(this),
887
- "vibecheck.autopilot_plan": this.handleAutopilotPlan.bind(this),
888
- "vibecheck.autopilot_apply": this.handleAutopilotApply.bind(this),
889
- "vibecheck.badge": this.handleBadge.bind(this),
890
- "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;
891
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;
892
1347
  }
893
1348
 
894
1349
  // ============================================================================
@@ -901,70 +1356,208 @@ class VibecheckMCP {
901
1356
  skipAuth = true,
902
1357
  } = options;
903
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
+
904
1387
  // Build argument array - this prevents command injection
905
- 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
+ }
906
1402
 
907
1403
  const execEnv = {
908
- ...process.env,
1404
+ ...safeEnv,
909
1405
  ...CONFIG.ENV_DEFAULTS,
910
1406
  ...(skipAuth ? { VIBECHECK_SKIP_AUTH: "1" } : {}),
911
1407
  ...env,
1408
+ // Ensure Node.js doesn't prompt for anything
1409
+ NODE_NO_READLINE: "1",
1410
+ FORCE_COLOR: "0",
912
1411
  };
913
1412
 
1413
+ // ========================================================================
1414
+ // HARDENING: Bounded timeout
1415
+ // ========================================================================
1416
+ const boundedTimeout = sanitizeNumber(timeout, 1000, 900000, CONFIG.TIMEOUTS.DEFAULT);
1417
+
914
1418
  try {
915
1419
  const { stdout, stderr } = await execFileAsync(process.execPath, finalArgs, {
916
- cwd,
1420
+ cwd: resolvedCwd,
917
1421
  encoding: "utf8",
918
1422
  maxBuffer: CONFIG.MAX_BUFFER,
919
- timeout,
1423
+ timeout: boundedTimeout,
920
1424
  env: execEnv,
1425
+ // Don't inherit stdin - prevents hanging
1426
+ stdio: ['ignore', 'pipe', 'pipe'],
921
1427
  });
922
- 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
+ };
923
1435
  } catch (error) {
924
- // Attach partial output for graceful degradation
925
- error.partialOutput = error.stdout || "";
926
- 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
+
927
1446
  throw error;
928
1447
  }
929
1448
  }
930
1449
 
931
1450
  // ============================================================================
932
- // UTILITY HELPERS
1451
+ // HARDENED UTILITY HELPERS
933
1452
  // ============================================================================
934
1453
 
935
- // 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
+ */
936
1459
  stripAnsi(str) {
937
- 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, "");
938
1470
  }
939
1471
 
940
- // 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
+ */
941
1477
  async parseSummaryFromDisk(projectPath) {
942
1478
  const summaryPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "summary.json");
1479
+
943
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
+
944
1488
  const content = await fs.readFile(summaryPath, "utf-8");
945
- return JSON.parse(content);
946
- } 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
947
1499
  return null;
948
1500
  }
949
1501
  }
950
1502
 
951
- // 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
+ */
952
1509
  formatScanOutput(summary, projectPath) {
953
- let output = `## Score: ${summary.score}/100 (${summary.grade})\n\n`;
954
- 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`;
955
1521
 
956
- if (summary.counts) {
1522
+ if (summary.counts && typeof summary.counts === 'object') {
957
1523
  output += "### Checks\n\n";
958
1524
  output += "| Category | Issues |\n|----------|--------|\n";
959
- for (const [key, count] of Object.entries(summary.counts)) {
960
- const icon = count === 0 ? "✅" : "⚠️";
961
- 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`;
962
1533
  }
963
1534
  }
964
1535
 
965
1536
  output += `\n📄 **Report:** ${CONFIG.OUTPUT_DIR}/report.html\n`;
966
1537
  return output;
967
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
+ }
968
1561
 
969
1562
  setupHandlers() {
970
1563
  // List tools
@@ -974,24 +1567,95 @@ class VibecheckMCP {
974
1567
 
975
1568
  // Call tool - main dispatch handler
976
1569
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
977
- const { name, arguments: args } = request.params;
978
- const projectPath = path.resolve(args?.projectPath || ".");
979
1570
  const startTime = Date.now();
980
-
981
- // Emit audit event for tool invocation start
982
- emitToolInvoke(name, args, "success", { projectPath });
983
-
1571
+ let toolName = 'unknown';
1572
+ let projectPath = '.';
1573
+
984
1574
  try {
985
- // TRUTH FIREWALL CHECK - enforce validation before code-changing tools
986
- const firewallCheck = checkTruthFirewallBlock(name, args, projectPath);
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;
1617
+
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
+ }
1635
+
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
+
987
1647
  if (firewallCheck.blocked) {
988
1648
  const policy = getTruthPolicy(args);
989
- await emitGuardrailMetric(projectPath, {
990
- event: "truth_firewall_block",
991
- tool: name,
992
- policy,
993
- reason: firewallCheck.code || "no_recent_claim_validation",
994
- });
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
+ }
995
1659
  return this.error(firewallCheck.reason, {
996
1660
  code: firewallCheck.code,
997
1661
  suggestion: firewallCheck.suggestion,
@@ -1000,98 +1664,142 @@ class VibecheckMCP {
1000
1664
  }
1001
1665
 
1002
1666
  // Handle v3 tools (10 consolidated tools, STARTER+ only)
1003
- if (USE_V3_TOOLS && V3_TOOL_TIERS[name]) {
1004
- const userTier = args?.tier || process.env.VIBECHECK_TIER || 'free';
1005
- 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 });
1006
1675
 
1007
1676
  if (result.error) {
1008
1677
  return this.error(result.error, { tier: result.tier, required: result.required });
1009
1678
  }
1010
1679
 
1680
+ // Sanitize and truncate output
1681
+ const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1011
1682
  return {
1012
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1683
+ content: [{ type: "text", text: outputText }],
1013
1684
  };
1014
1685
  }
1015
1686
 
1016
1687
  // 1. Check tool registry first (local CLI handlers)
1017
- if (this.toolRegistry[name]) {
1018
- return await this.toolRegistry[name](projectPath, args);
1688
+ if (this.toolRegistry[toolName]) {
1689
+ return await this.toolRegistry[toolName](projectPath, args);
1019
1690
  }
1020
1691
 
1021
1692
  // 2. Handle external module tools by prefix/pattern
1022
- if (name.startsWith("vibecheck.intelligence.")) {
1023
- return await handleIntelligenceTool(name, args, __dirname);
1693
+ if (toolName.startsWith("vibecheck.intelligence.")) {
1694
+ return await handleIntelligenceTool(toolName, args, __dirname);
1024
1695
  }
1025
1696
 
1026
1697
  // Handle AI vibecheck tools
1027
1698
  if (["vibecheck.verify", "vibecheck.quality", "vibecheck.smells",
1028
1699
  "vibecheck.hallucination", "vibecheck.breaking", "vibecheck.mdc",
1029
- "vibecheck.coverage"].includes(name)) {
1030
- 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)));
1031
1703
  return {
1032
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1704
+ content: [{ type: "text", text: outputText }],
1033
1705
  };
1034
1706
  }
1035
1707
 
1036
1708
  // Handle agent checkpoint tools
1037
- if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(name)) {
1038
- return await handleCheckpointTool(name, args);
1709
+ if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(toolName)) {
1710
+ return await handleCheckpointTool(toolName, args);
1039
1711
  }
1040
1712
 
1041
1713
  // Handle architect tools
1042
1714
  if (["vibecheck_architect_review", "vibecheck_architect_suggest",
1043
- "vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(name)) {
1044
- 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
+ };
1045
1726
  }
1046
1727
 
1047
1728
  // Handle codebase architect tools
1048
1729
  if (["vibecheck_architect_context", "vibecheck_architect_guide",
1049
1730
  "vibecheck_architect_validate", "vibecheck_architect_patterns",
1050
- "vibecheck_architect_dependencies"].includes(name)) {
1051
- return await handleCodebaseArchitectTool(name, args);
1731
+ "vibecheck_architect_dependencies"].includes(toolName)) {
1732
+ return await handleCodebaseArchitectTool(toolName, args);
1052
1733
  }
1053
1734
 
1054
1735
  // Handle vibecheck 2.0 tools
1055
- if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(name)) {
1056
- return await handleVibecheck2Tool(name, args, __dirname);
1736
+ if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(toolName)) {
1737
+ return await handleVibecheck2Tool(toolName, args, __dirname);
1057
1738
  }
1058
1739
 
1059
1740
  // Handle intent drift tools
1060
- if (name.startsWith("vibecheck_intent_")) {
1061
- const tool = intentDriftTools.find(t => t.name === name);
1741
+ if (toolName.startsWith("vibecheck_intent_")) {
1742
+ const tool = intentDriftTools.find(t => t.name === toolName);
1062
1743
  if (tool && tool.handler) {
1063
1744
  const result = await tool.handler(args);
1745
+ const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1064
1746
  return {
1065
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1747
+ content: [{ type: "text", text: outputText }],
1066
1748
  };
1067
1749
  }
1068
1750
  }
1069
1751
 
1070
1752
  // Handle MDC generator
1071
- if (name === "generate_mdc") {
1753
+ if (toolName === "generate_mdc") {
1072
1754
  return await handleMDCGeneration(args);
1073
1755
  }
1074
1756
 
1075
1757
  // Handle Truth Context tools (Evidence Pack / Truth Pack)
1076
- if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(name)) {
1077
- return await handleTruthContextTool(name, args);
1758
+ if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(toolName)) {
1759
+ return await handleTruthContextTool(toolName, args);
1078
1760
  }
1079
1761
 
1080
1762
  // Handle Truth Firewall tools (Hallucination Stopper)
1081
1763
  if (["vibecheck.get_truthpack", "vibecheck.validate_claim", "vibecheck.compile_context",
1082
1764
  "vibecheck.search_evidence", "vibecheck.find_counterexamples", "vibecheck.propose_patch",
1083
- "vibecheck.check_invariants", "vibecheck.add_assumption"].includes(name)) {
1084
- return await handleTruthFirewallTool(name, args, projectPath);
1765
+ "vibecheck.check_invariants", "vibecheck.add_assumption"].includes(toolName)) {
1766
+ return await handleTruthFirewallTool(toolName, args, projectPath);
1085
1767
  }
1086
1768
 
1087
- 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
+ });
1088
1774
  } catch (err) {
1089
- // Emit audit event for tool error
1090
- emitToolComplete(name, "error", {
1091
- errorMessage: err.message,
1092
- 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
+ ],
1093
1802
  });
1094
- return this.error(`${name} failed: ${err.message}`);
1095
1803
  }
1096
1804
  });
1097
1805
 
@@ -1152,15 +1860,41 @@ class VibecheckMCP {
1152
1860
  this.server.setRequestHandler(
1153
1861
  ReadResourceRequestSchema,
1154
1862
  async (request) => {
1155
- 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
+
1156
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
+ };
1157
1889
 
1158
1890
  if (uri === "vibecheck://config") {
1159
1891
  const configPath = path.join(projectPath, "vibecheck.config.json");
1160
1892
  try {
1161
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));
1162
1896
  return {
1163
- contents: [{ uri, mimeType: "application/json", text: content }],
1897
+ contents: [{ uri, mimeType: "application/json", text: sanitized }],
1164
1898
  };
1165
1899
  } catch {
1166
1900
  return {
@@ -1170,141 +1904,161 @@ class VibecheckMCP {
1170
1904
  }
1171
1905
 
1172
1906
  if (uri === "vibecheck://summary") {
1173
- const summaryPath = path.join(
1174
- projectPath,
1175
- ".vibecheck",
1176
- "summary.json",
1177
- );
1178
- try {
1179
- const content = await fs.readFile(summaryPath, "utf-8");
1180
- return {
1181
- contents: [{ uri, mimeType: "application/json", text: content }],
1182
- };
1183
- } catch {
1184
- return {
1185
- contents: [
1186
- {
1187
- uri,
1188
- mimeType: "application/json",
1189
- text: '{"message": "No scan found. Run vibecheck.scan first."}',
1190
- },
1191
- ],
1192
- };
1193
- }
1907
+ const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
1908
+ return await safeReadResource(summaryPath, "No scan found. Run vibecheck.scan first.");
1194
1909
  }
1195
1910
 
1196
1911
  if (uri === "vibecheck://truthpack") {
1197
1912
  const truthpackPath = path.join(projectPath, ".vibecheck", "truth", "truthpack.json");
1198
- try {
1199
- const content = await fs.readFile(truthpackPath, "utf-8");
1200
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1201
- } catch {
1202
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No truthpack. Run vibecheck.ctx first."}' }] };
1203
- }
1913
+ return await safeReadResource(truthpackPath, "No truthpack. Run vibecheck.ctx first.");
1204
1914
  }
1205
1915
 
1206
1916
  if (uri === "vibecheck://missions") {
1207
1917
  const missionsDir = path.join(projectPath, ".vibecheck", "missions");
1208
1918
  try {
1919
+ // HARDENING: Validate directory read
1209
1920
  const dirs = await fs.readdir(missionsDir);
1210
- 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
+
1211
1924
  if (latest) {
1212
1925
  const missionPath = path.join(missionsDir, latest, "missions.json");
1213
- const content = await fs.readFile(missionPath, "utf-8");
1214
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1926
+ return await safeReadResource(missionPath, "No missions found in latest directory.");
1215
1927
  }
1216
- } catch {}
1928
+ } catch (err) {
1929
+ console.error(`[MCP] Error reading missions: ${err.message}`);
1930
+ }
1217
1931
  return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No missions. Run vibecheck.fix first."}' }] };
1218
1932
  }
1219
1933
 
1220
1934
  if (uri === "vibecheck://reality") {
1221
1935
  const realityPath = path.join(projectPath, ".vibecheck", "reality", "last_reality.json");
1222
- try {
1223
- const content = await fs.readFile(realityPath, "utf-8");
1224
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1225
- } catch {
1226
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No reality results. Run vibecheck verify first."}' }] };
1227
- }
1936
+ return await safeReadResource(realityPath, "No reality results. Run vibecheck verify first.");
1228
1937
  }
1229
1938
 
1230
1939
  if (uri === "vibecheck://findings") {
1231
1940
  const findingsPath = path.join(projectPath, ".vibecheck", "findings.json");
1232
- try {
1233
- const content = await fs.readFile(findingsPath, "utf-8");
1234
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1235
- } catch {
1236
- // Try summary.json as fallback
1237
- const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
1238
- try {
1239
- const summary = JSON.parse(await fs.readFile(summaryPath, "utf-8"));
1240
- 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);
1241
1960
  return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ findings }, null, 2) }] };
1242
- } catch {
1243
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
1244
1961
  }
1245
1962
  }
1963
+
1964
+ return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
1246
1965
  }
1247
1966
 
1248
1967
  if (uri === "vibecheck://share") {
1249
1968
  const missionsDir = path.join(projectPath, ".vibecheck", "missions");
1250
1969
  try {
1970
+ // HARDENING: Safe directory read
1251
1971
  const dirs = await fs.readdir(missionsDir);
1252
- 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
+
1253
1975
  if (latest) {
1254
1976
  const sharePath = path.join(missionsDir, latest, "share", "share.json");
1255
- try {
1256
- const content = await fs.readFile(sharePath, "utf-8");
1257
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1258
- } catch {
1259
- // Fallback to missions.json
1260
- const missionPath = path.join(missionsDir, latest, "missions.json");
1261
- const content = await fs.readFile(missionPath, "utf-8");
1262
- 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
+ }
1263
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.");
1264
1991
  }
1265
- } catch {}
1992
+ } catch (err) {
1993
+ console.error(`[MCP] Error reading share pack: ${err.message}`);
1994
+ }
1266
1995
  return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No share pack. Run vibecheck.fix --share first."}' }] };
1267
1996
  }
1268
1997
 
1269
1998
  if (uri === "vibecheck://prove") {
1270
1999
  const provePath = path.join(projectPath, ".vibecheck", "prove", "last_prove.json");
1271
- try {
1272
- const content = await fs.readFile(provePath, "utf-8");
1273
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1274
- } catch {
1275
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No prove results. Run vibecheck.prove first."}' }] };
1276
- }
2000
+ return await safeReadResource(provePath, "No prove results. Run vibecheck.prove first.");
1277
2001
  }
1278
2002
 
1279
- return { contents: [] };
2003
+ return {
2004
+ contents: [{
2005
+ uri,
2006
+ mimeType: "application/json",
2007
+ text: JSON.stringify({ error: "Unknown resource URI" })
2008
+ }]
2009
+ };
1280
2010
  },
1281
2011
  );
1282
2012
  }
1283
2013
 
1284
- // 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
+ */
1285
2024
  success(text, includeAttribution = true) {
2025
+ // Sanitize output: redact secrets and truncate
2026
+ let sanitized = redactSensitive(sanitizeString(text, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
2027
+
1286
2028
  const finalText = includeAttribution
1287
- ? `${text}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
1288
- : text;
2029
+ ? `${sanitized}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
2030
+ : sanitized;
2031
+
1289
2032
  return { content: [{ type: "text", text: finalText }] };
1290
2033
  }
1291
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
+ */
1292
2041
  error(text, options = {}) {
1293
2042
  const { code, suggestion, nextSteps = [] } = options;
1294
2043
 
1295
- 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}`;
1296
2050
 
1297
2051
  if (code) {
1298
- errorText += `\n\n**Error Code:** \`${code}\``;
2052
+ errorText += `\n\n**Error Code:** \`${sanitizeString(code, 50)}\``;
1299
2053
  }
1300
2054
 
1301
- if (suggestion) {
1302
- errorText += `\n\n💡 **Suggestion:** ${suggestion}`;
2055
+ if (sanitizedSuggestion) {
2056
+ errorText += `\n\n💡 **Suggestion:** ${sanitizedSuggestion}`;
1303
2057
  }
1304
2058
 
1305
- if (nextSteps.length > 0) {
2059
+ if (sanitizedSteps.length > 0) {
1306
2060
  errorText += `\n\n**Next Steps:**\n`;
1307
- nextSteps.forEach((step, i) => {
2061
+ sanitizedSteps.forEach((step, i) => {
1308
2062
  errorText += `${i + 1}. ${step}\n`;
1309
2063
  });
1310
2064
  }
@@ -1357,8 +2111,54 @@ class VibecheckMCP {
1357
2111
  });
1358
2112
  }
1359
2113
 
1360
- const profile = args?.profile || "quick";
1361
- 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));
2120
+
2121
+ // Initialize API integration with timeout and circuit breaker
2122
+ let apiScan = null;
2123
+ let apiConnected = false;
2124
+
2125
+ // HARDENING: Check circuit breaker before attempting API calls
2126
+ const circuitCheck = checkCircuitBreaker();
2127
+
2128
+ // Try to connect to API for dashboard integration
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
2158
+ }
2159
+ } else {
2160
+ console.error(`[MCP] ${circuitCheck.reason}`);
2161
+ }
1362
2162
 
1363
2163
  let output = "# 🔍 vibecheck Scan\n\n";
1364
2164
  output += `**Profile:** ${profile}\n`;
@@ -1366,7 +2166,9 @@ class VibecheckMCP {
1366
2166
 
1367
2167
  // Build CLI arguments array (secure - no injection possible)
1368
2168
  const cliArgs = [`--profile=${profile}`, "--json"];
1369
- if (only?.length) cliArgs.push(`--only=${only.join(",")}`);
2169
+ if (only.length > 0) {
2170
+ cliArgs.push(`--only=${only.join(",")}`);
2171
+ }
1370
2172
 
1371
2173
  try {
1372
2174
  await this.runCLI("scan", cliArgs, projectPath, { timeout: CONFIG.TIMEOUTS.SCAN });
@@ -1375,6 +2177,34 @@ class VibecheckMCP {
1375
2177
  const summary = await this.parseSummaryFromDisk(projectPath);
1376
2178
  if (summary) {
1377
2179
  output += this.formatScanOutput(summary, projectPath);
2180
+
2181
+ // Submit results to dashboard if connected
2182
+ if (apiConnected && apiScan) {
2183
+ try {
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),
2192
+ metadata: {
2193
+ profile,
2194
+ source: 'mcp-server',
2195
+ version: CONFIG.VERSION,
2196
+ },
2197
+ });
2198
+ const submitTimeout = new Promise((_, reject) =>
2199
+ setTimeout(() => reject(new Error('Submit timeout')), 10000)
2200
+ );
2201
+
2202
+ await Promise.race([submitPromise, submitTimeout]);
2203
+ console.error(`[MCP] Results sent to dashboard`);
2204
+ } catch (err) {
2205
+ console.error(`[MCP] Failed to send results to dashboard: ${err.message}`);
2206
+ }
2207
+ }
1378
2208
  }
1379
2209
  output += "\n---\n_Context Enhanced by vibecheck AI_\n";
1380
2210
  return this.success(output);
@@ -1383,10 +2213,55 @@ class VibecheckMCP {
1383
2213
  const summary = await this.parseSummaryFromDisk(projectPath);
1384
2214
  if (summary) {
1385
2215
  output += this.formatScanOutput(summary, projectPath);
2216
+
2217
+ // Submit results to dashboard if connected
2218
+ if (apiConnected && apiScan) {
2219
+ try {
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),
2228
+ metadata: {
2229
+ profile,
2230
+ source: 'mcp-server',
2231
+ version: CONFIG.VERSION,
2232
+ error: sanitizeString(err.message, 500),
2233
+ },
2234
+ });
2235
+ const submitTimeout = new Promise((_, reject) =>
2236
+ setTimeout(() => reject(new Error('Submit timeout')), 10000)
2237
+ );
2238
+
2239
+ await Promise.race([submitPromise, submitTimeout]);
2240
+ console.error(`[MCP] Results sent to dashboard (with error)`);
2241
+ } catch (apiErr) {
2242
+ console.error(`[MCP] Failed to send results to dashboard: ${apiErr.message}`);
2243
+ }
2244
+ }
1386
2245
  output += `\n⚠️ Scan completed with findings (exit code ${err.code || 1})\n`;
1387
2246
  return this.success(output);
1388
2247
  }
1389
2248
 
2249
+ // Report error to dashboard if connected
2250
+ if (apiConnected && apiScan) {
2251
+ try {
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]);
2259
+ console.error(`[MCP] Error reported to dashboard`);
2260
+ } catch (apiErr) {
2261
+ console.error(`[MCP] Failed to report error to dashboard: ${apiErr.message}`);
2262
+ }
2263
+ }
2264
+
1390
2265
  return this.error(`Scan failed: ${err.message}`, {
1391
2266
  code: "SCAN_ERROR",
1392
2267
  suggestion: "Check that the project path is valid and contains scanable code",
@@ -1405,7 +2280,7 @@ class VibecheckMCP {
1405
2280
  // ============================================================================
1406
2281
  async handleGate(projectPath, args) {
1407
2282
  // Check tier access (STARTER tier required)
1408
- const access = await checkFeatureAccess("gate", args?.apiKey);
2283
+ const access = await getFeatureAccessStatus("gate", args?.apiKey);
1409
2284
  if (!access.hasAccess) {
1410
2285
  return {
1411
2286
  content: [{
@@ -1445,7 +2320,7 @@ class VibecheckMCP {
1445
2320
  async handleFix(projectPath, args) {
1446
2321
  // Check tier access for --apply and --autopilot (PRO tier required)
1447
2322
  if (args?.apply || args?.autopilot) {
1448
- const access = await checkFeatureAccess("fix.apply_patches", args?.apiKey);
2323
+ const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
1449
2324
  if (!access.hasAccess) {
1450
2325
  return {
1451
2326
  content: [{
@@ -1620,7 +2495,7 @@ class VibecheckMCP {
1620
2495
  // ============================================================================
1621
2496
  async handleProve(projectPath, args) {
1622
2497
  // Check tier access (PRO tier required)
1623
- const access = await checkFeatureAccess("prove", args?.apiKey);
2498
+ const access = await getFeatureAccessStatus("prove", args?.apiKey);
1624
2499
  if (!access.hasAccess) {
1625
2500
  return {
1626
2501
  content: [{
@@ -1823,6 +2698,16 @@ class VibecheckMCP {
1823
2698
  // SHIP - Quick health check
1824
2699
  // ============================================================================
1825
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
+
1826
2711
  let output = "# 🚀 vibecheck Ship\n\n";
1827
2712
  output += `**Path:** ${projectPath}\n\n`;
1828
2713
 
@@ -1847,20 +2732,34 @@ class VibecheckMCP {
1847
2732
  // VERIFY - Runtime browser testing
1848
2733
  // ============================================================================
1849
2734
  async handleVerify(projectPath, args) {
1850
- const url = args?.url;
1851
- 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;
1852
2745
 
1853
2746
  let output = "# 🧪 vibecheck Verify\n\n";
1854
2747
  output += `**URL:** ${url}\n`;
1855
- 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`;
1856
2752
  if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
1857
2753
  if (args?.record) output += `**Recording:** Enabled\n`;
1858
2754
  output += "\n";
1859
2755
 
1860
2756
  // Build CLI arguments array (secure)
1861
2757
  const cliArgs = ["--url", url];
1862
- if (args?.auth) cliArgs.push("--auth", args.auth);
1863
- 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(","));
1864
2763
  if (args?.headed) cliArgs.push("--headed");
1865
2764
  if (args?.record) cliArgs.push("--record");
1866
2765
 
@@ -1910,28 +2809,67 @@ class VibecheckMCP {
1910
2809
  // REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
1911
2810
  // ============================================================================
1912
2811
  async handleReality(projectPath, args) {
1913
- const url = args?.url;
1914
- 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;
1915
2822
 
1916
2823
  let output = "# 🧪 vibecheck Reality v2\n\n";
1917
2824
  output += `**URL:** ${url}\n`;
1918
2825
  output += `**Two-Pass Auth:** ${args?.verifyAuth ? "Yes" : "No"}\n`;
1919
- if (args?.auth) output += `**Auth:** ${args.auth.split(":")[0]}:***\n`;
1920
- 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`;
1921
2834
  if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
1922
2835
  if (args?.danger) output += `**Danger Mode:** Enabled (risky clicks allowed)\n`;
1923
2836
  output += "\n";
1924
2837
 
1925
2838
  // Build CLI arguments array (secure)
1926
2839
  const cliArgs = ["--url", url];
1927
- 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
+ }
1928
2843
  if (args?.verifyAuth) cliArgs.push("--verify-auth");
1929
- if (args?.storageState) cliArgs.push("--storage-state", args.storageState);
1930
- if (args?.saveStorageState) cliArgs.push("--save-storage-state", args.saveStorageState);
1931
- 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
+ }
1932
2864
  if (args?.headed) cliArgs.push("--headed");
1933
- if (args?.maxPages) cliArgs.push("--max-pages", String(args.maxPages));
1934
- 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
+ }
1935
2873
  if (args?.danger) cliArgs.push("--danger");
1936
2874
 
1937
2875
  try {
@@ -2012,16 +2950,27 @@ class VibecheckMCP {
2012
2950
  // AI-TEST - AI Agent testing
2013
2951
  // ============================================================================
2014
2952
  async handleAITest(projectPath, args) {
2015
- const url = args?.url;
2016
- 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";
2017
2966
 
2018
2967
  let output = "# 🤖 vibecheck AI Agent\n\n";
2019
2968
  output += `**URL:** ${url}\n`;
2020
- output += `**Goal:** ${args?.goal || "Test all features"}\n\n`;
2969
+ output += `**Goal:** ${goal}\n\n`;
2021
2970
 
2022
2971
  // Build CLI arguments array (secure)
2023
2972
  const cliArgs = ["--url", url];
2024
- if (args?.goal) cliArgs.push("--goal", args.goal);
2973
+ if (goal) cliArgs.push("--goal", goal);
2025
2974
  if (args?.headed) cliArgs.push("--headed");
2026
2975
 
2027
2976
  try {
@@ -2077,7 +3026,7 @@ class VibecheckMCP {
2077
3026
  // ============================================================================
2078
3027
  async handleAutopilotPlan(projectPath, args) {
2079
3028
  // Check tier access (PRO tier required)
2080
- const access = await checkFeatureAccess("fix.apply_patches", args?.apiKey);
3029
+ const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
2081
3030
  if (!access.hasAccess) {
2082
3031
  return {
2083
3032
  content: [{
@@ -2164,7 +3113,7 @@ class VibecheckMCP {
2164
3113
  // ============================================================================
2165
3114
  async handleAutopilotApply(projectPath, args) {
2166
3115
  // Check tier access (PRO tier required)
2167
- const access = await checkFeatureAccess("fix.apply_patches", args?.apiKey);
3116
+ const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
2168
3117
  if (!access.hasAccess) {
2169
3118
  return {
2170
3119
  content: [{
@@ -2225,7 +3174,7 @@ class VibecheckMCP {
2225
3174
  // ============================================================================
2226
3175
  async handleBadge(projectPath, args) {
2227
3176
  // Check tier access (STARTER tier required)
2228
- const access = await checkFeatureAccess("badge", args?.apiKey);
3177
+ const access = await getFeatureAccessStatus("badge", args?.apiKey);
2229
3178
  if (!access.hasAccess) {
2230
3179
  return {
2231
3180
  content: [{
@@ -2381,15 +3330,57 @@ class VibecheckMCP {
2381
3330
  }
2382
3331
 
2383
3332
  // ============================================================================
2384
- // RUN
3333
+ // RUN - with graceful shutdown handling
2385
3334
  // ============================================================================
2386
3335
  async run() {
2387
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
+
2388
3373
  await this.server.connect(transport);
2389
- console.error("vibecheck MCP Server v2.0 running on stdio");
3374
+ console.error(`vibecheck MCP Server v${VERSION} running on stdio (hardened)`);
2390
3375
  }
2391
3376
  }
2392
3377
 
2393
- // Main
3378
+ // ============================================================================
3379
+ // MAIN - with error handling
3380
+ // ============================================================================
2394
3381
  const server = new VibecheckMCP();
2395
- 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
+ });