@vibecheckai/cli 3.2.6 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/registry.js +192 -5
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +81 -18
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +148 -0
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +63 -44
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +106 -19
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1149 -243
- package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +245 -35
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- package/mcp-server/package-lock.json +0 -165
package/mcp-server/tier-auth.js
CHANGED
|
@@ -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
|
-
*
|
|
175
|
-
*
|
|
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
|
|
340
|
+
if (!apiKey || typeof apiKey !== 'string') return null;
|
|
179
341
|
|
|
180
|
-
//
|
|
181
|
-
if (apiKey.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
+
tier = 'starter';
|
|
387
|
+
break;
|
|
205
388
|
case 'pro':
|
|
206
|
-
|
|
389
|
+
tier = 'pro';
|
|
390
|
+
break;
|
|
207
391
|
case 'enterprise':
|
|
208
|
-
|
|
392
|
+
tier = 'enterprise';
|
|
393
|
+
break;
|
|
209
394
|
default:
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
}
|
|
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
|
|
249
|
-
|
|
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
|
-
|
|
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:
|
|
467
|
+
hasAccess: true,
|
|
264
468
|
tier: currentTier,
|
|
265
|
-
reason:
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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": ">=
|
|
79
|
+
"node": ">=20.11"
|
|
81
80
|
},
|
|
82
81
|
"publishConfig": {
|
|
83
82
|
"access": "public"
|