@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
@@ -3,25 +3,91 @@
3
3
  *
4
4
  * Intercepts file write/patch tool calls from AI agents.
5
5
  * Validates changes against truthpack and policy before allowing writes.
6
+ *
7
+ * Codename: Sentinel
8
+ *
9
+ * Tier-based enforcement:
10
+ * - FREE: Observe mode (logs violations but allows writes)
11
+ * - STARTER: Advisory mode (warns but allows with confirmation)
12
+ * - PRO: Enforce mode (blocks violations)
13
+ * - ENTERPRISE: Enforce mode + audit trail
14
+ *
15
+ * SECURITY: Tier is NEVER trusted from client - always derived from validated API key
6
16
  */
7
17
 
8
- "use strict";
18
+ import path from "path";
19
+ import fs from "fs";
20
+ import { createRequire } from "module";
21
+
22
+ // Import tier auth for secure tier validation
23
+ import { getMcpToolAccess } from "./tier-auth.js";
9
24
 
10
- const path = require("path");
11
- const fs = require("fs");
12
- const { interceptFileWrite, interceptMultiFileWrite } = require("../../bin/runners/lib/agent-firewall/interceptor/base");
25
+ const require = createRequire(import.meta.url);
26
+
27
+ // Import core firewall modules
28
+ const { interceptFileWrite, interceptMultiFileWrite } = require("../bin/runners/lib/agent-firewall/interceptor/base");
29
+ const { loadPolicy } = require("../bin/runners/lib/agent-firewall/policy/loader");
30
+
31
+ // Import new Sentinel modules
32
+ let reality, risk, simulator, proposal, critic, proofBuilder;
33
+ try {
34
+ reality = require("../bin/runners/lib/agent-firewall/reality");
35
+ risk = require("../bin/runners/lib/agent-firewall/risk");
36
+ simulator = require("../bin/runners/lib/agent-firewall/simulator");
37
+ proposal = require("../bin/runners/lib/agent-firewall/proposal");
38
+ critic = require("../bin/runners/lib/agent-firewall/critic");
39
+ proofBuilder = require("../bin/runners/lib/agent-firewall/change-packet/builder");
40
+ } catch (err) {
41
+ console.warn(`[Agent Firewall] Some Sentinel modules not available: ${err.message}`);
42
+ }
43
+
44
+ // Tier-based policy mode mapping
45
+ const TIER_POLICY_MODES = {
46
+ FREE: "observe", // Log only, never block
47
+ STARTER: "advisory", // Warn, allow with confirmation
48
+ PRO: "enforce", // Block violations
49
+ ENTERPRISE: "enforce", // Block + full audit
50
+ };
51
+
52
+ /**
53
+ * Get effective policy mode based on tier
54
+ * @param {string} tier - User's subscription tier
55
+ * @param {object} policy - Policy configuration
56
+ * @returns {string} Effective mode
57
+ */
58
+ function getEffectivePolicyMode(tier, policy) {
59
+ // If policy explicitly sets mode, respect it for PRO+
60
+ if (policy.mode && (tier === "PRO" || tier === "ENTERPRISE")) {
61
+ return policy.mode;
62
+ }
63
+
64
+ // Otherwise use tier-based defaults
65
+ return TIER_POLICY_MODES[tier] || "observe";
66
+ }
13
67
 
14
68
  /**
15
69
  * MCP Tool Definition
16
70
  */
17
71
  const AGENT_FIREWALL_TOOL = {
18
72
  name: "vibecheck_agent_firewall_intercept",
19
- description: `🛡️ Agent Firewall - Intercepts AI code changes and validates against repo truth.
73
+ description: `🛡️ Agent Firewall (Sentinel) - Intercepts AI code changes and validates against repo truth.
20
74
 
21
75
  This tool MUST be called before any file write/patch operations.
22
- It validates changes against truthpack and policy rules.
76
+ It validates changes against truthpack, policy rules, and reality state.
77
+
78
+ Features:
79
+ - Reality state validation (routes, env vars, services)
80
+ - Risk scoring (surface area, blast radius, irreversibility)
81
+ - Diff simulation (broken imports, orphaned files)
82
+ - Assumption verification
83
+ - Proof artifact generation
84
+
85
+ Tier modes:
86
+ - FREE: Observe (logs only)
87
+ - STARTER: Advisory (warns)
88
+ - PRO/ENTERPRISE: Enforce (blocks)
23
89
 
24
- Returns: { allowed, verdict, violations, unblockPlan }`,
90
+ Returns: { allowed, verdict, riskScore, violations, unblockPlan, proofId }`,
25
91
  inputSchema: {
26
92
  type: "object",
27
93
  required: ["agentId", "filePath", "content"],
@@ -46,17 +112,50 @@ Returns: { allowed, verdict, violations, unblockPlan }`,
46
112
  type: "string",
47
113
  description: "Agent's stated intent for this change"
48
114
  },
115
+ summary: {
116
+ type: "string",
117
+ description: "Human-readable summary of the change"
118
+ },
119
+ assumptions: {
120
+ type: "array",
121
+ description: "Declared assumptions (auto-extracted if not provided)",
122
+ items: {
123
+ type: "object",
124
+ properties: {
125
+ type: { type: "string", enum: ["env", "route", "service", "file"] },
126
+ key: { type: "string" },
127
+ reason: { type: "string" }
128
+ }
129
+ }
130
+ },
131
+ confidence: {
132
+ type: "number",
133
+ description: "Confidence level (0-1) that the change is correct",
134
+ minimum: 0,
135
+ maximum: 1
136
+ },
49
137
  projectRoot: {
50
138
  type: "string",
51
139
  default: ".",
52
140
  description: "Project root directory"
141
+ },
142
+ apiKey: {
143
+ type: "string",
144
+ description: "API key for authentication (tier is derived from this, never client-provided)"
53
145
  }
146
+ // NOTE: 'tier' parameter removed for security - tier is now derived from validated apiKey
147
+ // Accepting tier from client allowed privilege escalation attacks
54
148
  }
55
149
  }
56
150
  };
57
151
 
58
152
  /**
59
153
  * Handle MCP tool call
154
+ *
155
+ * SECURITY: Tier is NEVER accepted from client args - always derived from validated API key.
156
+ * Previous implementation accepted args.tier which allowed attackers to escalate privileges
157
+ * by simply passing tier: "ENTERPRISE".
158
+ *
60
159
  * @param {string} name - Tool name (unused, for consistency)
61
160
  * @param {object} args - Tool arguments
62
161
  * @returns {object} MCP tool response
@@ -69,6 +168,19 @@ async function handleAgentFirewallIntercept(name, args) {
69
168
  const oldContent = args.oldContent || null;
70
169
  const intent = args.intent || "No intent provided";
71
170
 
171
+ // SECURITY FIX: Derive tier from validated API key, NEVER trust client-provided tier
172
+ let tier = "FREE"; // Default to most restrictive (observe mode)
173
+ try {
174
+ const access = await getMcpToolAccess("vibecheck_agent_firewall_intercept", args.apiKey);
175
+ if (access.tier) {
176
+ tier = access.tier.toUpperCase();
177
+ }
178
+ // Note: Even if access check fails, we continue with FREE tier (observe mode)
179
+ // This allows the tool to still provide value while logging violations
180
+ } catch (err) {
181
+ console.warn(`[Agent Firewall] Tier validation failed, defaulting to FREE: ${err.message}`);
182
+ }
183
+
72
184
  // Validate file path is within project root
73
185
  const fileAbs = path.resolve(projectRoot, filePath);
74
186
  if (!fileAbs.startsWith(projectRoot + path.sep) && fileAbs !== projectRoot) {
@@ -82,13 +194,97 @@ async function handleAgentFirewallIntercept(name, args) {
82
194
  }
83
195
 
84
196
  try {
197
+ // Load policy and determine effective mode based on tier
198
+ const policy = loadPolicy(projectRoot);
199
+ const effectiveMode = getEffectivePolicyMode(tier, policy);
200
+
85
201
  // Read old content if not provided
202
+ // SECURITY FIX: Compute content hash to detect concurrent modifications
203
+ // Between reading and validation, another agent could modify the file.
204
+ // We provide the hash so callers can verify before actual write.
86
205
  let actualOldContent = oldContent;
206
+ let oldContentHash = null;
207
+ const crypto = require('crypto');
208
+
87
209
  if (!actualOldContent && fs.existsSync(fileAbs)) {
88
210
  actualOldContent = fs.readFileSync(fileAbs, "utf8");
89
211
  }
90
212
 
91
- // Intercept the write
213
+ // Compute hash of the content we're validating against
214
+ if (actualOldContent) {
215
+ oldContentHash = crypto.createHash('sha256').update(actualOldContent).digest('hex');
216
+ }
217
+
218
+ // Build structured proposal from args
219
+ let structuredProposal = null;
220
+ if (proposal) {
221
+ structuredProposal = proposal.proposal.create(
222
+ proposal.proposal.normalizeIntent(intent),
223
+ [{ type: actualOldContent ? "modify" : "create", path: filePath, content }]
224
+ );
225
+ structuredProposal.summary = args.summary || intent;
226
+ structuredProposal.assumptions = args.assumptions || [];
227
+ structuredProposal.confidence = args.confidence ?? 0.5;
228
+
229
+ // Auto-extract assumptions if not provided
230
+ if (structuredProposal.assumptions.length === 0) {
231
+ const extracted = proposal.extractFromOperations(structuredProposal.operations);
232
+ structuredProposal.assumptions = extracted;
233
+ }
234
+
235
+ // Validate proposal
236
+ const validationResult = proposal.proposal.validate(structuredProposal);
237
+ if (!validationResult.valid && effectiveMode === "enforce") {
238
+ return {
239
+ content: [{
240
+ type: "text",
241
+ text: `❌ INVALID PROPOSAL: ${validationResult.errors.map(e => e.message).join(", ")}`
242
+ }],
243
+ isError: true
244
+ };
245
+ }
246
+ }
247
+
248
+ // Get reality state
249
+ let realityState = null;
250
+ if (reality) {
251
+ try {
252
+ realityState = reality.reality.getState(projectRoot);
253
+ } catch (err) {
254
+ console.warn(`[Agent Firewall] Reality state unavailable: ${err.message}`);
255
+ }
256
+ }
257
+
258
+ // Calculate risk score
259
+ let riskScore = null;
260
+ if (risk && structuredProposal) {
261
+ try {
262
+ riskScore = risk.calculateRiskScore({
263
+ files: [{ path: filePath }],
264
+ operations: structuredProposal.operations,
265
+ claims: [],
266
+ evidence: [],
267
+ intent,
268
+ assumptions: structuredProposal.assumptions,
269
+ proposalConfidence: structuredProposal.confidence,
270
+ policy,
271
+ });
272
+ } catch (err) {
273
+ console.warn(`[Agent Firewall] Risk scoring failed: ${err.message}`);
274
+ }
275
+ }
276
+
277
+ // Run diff simulation
278
+ let simulationResult = null;
279
+ if (simulator && content) {
280
+ try {
281
+ simulationResult = simulator.quickSimulate(projectRoot, filePath, content, actualOldContent);
282
+ } catch (err) {
283
+ console.warn(`[Agent Firewall] Simulation failed: ${err.message}`);
284
+ }
285
+ }
286
+
287
+ // Intercept the write with core firewall
92
288
  const result = await interceptFileWrite({
93
289
  projectRoot,
94
290
  agentId,
@@ -98,42 +294,182 @@ async function handleAgentFirewallIntercept(name, args) {
98
294
  oldContent: actualOldContent
99
295
  });
100
296
 
101
- // Check policy mode (already loaded in interceptFileWrite, but we need it for mode check)
102
- const { loadPolicy } = require("../../bin/runners/lib/agent-firewall/policy/loader");
103
- const policy = loadPolicy(projectRoot);
297
+ // Get critic verdict (rule-based, LLM optional)
298
+ let criticVerdict = null;
299
+ if (critic) {
300
+ try {
301
+ criticVerdict = await critic.critic.evaluate({
302
+ proposal: structuredProposal,
303
+ validationResults: result,
304
+ riskScore,
305
+ simulationResult,
306
+ realityState,
307
+ });
308
+ } catch (err) {
309
+ console.warn(`[Agent Firewall] Critic evaluation failed: ${err.message}`);
310
+ }
311
+ }
104
312
 
105
- if (policy.mode === "observe") {
106
- // Observe mode - log but don't block
107
- return {
108
- content: [{
109
- type: "text",
110
- text: `📊 OBSERVE MODE: ${result.verdict}\n\n${result.message}\n\nPacket ID: ${result.packetId}`
111
- }]
112
- };
313
+ // Build enhanced proof artifact
314
+ let proofId = result.packetId;
315
+ if (proofBuilder && riskScore) {
316
+ try {
317
+ const proofArtifact = proofBuilder.buildProofArtifact({
318
+ changeId: `c-${result.packetId}`,
319
+ decision: result.verdict,
320
+ rulesTriggered: (result.violations || []).map(v => v.rule),
321
+ assumptionsFailed: (result.violations || []).filter(v => v.type === "assumption").map(v => v.key),
322
+ riskScore,
323
+ simulationResult,
324
+ criticVerdict,
325
+ });
326
+ proofId = proofArtifact.changeId;
327
+ } catch (err) {
328
+ console.warn(`[Agent Firewall] Proof artifact generation failed: ${err.message}`);
329
+ }
113
330
  }
114
331
 
115
- // Enforce mode - block if not allowed
116
- if (!result.allowed) {
117
- let message = `❌ BLOCKED: ${result.message}\n\n`;
332
+ // Format response based on mode
333
+ const formatResponse = (isBlocked) => {
334
+ let message = "";
335
+ const icon = isBlocked ? "❌" : (effectiveMode === "observe" ? "📊" : "✅");
336
+ const modeLabel = effectiveMode.toUpperCase();
337
+
338
+ message += `${icon} ${modeLabel} MODE: ${result.verdict}\n\n`;
339
+
340
+ // Risk score
341
+ if (riskScore) {
342
+ message += `Risk: ${riskScore.total} (${riskScore.level})\n`;
343
+ if (riskScore.reasons.length > 0) {
344
+ message += `Factors: ${riskScore.reasons.slice(0, 3).join(", ")}\n`;
345
+ }
346
+ message += "\n";
347
+ }
118
348
 
349
+ // Simulation result
350
+ if (simulationResult) {
351
+ message += `Simulation: ${simulationResult.passed ? "✅ Passed" : "❌ Failed"}\n`;
352
+ if (!simulationResult.passed && simulationResult.errors.length > 0) {
353
+ message += `Errors: ${simulationResult.errors.slice(0, 2).map(e => e.message).join("; ")}\n`;
354
+ }
355
+ message += "\n";
356
+ }
357
+
358
+ // Violations
119
359
  if (result.violations && result.violations.length > 0) {
120
360
  message += "Violations:\n";
121
- for (const violation of result.violations) {
122
- message += ` - ${violation.rule}: ${violation.message}\n`;
361
+ for (const violation of result.violations.slice(0, 5)) {
362
+ message += ` - ${violation.rule || violation.type}: ${violation.message}\n`;
123
363
  }
364
+ message += "\n";
124
365
  }
125
366
 
126
- if (result.unblockPlan && result.unblockPlan.steps.length > 0) {
127
- message += "\nTo unblock:\n";
128
- for (const step of result.unblockPlan.steps) {
129
- message += ` ${step.action === "create" ? "Create" : "Modify"} ${step.file || "file"}: ${step.description}\n`;
367
+ // Critic verdict (if blocked)
368
+ if (criticVerdict && criticVerdict.verdict === "BLOCK") {
369
+ message += `Critic: ${criticVerdict.verdict} (${(criticVerdict.confidence * 100).toFixed(0)}% confidence)\n`;
370
+ if (criticVerdict.reasoning.length > 0) {
371
+ message += `Reasoning: ${criticVerdict.reasoning[0]}\n`;
130
372
  }
373
+ message += "\n";
131
374
  }
132
375
 
376
+ // Unblock plan
377
+ if (isBlocked && result.unblockPlan && result.unblockPlan.steps?.length > 0) {
378
+ message += "To unblock:\n";
379
+ for (const step of result.unblockPlan.steps.slice(0, 3)) {
380
+ message += ` ${step.action === "create" ? "➕ Create" : "✏️ Modify"} ${step.file || "file"}: ${step.description}\n`;
381
+ }
382
+ message += "\n";
383
+ }
384
+
385
+ message += `Proof ID: ${proofId}\n`;
386
+
387
+ // SECURITY: Include content hash for race condition protection
388
+ // Callers MUST verify this hash matches current file content before writing
389
+ // to prevent TOCTOU (time-of-check-time-of-use) vulnerabilities
390
+ if (oldContentHash) {
391
+ message += `\n⚠️ IMPORTANT: Before writing, verify file hash matches:\n`;
392
+ message += `Content Hash: ${oldContentHash}\n`;
393
+ message += `(Re-read file and compare SHA-256 hash to detect concurrent modifications)`;
394
+ } else {
395
+ message += `\nNote: New file (no existing content to verify)`;
396
+ }
397
+
398
+ return message;
399
+ };
400
+
401
+ // Determine final decision based on mode
402
+ if (effectiveMode === "observe") {
403
+ // Observe mode - always allow, log everything
404
+ return {
405
+ content: [{
406
+ type: "text",
407
+ text: formatResponse(false)
408
+ }]
409
+ };
410
+ }
411
+
412
+ if (effectiveMode === "advisory") {
413
+ // Advisory mode - warn but allow (for STARTER tier)
414
+ const shouldWarn = !result.allowed || (riskScore && riskScore.total > 50);
415
+ return {
416
+ content: [{
417
+ type: "text",
418
+ text: formatResponse(false) + (shouldWarn ? "\n\n⚠️ Consider reviewing before proceeding." : "")
419
+ }]
420
+ };
421
+ }
422
+
423
+ // Enforce mode - block if not allowed
424
+ // More intelligent blocking logic to reduce false positives:
425
+ // 1. Base interception result must say not allowed, OR
426
+ // 2. Simulation failed with actual errors (not just warnings), OR
427
+ // 3. Critic says BLOCK with very high confidence (90%+), OR
428
+ // 4. Risk score decision is BLOCK (but respect lowered thresholds)
429
+
430
+ // Count the number of blocking signals
431
+ let blockingSignals = 0;
432
+ const blockReasons = [];
433
+
434
+ if (!result.allowed) {
435
+ blockingSignals++;
436
+ blockReasons.push("policy_violation");
437
+ }
438
+
439
+ // Only count simulation failure if it has actual errors (not just warnings)
440
+ if (simulationResult && !simulationResult.passed) {
441
+ const hasActualErrors = simulationResult.errors?.some(e =>
442
+ e.severity === 'error' || e.type === 'broken_import' || e.type === 'syntax_error'
443
+ );
444
+ if (hasActualErrors) {
445
+ blockingSignals++;
446
+ blockReasons.push("simulation_failed");
447
+ }
448
+ }
449
+
450
+ // Critic verdict - require very high confidence (90%+) to block
451
+ if (criticVerdict && criticVerdict.verdict === "BLOCK" && criticVerdict.confidence >= 0.9) {
452
+ blockingSignals++;
453
+ blockReasons.push("critic_blocked");
454
+ }
455
+
456
+ // Risk score decision
457
+ if (riskScore && riskScore.decision?.decision === "BLOCK") {
458
+ blockingSignals++;
459
+ blockReasons.push("risk_threshold_exceeded");
460
+ }
461
+
462
+ // Only block if we have at least 2 independent blocking signals
463
+ // This prevents a single false positive from blocking legitimate changes
464
+ // Exception: if risk score is CRITICAL (total >= 120), block with 1 signal
465
+ const isCriticalRisk = riskScore && riskScore.total >= 120;
466
+ const shouldBlock = (blockingSignals >= 2) || (isCriticalRisk && blockingSignals >= 1);
467
+
468
+ if (shouldBlock) {
133
469
  return {
134
470
  content: [{
135
471
  type: "text",
136
- text: message
472
+ text: formatResponse(true) + `\n\nBlocking reasons: ${blockReasons.join(", ")}`
137
473
  }],
138
474
  isError: true
139
475
  };
@@ -143,7 +479,7 @@ async function handleAgentFirewallIntercept(name, args) {
143
479
  return {
144
480
  content: [{
145
481
  type: "text",
146
- text: `✅ ALLOWED: ${result.message}\n\nPacket ID: ${result.packetId}`
482
+ text: formatResponse(false)
147
483
  }]
148
484
  };
149
485
 
@@ -158,7 +494,7 @@ async function handleAgentFirewallIntercept(name, args) {
158
494
  }
159
495
  }
160
496
 
161
- module.exports = {
497
+ export {
162
498
  AGENT_FIREWALL_TOOL,
163
499
  handleAgentFirewallIntercept
164
500
  };