@vibecheckai/cli 3.8.0 → 3.9.1
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/enforcement/index.js +98 -98
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -318
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -484
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -418
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -333
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -622
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -102
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -352
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -283
- package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
- package/bin/runners/lib/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/unified-cli-output.js +16 -0
- package/bin/runners/runCI.js +353 -0
- package/bin/runners/runCheckpoint.js +2 -2
- package/bin/runners/runIntent.js +906 -906
- package/bin/runners/runPacks.js +2089 -2089
- package/bin/runners/runReality.js +178 -1
- package/bin/runners/runShield.js +1282 -1282
- package/mcp-server/handlers/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +47 -8
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry.test.ts +18 -12
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
|
@@ -1,484 +1,484 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Firewall Orchestrator - Main Enforcement Entry Point
|
|
3
|
-
*
|
|
4
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
-
* AGENT FIREWALL™ - ENFORCEMENT ORCHESTRATOR
|
|
6
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
-
*
|
|
8
|
-
* The Agent Firewall is a mandatory enforcement layer that intercepts AI-generated
|
|
9
|
-
* actions and BLOCKS them unless they are:
|
|
10
|
-
*
|
|
11
|
-
* 1) Explicitly aligned with declared user INTENT
|
|
12
|
-
* 2) Structurally valid (routes, env vars, permissions exist)
|
|
13
|
-
* 3) Proven real at runtime via Reality Mode
|
|
14
|
-
*
|
|
15
|
-
* If proof is missing → the action MUST NOT SHIP.
|
|
16
|
-
*
|
|
17
|
-
* This is enforcement infrastructure, not developer guidance.
|
|
18
|
-
*
|
|
19
|
-
* @module enforcement/orchestrator
|
|
20
|
-
* @version 2.0.0
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
"use strict";
|
|
24
|
-
|
|
25
|
-
const path = require("path");
|
|
26
|
-
const fs = require("fs");
|
|
27
|
-
const crypto = require("crypto");
|
|
28
|
-
|
|
29
|
-
// Intent system
|
|
30
|
-
const { IntentStore } = require("../intent/store");
|
|
31
|
-
const { checkAlignmentBatch, VIOLATION_CODES } = require("../intent/alignment-engine");
|
|
32
|
-
const { verifyIntentIntegrity, isIntentExpired } = require("../intent/schema");
|
|
33
|
-
|
|
34
|
-
// Verdict system
|
|
35
|
-
const { generateVerdict, VERDICT, OBSERVATION, MODE, createVerdictManifest } = require("./verdict-v2");
|
|
36
|
-
|
|
37
|
-
// Proof system
|
|
38
|
-
const { ProofCollection, determineRequiredProofs, PROOF_STATUS } = require("./proof-artifact");
|
|
39
|
-
|
|
40
|
-
// Mode system
|
|
41
|
-
const { ModeConfig, createModeEvent, formatModeBanner } = require("./mode");
|
|
42
|
-
|
|
43
|
-
// Existing components
|
|
44
|
-
const { resolveEvidence } = require("../evidence/resolver");
|
|
45
|
-
const { evaluatePolicy } = require("../policy/engine");
|
|
46
|
-
const { buildChangePacket, buildMultiFileChangePacket } = require("../change-packet/builder");
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Change Event types
|
|
50
|
-
*/
|
|
51
|
-
const CHANGE_EVENT_TYPE = {
|
|
52
|
-
FILE_WRITE: "file_write",
|
|
53
|
-
FILE_CREATE: "file_create",
|
|
54
|
-
FILE_DELETE: "file_delete",
|
|
55
|
-
ROUTE_ADD: "route_add",
|
|
56
|
-
ROUTE_MODIFY: "route_modify",
|
|
57
|
-
ENV_REF: "env_ref",
|
|
58
|
-
PERMISSION_CHANGE: "permission_change",
|
|
59
|
-
UI_STATE: "ui_state",
|
|
60
|
-
CONFIG_CHANGE: "config_change",
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Normalize a raw change into a Change Event
|
|
65
|
-
* @param {Object} rawChange - Raw change from interception
|
|
66
|
-
* @returns {Object} Normalized Change Event
|
|
67
|
-
*/
|
|
68
|
-
function normalizeChangeEvent(rawChange) {
|
|
69
|
-
return {
|
|
70
|
-
type: rawChange.type || CHANGE_EVENT_TYPE.FILE_WRITE,
|
|
71
|
-
location: rawChange.path || rawChange.file || rawChange.location,
|
|
72
|
-
diff: rawChange.diff || null,
|
|
73
|
-
content: rawChange.content || rawChange.diff?.after || null,
|
|
74
|
-
resource: rawChange.resource || rawChange.path || null,
|
|
75
|
-
domain: rawChange.domain || classifyDomain(rawChange.path),
|
|
76
|
-
claims: rawChange.claims || [],
|
|
77
|
-
method: rawChange.method || null,
|
|
78
|
-
env_exists: rawChange.env_exists,
|
|
79
|
-
includes_tests: rawChange.includes_tests || false,
|
|
80
|
-
file_count: rawChange.file_count || 1,
|
|
81
|
-
timestamp: rawChange.timestamp || new Date().toISOString(),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Classify file domain
|
|
87
|
-
* @param {string} filePath - File path
|
|
88
|
-
* @returns {string} Domain
|
|
89
|
-
*/
|
|
90
|
-
function classifyDomain(filePath) {
|
|
91
|
-
if (!filePath) return "general";
|
|
92
|
-
const s = filePath.toLowerCase();
|
|
93
|
-
if (s.includes("auth")) return "auth";
|
|
94
|
-
if (s.includes("stripe") || s.includes("payment")) return "payments";
|
|
95
|
-
if (s.includes("routes") || s.includes("router") || s.includes("api")) return "routes";
|
|
96
|
-
if (s.includes("schema") || s.includes("contract")) return "contracts";
|
|
97
|
-
if (s.includes("ui") || s.includes("components") || s.includes("pages")) return "ui";
|
|
98
|
-
if (s.includes("database") || s.includes("prisma") || s.includes("migration")) return "database";
|
|
99
|
-
return "general";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Firewall Orchestrator
|
|
104
|
-
*
|
|
105
|
-
* Main entry point for all enforcement operations.
|
|
106
|
-
*/
|
|
107
|
-
class FirewallOrchestrator {
|
|
108
|
-
constructor(projectRoot) {
|
|
109
|
-
this.projectRoot = projectRoot;
|
|
110
|
-
this.intentStore = new IntentStore(projectRoot);
|
|
111
|
-
this.modeConfig = new ModeConfig(projectRoot);
|
|
112
|
-
this.runId = crypto.randomBytes(8).toString("hex");
|
|
113
|
-
this.startTime = Date.now();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Main enforcement entry point
|
|
118
|
-
*
|
|
119
|
-
* @param {Object} params - Enforcement parameters
|
|
120
|
-
* @param {Object[]} params.changes - Array of raw changes
|
|
121
|
-
* @param {string} params.agentId - Agent identifier
|
|
122
|
-
* @param {Object} params.proposal - Structured proposal (optional)
|
|
123
|
-
* @param {Object} params.options - Options
|
|
124
|
-
* @returns {Promise<Object>} Enforcement result with verdict
|
|
125
|
-
*/
|
|
126
|
-
async enforce({ changes, agentId, proposal = null, options = {} }) {
|
|
127
|
-
const mode = this.modeConfig.getMode();
|
|
128
|
-
const result = {
|
|
129
|
-
runId: this.runId,
|
|
130
|
-
mode,
|
|
131
|
-
blocked: false,
|
|
132
|
-
verdict: null,
|
|
133
|
-
violations: [],
|
|
134
|
-
proofs: [],
|
|
135
|
-
intent_hash: null,
|
|
136
|
-
timestamp: new Date().toISOString(),
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
// Step 1: Get and validate intent
|
|
141
|
-
const intent = this.intentStore.getCurrent();
|
|
142
|
-
result.intent_hash = intent?.hash || null;
|
|
143
|
-
|
|
144
|
-
// Verify intent integrity
|
|
145
|
-
if (intent) {
|
|
146
|
-
const integrityCheck = verifyIntentIntegrity(intent);
|
|
147
|
-
if (!integrityCheck.valid) {
|
|
148
|
-
result.violations.push({
|
|
149
|
-
code: VIOLATION_CODES.INTENT_CORRUPTED,
|
|
150
|
-
rule: "intent_integrity",
|
|
151
|
-
message: `Intent integrity check failed: ${integrityCheck.reason}`,
|
|
152
|
-
resource: "intent",
|
|
153
|
-
intent_ref: "system",
|
|
154
|
-
severity: "block",
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Check expiration
|
|
159
|
-
if (isIntentExpired(intent)) {
|
|
160
|
-
result.violations.push({
|
|
161
|
-
code: VIOLATION_CODES.INTENT_EXPIRED,
|
|
162
|
-
rule: "intent_expiration",
|
|
163
|
-
message: "Intent has expired",
|
|
164
|
-
resource: "intent",
|
|
165
|
-
intent_ref: "system",
|
|
166
|
-
severity: "block",
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Step 2: Normalize change events
|
|
172
|
-
const changeEvents = changes.map(normalizeChangeEvent);
|
|
173
|
-
|
|
174
|
-
// Step 3: Run intent alignment check
|
|
175
|
-
const alignmentResult = checkAlignmentBatch(intent, changeEvents);
|
|
176
|
-
result.violations.push(...alignmentResult.violations);
|
|
177
|
-
|
|
178
|
-
// Step 4: Extract claims and resolve evidence (existing system)
|
|
179
|
-
const allClaims = changeEvents.flatMap(e => e.claims || []);
|
|
180
|
-
const evidence = resolveEvidence(this.projectRoot, allClaims);
|
|
181
|
-
|
|
182
|
-
// Step 5: Run policy engine (existing rules)
|
|
183
|
-
const policyResult = await evaluatePolicy({
|
|
184
|
-
policy: this.loadPolicy(),
|
|
185
|
-
claims: allClaims,
|
|
186
|
-
evidence,
|
|
187
|
-
files: changeEvents.map(e => ({ path: e.location, domain: e.domain })),
|
|
188
|
-
intent: intent?.summary || "",
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Convert policy violations to v2 format
|
|
192
|
-
if (policyResult.violations) {
|
|
193
|
-
const policyViolations = policyResult.violations.map(v => ({
|
|
194
|
-
code: v.rule?.toUpperCase().replace(/-/g, "_") || "POLICY_VIOLATION",
|
|
195
|
-
rule: v.rule || "unknown",
|
|
196
|
-
message: v.message || "Policy violation",
|
|
197
|
-
resource: v.claimId || v.file || "unknown",
|
|
198
|
-
intent_ref: "policy",
|
|
199
|
-
severity: "block",
|
|
200
|
-
}));
|
|
201
|
-
result.violations.push(...policyViolations);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Step 6: Collect proofs (if required)
|
|
205
|
-
const proofCollection = new ProofCollection();
|
|
206
|
-
const requiredProofs = determineRequiredProofs(allClaims, intent);
|
|
207
|
-
|
|
208
|
-
for (const proofType of requiredProofs) {
|
|
209
|
-
proofCollection.require(proofType);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// In reality mode, we'd run actual verification here
|
|
213
|
-
// For now, mark proofs based on evidence
|
|
214
|
-
for (const e of evidence) {
|
|
215
|
-
if (e.result === "PROVEN") {
|
|
216
|
-
proofCollection.add({
|
|
217
|
-
id: `prf-${e.claimId}`,
|
|
218
|
-
type: this.claimTypeToProofType(allClaims.find(c => c.type === e.type)?.type),
|
|
219
|
-
status: PROOF_STATUS.VERIFIED,
|
|
220
|
-
target: e.pointer || e.claimId,
|
|
221
|
-
trace: e.sources?.[0]?.pointer,
|
|
222
|
-
});
|
|
223
|
-
} else if (e.result === "UNPROVEN" || e.result === "CONTRADICTS") {
|
|
224
|
-
proofCollection.add({
|
|
225
|
-
id: `prf-${e.claimId}`,
|
|
226
|
-
type: this.claimTypeToProofType(allClaims.find(c => c.type === e.type)?.type),
|
|
227
|
-
status: PROOF_STATUS.FAILED,
|
|
228
|
-
target: e.pointer || e.claimId,
|
|
229
|
-
error: e.reason || "Could not verify claim",
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
result.proofs = proofCollection.getAll();
|
|
235
|
-
|
|
236
|
-
// Step 7: Generate final verdict
|
|
237
|
-
result.verdict = generateVerdict({
|
|
238
|
-
alignmentResult: { violations: result.violations },
|
|
239
|
-
proofs: result.proofs,
|
|
240
|
-
mode,
|
|
241
|
-
intent_hash: result.intent_hash,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
result.blocked = result.verdict.blocked;
|
|
245
|
-
|
|
246
|
-
// Step 8: Store change packet for audit
|
|
247
|
-
const packet = buildMultiFileChangePacket({
|
|
248
|
-
agentId,
|
|
249
|
-
intent: intent?.summary || "NO INTENT",
|
|
250
|
-
changes: changes.map((c, i) => ({
|
|
251
|
-
filePath: changeEvents[i].location,
|
|
252
|
-
diff: c.diff,
|
|
253
|
-
claims: changeEvents[i].claims,
|
|
254
|
-
})),
|
|
255
|
-
evidence,
|
|
256
|
-
verdict: {
|
|
257
|
-
decision: result.verdict.decision,
|
|
258
|
-
violations: result.violations,
|
|
259
|
-
message: result.verdict.summary,
|
|
260
|
-
},
|
|
261
|
-
policy: this.loadPolicy(),
|
|
262
|
-
proposal,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
this.storePacket(packet);
|
|
266
|
-
|
|
267
|
-
// Step 9: Store verdict manifest
|
|
268
|
-
const manifest = createVerdictManifest(result.verdict, {
|
|
269
|
-
runId: this.runId,
|
|
270
|
-
agentId,
|
|
271
|
-
fileCount: changes.length,
|
|
272
|
-
});
|
|
273
|
-
this.storeVerdictManifest(manifest);
|
|
274
|
-
|
|
275
|
-
return result;
|
|
276
|
-
|
|
277
|
-
} catch (error) {
|
|
278
|
-
// On error, default to BLOCK
|
|
279
|
-
result.violations.push({
|
|
280
|
-
code: "ENFORCEMENT_ERROR",
|
|
281
|
-
rule: "system",
|
|
282
|
-
message: `Enforcement error: ${error.message}`,
|
|
283
|
-
resource: "system",
|
|
284
|
-
intent_ref: "system",
|
|
285
|
-
severity: "block",
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
result.verdict = generateVerdict({
|
|
289
|
-
alignmentResult: { violations: result.violations },
|
|
290
|
-
proofs: [],
|
|
291
|
-
mode,
|
|
292
|
-
intent_hash: result.intent_hash,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
result.blocked = true;
|
|
296
|
-
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Quick check - fast path for single file changes
|
|
303
|
-
* @param {Object} change - Single change
|
|
304
|
-
* @returns {Object} Quick check result
|
|
305
|
-
*/
|
|
306
|
-
quickCheck(change) {
|
|
307
|
-
const intent = this.intentStore.getCurrent();
|
|
308
|
-
const changeEvent = normalizeChangeEvent(change);
|
|
309
|
-
const alignmentResult = checkAlignmentBatch(intent, [changeEvent]);
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
allowed: alignmentResult.aligned,
|
|
313
|
-
decision: alignmentResult.decision,
|
|
314
|
-
violations: alignmentResult.violations,
|
|
315
|
-
intent_hash: intent?.hash || null,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Map claim type to proof type
|
|
321
|
-
* @param {string} claimType - Claim type
|
|
322
|
-
* @returns {string} Proof type
|
|
323
|
-
*/
|
|
324
|
-
claimTypeToProofType(claimType) {
|
|
325
|
-
const mapping = {
|
|
326
|
-
route: "route",
|
|
327
|
-
auth_boundary: "auth",
|
|
328
|
-
ui_success_claim: "ui",
|
|
329
|
-
http_call: "integration",
|
|
330
|
-
env_used: "env",
|
|
331
|
-
data_contract: "contract",
|
|
332
|
-
};
|
|
333
|
-
return mapping[claimType] || "integration";
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Load policy configuration
|
|
338
|
-
* @returns {Object} Policy config
|
|
339
|
-
*/
|
|
340
|
-
loadPolicy() {
|
|
341
|
-
const policyPath = path.join(this.projectRoot, ".vibecheck", "policy.json");
|
|
342
|
-
|
|
343
|
-
if (fs.existsSync(policyPath)) {
|
|
344
|
-
try {
|
|
345
|
-
return JSON.parse(fs.readFileSync(policyPath, "utf-8"));
|
|
346
|
-
} catch {
|
|
347
|
-
// Return default policy
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
version: "2.0.0",
|
|
353
|
-
profile: "strict",
|
|
354
|
-
mode: this.modeConfig.getMode(),
|
|
355
|
-
rules: {},
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Store change packet
|
|
361
|
-
* @param {Object} packet - Change packet
|
|
362
|
-
*/
|
|
363
|
-
storePacket(packet) {
|
|
364
|
-
const date = new Date().toISOString().split("T")[0];
|
|
365
|
-
const packetDir = path.join(this.projectRoot, ".vibecheck", "packets", date);
|
|
366
|
-
|
|
367
|
-
if (!fs.existsSync(packetDir)) {
|
|
368
|
-
fs.mkdirSync(packetDir, { recursive: true });
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const packetPath = path.join(packetDir, `${packet.id}.json`);
|
|
372
|
-
fs.writeFileSync(packetPath, JSON.stringify(packet, null, 2));
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Store verdict manifest
|
|
377
|
-
* @param {Object} manifest - Verdict manifest
|
|
378
|
-
*/
|
|
379
|
-
storeVerdictManifest(manifest) {
|
|
380
|
-
const verdictDir = path.join(this.projectRoot, ".vibecheck", "verdicts");
|
|
381
|
-
|
|
382
|
-
if (!fs.existsSync(verdictDir)) {
|
|
383
|
-
fs.mkdirSync(verdictDir, { recursive: true });
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const verdictPath = path.join(verdictDir, `${manifest.verdict.id}.json`);
|
|
387
|
-
fs.writeFileSync(verdictPath, JSON.stringify(manifest, null, 2));
|
|
388
|
-
|
|
389
|
-
// Also write latest
|
|
390
|
-
const latestPath = path.join(verdictDir, "latest.json");
|
|
391
|
-
fs.writeFileSync(latestPath, JSON.stringify(manifest, null, 2));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Get latest verdict
|
|
396
|
-
* @returns {Object|null} Latest verdict manifest
|
|
397
|
-
*/
|
|
398
|
-
getLatestVerdict() {
|
|
399
|
-
const latestPath = path.join(this.projectRoot, ".vibecheck", "verdicts", "latest.json");
|
|
400
|
-
|
|
401
|
-
if (fs.existsSync(latestPath)) {
|
|
402
|
-
try {
|
|
403
|
-
return JSON.parse(fs.readFileSync(latestPath, "utf-8"));
|
|
404
|
-
} catch {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return null;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Format enforcement result for output
|
|
414
|
-
* @param {Object} result - Enforcement result
|
|
415
|
-
* @param {Object} options - Output options
|
|
416
|
-
* @returns {string} Formatted output
|
|
417
|
-
*/
|
|
418
|
-
formatResult(result, options = {}) {
|
|
419
|
-
const { ansi = {} } = options;
|
|
420
|
-
const r = ansi.reset || "";
|
|
421
|
-
const b = ansi.bold || "";
|
|
422
|
-
const g = ansi.green || "";
|
|
423
|
-
const y = ansi.yellow || "";
|
|
424
|
-
const red = ansi.red || "";
|
|
425
|
-
const d = ansi.dim || "";
|
|
426
|
-
|
|
427
|
-
const lines = [];
|
|
428
|
-
|
|
429
|
-
// Mode banner
|
|
430
|
-
lines.push(formatModeBanner(result.mode, ansi));
|
|
431
|
-
|
|
432
|
-
// Verdict
|
|
433
|
-
const verdictColor = result.verdict?.decision === "PASS" || result.verdict?.decision === "WOULD_PASS" ? g : red;
|
|
434
|
-
lines.push(`\n${b}Verdict:${r} ${verdictColor}${result.verdict?.decision}${r}`);
|
|
435
|
-
|
|
436
|
-
// Summary
|
|
437
|
-
if (result.verdict?.summary) {
|
|
438
|
-
lines.push(`${d}${result.verdict.summary}${r}`);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Violations
|
|
442
|
-
if (result.violations.length > 0) {
|
|
443
|
-
lines.push(`\n${b}Violations (${result.violations.length}):${r}`);
|
|
444
|
-
for (const v of result.violations.slice(0, 5)) {
|
|
445
|
-
lines.push(` ${red}•${r} [${v.code}] ${v.message}`);
|
|
446
|
-
lines.push(` ${d}Resource: ${v.resource}${r}`);
|
|
447
|
-
lines.push(` ${d}Intent ref: ${v.intent_ref}${r}`);
|
|
448
|
-
}
|
|
449
|
-
if (result.violations.length > 5) {
|
|
450
|
-
lines.push(` ${d}... and ${result.violations.length - 5} more${r}`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Proofs
|
|
455
|
-
if (result.proofs.length > 0) {
|
|
456
|
-
const verified = result.proofs.filter(p => p.status === "verified").length;
|
|
457
|
-
const failed = result.proofs.filter(p => p.status === "failed").length;
|
|
458
|
-
lines.push(`\n${b}Proofs:${r} ${g}${verified} verified${r}, ${red}${failed} failed${r}`);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Intent
|
|
462
|
-
lines.push(`\n${d}Intent hash: ${result.intent_hash || "NONE"}${r}`);
|
|
463
|
-
lines.push(`${d}Run ID: ${result.runId}${r}`);
|
|
464
|
-
|
|
465
|
-
return lines.join("\n");
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Create orchestrator instance
|
|
471
|
-
* @param {string} projectRoot - Project root
|
|
472
|
-
* @returns {FirewallOrchestrator} Orchestrator instance
|
|
473
|
-
*/
|
|
474
|
-
function createOrchestrator(projectRoot) {
|
|
475
|
-
return new FirewallOrchestrator(projectRoot);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
module.exports = {
|
|
479
|
-
FirewallOrchestrator,
|
|
480
|
-
createOrchestrator,
|
|
481
|
-
normalizeChangeEvent,
|
|
482
|
-
classifyDomain,
|
|
483
|
-
CHANGE_EVENT_TYPE,
|
|
484
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Firewall Orchestrator - Main Enforcement Entry Point
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* AGENT FIREWALL™ - ENFORCEMENT ORCHESTRATOR
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* The Agent Firewall is a mandatory enforcement layer that intercepts AI-generated
|
|
9
|
+
* actions and BLOCKS them unless they are:
|
|
10
|
+
*
|
|
11
|
+
* 1) Explicitly aligned with declared user INTENT
|
|
12
|
+
* 2) Structurally valid (routes, env vars, permissions exist)
|
|
13
|
+
* 3) Proven real at runtime via Reality Mode
|
|
14
|
+
*
|
|
15
|
+
* If proof is missing → the action MUST NOT SHIP.
|
|
16
|
+
*
|
|
17
|
+
* This is enforcement infrastructure, not developer guidance.
|
|
18
|
+
*
|
|
19
|
+
* @module enforcement/orchestrator
|
|
20
|
+
* @version 2.0.0
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const crypto = require("crypto");
|
|
28
|
+
|
|
29
|
+
// Intent system
|
|
30
|
+
const { IntentStore } = require("../intent/store");
|
|
31
|
+
const { checkAlignmentBatch, VIOLATION_CODES } = require("../intent/alignment-engine");
|
|
32
|
+
const { verifyIntentIntegrity, isIntentExpired } = require("../intent/schema");
|
|
33
|
+
|
|
34
|
+
// Verdict system
|
|
35
|
+
const { generateVerdict, VERDICT, OBSERVATION, MODE, createVerdictManifest } = require("./verdict-v2");
|
|
36
|
+
|
|
37
|
+
// Proof system
|
|
38
|
+
const { ProofCollection, determineRequiredProofs, PROOF_STATUS } = require("./proof-artifact");
|
|
39
|
+
|
|
40
|
+
// Mode system
|
|
41
|
+
const { ModeConfig, createModeEvent, formatModeBanner } = require("./mode");
|
|
42
|
+
|
|
43
|
+
// Existing components
|
|
44
|
+
const { resolveEvidence } = require("../evidence/resolver");
|
|
45
|
+
const { evaluatePolicy } = require("../policy/engine");
|
|
46
|
+
const { buildChangePacket, buildMultiFileChangePacket } = require("../change-packet/builder");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Change Event types
|
|
50
|
+
*/
|
|
51
|
+
const CHANGE_EVENT_TYPE = {
|
|
52
|
+
FILE_WRITE: "file_write",
|
|
53
|
+
FILE_CREATE: "file_create",
|
|
54
|
+
FILE_DELETE: "file_delete",
|
|
55
|
+
ROUTE_ADD: "route_add",
|
|
56
|
+
ROUTE_MODIFY: "route_modify",
|
|
57
|
+
ENV_REF: "env_ref",
|
|
58
|
+
PERMISSION_CHANGE: "permission_change",
|
|
59
|
+
UI_STATE: "ui_state",
|
|
60
|
+
CONFIG_CHANGE: "config_change",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalize a raw change into a Change Event
|
|
65
|
+
* @param {Object} rawChange - Raw change from interception
|
|
66
|
+
* @returns {Object} Normalized Change Event
|
|
67
|
+
*/
|
|
68
|
+
function normalizeChangeEvent(rawChange) {
|
|
69
|
+
return {
|
|
70
|
+
type: rawChange.type || CHANGE_EVENT_TYPE.FILE_WRITE,
|
|
71
|
+
location: rawChange.path || rawChange.file || rawChange.location,
|
|
72
|
+
diff: rawChange.diff || null,
|
|
73
|
+
content: rawChange.content || rawChange.diff?.after || null,
|
|
74
|
+
resource: rawChange.resource || rawChange.path || null,
|
|
75
|
+
domain: rawChange.domain || classifyDomain(rawChange.path),
|
|
76
|
+
claims: rawChange.claims || [],
|
|
77
|
+
method: rawChange.method || null,
|
|
78
|
+
env_exists: rawChange.env_exists,
|
|
79
|
+
includes_tests: rawChange.includes_tests || false,
|
|
80
|
+
file_count: rawChange.file_count || 1,
|
|
81
|
+
timestamp: rawChange.timestamp || new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Classify file domain
|
|
87
|
+
* @param {string} filePath - File path
|
|
88
|
+
* @returns {string} Domain
|
|
89
|
+
*/
|
|
90
|
+
function classifyDomain(filePath) {
|
|
91
|
+
if (!filePath) return "general";
|
|
92
|
+
const s = filePath.toLowerCase();
|
|
93
|
+
if (s.includes("auth")) return "auth";
|
|
94
|
+
if (s.includes("stripe") || s.includes("payment")) return "payments";
|
|
95
|
+
if (s.includes("routes") || s.includes("router") || s.includes("api")) return "routes";
|
|
96
|
+
if (s.includes("schema") || s.includes("contract")) return "contracts";
|
|
97
|
+
if (s.includes("ui") || s.includes("components") || s.includes("pages")) return "ui";
|
|
98
|
+
if (s.includes("database") || s.includes("prisma") || s.includes("migration")) return "database";
|
|
99
|
+
return "general";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Firewall Orchestrator
|
|
104
|
+
*
|
|
105
|
+
* Main entry point for all enforcement operations.
|
|
106
|
+
*/
|
|
107
|
+
class FirewallOrchestrator {
|
|
108
|
+
constructor(projectRoot) {
|
|
109
|
+
this.projectRoot = projectRoot;
|
|
110
|
+
this.intentStore = new IntentStore(projectRoot);
|
|
111
|
+
this.modeConfig = new ModeConfig(projectRoot);
|
|
112
|
+
this.runId = crypto.randomBytes(8).toString("hex");
|
|
113
|
+
this.startTime = Date.now();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Main enforcement entry point
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} params - Enforcement parameters
|
|
120
|
+
* @param {Object[]} params.changes - Array of raw changes
|
|
121
|
+
* @param {string} params.agentId - Agent identifier
|
|
122
|
+
* @param {Object} params.proposal - Structured proposal (optional)
|
|
123
|
+
* @param {Object} params.options - Options
|
|
124
|
+
* @returns {Promise<Object>} Enforcement result with verdict
|
|
125
|
+
*/
|
|
126
|
+
async enforce({ changes, agentId, proposal = null, options = {} }) {
|
|
127
|
+
const mode = this.modeConfig.getMode();
|
|
128
|
+
const result = {
|
|
129
|
+
runId: this.runId,
|
|
130
|
+
mode,
|
|
131
|
+
blocked: false,
|
|
132
|
+
verdict: null,
|
|
133
|
+
violations: [],
|
|
134
|
+
proofs: [],
|
|
135
|
+
intent_hash: null,
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Step 1: Get and validate intent
|
|
141
|
+
const intent = this.intentStore.getCurrent();
|
|
142
|
+
result.intent_hash = intent?.hash || null;
|
|
143
|
+
|
|
144
|
+
// Verify intent integrity
|
|
145
|
+
if (intent) {
|
|
146
|
+
const integrityCheck = verifyIntentIntegrity(intent);
|
|
147
|
+
if (!integrityCheck.valid) {
|
|
148
|
+
result.violations.push({
|
|
149
|
+
code: VIOLATION_CODES.INTENT_CORRUPTED,
|
|
150
|
+
rule: "intent_integrity",
|
|
151
|
+
message: `Intent integrity check failed: ${integrityCheck.reason}`,
|
|
152
|
+
resource: "intent",
|
|
153
|
+
intent_ref: "system",
|
|
154
|
+
severity: "block",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check expiration
|
|
159
|
+
if (isIntentExpired(intent)) {
|
|
160
|
+
result.violations.push({
|
|
161
|
+
code: VIOLATION_CODES.INTENT_EXPIRED,
|
|
162
|
+
rule: "intent_expiration",
|
|
163
|
+
message: "Intent has expired",
|
|
164
|
+
resource: "intent",
|
|
165
|
+
intent_ref: "system",
|
|
166
|
+
severity: "block",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 2: Normalize change events
|
|
172
|
+
const changeEvents = changes.map(normalizeChangeEvent);
|
|
173
|
+
|
|
174
|
+
// Step 3: Run intent alignment check
|
|
175
|
+
const alignmentResult = checkAlignmentBatch(intent, changeEvents);
|
|
176
|
+
result.violations.push(...alignmentResult.violations);
|
|
177
|
+
|
|
178
|
+
// Step 4: Extract claims and resolve evidence (existing system)
|
|
179
|
+
const allClaims = changeEvents.flatMap(e => e.claims || []);
|
|
180
|
+
const evidence = resolveEvidence(this.projectRoot, allClaims);
|
|
181
|
+
|
|
182
|
+
// Step 5: Run policy engine (existing rules)
|
|
183
|
+
const policyResult = await evaluatePolicy({
|
|
184
|
+
policy: this.loadPolicy(),
|
|
185
|
+
claims: allClaims,
|
|
186
|
+
evidence,
|
|
187
|
+
files: changeEvents.map(e => ({ path: e.location, domain: e.domain })),
|
|
188
|
+
intent: intent?.summary || "",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Convert policy violations to v2 format
|
|
192
|
+
if (policyResult.violations) {
|
|
193
|
+
const policyViolations = policyResult.violations.map(v => ({
|
|
194
|
+
code: v.rule?.toUpperCase().replace(/-/g, "_") || "POLICY_VIOLATION",
|
|
195
|
+
rule: v.rule || "unknown",
|
|
196
|
+
message: v.message || "Policy violation",
|
|
197
|
+
resource: v.claimId || v.file || "unknown",
|
|
198
|
+
intent_ref: "policy",
|
|
199
|
+
severity: "block",
|
|
200
|
+
}));
|
|
201
|
+
result.violations.push(...policyViolations);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Step 6: Collect proofs (if required)
|
|
205
|
+
const proofCollection = new ProofCollection();
|
|
206
|
+
const requiredProofs = determineRequiredProofs(allClaims, intent);
|
|
207
|
+
|
|
208
|
+
for (const proofType of requiredProofs) {
|
|
209
|
+
proofCollection.require(proofType);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// In reality mode, we'd run actual verification here
|
|
213
|
+
// For now, mark proofs based on evidence
|
|
214
|
+
for (const e of evidence) {
|
|
215
|
+
if (e.result === "PROVEN") {
|
|
216
|
+
proofCollection.add({
|
|
217
|
+
id: `prf-${e.claimId}`,
|
|
218
|
+
type: this.claimTypeToProofType(allClaims.find(c => c.type === e.type)?.type),
|
|
219
|
+
status: PROOF_STATUS.VERIFIED,
|
|
220
|
+
target: e.pointer || e.claimId,
|
|
221
|
+
trace: e.sources?.[0]?.pointer,
|
|
222
|
+
});
|
|
223
|
+
} else if (e.result === "UNPROVEN" || e.result === "CONTRADICTS") {
|
|
224
|
+
proofCollection.add({
|
|
225
|
+
id: `prf-${e.claimId}`,
|
|
226
|
+
type: this.claimTypeToProofType(allClaims.find(c => c.type === e.type)?.type),
|
|
227
|
+
status: PROOF_STATUS.FAILED,
|
|
228
|
+
target: e.pointer || e.claimId,
|
|
229
|
+
error: e.reason || "Could not verify claim",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
result.proofs = proofCollection.getAll();
|
|
235
|
+
|
|
236
|
+
// Step 7: Generate final verdict
|
|
237
|
+
result.verdict = generateVerdict({
|
|
238
|
+
alignmentResult: { violations: result.violations },
|
|
239
|
+
proofs: result.proofs,
|
|
240
|
+
mode,
|
|
241
|
+
intent_hash: result.intent_hash,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
result.blocked = result.verdict.blocked;
|
|
245
|
+
|
|
246
|
+
// Step 8: Store change packet for audit
|
|
247
|
+
const packet = buildMultiFileChangePacket({
|
|
248
|
+
agentId,
|
|
249
|
+
intent: intent?.summary || "NO INTENT",
|
|
250
|
+
changes: changes.map((c, i) => ({
|
|
251
|
+
filePath: changeEvents[i].location,
|
|
252
|
+
diff: c.diff,
|
|
253
|
+
claims: changeEvents[i].claims,
|
|
254
|
+
})),
|
|
255
|
+
evidence,
|
|
256
|
+
verdict: {
|
|
257
|
+
decision: result.verdict.decision,
|
|
258
|
+
violations: result.violations,
|
|
259
|
+
message: result.verdict.summary,
|
|
260
|
+
},
|
|
261
|
+
policy: this.loadPolicy(),
|
|
262
|
+
proposal,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
this.storePacket(packet);
|
|
266
|
+
|
|
267
|
+
// Step 9: Store verdict manifest
|
|
268
|
+
const manifest = createVerdictManifest(result.verdict, {
|
|
269
|
+
runId: this.runId,
|
|
270
|
+
agentId,
|
|
271
|
+
fileCount: changes.length,
|
|
272
|
+
});
|
|
273
|
+
this.storeVerdictManifest(manifest);
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
|
|
277
|
+
} catch (error) {
|
|
278
|
+
// On error, default to BLOCK
|
|
279
|
+
result.violations.push({
|
|
280
|
+
code: "ENFORCEMENT_ERROR",
|
|
281
|
+
rule: "system",
|
|
282
|
+
message: `Enforcement error: ${error.message}`,
|
|
283
|
+
resource: "system",
|
|
284
|
+
intent_ref: "system",
|
|
285
|
+
severity: "block",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
result.verdict = generateVerdict({
|
|
289
|
+
alignmentResult: { violations: result.violations },
|
|
290
|
+
proofs: [],
|
|
291
|
+
mode,
|
|
292
|
+
intent_hash: result.intent_hash,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
result.blocked = true;
|
|
296
|
+
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Quick check - fast path for single file changes
|
|
303
|
+
* @param {Object} change - Single change
|
|
304
|
+
* @returns {Object} Quick check result
|
|
305
|
+
*/
|
|
306
|
+
quickCheck(change) {
|
|
307
|
+
const intent = this.intentStore.getCurrent();
|
|
308
|
+
const changeEvent = normalizeChangeEvent(change);
|
|
309
|
+
const alignmentResult = checkAlignmentBatch(intent, [changeEvent]);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
allowed: alignmentResult.aligned,
|
|
313
|
+
decision: alignmentResult.decision,
|
|
314
|
+
violations: alignmentResult.violations,
|
|
315
|
+
intent_hash: intent?.hash || null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Map claim type to proof type
|
|
321
|
+
* @param {string} claimType - Claim type
|
|
322
|
+
* @returns {string} Proof type
|
|
323
|
+
*/
|
|
324
|
+
claimTypeToProofType(claimType) {
|
|
325
|
+
const mapping = {
|
|
326
|
+
route: "route",
|
|
327
|
+
auth_boundary: "auth",
|
|
328
|
+
ui_success_claim: "ui",
|
|
329
|
+
http_call: "integration",
|
|
330
|
+
env_used: "env",
|
|
331
|
+
data_contract: "contract",
|
|
332
|
+
};
|
|
333
|
+
return mapping[claimType] || "integration";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Load policy configuration
|
|
338
|
+
* @returns {Object} Policy config
|
|
339
|
+
*/
|
|
340
|
+
loadPolicy() {
|
|
341
|
+
const policyPath = path.join(this.projectRoot, ".vibecheck", "policy.json");
|
|
342
|
+
|
|
343
|
+
if (fs.existsSync(policyPath)) {
|
|
344
|
+
try {
|
|
345
|
+
return JSON.parse(fs.readFileSync(policyPath, "utf-8"));
|
|
346
|
+
} catch {
|
|
347
|
+
// Return default policy
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
version: "2.0.0",
|
|
353
|
+
profile: "strict",
|
|
354
|
+
mode: this.modeConfig.getMode(),
|
|
355
|
+
rules: {},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Store change packet
|
|
361
|
+
* @param {Object} packet - Change packet
|
|
362
|
+
*/
|
|
363
|
+
storePacket(packet) {
|
|
364
|
+
const date = new Date().toISOString().split("T")[0];
|
|
365
|
+
const packetDir = path.join(this.projectRoot, ".vibecheck", "packets", date);
|
|
366
|
+
|
|
367
|
+
if (!fs.existsSync(packetDir)) {
|
|
368
|
+
fs.mkdirSync(packetDir, { recursive: true });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const packetPath = path.join(packetDir, `${packet.id}.json`);
|
|
372
|
+
fs.writeFileSync(packetPath, JSON.stringify(packet, null, 2));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Store verdict manifest
|
|
377
|
+
* @param {Object} manifest - Verdict manifest
|
|
378
|
+
*/
|
|
379
|
+
storeVerdictManifest(manifest) {
|
|
380
|
+
const verdictDir = path.join(this.projectRoot, ".vibecheck", "verdicts");
|
|
381
|
+
|
|
382
|
+
if (!fs.existsSync(verdictDir)) {
|
|
383
|
+
fs.mkdirSync(verdictDir, { recursive: true });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const verdictPath = path.join(verdictDir, `${manifest.verdict.id}.json`);
|
|
387
|
+
fs.writeFileSync(verdictPath, JSON.stringify(manifest, null, 2));
|
|
388
|
+
|
|
389
|
+
// Also write latest
|
|
390
|
+
const latestPath = path.join(verdictDir, "latest.json");
|
|
391
|
+
fs.writeFileSync(latestPath, JSON.stringify(manifest, null, 2));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Get latest verdict
|
|
396
|
+
* @returns {Object|null} Latest verdict manifest
|
|
397
|
+
*/
|
|
398
|
+
getLatestVerdict() {
|
|
399
|
+
const latestPath = path.join(this.projectRoot, ".vibecheck", "verdicts", "latest.json");
|
|
400
|
+
|
|
401
|
+
if (fs.existsSync(latestPath)) {
|
|
402
|
+
try {
|
|
403
|
+
return JSON.parse(fs.readFileSync(latestPath, "utf-8"));
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Format enforcement result for output
|
|
414
|
+
* @param {Object} result - Enforcement result
|
|
415
|
+
* @param {Object} options - Output options
|
|
416
|
+
* @returns {string} Formatted output
|
|
417
|
+
*/
|
|
418
|
+
formatResult(result, options = {}) {
|
|
419
|
+
const { ansi = {} } = options;
|
|
420
|
+
const r = ansi.reset || "";
|
|
421
|
+
const b = ansi.bold || "";
|
|
422
|
+
const g = ansi.green || "";
|
|
423
|
+
const y = ansi.yellow || "";
|
|
424
|
+
const red = ansi.red || "";
|
|
425
|
+
const d = ansi.dim || "";
|
|
426
|
+
|
|
427
|
+
const lines = [];
|
|
428
|
+
|
|
429
|
+
// Mode banner
|
|
430
|
+
lines.push(formatModeBanner(result.mode, ansi));
|
|
431
|
+
|
|
432
|
+
// Verdict
|
|
433
|
+
const verdictColor = result.verdict?.decision === "PASS" || result.verdict?.decision === "WOULD_PASS" ? g : red;
|
|
434
|
+
lines.push(`\n${b}Verdict:${r} ${verdictColor}${result.verdict?.decision}${r}`);
|
|
435
|
+
|
|
436
|
+
// Summary
|
|
437
|
+
if (result.verdict?.summary) {
|
|
438
|
+
lines.push(`${d}${result.verdict.summary}${r}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Violations
|
|
442
|
+
if (result.violations.length > 0) {
|
|
443
|
+
lines.push(`\n${b}Violations (${result.violations.length}):${r}`);
|
|
444
|
+
for (const v of result.violations.slice(0, 5)) {
|
|
445
|
+
lines.push(` ${red}•${r} [${v.code}] ${v.message}`);
|
|
446
|
+
lines.push(` ${d}Resource: ${v.resource}${r}`);
|
|
447
|
+
lines.push(` ${d}Intent ref: ${v.intent_ref}${r}`);
|
|
448
|
+
}
|
|
449
|
+
if (result.violations.length > 5) {
|
|
450
|
+
lines.push(` ${d}... and ${result.violations.length - 5} more${r}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Proofs
|
|
455
|
+
if (result.proofs.length > 0) {
|
|
456
|
+
const verified = result.proofs.filter(p => p.status === "verified").length;
|
|
457
|
+
const failed = result.proofs.filter(p => p.status === "failed").length;
|
|
458
|
+
lines.push(`\n${b}Proofs:${r} ${g}${verified} verified${r}, ${red}${failed} failed${r}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Intent
|
|
462
|
+
lines.push(`\n${d}Intent hash: ${result.intent_hash || "NONE"}${r}`);
|
|
463
|
+
lines.push(`${d}Run ID: ${result.runId}${r}`);
|
|
464
|
+
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Create orchestrator instance
|
|
471
|
+
* @param {string} projectRoot - Project root
|
|
472
|
+
* @returns {FirewallOrchestrator} Orchestrator instance
|
|
473
|
+
*/
|
|
474
|
+
function createOrchestrator(projectRoot) {
|
|
475
|
+
return new FirewallOrchestrator(projectRoot);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = {
|
|
479
|
+
FirewallOrchestrator,
|
|
480
|
+
createOrchestrator,
|
|
481
|
+
normalizeChangeEvent,
|
|
482
|
+
classifyDomain,
|
|
483
|
+
CHANGE_EVENT_TYPE,
|
|
484
|
+
};
|