@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.
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +214 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/analysis-core.js +198 -180
- package/bin/runners/lib/analyzers.js +1119 -536
- package/bin/runners/lib/cli-output.js +236 -210
- package/bin/runners/lib/detectors-v2.js +547 -785
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/route-truth.js +1167 -322
- package/bin/runners/lib/scan-output.js +144 -738
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/terminal-ui.js +188 -770
- package/bin/runners/lib/truth.js +1004 -321
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runScan.js +113 -10
- package/bin/runners/runShip.js +7 -8
- package/bin/runners/runTruth.js +89 -0
- package/mcp-server/agent-firewall-interceptor.js +164 -0
- package/mcp-server/index.js +347 -313
- package/mcp-server/truth-context.js +131 -90
- package/mcp-server/truth-firewall-tools.js +1412 -1045
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Pre-Commit Hook
|
|
3
|
+
*
|
|
4
|
+
* Validates all staged changes against firewall policy before allowing commit.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const { execSync } = require("child_process");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { interceptFileWrite } = require("../interceptor/base");
|
|
13
|
+
const { loadPolicy } = require("../policy/loader");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run pre-commit validation
|
|
17
|
+
* @param {string} projectRoot - Project root directory
|
|
18
|
+
* @returns {object} Validation result
|
|
19
|
+
*/
|
|
20
|
+
async function validatePreCommit(projectRoot) {
|
|
21
|
+
const policy = loadPolicy(projectRoot);
|
|
22
|
+
|
|
23
|
+
// Only enforce in enforce mode
|
|
24
|
+
if (policy.mode !== "enforce") {
|
|
25
|
+
return { allowed: true, message: "Firewall in observe mode - commit allowed" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Get staged files
|
|
30
|
+
const stagedFiles = getStagedFiles(projectRoot);
|
|
31
|
+
|
|
32
|
+
if (stagedFiles.length === 0) {
|
|
33
|
+
return { allowed: true, message: "No files staged" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`🛡️ Agent Firewall: Validating ${stagedFiles.length} staged file(s)...\n`);
|
|
37
|
+
|
|
38
|
+
const violations = [];
|
|
39
|
+
const blockedFiles = [];
|
|
40
|
+
|
|
41
|
+
// Validate each staged file
|
|
42
|
+
for (const filePath of stagedFiles) {
|
|
43
|
+
const fileAbs = path.join(projectRoot, filePath);
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(fileAbs)) {
|
|
46
|
+
continue; // File was deleted, skip
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get old content from git
|
|
50
|
+
let oldContent = null;
|
|
51
|
+
try {
|
|
52
|
+
oldContent = execSync(
|
|
53
|
+
`git show :${filePath}`,
|
|
54
|
+
{ cwd: projectRoot, encoding: "utf8", stdio: "pipe" }
|
|
55
|
+
);
|
|
56
|
+
} catch {
|
|
57
|
+
// File not in git (new file), that's okay
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Get new content
|
|
61
|
+
const newContent = fs.readFileSync(fileAbs, "utf8");
|
|
62
|
+
|
|
63
|
+
// Intercept the change
|
|
64
|
+
const result = await interceptFileWrite({
|
|
65
|
+
projectRoot,
|
|
66
|
+
agentId: "git-pre-commit",
|
|
67
|
+
intent: `Git commit: ${filePath}`,
|
|
68
|
+
filePath: filePath,
|
|
69
|
+
content: newContent,
|
|
70
|
+
oldContent: oldContent
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!result.allowed) {
|
|
74
|
+
violations.push(...(result.violations || []));
|
|
75
|
+
blockedFiles.push({
|
|
76
|
+
file: filePath,
|
|
77
|
+
violations: result.violations || [],
|
|
78
|
+
unblockPlan: result.unblockPlan
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (blockedFiles.length > 0) {
|
|
84
|
+
console.error("\n❌ COMMIT BLOCKED by Agent Firewall\n");
|
|
85
|
+
console.error("Violations detected:\n");
|
|
86
|
+
|
|
87
|
+
blockedFiles.forEach(({ file, violations: fileViolations }) => {
|
|
88
|
+
console.error(` ${file}:`);
|
|
89
|
+
fileViolations.forEach(v => {
|
|
90
|
+
console.error(` - ${v.rule}: ${v.message}`);
|
|
91
|
+
});
|
|
92
|
+
console.error("");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Show unblock plans
|
|
96
|
+
const allPlans = blockedFiles
|
|
97
|
+
.filter(bf => bf.unblockPlan && bf.unblockPlan.steps.length > 0)
|
|
98
|
+
.flatMap(bf => bf.unblockPlan.steps);
|
|
99
|
+
|
|
100
|
+
if (allPlans.length > 0) {
|
|
101
|
+
console.error("To fix:");
|
|
102
|
+
const uniqueSteps = new Map();
|
|
103
|
+
allPlans.forEach(step => {
|
|
104
|
+
const key = `${step.action}:${step.file}`;
|
|
105
|
+
if (!uniqueSteps.has(key)) {
|
|
106
|
+
uniqueSteps.set(key, step);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
Array.from(uniqueSteps.values()).forEach((step, i) => {
|
|
110
|
+
console.error(` ${i + 1}. ${step.action.toUpperCase()}: ${step.file}`);
|
|
111
|
+
console.error(` ${step.description}`);
|
|
112
|
+
});
|
|
113
|
+
console.error("");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
allowed: false,
|
|
118
|
+
message: `Commit blocked: ${blockedFiles.length} file(s) have violations`,
|
|
119
|
+
blockedFiles,
|
|
120
|
+
violations
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log("✅ All staged files validated - commit allowed\n");
|
|
125
|
+
return { allowed: true, message: "All files validated" };
|
|
126
|
+
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`Error validating commit: ${error.message}`);
|
|
129
|
+
// In case of error, allow commit (fail open)
|
|
130
|
+
return { allowed: true, message: `Validation error: ${error.message}` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get list of staged files
|
|
136
|
+
* @param {string} projectRoot - Project root directory
|
|
137
|
+
* @returns {array} Array of staged file paths
|
|
138
|
+
*/
|
|
139
|
+
function getStagedFiles(projectRoot) {
|
|
140
|
+
try {
|
|
141
|
+
const output = execSync(
|
|
142
|
+
"git diff --cached --name-only --diff-filter=ACMR",
|
|
143
|
+
{ cwd: projectRoot, encoding: "utf8", stdio: "pipe" }
|
|
144
|
+
);
|
|
145
|
+
return output
|
|
146
|
+
.trim()
|
|
147
|
+
.split("\n")
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.filter(file => {
|
|
150
|
+
// Only check code files
|
|
151
|
+
const ext = path.extname(file);
|
|
152
|
+
return [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"].includes(ext);
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// Not a git repo or no staged files
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
validatePreCommit,
|
|
162
|
+
getStagedFiles
|
|
163
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor IDE Extension Hook
|
|
3
|
+
*
|
|
4
|
+
* Intercepts file writes at the IDE level for Cursor.
|
|
5
|
+
* Uses Cursor's extension API to hook into file save events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { interceptFileWrite } = require("../interceptor/base");
|
|
11
|
+
const { loadPolicy } = require("../policy/loader");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cursor extension hook
|
|
17
|
+
* This would be called by a Cursor extension
|
|
18
|
+
*/
|
|
19
|
+
class CursorFirewallHook {
|
|
20
|
+
constructor(projectRoot) {
|
|
21
|
+
this.projectRoot = projectRoot;
|
|
22
|
+
this.isEnabled = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enable the hook
|
|
27
|
+
*/
|
|
28
|
+
enable() {
|
|
29
|
+
this.isEnabled = true;
|
|
30
|
+
this.createCursorConfig();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create Cursor configuration file
|
|
35
|
+
*/
|
|
36
|
+
createCursorConfig() {
|
|
37
|
+
const cursorDir = path.join(this.projectRoot, ".cursor");
|
|
38
|
+
if (!fs.existsSync(cursorDir)) {
|
|
39
|
+
fs.mkdirSync(cursorDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configFile = path.join(cursorDir, "firewall-hook.json");
|
|
43
|
+
const config = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
mode: "enforce",
|
|
46
|
+
interceptOnSave: true,
|
|
47
|
+
interceptOnCreate: true,
|
|
48
|
+
interceptOnEdit: false // Only on save/create, not every keystroke
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Intercept file save (called by Cursor extension)
|
|
56
|
+
*/
|
|
57
|
+
async interceptSave(filePath, content, oldContent) {
|
|
58
|
+
if (!this.isEnabled) {
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const policy = loadPolicy(this.projectRoot);
|
|
63
|
+
|
|
64
|
+
// In observe mode, just log
|
|
65
|
+
if (policy.mode === "observe") {
|
|
66
|
+
const result = await interceptFileWrite({
|
|
67
|
+
projectRoot: this.projectRoot,
|
|
68
|
+
agentId: "cursor-ide",
|
|
69
|
+
intent: "File save in Cursor",
|
|
70
|
+
filePath: filePath,
|
|
71
|
+
content: content,
|
|
72
|
+
oldContent: oldContent
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (result.violations && result.violations.length > 0) {
|
|
76
|
+
console.log(`📊 OBSERVE: ${filePath} - violations logged`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { allowed: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// In enforce mode, block violations
|
|
83
|
+
const result = await interceptFileWrite({
|
|
84
|
+
projectRoot: this.projectRoot,
|
|
85
|
+
agentId: "cursor-ide",
|
|
86
|
+
intent: "File save in Cursor",
|
|
87
|
+
filePath: filePath,
|
|
88
|
+
content: content,
|
|
89
|
+
oldContent: oldContent
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!result.allowed) {
|
|
93
|
+
return {
|
|
94
|
+
allowed: false,
|
|
95
|
+
message: result.message,
|
|
96
|
+
violations: result.violations,
|
|
97
|
+
unblockPlan: result.unblockPlan
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { allowed: true };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
CursorFirewallHook
|
|
107
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VS Code Extension Hook
|
|
3
|
+
*
|
|
4
|
+
* Intercepts file writes at the IDE level for VS Code.
|
|
5
|
+
* Uses VS Code's extension API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { interceptFileWrite } = require("../interceptor/base");
|
|
11
|
+
const { loadPolicy } = require("../policy/loader");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* VS Code extension hook
|
|
15
|
+
* This would be called by a VS Code extension
|
|
16
|
+
*/
|
|
17
|
+
class VSCodeFirewallHook {
|
|
18
|
+
constructor(projectRoot) {
|
|
19
|
+
this.projectRoot = projectRoot;
|
|
20
|
+
this.isEnabled = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Enable the hook
|
|
25
|
+
*/
|
|
26
|
+
enable() {
|
|
27
|
+
this.isEnabled = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Intercept file save (called by VS Code extension)
|
|
32
|
+
*/
|
|
33
|
+
async interceptSave(filePath, content, oldContent) {
|
|
34
|
+
if (!this.isEnabled) {
|
|
35
|
+
return { allowed: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const policy = loadPolicy(this.projectRoot);
|
|
39
|
+
|
|
40
|
+
const result = await interceptFileWrite({
|
|
41
|
+
projectRoot: this.projectRoot,
|
|
42
|
+
agentId: "vscode-ide",
|
|
43
|
+
intent: "File save in VS Code",
|
|
44
|
+
filePath: filePath,
|
|
45
|
+
content: content,
|
|
46
|
+
oldContent: oldContent
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (policy.mode === "observe") {
|
|
50
|
+
return { allowed: true }; // Always allow in observe mode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!result.allowed) {
|
|
54
|
+
return {
|
|
55
|
+
allowed: false,
|
|
56
|
+
message: result.message,
|
|
57
|
+
violations: result.violations,
|
|
58
|
+
unblockPlan: result.unblockPlan
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { allowed: true };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
VSCodeFirewallHook
|
|
68
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windsurf IDE Extension Hook
|
|
3
|
+
*
|
|
4
|
+
* Intercepts file writes at the IDE level for Windsurf.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const { interceptFileWrite } = require("../interceptor/base");
|
|
10
|
+
const { loadPolicy } = require("../policy/loader");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Windsurf extension hook
|
|
14
|
+
*/
|
|
15
|
+
class WindsurfFirewallHook {
|
|
16
|
+
constructor(projectRoot) {
|
|
17
|
+
this.projectRoot = projectRoot;
|
|
18
|
+
this.isEnabled = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enable the hook
|
|
23
|
+
*/
|
|
24
|
+
enable() {
|
|
25
|
+
this.isEnabled = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Intercept file save
|
|
30
|
+
*/
|
|
31
|
+
async interceptSave(filePath, content, oldContent) {
|
|
32
|
+
if (!this.isEnabled) {
|
|
33
|
+
return { allowed: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const policy = loadPolicy(this.projectRoot);
|
|
37
|
+
|
|
38
|
+
const result = await interceptFileWrite({
|
|
39
|
+
projectRoot: this.projectRoot,
|
|
40
|
+
agentId: "windsurf-ide",
|
|
41
|
+
intent: "File save in Windsurf",
|
|
42
|
+
filePath: filePath,
|
|
43
|
+
content: content,
|
|
44
|
+
oldContent: oldContent
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (policy.mode === "observe") {
|
|
48
|
+
return { allowed: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!result.allowed) {
|
|
52
|
+
return {
|
|
53
|
+
allowed: false,
|
|
54
|
+
message: result.message,
|
|
55
|
+
violations: result.violations,
|
|
56
|
+
unblockPlan: result.unblockPlan
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { allowed: true };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
WindsurfFirewallHook
|
|
66
|
+
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Interceptor
|
|
3
|
+
*
|
|
4
|
+
* Common logic for all IDE interceptors.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const { extractClaims } = require("../claims/extractor");
|
|
12
|
+
const { resolveEvidence } = require("../evidence/resolver");
|
|
13
|
+
const { evaluatePolicy } = require("../policy/engine");
|
|
14
|
+
const { buildChangePacket, buildMultiFileChangePacket } = require("../change-packet/builder");
|
|
15
|
+
const { storePacket } = require("../change-packet/store");
|
|
16
|
+
const { generateUnblockPlan } = require("../unblock/planner");
|
|
17
|
+
const { loadPolicy } = require("../policy/loader");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Intercept a file write attempt
|
|
21
|
+
* @param {object} params
|
|
22
|
+
* @param {string} params.projectRoot - Project root directory
|
|
23
|
+
* @param {string} params.agentId - Agent identifier
|
|
24
|
+
* @param {string} params.intent - Agent intent message
|
|
25
|
+
* @param {string} params.filePath - File path (relative to project root)
|
|
26
|
+
* @param {string} params.content - New file content
|
|
27
|
+
* @param {string} params.oldContent - Old file content (optional)
|
|
28
|
+
* @returns {object} Interception result
|
|
29
|
+
*/
|
|
30
|
+
async function interceptFileWrite({
|
|
31
|
+
projectRoot,
|
|
32
|
+
agentId,
|
|
33
|
+
intent,
|
|
34
|
+
filePath,
|
|
35
|
+
content,
|
|
36
|
+
oldContent = null
|
|
37
|
+
}) {
|
|
38
|
+
// Load policy
|
|
39
|
+
const policy = loadPolicy(projectRoot);
|
|
40
|
+
|
|
41
|
+
// Generate diff
|
|
42
|
+
const diff = generateDiff(oldContent, content);
|
|
43
|
+
|
|
44
|
+
// Extract claims - write content to temp file if file doesn't exist
|
|
45
|
+
const fileAbs = path.join(projectRoot, filePath);
|
|
46
|
+
let tempFile = null;
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(fileAbs) && content) {
|
|
49
|
+
// Write content to temp file for extraction
|
|
50
|
+
const tempDir = path.join(projectRoot, ".vibecheck", "temp");
|
|
51
|
+
if (!fs.existsSync(tempDir)) {
|
|
52
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
tempFile = path.join(tempDir, path.basename(filePath));
|
|
55
|
+
fs.writeFileSync(tempFile, content, "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fileToExtract = fs.existsSync(fileAbs) ? fileAbs : (tempFile || fileAbs);
|
|
59
|
+
const { claims } = extractClaims({
|
|
60
|
+
repoRoot: projectRoot,
|
|
61
|
+
changedFilesAbs: [fileToExtract]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Clean up temp file
|
|
65
|
+
if (tempFile && fs.existsSync(tempFile)) {
|
|
66
|
+
fs.unlinkSync(tempFile);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve evidence
|
|
70
|
+
const evidence = resolveEvidence(projectRoot, claims);
|
|
71
|
+
|
|
72
|
+
// Build file info
|
|
73
|
+
const files = [{
|
|
74
|
+
path: filePath,
|
|
75
|
+
linesChanged: calculateLinesChanged(diff),
|
|
76
|
+
domain: classifyFileDomain(filePath)
|
|
77
|
+
}];
|
|
78
|
+
|
|
79
|
+
// Evaluate policy
|
|
80
|
+
const verdict = evaluatePolicy({
|
|
81
|
+
policy,
|
|
82
|
+
claims,
|
|
83
|
+
evidence,
|
|
84
|
+
files,
|
|
85
|
+
intent
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Generate unblock plan if blocked
|
|
89
|
+
let unblockPlan = null;
|
|
90
|
+
if (verdict.decision === "BLOCK") {
|
|
91
|
+
unblockPlan = generateUnblockPlan({
|
|
92
|
+
violations: verdict.violations,
|
|
93
|
+
claims,
|
|
94
|
+
projectRoot
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build change packet
|
|
99
|
+
const packet = buildChangePacket({
|
|
100
|
+
agentId,
|
|
101
|
+
intent,
|
|
102
|
+
diff,
|
|
103
|
+
filePath,
|
|
104
|
+
claims,
|
|
105
|
+
evidence,
|
|
106
|
+
verdict,
|
|
107
|
+
unblockPlan,
|
|
108
|
+
policy
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Store packet if policy requires it
|
|
112
|
+
if (policy.output?.write_change_packets !== false) {
|
|
113
|
+
storePacket(projectRoot, packet);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Return interception result
|
|
117
|
+
return {
|
|
118
|
+
allowed: verdict.decision === "ALLOW",
|
|
119
|
+
verdict: verdict.decision,
|
|
120
|
+
violations: verdict.violations,
|
|
121
|
+
unblockPlan,
|
|
122
|
+
packetId: packet.id,
|
|
123
|
+
message: verdict.message
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Intercept multiple file writes
|
|
129
|
+
*/
|
|
130
|
+
async function interceptMultiFileWrite({
|
|
131
|
+
projectRoot,
|
|
132
|
+
agentId,
|
|
133
|
+
intent,
|
|
134
|
+
changes
|
|
135
|
+
}) {
|
|
136
|
+
// Load policy
|
|
137
|
+
const policy = loadPolicy(projectRoot);
|
|
138
|
+
|
|
139
|
+
// Extract claims from all files
|
|
140
|
+
const allClaims = [];
|
|
141
|
+
const allFiles = [];
|
|
142
|
+
|
|
143
|
+
for (const change of changes) {
|
|
144
|
+
const fileAbs = path.join(projectRoot, change.filePath);
|
|
145
|
+
const { claims } = extractClaims({
|
|
146
|
+
repoRoot: projectRoot,
|
|
147
|
+
changedFilesAbs: [fileAbs]
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
allClaims.push(...claims.map(c => ({ ...c, file: change.filePath })));
|
|
151
|
+
allFiles.push({
|
|
152
|
+
path: change.filePath,
|
|
153
|
+
linesChanged: calculateLinesChanged(change.diff),
|
|
154
|
+
domain: classifyFileDomain(change.filePath)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Resolve evidence
|
|
159
|
+
const evidence = resolveEvidence(projectRoot, allClaims);
|
|
160
|
+
|
|
161
|
+
// Evaluate policy
|
|
162
|
+
const verdict = evaluatePolicy({
|
|
163
|
+
policy,
|
|
164
|
+
claims: allClaims,
|
|
165
|
+
evidence,
|
|
166
|
+
files: allFiles,
|
|
167
|
+
intent
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Generate unblock plan if blocked
|
|
171
|
+
let unblockPlan = null;
|
|
172
|
+
if (verdict.decision === "BLOCK") {
|
|
173
|
+
unblockPlan = generateUnblockPlan({
|
|
174
|
+
violations: verdict.violations,
|
|
175
|
+
claims: allClaims,
|
|
176
|
+
projectRoot
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Build change packet
|
|
181
|
+
const packet = buildMultiFileChangePacket({
|
|
182
|
+
agentId,
|
|
183
|
+
intent,
|
|
184
|
+
changes: changes.map(c => ({
|
|
185
|
+
filePath: c.filePath,
|
|
186
|
+
diff: c.diff,
|
|
187
|
+
claims: allClaims.filter(cl => cl.file === c.filePath)
|
|
188
|
+
})),
|
|
189
|
+
evidence,
|
|
190
|
+
verdict,
|
|
191
|
+
unblockPlan,
|
|
192
|
+
policy
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Store packet if policy requires it
|
|
196
|
+
if (policy.output?.write_change_packets !== false) {
|
|
197
|
+
storePacket(projectRoot, packet);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Return interception result
|
|
201
|
+
return {
|
|
202
|
+
allowed: verdict.decision === "ALLOW",
|
|
203
|
+
verdict: verdict.decision,
|
|
204
|
+
violations: verdict.violations,
|
|
205
|
+
unblockPlan,
|
|
206
|
+
packetId: packet.id,
|
|
207
|
+
message: verdict.message
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generate unified diff
|
|
213
|
+
*/
|
|
214
|
+
function generateDiff(oldContent, newContent) {
|
|
215
|
+
if (!oldContent) {
|
|
216
|
+
return {
|
|
217
|
+
before: "",
|
|
218
|
+
after: newContent,
|
|
219
|
+
unified: generateUnifiedDiff("", newContent)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
before: oldContent,
|
|
225
|
+
after: newContent,
|
|
226
|
+
unified: generateUnifiedDiff(oldContent, newContent)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate unified diff format
|
|
232
|
+
*/
|
|
233
|
+
function generateUnifiedDiff(oldContent, newContent) {
|
|
234
|
+
const oldLines = oldContent.split(/\r?\n/);
|
|
235
|
+
const newLines = newContent.split(/\r?\n/);
|
|
236
|
+
|
|
237
|
+
const diff = [];
|
|
238
|
+
let i = 0, j = 0;
|
|
239
|
+
|
|
240
|
+
while (i < oldLines.length || j < newLines.length) {
|
|
241
|
+
if (i >= oldLines.length) {
|
|
242
|
+
diff.push(`+${newLines[j]}`);
|
|
243
|
+
j++;
|
|
244
|
+
} else if (j >= newLines.length) {
|
|
245
|
+
diff.push(`-${oldLines[i]}`);
|
|
246
|
+
i++;
|
|
247
|
+
} else if (oldLines[i] === newLines[j]) {
|
|
248
|
+
diff.push(` ${oldLines[i]}`);
|
|
249
|
+
i++;
|
|
250
|
+
j++;
|
|
251
|
+
} else {
|
|
252
|
+
// Try to find matching line ahead
|
|
253
|
+
let found = false;
|
|
254
|
+
for (let k = j + 1; k < Math.min(j + 10, newLines.length); k++) {
|
|
255
|
+
if (oldLines[i] === newLines[k]) {
|
|
256
|
+
// Add new lines
|
|
257
|
+
for (let l = j; l < k; l++) {
|
|
258
|
+
diff.push(`+${newLines[l]}`);
|
|
259
|
+
}
|
|
260
|
+
j = k;
|
|
261
|
+
found = true;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!found) {
|
|
267
|
+
diff.push(`-${oldLines[i]}`);
|
|
268
|
+
diff.push(`+${newLines[j]}`);
|
|
269
|
+
i++;
|
|
270
|
+
j++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return diff.join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Calculate lines changed from diff
|
|
280
|
+
*/
|
|
281
|
+
function calculateLinesChanged(diff) {
|
|
282
|
+
if (!diff || !diff.unified) return 0;
|
|
283
|
+
return diff.unified.split('\n').filter(line =>
|
|
284
|
+
line.startsWith('+') || line.startsWith('-')
|
|
285
|
+
).length;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Classify file domain
|
|
290
|
+
*/
|
|
291
|
+
function classifyFileDomain(filePath) {
|
|
292
|
+
const s = filePath.toLowerCase();
|
|
293
|
+
if (s.includes("auth")) return "auth";
|
|
294
|
+
if (s.includes("stripe") || s.includes("payment")) return "payments";
|
|
295
|
+
if (s.includes("routes") || s.includes("router") || s.includes("api")) return "routes";
|
|
296
|
+
if (s.includes("schema") || s.includes("contract") || s.includes("openapi")) return "contracts";
|
|
297
|
+
if (s.includes("ui") || s.includes("components") || s.includes("pages")) return "ui";
|
|
298
|
+
return "general";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
interceptFileWrite,
|
|
303
|
+
interceptMultiFileWrite
|
|
304
|
+
};
|