@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
@@ -1,189 +1,193 @@
1
1
  /**
2
- * Unified Output System
3
- *
4
- * Provides consistent, deterministic output across all vibecheck commands.
5
- * This is what makes vibecheck feel "enterprise-grade".
2
+ * Unified Output System - World Class
3
+ * * The central nervous system for Vibecheck's CLI output.
4
+ * Delivers consistent, high-fidelity visualization and strict exit code compliance.
6
5
  */
7
6
 
8
- // Graceful fallback for missing compiled modules
9
- let EXIT_CODES, formatVerdictOutput;
7
+ "use strict";
10
8
 
11
- // Exit codes per product spec:
12
- // 0 = SHIP / no blockers
13
- // 1 = WARN (allowed if you choose)
14
- // 2 = BLOCK / blockers found
15
- EXIT_CODES = {
9
+ const chalk = require('chalk');
10
+ const os = require('os');
11
+
12
+ // ═══════════════════════════════════════════════════════════════════════════════
13
+ // CONFIGURATION & CONSTANTS
14
+ // ═══════════════════════════════════════════════════════════════════════════════
15
+
16
+ const EXIT_CODES = {
16
17
  SHIP: 0, // Ready to ship, no blockers
17
- PASS: 0, // Alias for SHIP
18
- WARN: 1, // Warnings found (allowed)
19
- BLOCK: 2, // Blockers found, do not ship
20
- FAIL: 2, // Alias for BLOCK
21
- MISCONFIG: 2, // Configuration error = block
22
- INTERNAL: 2, // Internal error = block (safe default)
18
+ WARN: 1, // Warnings found (allowed via flag)
19
+ BLOCK: 2, // Blockers found (hard stop)
20
+ INTERNAL: 2, // Internal error (hard stop)
21
+ MISCONFIG: 2, // Configuration error (hard stop)
23
22
  };
24
23
 
25
- try {
26
- const verdictFormatter = require('../../../dist/lib/cli/verdict-formatter');
27
- formatVerdictOutput = verdictFormatter.formatVerdictOutput;
28
- } catch (err) {
29
- // Fallback when dist not built
30
- formatVerdictOutput = (verdict, options) => {
31
- const c = {
32
- reset: '\x1b[0m',
33
- green: '\x1b[32m',
34
- red: '\x1b[31m',
35
- yellow: '\x1b[33m',
36
- cyan: '\x1b[36m',
37
- dim: '\x1b[2m',
38
- bold: '\x1b[1m',
39
- };
40
-
41
- if (!verdict) return 'No verdict available';
42
-
43
- const icon = verdict.verdict === 'PASS' ? `${c.green}✓${c.reset}` :
44
- verdict.verdict === 'FAIL' ? `${c.red}✗${c.reset}` :
45
- `${c.yellow}⚠${c.reset}`;
46
-
47
- let output = `\n${icon} ${c.bold}${verdict.verdict}${c.reset}\n`;
48
-
49
- if (verdict.summary) {
50
- output += `\n${c.dim}Summary:${c.reset}\n`;
51
- output += ` Blockers: ${verdict.summary.blockers || 0}\n`;
52
- output += ` Warnings: ${verdict.summary.warnings || 0}\n`;
53
- }
54
-
55
- if (verdict.findings && verdict.findings.length > 0) {
56
- output += `\n${c.dim}Findings:${c.reset}\n`;
57
- for (const finding of verdict.findings.slice(0, 10)) {
58
- const sevColor = finding.severity === 'critical' ? c.red :
59
- finding.severity === 'high' ? c.red :
60
- finding.severity === 'medium' ? c.yellow : c.dim;
61
- output += ` ${sevColor}[${finding.severity?.toUpperCase() || 'INFO'}]${c.reset} ${finding.title || finding.message || 'Unknown issue'}\n`;
62
- if (finding.file) {
63
- output += ` ${c.cyan}${finding.file}${finding.line ? `:${finding.line}` : ''}${c.reset}\n`;
64
- }
65
- }
66
- if (verdict.findings.length > 10) {
67
- output += ` ${c.dim}... and ${verdict.findings.length - 10} more${c.reset}\n`;
68
- }
69
- }
70
-
71
- return output;
72
- };
24
+ const WIDTH = 76;
25
+
26
+ const BOX = {
27
+ tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║',
28
+ trT: '╠', tlT: '╣',
29
+ // Light (Inner)
30
+ ltl: '┌', ltr: '┐', lbl: '└', lbr: '┘', lh: '─', lv: '│',
31
+ lt: '', lb: '┴', lx: '┼', ltrT: '├', ltlT: '┤'
32
+ };
33
+
34
+ // ═══════════════════════════════════════════════════════════════════════════════
35
+ // VISUAL RENDERING ENGINE
36
+ // ═══════════════════════════════════════════════════════════════════════════════
37
+
38
+ function padCenter(str, width) {
39
+ const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
40
+ const padding = Math.max(0, width - visibleLen);
41
+ const left = Math.floor(padding / 2);
42
+ return ' '.repeat(left) + str + ' '.repeat(padding - left);
73
43
  }
74
44
 
75
- /**
76
- * Format scan output with unified contract
77
- */
78
- function formatScanOutput(result, options = {}) {
79
- const { verbose = false, json = false } = options;
80
-
81
- if (json) {
82
- return JSON.stringify(result, null, 2);
83
- }
84
-
85
- return formatVerdictOutput(result.verdict, { verbose, json });
45
+ function padRight(str, len) {
46
+ const visibleLen = str.length;
47
+ const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
48
+ return truncated + ' '.repeat(Math.max(0, len - truncated.length));
86
49
  }
87
50
 
88
- /**
89
- * Get exit code from verdict
90
- * Per spec: 0=SHIP, 1=WARN, 2=BLOCK
91
- */
92
- function getExitCode(verdict) {
93
- if (!verdict) return EXIT_CODES.BLOCK; // Safe default
94
-
95
- switch (verdict.verdict) {
96
- case 'SHIP':
97
- case 'PASS':
98
- return EXIT_CODES.SHIP; // 0
99
- case 'WARN':
100
- return EXIT_CODES.WARN; // 1
101
- case 'BLOCK':
102
- case 'FAIL':
103
- return EXIT_CODES.BLOCK; // 2
104
- case 'ERROR':
105
- case 'MISCONFIG':
106
- return EXIT_CODES.BLOCK; // 2 (safe default)
107
- default:
108
- return EXIT_CODES.BLOCK; // 2 (safe default)
109
- }
51
+ function renderProgressBar(score, width = 20) {
52
+ const filled = Math.round((score / 100) * width);
53
+ return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
110
54
  }
111
55
 
112
- /**
113
- * Handle errors with proper exit codes
114
- */
115
- function handleError(error, context = '') {
116
- const errorType = classifyError(error);
56
+ // --- The Core Formatter ---
57
+
58
+ function renderVerdict(verdictData, options = {}) {
59
+ const {
60
+ score = 0,
61
+ findings = [],
62
+ duration = 0,
63
+ scannedFiles = 0
64
+ } = verdictData;
65
+
66
+ const lines = [];
67
+ const hasIssues = findings.length > 0;
117
68
 
118
- let exitCode = EXIT_CODES.INTERNAL;
119
- let message = error.message || 'Unknown error';
120
- let nextStep = 'Run: vibecheck doctor';
69
+ // 1. FRAME HEADER
70
+ lines.push(chalk.gray(BOX.tl + BOX.h.repeat(WIDTH - 2) + BOX.tr));
71
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
72
+
73
+ // 2. CLEAN HEADER (No ASCII Logo)
74
+ const titleColor = hasIssues ? chalk.red.bold : chalk.cyan.bold;
75
+ const subTitle = hasIssues ? 'INTEGRITY SCAN • ISSUES DETECTED' : 'INTEGRITY SCAN • CLEAN';
121
76
 
122
- switch (errorType) {
123
- case 'MISCONFIG':
124
- exitCode = EXIT_CODES.MISCONFIG;
125
- message = `Configuration error: ${message}`;
126
- nextStep = 'Run: vibecheck doctor --fix';
127
- break;
128
- case 'MISSING_DEPS':
129
- exitCode = EXIT_CODES.MISCONFIG;
130
- message = `Missing dependencies: ${message}`;
131
- nextStep = 'Run: vibecheck doctor';
132
- break;
133
- case 'PERMISSION':
134
- exitCode = EXIT_CODES.MISCONFIG;
135
- message = `Permission error: ${message}`;
136
- nextStep = 'Check file permissions and run: vibecheck doctor';
137
- break;
138
- default:
139
- exitCode = EXIT_CODES.INTERNAL;
140
- message = `Internal error: ${message}`;
141
- nextStep = 'This looks like a bug. Run: vibecheck doctor';
77
+ // Add some vertical padding for elegance
78
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
79
+ lines.push(chalk.gray(BOX.v) + padCenter(titleColor(subTitle), WIDTH - 2) + chalk.gray(BOX.v));
80
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
81
+
82
+ // 3. TELEMETRY
83
+ lines.push(chalk.gray(BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT));
84
+ const heapMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
85
+ const stats = `📡 TELEMETRY │ ${duration}ms │ 📂 ${scannedFiles || '?'} Files │ 📦 ${heapMB}MB`;
86
+ lines.push(chalk.gray(BOX.v) + padCenter(stats, WIDTH - 2) + chalk.gray(BOX.v));
87
+ lines.push(chalk.gray(BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT));
88
+
89
+ if (!hasIssues) {
90
+ // CLEAN STATE
91
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
92
+ lines.push(chalk.gray(BOX.v) + padCenter(chalk.green('✅ NO STATIC ISSUES FOUND'), WIDTH - 2) + chalk.gray(BOX.v));
93
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
94
+ } else {
95
+ // ISSUES STATE
96
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
97
+ const scoreBar = renderProgressBar(score, 20);
98
+ lines.push(chalk.gray(BOX.v) + padCenter(`HEALTH SCORE [${scoreBar}] ${score} / 100`, WIDTH + 18) + chalk.gray(BOX.v));
99
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
100
+
101
+ // TABLE HEADER
102
+ lines.push(chalk.gray(BOX.v) + padCenter('DETECTED VULNERABILITIES', WIDTH - 2) + chalk.gray(BOX.v));
103
+
104
+ // Hardcoded column widths for perfect alignment
105
+ const C1=10, C2=13, C3=41;
106
+
107
+ const tTop = ` ${BOX.ltl}${BOX.lh.repeat(C1)}${BOX.lt}${BOX.lh.repeat(C2)}${BOX.lt}${BOX.lh.repeat(C3)}${BOX.ltr} `;
108
+ const tMid = ` ${BOX.ltrT}${BOX.lh.repeat(C1)}${BOX.lx}${BOX.lh.repeat(C2)}${BOX.lx}${BOX.lh.repeat(C3)}${BOX.ltlT} `;
109
+ const tBot = ` ${BOX.lbl}${BOX.lh.repeat(C1)}${BOX.lb}${BOX.lh.repeat(C2)}${BOX.lb}${BOX.lh.repeat(C3)}${BOX.lbr} `;
110
+
111
+ lines.push(chalk.gray(BOX.v) + chalk.gray(tTop) + chalk.gray(BOX.v));
112
+ const header = ` ${BOX.lv}${padRight(' SEVERITY', C1)}${BOX.lv}${padRight(' TYPE', C2)}${BOX.lv}${padRight(' FINDING', C3)}${BOX.lv} `;
113
+ lines.push(chalk.gray(BOX.v) + chalk.bold(header) + chalk.gray(BOX.v));
114
+ lines.push(chalk.gray(BOX.v) + chalk.gray(tMid) + chalk.gray(BOX.v));
115
+
116
+ // ROWS
117
+ findings.slice(0, 5).forEach(f => {
118
+ const sev = (f.severity === 'critical' || f.severity === 'BLOCK') ? chalk.red('🛑 CRIT ') : chalk.yellow('🟡 WARN ');
119
+ const row = ` ${chalk.gray(BOX.lv)}${sev}${chalk.gray(BOX.lv)}${padRight(' '+(f.category||'General'), C2)}${chalk.gray(BOX.lv)}${padRight(' '+(f.message||f.title), C3)}${chalk.gray(BOX.lv)} `;
120
+ lines.push(chalk.gray(BOX.v) + row + chalk.gray(BOX.v));
121
+ });
122
+
123
+ lines.push(chalk.gray(BOX.v) + chalk.gray(tBot) + chalk.gray(BOX.v));
124
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
125
+
126
+ // UPSELL (REDACTED STYLE)
127
+ lines.push(chalk.gray(BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT));
128
+ const lockTitle = chalk.white.bold('🔒 DEEP SCAN ANALYSIS: ') + chalk.gray('UNAVAILABLE');
129
+ lines.push(chalk.gray(BOX.v) + padCenter(lockTitle, WIDTH + 9) + chalk.gray(BOX.v));
130
+ lines.push(chalk.gray(BOX.v) + padCenter(chalk.gray('─'.repeat(60)), WIDTH + 9) + chalk.gray(BOX.v));
131
+
132
+ // Redacted lines
133
+ ['Runtime API Schema Validation', 'Live Auth Boundary Verification', 'Env Config Resolution'].forEach(txt => {
134
+ const line = chalk.dim(`░░ [REDACTED] 🔒 ${txt}`);
135
+ lines.push(chalk.gray(BOX.v) + padCenter(line, WIDTH + 9) + chalk.gray(BOX.v));
136
+ });
137
+
138
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
139
+ lines.push(chalk.gray(BOX.v) + padCenter('Run ' + chalk.cyan('vibecheck upgrade pro') + ' to unlock.', WIDTH + 9) + chalk.gray(BOX.v));
142
140
  }
143
-
144
- return {
145
- exitCode,
146
- message: `${context ? `[${context}] ` : ''}${message}`,
147
- nextStep,
148
- errorType,
149
- };
141
+
142
+ // BOTTOM FRAME
143
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
144
+ lines.push(chalk.gray(BOX.bl + BOX.h.repeat(WIDTH - 2) + BOX.br));
145
+
146
+ return lines.join('\n');
150
147
  }
151
148
 
152
- function classifyError(error) {
153
- const message = (error.message || '').toLowerCase();
154
- const code = error.code || '';
155
-
156
- if (code === 'ENOENT' || message.includes('not found') || message.includes('missing')) {
157
- return 'MISSING_DEPS';
158
- }
159
-
160
- if (code === 'EACCES' || code === 'EPERM' || message.includes('permission')) {
161
- return 'PERMISSION';
162
- }
163
-
164
- if (message.includes('config') || message.includes('environment') || message.includes('env')) {
165
- return 'MISCONFIG';
166
- }
167
-
168
- return 'INTERNAL';
149
+ // ═══════════════════════════════════════════════════════════════════════════════
150
+ // PUBLIC API
151
+ // ═══════════════════════════════════════════════════════════════════════════════
152
+
153
+ function formatScanOutput(result, options = {}) {
154
+ const { json = false } = options;
155
+ if (json) return JSON.stringify(result, null, 2);
156
+ return renderVerdict(result, options);
157
+ }
158
+
159
+ function getExitCode(verdict) {
160
+ if (!verdict) return EXIT_CODES.BLOCK;
161
+ const v = (typeof verdict === 'string' ? verdict : verdict.verdict).toUpperCase();
162
+ if (v === 'SHIP' || v === 'PASS') return EXIT_CODES.SHIP;
163
+ if (v === 'WARN') return EXIT_CODES.WARN;
164
+ return EXIT_CODES.BLOCK;
169
165
  }
170
166
 
171
- /**
172
- * Print error with proper formatting
173
- */
174
167
  function printError(error, context = '') {
175
- const handled = handleError(error, context);
168
+ const isConfig = error.message.toLowerCase().includes('config') || error.code === 'ENOENT';
169
+ const exitCode = isConfig ? EXIT_CODES.MISCONFIG : EXIT_CODES.INTERNAL;
176
170
 
177
- console.error(`\n${handled.message}`);
178
- console.error(`\n${handled.nextStep}\n`);
171
+ const lines = [];
172
+ lines.push('');
173
+ lines.push(chalk.bgRed.white.bold(` 🛑 SYSTEM ALERT: ${context || 'INTERNAL_ERROR'} `));
174
+ lines.push(chalk.red('─'.repeat(WIDTH)));
175
+ lines.push(`${chalk.bold('Error:')} ${error.message}`);
176
+ if (error.code) lines.push(`${chalk.bold('Code:')} ${error.code}`);
177
+ lines.push('');
179
178
 
180
- return handled.exitCode;
179
+ const fixCmd = isConfig ? 'vibecheck doctor --fix' : 'vibecheck doctor';
180
+ lines.push(chalk.gray('Recovery Step:'));
181
+ lines.push(`$ ${chalk.cyan(fixCmd)}`);
182
+ lines.push('');
183
+
184
+ console.error(lines.join('\n'));
185
+ return exitCode;
181
186
  }
182
187
 
183
188
  module.exports = {
184
189
  formatScanOutput,
185
190
  getExitCode,
186
- handleError,
187
191
  printError,
188
192
  EXIT_CODES,
189
193
  };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Agent Management Command Handler
3
+ *
4
+ * Install/uninstall hooks, status checks.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const { loadPolicy } = require("./lib/agent-firewall/policy/loader");
12
+
13
+ /**
14
+ * Run agent command
15
+ * @param {object} options - Command options
16
+ * @param {string} options.action - Action: 'install', 'status', 'lock', 'unlock'
17
+ * @param {string} options.projectRoot - Project root directory
18
+ */
19
+ async function runAgent(options = {}) {
20
+ const projectRoot = options.projectRoot || process.cwd();
21
+ const action = options.action || "status";
22
+
23
+ switch (action) {
24
+ case "install":
25
+ return installHooks(projectRoot);
26
+
27
+ case "status":
28
+ return showAgentStatus(projectRoot);
29
+
30
+ case "lock":
31
+ return lockRepo(projectRoot);
32
+
33
+ case "unlock":
34
+ return unlockRepo(projectRoot);
35
+
36
+ default:
37
+ throw new Error(`Unknown action: ${action}`);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Install IDE hooks
43
+ */
44
+ function installHooks(projectRoot) {
45
+ // Create .vibecheck directory if it doesn't exist
46
+ const vibecheckDir = path.join(projectRoot, ".vibecheck");
47
+ if (!fs.existsSync(vibecheckDir)) {
48
+ fs.mkdirSync(vibecheckDir, { recursive: true });
49
+ }
50
+
51
+ // Create hook marker file
52
+ const hookFile = path.join(vibecheckDir, "agent-firewall-enabled");
53
+ fs.writeFileSync(hookFile, JSON.stringify({
54
+ enabled: true,
55
+ installedAt: new Date().toISOString()
56
+ }, null, 2));
57
+
58
+ return {
59
+ success: true,
60
+ message: "Agent Firewall hooks installed. Firewall will intercept AI code changes."
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Show agent status
66
+ */
67
+ function showAgentStatus(projectRoot) {
68
+ const hookFile = path.join(projectRoot, ".vibecheck", "agent-firewall-enabled");
69
+ const installed = fs.existsSync(hookFile);
70
+
71
+ let policy;
72
+ try {
73
+ policy = loadPolicy(projectRoot);
74
+ } catch {
75
+ policy = { mode: "observe", profile: "default" };
76
+ }
77
+
78
+ return {
79
+ installed,
80
+ mode: policy.mode,
81
+ profile: policy.profile,
82
+ locked: policy.mode === "enforce" && policy.profile === "repo-lock"
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Lock repo (enable repo-lock mode)
88
+ */
89
+ function lockRepo(projectRoot) {
90
+ const { loadPolicy, savePolicy, getDefaultPolicy } = require("./lib/agent-firewall/policy/loader");
91
+
92
+ let policy;
93
+ try {
94
+ policy = loadPolicy(projectRoot);
95
+ } catch {
96
+ policy = getDefaultPolicy();
97
+ }
98
+
99
+ policy.mode = "enforce";
100
+ policy.profile = "repo-lock";
101
+
102
+ // Enable all hard rules
103
+ if (!policy.rules) {
104
+ policy.rules = {};
105
+ }
106
+
107
+ const hardRules = [
108
+ "ghost_route",
109
+ "ghost_env",
110
+ "auth_drift",
111
+ "contract_drift",
112
+ "scope_explosion",
113
+ "unsafe_side_effect"
114
+ ];
115
+
116
+ for (const rule of hardRules) {
117
+ if (!policy.rules[rule]) {
118
+ policy.rules[rule] = {};
119
+ }
120
+ policy.rules[rule].enabled = true;
121
+ policy.rules[rule].severity = "block";
122
+ }
123
+
124
+ savePolicy(projectRoot, policy);
125
+
126
+ return {
127
+ success: true,
128
+ message: "Repo Lock Mode enabled. All hard rules enforced."
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Unlock repo (disable repo-lock mode)
134
+ */
135
+ function unlockRepo(projectRoot) {
136
+ const { loadPolicy, savePolicy } = require("./lib/agent-firewall/policy/loader");
137
+
138
+ let policy;
139
+ try {
140
+ policy = loadPolicy(projectRoot);
141
+ } catch {
142
+ return {
143
+ success: false,
144
+ message: "No policy found to unlock"
145
+ };
146
+ }
147
+
148
+ policy.mode = "observe";
149
+ policy.profile = "balanced";
150
+
151
+ savePolicy(projectRoot, policy);
152
+
153
+ return {
154
+ success: true,
155
+ message: "Repo Lock Mode disabled. Firewall in observe mode."
156
+ };
157
+ }
158
+
159
+ module.exports = {
160
+ runAgent
161
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Firewall Command Handler
3
+ *
4
+ * Main firewall command handler.
5
+ * Enable/disable firewall, set mode, show status and statistics.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { loadPolicy, savePolicy, getDefaultPolicy } = require("./lib/agent-firewall/policy/loader");
13
+ const { getPacketStats, queryPackets } = require("./lib/agent-firewall/change-packet/store");
14
+ const { isTruthpackFresh } = require("./lib/agent-firewall/truthpack/loader");
15
+
16
+ /**
17
+ * Run firewall command
18
+ * @param {object} options - Command options
19
+ * @param {string} options.mode - Set mode: 'observe' or 'enforce'
20
+ * @param {boolean} options.status - Show firewall status
21
+ * @param {boolean} options.stats - Show statistics
22
+ * @param {string} options.projectRoot - Project root directory
23
+ */
24
+ async function runFirewall(options = {}) {
25
+ const projectRoot = options.projectRoot || process.cwd();
26
+
27
+ if (options.status) {
28
+ return showStatus(projectRoot);
29
+ }
30
+
31
+ if (options.stats) {
32
+ return showStats(projectRoot);
33
+ }
34
+
35
+ if (options.mode) {
36
+ return setMode(projectRoot, options.mode);
37
+ }
38
+
39
+ // Default: show status
40
+ return showStatus(projectRoot);
41
+ }
42
+
43
+ /**
44
+ * Show firewall status
45
+ */
46
+ function showStatus(projectRoot) {
47
+ try {
48
+ const policy = loadPolicy(projectRoot);
49
+ const truthpackFresh = isTruthpackFresh(projectRoot);
50
+
51
+ const output = {
52
+ mode: policy.mode || "observe",
53
+ profile: policy.profile || "default",
54
+ truthpackFresh,
55
+ rulesEnabled: Object.keys(policy.rules || {}).filter(
56
+ key => policy.rules[key]?.enabled !== false
57
+ ).length
58
+ };
59
+
60
+ return output;
61
+ } catch (error) {
62
+ return {
63
+ error: error.message,
64
+ mode: "unknown",
65
+ truthpackFresh: false
66
+ };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Show firewall statistics
72
+ */
73
+ function showStats(projectRoot) {
74
+ try {
75
+ const stats = getPacketStats(projectRoot);
76
+ const recentPackets = queryPackets(projectRoot, { limit: 10 });
77
+
78
+ return {
79
+ total: stats.total,
80
+ byAgent: stats.byAgent,
81
+ byVerdict: stats.byVerdict,
82
+ byDate: stats.byDate,
83
+ recent: recentPackets.map(p => ({
84
+ id: p.id,
85
+ timestamp: p.timestamp,
86
+ agentId: p.agentId,
87
+ verdict: p.verdict?.decision,
88
+ files: p.files.length
89
+ }))
90
+ };
91
+ } catch (error) {
92
+ return {
93
+ error: error.message,
94
+ total: 0
95
+ };
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Set firewall mode
101
+ */
102
+ function setMode(projectRoot, mode) {
103
+ if (mode !== "observe" && mode !== "enforce") {
104
+ throw new Error(`Invalid mode: ${mode}. Must be 'observe' or 'enforce'`);
105
+ }
106
+
107
+ try {
108
+ let policy;
109
+ try {
110
+ policy = loadPolicy(projectRoot);
111
+ } catch {
112
+ // Policy doesn't exist, use default
113
+ policy = getDefaultPolicy();
114
+ }
115
+
116
+ policy.mode = mode;
117
+ savePolicy(projectRoot, policy);
118
+
119
+ return {
120
+ success: true,
121
+ mode,
122
+ message: `Firewall mode set to: ${mode}`
123
+ };
124
+ } catch (error) {
125
+ return {
126
+ success: false,
127
+ error: error.message
128
+ };
129
+ }
130
+ }
131
+
132
+ module.exports = {
133
+ runFirewall
134
+ };