@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,3 +1,4 @@
1
+ /* vibecheck-disable */
1
2
  /**
2
3
  * MCP Server Tier Authentication & Authorization
3
4
  *
@@ -35,6 +36,12 @@ export const TIERS = {
35
36
  canIssueVerdicts: false,
36
37
  canEnforceCI: false,
37
38
  canGenerateProof: false,
39
+ // Agent Firewall configuration
40
+ firewall: {
41
+ mode: 'observe', // Log only, never block
42
+ canOverride: false, // Cannot override blocks
43
+ auditEnabled: false, // No audit trail
44
+ },
38
45
  // Limits
39
46
  limits: {
40
47
  scans: -1, // Unlimited scans
@@ -47,7 +54,20 @@ export const TIERS = {
47
54
  'vibecheck.get_truthpack',
48
55
  'vibecheck.compile_context',
49
56
  'vibecheck.search_evidence',
57
+ 'vibecheck_agent_firewall_intercept', // Observe mode
58
+ 'vibecheck_conductor_status', // Read-only coordination status
59
+ // Authority System - FREE tier
60
+ 'vibecheck.classify', // Inventory authority (read-only)
61
+ 'vibecheck.authority_list', // List available authorities
50
62
  ],
63
+ // Authority System configuration
64
+ authority: {
65
+ canClassify: true, // Can run inventory analysis
66
+ canApprove: false, // Cannot get verdicts
67
+ canEnforce: false, // Cannot enforce in CI
68
+ canGenerateBadge: false, // Cannot generate badges
69
+ availableAuthorities: ['inventory'],
70
+ },
51
71
  },
52
72
  starter: {
53
73
  name: 'STARTER',
@@ -58,6 +78,12 @@ export const TIERS = {
58
78
  canIssueVerdicts: true, // SHIP, WARN only
59
79
  canEnforceCI: false, // Cannot block builds
60
80
  canGenerateProof: false, // Cannot generate proof
81
+ // Agent Firewall configuration
82
+ firewall: {
83
+ mode: 'advisory', // Warn but allow
84
+ canOverride: true, // Can override with reason
85
+ auditEnabled: true, // Basic audit trail
86
+ },
61
87
  // Limits
62
88
  limits: {
63
89
  scans: -1, // Unlimited scans
@@ -77,7 +103,28 @@ export const TIERS = {
77
103
  'vibecheck.find_counterexamples',
78
104
  'vibecheck.check_invariants',
79
105
  'vibecheck.fix', // Plan mode only
106
+ 'vibecheck_agent_firewall_intercept', // Advisory mode
107
+ // Conductor - multi-agent coordination
108
+ 'vibecheck_conductor_register',
109
+ 'vibecheck_conductor_acquire_lock',
110
+ 'vibecheck_conductor_release_lock',
111
+ 'vibecheck_conductor_propose',
112
+ 'vibecheck_conductor_status',
113
+ 'vibecheck_conductor_terminate',
114
+ // Authority System - STARTER tier
115
+ 'vibecheck.classify', // Inventory authority
116
+ 'vibecheck.approve', // Advisory verdicts
117
+ 'vibecheck.authority_list', // List authorities
118
+ 'vibecheck.authority_approve', // Authority approval (advisory)
80
119
  ],
120
+ // Authority System configuration
121
+ authority: {
122
+ canClassify: true, // Can run inventory analysis
123
+ canApprove: true, // Can get advisory verdicts
124
+ canEnforce: false, // Cannot enforce in CI
125
+ canGenerateBadge: false, // Cannot generate badges
126
+ availableAuthorities: ['inventory', 'safe-consolidation'],
127
+ },
81
128
  },
82
129
  pro: {
83
130
  name: 'PRO',
@@ -88,6 +135,13 @@ export const TIERS = {
88
135
  canIssueVerdicts: true, // SHIP, WARN, BLOCK
89
136
  canEnforceCI: true, // Can block builds
90
137
  canGenerateProof: true, // Can generate cryptographic proof
138
+ // Agent Firewall configuration
139
+ firewall: {
140
+ mode: 'enforce', // Block violations
141
+ canOverride: true, // Can override with approval
142
+ auditEnabled: true, // Full audit trail
143
+ criticEnabled: true, // Enable Critic LLM
144
+ },
91
145
  // Limits
92
146
  limits: {
93
147
  scans: -1,
@@ -97,6 +151,14 @@ export const TIERS = {
97
151
  },
98
152
  // MCP tools - FULL ACCESS
99
153
  mcpTools: ['*'], // All tools unlocked
154
+ // Authority System configuration
155
+ authority: {
156
+ canClassify: true, // Can run inventory analysis
157
+ canApprove: true, // Can get verdicts
158
+ canEnforce: true, // Can enforce in CI (STOP verdicts block)
159
+ canGenerateBadge: true, // Can generate authority badges
160
+ availableAuthorities: ['inventory', 'safe-consolidation', 'security-remediation'],
161
+ },
100
162
  },
101
163
  enterprise: {
102
164
  name: 'ENTERPRISE',
@@ -107,6 +169,14 @@ export const TIERS = {
107
169
  canIssueVerdicts: true,
108
170
  canEnforceCI: true,
109
171
  canGenerateProof: true,
172
+ // Agent Firewall configuration
173
+ firewall: {
174
+ mode: 'enforce', // Block violations
175
+ canOverride: true, // Can override with multi-approval
176
+ auditEnabled: true, // Full compliance audit trail
177
+ criticEnabled: true, // Enable Critic LLM
178
+ complianceMode: true, // Generate compliance artifacts
179
+ },
110
180
  // Limits
111
181
  limits: {
112
182
  scans: -1,
@@ -116,9 +186,46 @@ export const TIERS = {
116
186
  },
117
187
  // MCP tools - FULL ACCESS + Compliance
118
188
  mcpTools: ['*'],
189
+ // Authority System configuration
190
+ authority: {
191
+ canClassify: true, // Can run inventory analysis
192
+ canApprove: true, // Can get verdicts
193
+ canEnforce: true, // Can enforce in CI (STOP verdicts block)
194
+ canGenerateBadge: true, // Can generate authority badges
195
+ canCustomize: true, // Can create custom authorities
196
+ availableAuthorities: ['*'], // All authorities including custom
197
+ },
119
198
  }
120
199
  };
121
200
 
201
+ // ═══════════════════════════════════════════════════════════════════════════════
202
+ // CLI FEATURE → TIER MAPPING
203
+ // Maps CLI features (not MCP tools) to minimum required tier
204
+ // ═══════════════════════════════════════════════════════════════════════════════
205
+ export const CLI_FEATURE_TIERS = {
206
+ // FREE - Basic scanning (always available)
207
+ 'scan': 'free',
208
+ 'ctx': 'free',
209
+ 'ship': 'free',
210
+ 'classify': 'free', // Authority: inventory (read-only)
211
+
212
+ // STARTER - Enhanced features
213
+ 'gate': 'starter',
214
+ 'badge': 'starter',
215
+ 'fix': 'starter', // Plan mode
216
+ 'approve': 'starter', // Authority: advisory verdicts
217
+ 'authority.starter': 'starter', // Authority System access
218
+
219
+ // PRO - Full enforcement
220
+ 'prove': 'pro',
221
+ 'fix.apply_patches': 'pro', // Apply mode
222
+ 'reality': 'pro',
223
+ 'autopilot': 'pro',
224
+ 'verify': 'pro',
225
+ 'authority.pro': 'pro', // Authority: enforced verdicts + badges
226
+ 'authority.enforce': 'pro', // Authority: CI blocking
227
+ };
228
+
122
229
  // ═══════════════════════════════════════════════════════════════════════════════
123
230
  // MCP TOOL → TIER MAPPING (Unified Authority System)
124
231
  // Aligned with packages/core/src/features.ts
@@ -129,6 +236,9 @@ export const MCP_TOOL_TIERS = {
129
236
  'vibecheck.get_truthpack': 'free',
130
237
  'vibecheck.compile_context': 'free',
131
238
  'vibecheck.search_evidence': 'free',
239
+ 'vibecheck_agent_firewall_intercept': 'free', // Observe mode only
240
+ 'vibecheck.classify': 'free', // Authority: inventory (read-only)
241
+ 'vibecheck.authority_list': 'free', // List available authorities
132
242
 
133
243
  // STARTER - Advisory verdict authority
134
244
  // Starter users may fix, but not prove
@@ -141,6 +251,8 @@ export const MCP_TOOL_TIERS = {
141
251
  'vibecheck.check_invariants': 'starter',
142
252
  'vibecheck.gate': 'starter', // Advisory gates
143
253
  'vibecheck.badge': 'starter', // Unverified badges
254
+ 'vibecheck.approve': 'starter', // Authority: advisory verdicts
255
+ 'vibecheck.authority_approve': 'starter', // Authority approval (advisory)
144
256
 
145
257
  // PRO - Full verdict authority & enforcement
146
258
  // Pro users may enforce reality
@@ -155,6 +267,8 @@ export const MCP_TOOL_TIERS = {
155
267
  'vibecheck.report': 'pro', // Advanced reporting
156
268
  'vibecheck.allowlist': 'pro', // Manage allowlists
157
269
  'vibecheck.status': 'pro', // Advanced status
270
+ 'vibecheck.authority_enforce': 'pro', // Authority: enforced verdicts
271
+ 'vibecheck.authority_badge': 'pro', // Authority: generate badges
158
272
  };
159
273
 
160
274
  /**
@@ -170,50 +284,149 @@ async function loadUserConfig() {
170
284
  }
171
285
  }
172
286
 
287
+ // ═══════════════════════════════════════════════════════════════════════════════
288
+ // SECURE TIER VALIDATION - API-First with Caching
289
+ // SECURITY: Never trust API key prefix alone - always validate with API
290
+ // ═══════════════════════════════════════════════════════════════════════════════
291
+
173
292
  /**
174
- * Determine tier from API key
175
- * Matches CLI entitlements-v2.js logic
293
+ * Tier cache to avoid hammering the API on every request
294
+ * Structure: Map<apiKeyHash, { tier, validatedAt, expiresAt }>
295
+ */
296
+ const tierCache = new Map();
297
+ const TIER_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
298
+ const TIER_CACHE_MAX_SIZE = 1000; // Prevent unbounded growth
299
+
300
+ /**
301
+ * Hash API key for cache key (don't store raw keys in memory)
302
+ */
303
+ function hashApiKey(apiKey) {
304
+ const crypto = require('crypto');
305
+ return crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 32);
306
+ }
307
+
308
+ /**
309
+ * Evict expired entries and enforce max size
310
+ */
311
+ function cleanupTierCache() {
312
+ const now = Date.now();
313
+ for (const [key, value] of tierCache) {
314
+ if (value.expiresAt < now) {
315
+ tierCache.delete(key);
316
+ }
317
+ }
318
+ // If still too large, evict oldest entries
319
+ if (tierCache.size > TIER_CACHE_MAX_SIZE) {
320
+ const entries = Array.from(tierCache.entries())
321
+ .sort((a, b) => a[1].validatedAt - b[1].validatedAt);
322
+ const toDelete = entries.slice(0, tierCache.size - TIER_CACHE_MAX_SIZE);
323
+ for (const [key] of toDelete) {
324
+ tierCache.delete(key);
325
+ }
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Determine tier from API key - ALWAYS validates with API first
331
+ *
332
+ * SECURITY FIX: Previous version checked prefix patterns BEFORE API validation,
333
+ * allowing attackers to spoof tier with fake keys like "gr_pro_anything".
334
+ * Now we ALWAYS validate with API first. Prefix is never authoritative.
335
+ *
336
+ * @param {string} apiKey - The API key to validate
337
+ * @returns {Promise<string|null>} - Tier name or null if invalid
176
338
  */
177
339
  async function getTierFromApiKey(apiKey) {
178
- if (!apiKey) return null; // No API key = no access
340
+ if (!apiKey || typeof apiKey !== 'string') return null;
179
341
 
180
- // Check API key prefix patterns (matches CLI)
181
- if (apiKey.startsWith('gr_starter_')) return 'starter';
182
- if (apiKey.startsWith('gr_pro_')) return 'pro';
183
- if (apiKey.startsWith('gr_enterprise_') || apiKey.startsWith('gr_ent_')) return 'enterprise';
184
- if (apiKey.startsWith('gr_free_')) return 'free';
342
+ // Validate key format (basic sanity check)
343
+ if (apiKey.length < 10 || apiKey.length > 256) return null;
185
344
 
186
- // Try to validate with API
345
+ const keyHash = hashApiKey(apiKey);
346
+ const now = Date.now();
347
+
348
+ // Check cache first (only if not expired)
349
+ const cached = tierCache.get(keyHash);
350
+ if (cached && cached.expiresAt > now) {
351
+ return cached.tier;
352
+ }
353
+
354
+ // ALWAYS validate with API - this is the authoritative source
187
355
  try {
356
+ const controller = new AbortController();
357
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
358
+
188
359
  const response = await fetch('https://api.vibecheckai.dev/whoami', {
189
360
  headers: {
190
361
  'Authorization': `Bearer ${apiKey}`,
191
362
  'Content-Type': 'application/json',
192
363
  },
364
+ signal: controller.signal,
193
365
  });
194
366
 
367
+ clearTimeout(timeoutId);
368
+
195
369
  if (!response.ok) {
196
- return null; // Invalid API key
370
+ // API explicitly rejected this key - it's invalid
371
+ // Cache the rejection briefly to avoid hammering on invalid keys
372
+ tierCache.set(keyHash, {
373
+ tier: null,
374
+ validatedAt: now,
375
+ expiresAt: now + 60000, // Cache rejections for 1 minute
376
+ });
377
+ return null;
197
378
  }
198
379
 
199
380
  const data = await response.json();
200
381
 
201
382
  // Map API response to tier
383
+ let tier;
202
384
  switch (data.plan?.toLowerCase()) {
203
385
  case 'starter':
204
- return 'starter';
386
+ tier = 'starter';
387
+ break;
205
388
  case 'pro':
206
- return 'pro';
389
+ tier = 'pro';
390
+ break;
207
391
  case 'enterprise':
208
- return 'enterprise';
392
+ tier = 'enterprise';
393
+ break;
209
394
  default:
210
- return 'free';
395
+ tier = 'free';
396
+ }
397
+
398
+ // Cache the validated tier
399
+ tierCache.set(keyHash, {
400
+ tier,
401
+ validatedAt: now,
402
+ expiresAt: now + TIER_CACHE_TTL_MS,
403
+ });
404
+
405
+ // Periodic cleanup
406
+ if (tierCache.size > TIER_CACHE_MAX_SIZE * 0.9) {
407
+ cleanupTierCache();
211
408
  }
409
+
410
+ return tier;
411
+
212
412
  } catch (error) {
213
- console.error('API validation failed:', error);
214
- return null; // On error, deny access
413
+ // Network error - check if we have a recent valid cache entry
414
+ // (This allows graceful degradation during brief network issues)
415
+ if (cached && cached.tier !== null) {
416
+ // Only use stale cache if it was validated within last 15 minutes
417
+ // and the original validation was successful (tier !== null)
418
+ const staleTolerance = 15 * 60 * 1000; // 15 minutes
419
+ if (now - cached.validatedAt < staleTolerance) {
420
+ console.error(`[TIER-AUTH] API unreachable, using stale cache for ${keyHash.slice(0, 8)}...`);
421
+ return cached.tier;
422
+ }
423
+ }
424
+
425
+ // No valid cache - fail closed for security
426
+ console.error('[TIER-AUTH] API validation failed with no valid cache, denying access:', error.message);
427
+ return null;
215
428
  }
216
- } // default for unknown keys
429
+ }
217
430
 
218
431
  /**
219
432
  * Check if user has access to a specific feature
@@ -245,28 +458,20 @@ export async function getFeatureAccessStatus(featureName, providedApiKey = null)
245
458
  }
246
459
  const currentTierConfig = TIERS[currentTier];
247
460
 
248
- // Find which tier has this feature
249
- let requiredTier = null;
250
- let requiredTierConfig = null;
461
+ // Find which tier requires this feature using CLI_FEATURE_TIERS mapping
462
+ const requiredTier = CLI_FEATURE_TIERS[featureName];
251
463
 
252
- for (const [tierName, tierConfig] of Object.entries(TIERS)) {
253
- if (tierConfig.features.includes(featureName)) {
254
- requiredTier = tierName;
255
- requiredTierConfig = tierConfig;
256
- break;
257
- }
258
- }
259
-
260
- // If feature not found in any tier, deny access
464
+ // If feature not found in mapping, allow it (default to free)
261
465
  if (!requiredTier) {
262
466
  return {
263
- hasAccess: false,
467
+ hasAccess: true,
264
468
  tier: currentTier,
265
- reason: `${featureName} is not available in any tier`,
266
- upgradeUrl: 'https://vibecheckai.dev'
469
+ reason: `Feature '${featureName}' has no tier restriction`
267
470
  };
268
471
  }
269
472
 
473
+ const requiredTierConfig = TIERS[requiredTier];
474
+
270
475
  // Check if current tier meets minimum requirement (using order)
271
476
  const hasAccess = currentTierConfig.order >= requiredTierConfig.order;
272
477
 
@@ -292,7 +497,7 @@ export async function getFeatureAccessStatus(featureName, providedApiKey = null)
292
497
  */
293
498
  export function withTierCheck(featureName, handler) {
294
499
  return async (args) => {
295
- const access = await checkFeatureAccess(featureName, args?.apiKey);
500
+ const access = await getFeatureAccessStatus(featureName, args?.apiKey);
296
501
 
297
502
  if (!access.hasAccess) {
298
503
  return {
@@ -398,7 +603,7 @@ export async function getMcpToolAccess(toolName, providedApiKey = null) {
398
603
  */
399
604
  export function withMcpToolCheck(toolName, handler) {
400
605
  return async (args) => {
401
- const access = await checkMcpToolAccess(toolName, args?.apiKey);
606
+ const access = await getMcpToolAccess(toolName, args?.apiKey);
402
607
 
403
608
  if (!access.hasAccess) {
404
609
  return {
@@ -432,12 +637,17 @@ export async function getUserInfo() {
432
637
  const tier = getTierFromApiKey(config.apiKey);
433
638
  const tierConfig = TIERS[tier];
434
639
 
640
+ // Get features available for this tier from CLI_FEATURE_TIERS
641
+ const availableFeatures = Object.entries(CLI_FEATURE_TIERS)
642
+ .filter(([, reqTier]) => TIERS[reqTier].order <= tierConfig.order)
643
+ .map(([feature]) => feature);
644
+
435
645
  return {
436
646
  authenticated: true,
437
647
  tier,
438
648
  email: config.email,
439
649
  authenticatedAt: config.authenticatedAt,
440
- features: tierConfig.features,
650
+ features: availableFeatures,
441
651
  limits: tierConfig.limits,
442
652
  mcpTools: tierConfig.mcpTools,
443
653
  };
@@ -570,8 +570,47 @@ export function enforceClaimResult(result, policy = "strict") {
570
570
  return { allowed: true, confidence: derived };
571
571
  }
572
572
 
573
+ // =============================================================================
574
+ // CLAIM VALIDATION WITH RACE CONDITION PROTECTION
575
+ //
576
+ // SECURITY FIX: Previous implementation had a TOCTOU race condition:
577
+ // 1. Thread A: hasRecentClaimValidation() returns true
578
+ // 2. Thread B: invalidates the claim (file change, etc.)
579
+ // 3. Thread A: proceeds with stale claim → invalid state
580
+ //
581
+ // New implementation uses atomic check-and-consume pattern with per-project locks.
582
+ // =============================================================================
583
+
584
+ /**
585
+ * Per-project validation locks to prevent concurrent operations
586
+ * from using the same validation state.
587
+ */
588
+ const validationLocks = new Map(); // Map<projectPath, { locked: boolean, queue: Promise }>
589
+
590
+ /**
591
+ * Acquire a validation lock for a project (serializes validation checks).
592
+ */
593
+ function acquireValidationLock(projectPath) {
594
+ let lockState = validationLocks.get(projectPath);
595
+ if (!lockState) {
596
+ lockState = { locked: false, queue: Promise.resolve() };
597
+ validationLocks.set(projectPath, lockState);
598
+ }
599
+
600
+ const acquirePromise = lockState.queue.then(() => {
601
+ lockState.locked = true;
602
+ return () => {
603
+ lockState.locked = false;
604
+ };
605
+ });
606
+
607
+ lockState.queue = acquirePromise.catch(() => {});
608
+ return acquirePromise;
609
+ }
610
+
573
611
  /**
574
- * Claim validation freshness per policy TTL.
612
+ * Check claim validation freshness (basic check, no lock).
613
+ * Use checkAndConsumeClaimValidation for atomic operations.
575
614
  */
576
615
  export function hasRecentClaimValidation(projectPath, policy = "strict") {
577
616
  const last = state.lastValidationByProject.get(projectPath);
@@ -580,6 +619,56 @@ export function hasRecentClaimValidation(projectPath, policy = "strict") {
580
619
  return Date.now() - last <= ttl;
581
620
  }
582
621
 
622
+ /**
623
+ * Atomic check-and-consume claim validation.
624
+ *
625
+ * SECURITY: Use this for operations that depend on claim validation.
626
+ * It ensures no other operation can use the same validation state concurrently.
627
+ *
628
+ * @param {string} projectPath - Project path
629
+ * @param {string} policy - Policy name (strict/balanced/permissive)
630
+ * @param {string} operationId - Unique ID for this operation (for audit)
631
+ * @returns {Promise<{ valid: boolean, consumedAt?: number, reason?: string }>}
632
+ */
633
+ export async function checkAndConsumeClaimValidation(projectPath, policy = "strict", operationId = null) {
634
+ const release = await acquireValidationLock(projectPath);
635
+
636
+ try {
637
+ const last = state.lastValidationByProject.get(projectPath);
638
+ const now = Date.now();
639
+
640
+ if (typeof last !== "number") {
641
+ return {
642
+ valid: false,
643
+ reason: "No claim validation found for this project"
644
+ };
645
+ }
646
+
647
+ const ttl = getPolicyConfig(policy).validationTTL;
648
+ const age = now - last;
649
+
650
+ if (age > ttl) {
651
+ return {
652
+ valid: false,
653
+ reason: `Claim validation expired (age: ${Math.round(age / 1000)}s, TTL: ${Math.round(ttl / 1000)}s)`
654
+ };
655
+ }
656
+
657
+ // Mark this validation as consumed by updating the timestamp
658
+ // This prevents replay/reuse of the same validation
659
+ state.lastValidationByProject.set(projectPath, now);
660
+
661
+ return {
662
+ valid: true,
663
+ consumedAt: now,
664
+ operationId,
665
+ };
666
+
667
+ } finally {
668
+ release();
669
+ }
670
+ }
671
+
583
672
  // =============================================================================
584
673
  // FINGERPRINT + WRAPPER
585
674
  // =============================================================================
@@ -1069,22 +1158,63 @@ async function proposePatch(projectPath, args) {
1069
1158
  return patch;
1070
1159
  }
1071
1160
 
1161
+ /**
1162
+ * Validate command against strict allowlist.
1163
+ *
1164
+ * SECURITY FIX: Previous allowlist was too permissive, allowing arbitrary code execution:
1165
+ * - "node -e 'require(\"child_process\").exec(\"rm -rf /\")'" would pass
1166
+ * - "npm exec malicious-package" would pass
1167
+ * - "pnpm dlx evil-tool" would pass
1168
+ *
1169
+ * New allowlist only permits specific safe subcommands.
1170
+ */
1072
1171
  function commandAllowlisted(cmd) {
1073
- // Keep this tight. Expand only if you mean it.
1074
- const allow = [
1075
- /^vibecheck(\s|$)/,
1076
- /^pnpm(\s|$)/,
1077
- /^npm(\s|$)/,
1078
- /^yarn(\s|$)/,
1079
- /^node(\s|$)/,
1080
- /^bun(\s|$)/,
1081
- /^vitest(\s|$)/,
1082
- /^jest(\s|$)/,
1083
- /^tsc(\s|$)/,
1084
- /^eslint(\s|$)/,
1085
- /^playwright(\s|$)/,
1172
+ const trimmed = cmd.trim();
1173
+
1174
+ // Reject commands with shell metacharacters that could enable injection
1175
+ // These are dangerous even in "safe" commands: ; | & $ ` \ ( ) { } < > \n
1176
+ if (/[;|&$`\\(){}<>\n]/.test(trimmed)) {
1177
+ return false;
1178
+ }
1179
+
1180
+ // Reject commands that use flags commonly used for code execution
1181
+ if (/\s-[eEc]\s|\s--eval\s|\s--exec\s/i.test(trimmed)) {
1182
+ return false;
1183
+ }
1184
+
1185
+ // Strict allowlist: only specific commands with specific safe subcommands
1186
+ const strictAllow = [
1187
+ // Vibecheck CLI - only specific safe commands
1188
+ /^vibecheck\s+(ship|scan|ctx|lint|status)\b/,
1189
+ /^vibecheck\s+--help\b/,
1190
+ /^vibecheck\s+--version\b/,
1191
+
1192
+ // Package managers - only test/build/lint (no exec, dlx, or install scripts)
1193
+ /^pnpm\s+(test|build|lint|typecheck|check|run\s+(test|build|lint|typecheck))\b/,
1194
+ /^npm\s+(test|run\s+(test|build|lint|typecheck))\b/,
1195
+ /^yarn\s+(test|build|lint|typecheck|run\s+(test|build|lint|typecheck))\b/,
1196
+ /^bun\s+(test|run\s+(test|build|lint))\b/,
1197
+
1198
+ // TypeScript compiler - only type checking (no emit)
1199
+ /^tsc\s+(--noEmit|--build)\b/,
1200
+ /^tsc$/, // Default tsc with no args is safe
1201
+
1202
+ // Linters - safe read-only operations
1203
+ /^eslint\s+/, // ESLint with any args (read-only)
1204
+ /^eslint$/,
1205
+
1206
+ // Test runners - only run tests
1207
+ /^vitest\s*(run|--run)?\b/,
1208
+ /^vitest$/,
1209
+ /^jest\s*(--ci|--coverage|--passWithNoTests)?\b/,
1210
+ /^jest$/,
1211
+
1212
+ // Playwright - only test mode (no codegen which opens browsers)
1213
+ /^playwright\s+test\b/,
1214
+ /^npx\s+playwright\s+test\b/,
1086
1215
  ];
1087
- return allow.some((re) => re.test(cmd.trim()));
1216
+
1217
+ return strictAllow.some((re) => re.test(trimmed));
1088
1218
  }
1089
1219
 
1090
1220
  async function verifyPatch(projectPath, args) {
@@ -13,7 +13,7 @@
13
13
  import path from "path";
14
14
  import fs from "fs/promises";
15
15
  import { execSync } from "child_process";
16
- import { withTierCheck, checkFeatureAccess } from "./tier-auth.js";
16
+ import { withTierCheck, getFeatureAccessStatus } from "./tier-auth.js";
17
17
 
18
18
  // ============================================================================
19
19
  // TOOL DEFINITIONS
@@ -292,7 +292,7 @@ export async function handleVibecheckTool(toolName, args) {
292
292
 
293
293
  const requiredFeature = featureMap[toolName];
294
294
  if (requiredFeature) {
295
- const access = await checkFeatureAccess(requiredFeature, args?.apiKey);
295
+ const access = await getFeatureAccessStatus(requiredFeature, args?.apiKey);
296
296
  if (!access.hasAccess) {
297
297
  return {
298
298
  content: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecheckai/cli",
3
- "version": "3.2.6",
3
+ "version": "3.3.0",
4
4
  "description": "Vibecheck CLI - Ship with confidence. One verdict: SHIP | WARN | BLOCK.",
5
5
  "main": "bin/vibecheck.js",
6
6
  "bin": {
@@ -37,7 +37,6 @@
37
37
  "js-yaml": "^4.1.0",
38
38
  "ora": "^8.0.0",
39
39
  "uuid": "^9.0.0",
40
- "yaml": "^2.3.0",
41
40
  "zod": "^3.23.0"
42
41
  },
43
42
  "optionalDependencies": {
@@ -77,7 +76,7 @@
77
76
  "url": "https://github.com/vibecheck-oss/vibecheck/issues"
78
77
  },
79
78
  "engines": {
80
- "node": ">=18.0.0"
79
+ "node": ">=20.11"
81
80
  },
82
81
  "publishConfig": {
83
82
  "access": "public"