@vibecheckai/cli 3.2.0 → 3.2.2

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 (60) hide show
  1. package/bin/runners/lib/agent-firewall/change-packet/builder.js +214 -0
  2. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  3. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  4. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  5. package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
  6. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  7. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  8. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  9. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
  10. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  11. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
  12. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  13. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  14. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  15. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  16. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  17. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  18. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  19. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  20. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  21. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  22. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  23. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  24. package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
  25. package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
  26. package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
  27. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  28. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  29. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
  30. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
  31. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
  32. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  33. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  34. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  35. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  36. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  37. package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
  38. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  39. package/bin/runners/lib/analysis-core.js +198 -180
  40. package/bin/runners/lib/analyzers.js +1119 -536
  41. package/bin/runners/lib/cli-output.js +236 -210
  42. package/bin/runners/lib/detectors-v2.js +547 -785
  43. package/bin/runners/lib/fingerprint.js +377 -0
  44. package/bin/runners/lib/route-truth.js +1167 -322
  45. package/bin/runners/lib/scan-output.js +144 -738
  46. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  47. package/bin/runners/lib/terminal-ui.js +188 -770
  48. package/bin/runners/lib/truth.js +1004 -321
  49. package/bin/runners/lib/unified-output.js +162 -158
  50. package/bin/runners/runAgent.js +161 -0
  51. package/bin/runners/runFirewall.js +134 -0
  52. package/bin/runners/runFirewallHook.js +56 -0
  53. package/bin/runners/runScan.js +113 -10
  54. package/bin/runners/runShip.js +7 -8
  55. package/bin/runners/runTruth.js +89 -0
  56. package/mcp-server/agent-firewall-interceptor.js +164 -0
  57. package/mcp-server/index.js +347 -313
  58. package/mcp-server/truth-context.js +131 -90
  59. package/mcp-server/truth-firewall-tools.js +1412 -1045
  60. package/package.json +1 -1
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Firewall Hook Manager
3
+ *
4
+ * Manages file system hook installation and control.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const { installFileSystemHook, startFileSystemHook, stopFileSystemHook } = require("./lib/agent-firewall/fs-hook/installer");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+
13
+ /**
14
+ * Run firewall hook command
15
+ * @param {object} options - Command options
16
+ * @param {string} options.action - Action: 'install', 'start', 'stop', 'status'
17
+ * @param {string} options.projectRoot - Project root directory
18
+ */
19
+ async function runFirewallHook(options = {}) {
20
+ const projectRoot = options.projectRoot || process.cwd();
21
+ const action = options.action || "status";
22
+
23
+ switch (action) {
24
+ case "install":
25
+ return installFileSystemHook(projectRoot);
26
+
27
+ case "start":
28
+ startFileSystemHook(projectRoot);
29
+ return {
30
+ success: true,
31
+ message: "File system hook started"
32
+ };
33
+
34
+ case "stop":
35
+ stopFileSystemHook();
36
+ return {
37
+ success: true,
38
+ message: "File system hook stopped"
39
+ };
40
+
41
+ case "status":
42
+ const markerFile = path.join(projectRoot, ".vibecheck", "fs-hook-enabled");
43
+ const installed = fs.existsSync(markerFile);
44
+ return {
45
+ installed,
46
+ running: false // Would need process management to check
47
+ };
48
+
49
+ default:
50
+ throw new Error(`Unknown action: ${action}`);
51
+ }
52
+ }
53
+
54
+ module.exports = {
55
+ runFirewallHook
56
+ };
@@ -42,6 +42,11 @@ const {
42
42
  calculateScore,
43
43
  } = require("./lib/scan-output");
44
44
 
45
+ const {
46
+ enrichFindings,
47
+ saveBaseline,
48
+ } = require("./lib/fingerprint");
49
+
45
50
  const BANNER = `
46
51
  ${ansi.rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
47
52
  ${ansi.rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
@@ -103,6 +108,9 @@ function parseArgs(args) {
103
108
  noBanner: globalFlags.noBanner || false,
104
109
  ci: globalFlags.ci || false,
105
110
  quiet: globalFlags.quiet || false,
111
+ // Baseline tracking (fingerprints)
112
+ baseline: true, // Compare against baseline by default
113
+ updateBaseline: false, // --update-baseline to save current findings as baseline
106
114
  // Allowlist subcommand
107
115
  allowlist: null, // null = not using allowlist, or 'list' | 'add' | 'remove' | 'check'
108
116
  allowlistId: null,
@@ -124,6 +132,8 @@ function parseArgs(args) {
124
132
  else if (arg === '--sarif') opts.sarif = true;
125
133
  else if (arg === '--autofix' || arg === '--fix' || arg === '-f') opts.autofix = true;
126
134
  else if (arg === '--no-save') opts.save = false;
135
+ else if (arg === '--no-baseline') opts.baseline = false;
136
+ else if (arg === '--update-baseline' || arg === '--set-baseline') opts.updateBaseline = true;
127
137
  else if (arg === '--path' || arg === '-p') opts.path = cleanArgs[++i] || process.cwd();
128
138
  else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
129
139
  // Allowlist subcommand support
@@ -753,6 +763,7 @@ async function runScan(args) {
753
763
  findDeprecatedApis,
754
764
  findEmptyCatch,
755
765
  findUnsafeRegex,
766
+ clearFileCache, // V3: Memory management
756
767
  } = require('./lib/analyzers');
757
768
 
758
769
  scanRouteIntegrity = async function({ projectPath, layers, baseUrl, verbose }) {
@@ -778,6 +789,9 @@ async function runScan(args) {
778
789
  findings.push(...findEmptyCatch(projectPath));
779
790
  findings.push(...findUnsafeRegex(projectPath));
780
791
 
792
+ // V3: Clear file cache to prevent memory leaks in large monorepos
793
+ clearFileCache();
794
+
781
795
  // Convert to scan format matching TypeScript scanner output
782
796
  const shipBlockers = findings.map((f, i) => ({
783
797
  id: f.id || `finding-${i}`,
@@ -1056,6 +1070,48 @@ async function runScan(args) {
1056
1070
  return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
1057
1071
  }
1058
1072
 
1073
+ // ═══════════════════════════════════════════════════════════════════════════
1074
+ // FINGERPRINTING & BASELINE COMPARISON
1075
+ // ═══════════════════════════════════════════════════════════════════════════
1076
+
1077
+ let diff = null;
1078
+ if (opts.baseline) {
1079
+ try {
1080
+ const enrichResult = enrichFindings(normalizedFindings, projectPath, true);
1081
+ diff = enrichResult.diff;
1082
+
1083
+ // Update findings with fingerprints and status
1084
+ for (let i = 0; i < normalizedFindings.length; i++) {
1085
+ if (enrichResult.findings[i]) {
1086
+ normalizedFindings[i].fingerprint = enrichResult.findings[i].fingerprint;
1087
+ normalizedFindings[i].status = enrichResult.findings[i].status;
1088
+ normalizedFindings[i].firstSeen = enrichResult.findings[i].firstSeen;
1089
+ }
1090
+ }
1091
+ } catch (fpError) {
1092
+ if (opts.verbose) {
1093
+ console.warn(` ${ansi.dim}Fingerprinting skipped: ${fpError.message}${ansi.reset}`);
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ // Update baseline if requested
1099
+ if (opts.updateBaseline) {
1100
+ try {
1101
+ saveBaseline(projectPath, normalizedFindings, {
1102
+ verdict: verdict?.verdict,
1103
+ scanTime: new Date().toISOString(),
1104
+ });
1105
+ if (!opts.json && !opts.quiet) {
1106
+ console.log(` ${colors.success}✓${ansi.reset} Baseline updated with ${normalizedFindings.length} findings`);
1107
+ }
1108
+ } catch (blError) {
1109
+ if (opts.verbose) {
1110
+ console.warn(` ${ansi.dim}Baseline save failed: ${blError.message}${ansi.reset}`);
1111
+ }
1112
+ }
1113
+ }
1114
+
1059
1115
  // ═══════════════════════════════════════════════════════════════════════════
1060
1116
  // ENHANCED OUTPUT
1061
1117
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1069,6 +1125,7 @@ async function runScan(args) {
1069
1125
  breakdown: report.score?.breakdown,
1070
1126
  timings,
1071
1127
  cached,
1128
+ diff, // Include diff for display
1072
1129
  };
1073
1130
  console.log(formatScanOutput(resultForOutput, { verbose: opts.verbose }));
1074
1131
 
@@ -1164,6 +1221,60 @@ async function runScan(args) {
1164
1221
 
1165
1222
  const verdict = criticalCount > 0 ? 'BLOCK' : warningCount > 0 ? 'WARN' : 'SHIP';
1166
1223
 
1224
+ // ═══════════════════════════════════════════════════════════════════════════
1225
+ // FINGERPRINTING & BASELINE COMPARISON (Legacy path)
1226
+ // ═══════════════════════════════════════════════════════════════════════════
1227
+
1228
+ const normalizedLegacyFindings = findings.map(f => ({
1229
+ severity: f.severity === 'critical' || f.severity === 'BLOCK' ? 'critical' :
1230
+ f.severity === 'warning' || f.severity === 'WARN' ? 'medium' : 'low',
1231
+ category: f.category || 'ROUTE',
1232
+ title: f.title || f.message,
1233
+ message: f.message || f.title,
1234
+ file: f.file || f.evidence?.[0]?.file,
1235
+ line: f.line || parseInt(f.evidence?.[0]?.lines?.split('-')[0]) || 1,
1236
+ evidence: f.evidence,
1237
+ fix: f.fixSuggestion,
1238
+ }));
1239
+
1240
+ let diff = null;
1241
+ if (opts.baseline) {
1242
+ try {
1243
+ const enrichResult = enrichFindings(normalizedLegacyFindings, projectPath, true);
1244
+ diff = enrichResult.diff;
1245
+
1246
+ // Update findings with fingerprints and status
1247
+ for (let i = 0; i < normalizedLegacyFindings.length; i++) {
1248
+ if (enrichResult.findings[i]) {
1249
+ normalizedLegacyFindings[i].fingerprint = enrichResult.findings[i].fingerprint;
1250
+ normalizedLegacyFindings[i].status = enrichResult.findings[i].status;
1251
+ normalizedLegacyFindings[i].firstSeen = enrichResult.findings[i].firstSeen;
1252
+ }
1253
+ }
1254
+ } catch (fpError) {
1255
+ if (opts.verbose) {
1256
+ console.warn(` ${ansi.dim}Fingerprinting skipped: ${fpError.message}${ansi.reset}`);
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ // Update baseline if requested
1262
+ if (opts.updateBaseline) {
1263
+ try {
1264
+ saveBaseline(projectPath, normalizedLegacyFindings, {
1265
+ verdict,
1266
+ scanTime: new Date().toISOString(),
1267
+ });
1268
+ if (!opts.json && !opts.quiet) {
1269
+ console.log(` ${colors.success}✓${ansi.reset} Baseline updated with ${normalizedLegacyFindings.length} findings`);
1270
+ }
1271
+ } catch (blError) {
1272
+ if (opts.verbose) {
1273
+ console.warn(` ${ansi.dim}Baseline save failed: ${blError.message}${ansi.reset}`);
1274
+ }
1275
+ }
1276
+ }
1277
+
1167
1278
  // Use enhanced output formatter for legacy fallback
1168
1279
  const severityCounts = {
1169
1280
  critical: criticalCount,
@@ -1175,18 +1286,10 @@ async function runScan(args) {
1175
1286
 
1176
1287
  const result = {
1177
1288
  verdict: { verdict, score },
1178
- findings: findings.map(f => ({
1179
- severity: f.severity === 'critical' || f.severity === 'BLOCK' ? 'critical' :
1180
- f.severity === 'warning' || f.severity === 'WARN' ? 'medium' : 'low',
1181
- category: f.category || 'ROUTE',
1182
- title: f.title || f.message,
1183
- message: f.message || f.title,
1184
- file: f.file,
1185
- line: f.line,
1186
- fix: f.fixSuggestion,
1187
- })),
1289
+ findings: normalizedLegacyFindings,
1188
1290
  layers: [],
1189
1291
  timings,
1292
+ diff,
1190
1293
  };
1191
1294
 
1192
1295
  console.log(formatScanOutput(result, { verbose: opts.verbose }));
@@ -54,7 +54,7 @@ const {
54
54
  } = require("./lib/terminal-ui");
55
55
 
56
56
  const {
57
- formatShipOutput,
57
+ formatShipOutput: formatShipOutputLegacy,
58
58
  renderVerdictCard,
59
59
  renderFixModeHeader,
60
60
  renderFixResults,
@@ -64,6 +64,10 @@ const {
64
64
  shipIcons,
65
65
  } = require("./lib/ship-output");
66
66
 
67
+ const {
68
+ formatShipOutput,
69
+ } = require("./lib/ship-output-enterprise");
70
+
67
71
  // ═══════════════════════════════════════════════════════════════════════════════
68
72
  // PREMIUM BANNER
69
73
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1031,7 +1035,7 @@ async function runShip(args, context = {}) {
1031
1035
  if (spinner) spinner.succeed('Safe fixes applied');
1032
1036
  }
1033
1037
 
1034
- // Human-readable output using ship-output module
1038
+ // Human-readable output using enterprise ship-output module
1035
1039
  const result = {
1036
1040
  verdict,
1037
1041
  score: results.score,
@@ -1047,14 +1051,9 @@ async function runShip(args, context = {}) {
1047
1051
  // Get current tier for output formatting
1048
1052
  const currentTier = context?.authInfo?.access?.tier || getCurrentTier() || "free";
1049
1053
 
1054
+ // Use enterprise format
1050
1055
  console.log(formatShipOutput(result, {
1051
- verbose: opts.verbose,
1052
- showFix: opts.fix,
1053
- showBadge: opts.badge,
1054
- outputDir,
1055
- projectPath,
1056
1056
  tier: currentTier,
1057
- isVerified: opts.withRuntime || false, // Reality testing = verified
1058
1057
  }));
1059
1058
 
1060
1059
  // Badge file generation (STARTER+ only)
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Truth Command Handler
3
+ *
4
+ * Enhanced truth command to generate truthpack files.
5
+ * Generates: routes.json, env.json, auth.json, contracts.json, ui.graph.json
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { buildTruthpackV2 } = require("./lib/extractors/truthpack-v2");
13
+
14
+ /**
15
+ * Run truth command
16
+ * @param {object} options - Command options
17
+ * @param {string} options.scope - Scope: 'routes', 'env', 'auth', 'contracts', 'all'
18
+ * @param {string} options.projectRoot - Project root directory
19
+ */
20
+ async function runTruth(options = {}) {
21
+ const projectRoot = options.projectRoot || process.cwd();
22
+ const scope = options.scope || "all";
23
+
24
+ const truthpackDir = path.join(projectRoot, ".vibecheck", "truthpack");
25
+
26
+ // Ensure directory exists
27
+ if (!fs.existsSync(truthpackDir)) {
28
+ fs.mkdirSync(truthpackDir, { recursive: true });
29
+ }
30
+
31
+ try {
32
+ // Build truthpack
33
+ const truthpack = await buildTruthpackV2({
34
+ repoRoot: projectRoot
35
+ });
36
+
37
+ // Write truthpack files based on scope
38
+ if (scope === "all" || scope === "routes") {
39
+ const routesFile = path.join(truthpackDir, "routes.json");
40
+ fs.writeFileSync(routesFile, JSON.stringify({
41
+ routes: truthpack.routes || [],
42
+ gaps: truthpack.gaps || [],
43
+ stack: truthpack.stack || {}
44
+ }, null, 2));
45
+ }
46
+
47
+ if (scope === "all" || scope === "env") {
48
+ const envFile = path.join(truthpackDir, "env.json");
49
+ fs.writeFileSync(envFile, JSON.stringify(truthpack.env || {}, null, 2));
50
+ }
51
+
52
+ if (scope === "all" || scope === "auth") {
53
+ const authFile = path.join(truthpackDir, "auth.json");
54
+ fs.writeFileSync(authFile, JSON.stringify(truthpack.auth || {}, null, 2));
55
+ }
56
+
57
+ if (scope === "all" || scope === "contracts") {
58
+ const contractsFile = path.join(truthpackDir, "contracts.json");
59
+ fs.writeFileSync(contractsFile, JSON.stringify(truthpack.contracts || {}, null, 2));
60
+ }
61
+
62
+ // UI graph (if Reality enabled)
63
+ if (scope === "all" && truthpack.uiGraph) {
64
+ const uiGraphFile = path.join(truthpackDir, "ui.graph.json");
65
+ fs.writeFileSync(uiGraphFile, JSON.stringify(truthpack.uiGraph, null, 2));
66
+ }
67
+
68
+ return {
69
+ success: true,
70
+ message: `Truthpack generated successfully (scope: ${scope})`,
71
+ files: {
72
+ routes: scope === "all" || scope === "routes" ? "routes.json" : null,
73
+ env: scope === "all" || scope === "env" ? "env.json" : null,
74
+ auth: scope === "all" || scope === "auth" ? "auth.json" : null,
75
+ contracts: scope === "all" || scope === "contracts" ? "contracts.json" : null,
76
+ uiGraph: scope === "all" && truthpack.uiGraph ? "ui.graph.json" : null
77
+ }
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ success: false,
82
+ error: error.message
83
+ };
84
+ }
85
+ }
86
+
87
+ module.exports = {
88
+ runTruth
89
+ };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Agent Firewall Interceptor - MCP Tool
3
+ *
4
+ * Intercepts file write/patch tool calls from AI agents.
5
+ * Validates changes against truthpack and policy before allowing writes.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const path = require("path");
11
+ const fs = require("fs");
12
+ const { interceptFileWrite, interceptMultiFileWrite } = require("../../bin/runners/lib/agent-firewall/interceptor/base");
13
+
14
+ /**
15
+ * MCP Tool Definition
16
+ */
17
+ const AGENT_FIREWALL_TOOL = {
18
+ name: "vibecheck_agent_firewall_intercept",
19
+ description: `🛡️ Agent Firewall - Intercepts AI code changes and validates against repo truth.
20
+
21
+ This tool MUST be called before any file write/patch operations.
22
+ It validates changes against truthpack and policy rules.
23
+
24
+ Returns: { allowed, verdict, violations, unblockPlan }`,
25
+ inputSchema: {
26
+ type: "object",
27
+ required: ["agentId", "filePath", "content"],
28
+ properties: {
29
+ agentId: {
30
+ type: "string",
31
+ description: "Agent identifier (e.g., 'cursor', 'windsurf', 'copilot')"
32
+ },
33
+ filePath: {
34
+ type: "string",
35
+ description: "File path relative to project root"
36
+ },
37
+ content: {
38
+ type: "string",
39
+ description: "New file content"
40
+ },
41
+ oldContent: {
42
+ type: "string",
43
+ description: "Old file content (optional, for diff generation)"
44
+ },
45
+ intent: {
46
+ type: "string",
47
+ description: "Agent's stated intent for this change"
48
+ },
49
+ projectRoot: {
50
+ type: "string",
51
+ default: ".",
52
+ description: "Project root directory"
53
+ }
54
+ }
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Handle MCP tool call
60
+ * @param {string} name - Tool name (unused, for consistency)
61
+ * @param {object} args - Tool arguments
62
+ * @returns {object} MCP tool response
63
+ */
64
+ async function handleAgentFirewallIntercept(name, args) {
65
+ const projectRoot = path.resolve(args.projectRoot || ".");
66
+ const agentId = args.agentId || "unknown";
67
+ const filePath = args.filePath;
68
+ const content = args.content;
69
+ const oldContent = args.oldContent || null;
70
+ const intent = args.intent || "No intent provided";
71
+
72
+ // Validate file path is within project root
73
+ const fileAbs = path.resolve(projectRoot, filePath);
74
+ if (!fileAbs.startsWith(projectRoot + path.sep) && fileAbs !== projectRoot) {
75
+ return {
76
+ content: [{
77
+ type: "text",
78
+ text: `❌ BLOCKED: File path outside project root: ${filePath}`
79
+ }],
80
+ isError: true
81
+ };
82
+ }
83
+
84
+ try {
85
+ // Read old content if not provided
86
+ let actualOldContent = oldContent;
87
+ if (!actualOldContent && fs.existsSync(fileAbs)) {
88
+ actualOldContent = fs.readFileSync(fileAbs, "utf8");
89
+ }
90
+
91
+ // Intercept the write
92
+ const result = await interceptFileWrite({
93
+ projectRoot,
94
+ agentId,
95
+ intent,
96
+ filePath,
97
+ content,
98
+ oldContent: actualOldContent
99
+ });
100
+
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);
104
+
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
+ };
113
+ }
114
+
115
+ // Enforce mode - block if not allowed
116
+ if (!result.allowed) {
117
+ let message = `❌ BLOCKED: ${result.message}\n\n`;
118
+
119
+ if (result.violations && result.violations.length > 0) {
120
+ message += "Violations:\n";
121
+ for (const violation of result.violations) {
122
+ message += ` - ${violation.rule}: ${violation.message}\n`;
123
+ }
124
+ }
125
+
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`;
130
+ }
131
+ }
132
+
133
+ return {
134
+ content: [{
135
+ type: "text",
136
+ text: message
137
+ }],
138
+ isError: true
139
+ };
140
+ }
141
+
142
+ // Allowed
143
+ return {
144
+ content: [{
145
+ type: "text",
146
+ text: `✅ ALLOWED: ${result.message}\n\nPacket ID: ${result.packetId}`
147
+ }]
148
+ };
149
+
150
+ } catch (error) {
151
+ return {
152
+ content: [{
153
+ type: "text",
154
+ text: `❌ Error intercepting write: ${error.message}\n\n${error.stack}`
155
+ }],
156
+ isError: true
157
+ };
158
+ }
159
+ }
160
+
161
+ module.exports = {
162
+ AGENT_FIREWALL_TOOL,
163
+ handleAgentFirewallIntercept
164
+ };