@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
|
@@ -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
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
//
|
|
116
|
-
|
|
117
|
-
let message =
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
497
|
+
export {
|
|
162
498
|
AGENT_FIREWALL_TOOL,
|
|
163
499
|
handleAgentFirewallIntercept
|
|
164
500
|
};
|