@vibecheckai/cli 3.2.6 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/bin/registry.js +192 -5
  2. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  3. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  4. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  5. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  6. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  7. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  8. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  11. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  12. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  14. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  15. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  16. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  17. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  18. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  19. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  20. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  21. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  22. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  23. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  24. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  25. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  26. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  27. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  28. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  29. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  30. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  31. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  32. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  35. package/bin/runners/lib/analyzers.js +81 -18
  36. package/bin/runners/lib/authority-badge.js +425 -0
  37. package/bin/runners/lib/cli-output.js +7 -1
  38. package/bin/runners/lib/error-handler.js +16 -9
  39. package/bin/runners/lib/exit-codes.js +275 -0
  40. package/bin/runners/lib/global-flags.js +37 -0
  41. package/bin/runners/lib/help-formatter.js +413 -0
  42. package/bin/runners/lib/logger.js +38 -0
  43. package/bin/runners/lib/unified-cli-output.js +604 -0
  44. package/bin/runners/lib/upsell.js +148 -0
  45. package/bin/runners/runApprove.js +1200 -0
  46. package/bin/runners/runAuth.js +324 -95
  47. package/bin/runners/runCheckpoint.js +39 -21
  48. package/bin/runners/runClassify.js +859 -0
  49. package/bin/runners/runContext.js +136 -24
  50. package/bin/runners/runDoctor.js +108 -68
  51. package/bin/runners/runFix.js +6 -5
  52. package/bin/runners/runGuard.js +212 -118
  53. package/bin/runners/runInit.js +3 -2
  54. package/bin/runners/runMcp.js +130 -52
  55. package/bin/runners/runPolish.js +43 -20
  56. package/bin/runners/runProve.js +1 -2
  57. package/bin/runners/runReport.js +3 -2
  58. package/bin/runners/runScan.js +63 -44
  59. package/bin/runners/runShip.js +3 -4
  60. package/bin/runners/runValidate.js +19 -2
  61. package/bin/runners/runWatch.js +104 -53
  62. package/bin/vibecheck.js +106 -19
  63. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  64. package/mcp-server/agent-firewall-interceptor.js +367 -31
  65. package/mcp-server/authority-tools.js +569 -0
  66. package/mcp-server/conductor/conflict-resolver.js +588 -0
  67. package/mcp-server/conductor/execution-planner.js +544 -0
  68. package/mcp-server/conductor/index.js +377 -0
  69. package/mcp-server/conductor/lock-manager.js +615 -0
  70. package/mcp-server/conductor/request-queue.js +550 -0
  71. package/mcp-server/conductor/session-manager.js +500 -0
  72. package/mcp-server/conductor/tools.js +510 -0
  73. package/mcp-server/index.js +1149 -243
  74. package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
  75. package/mcp-server/lib/logger.cjs +30 -0
  76. package/mcp-server/logger.js +173 -0
  77. package/mcp-server/package.json +2 -2
  78. package/mcp-server/premium-tools.js +2 -2
  79. package/mcp-server/tier-auth.js +245 -35
  80. package/mcp-server/truth-firewall-tools.js +145 -15
  81. package/mcp-server/vibecheck-tools.js +2 -2
  82. package/package.json +2 -3
  83. package/mcp-server/index.old.js +0 -4137
  84. package/mcp-server/package-lock.json +0 -165
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Risk Scoring Engine
3
+ *
4
+ * Calculates numerical risk scores for proposed changes.
5
+ * Uses configurable vectors and thresholds to determine risk levels.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const { RISK_VECTORS, RISK_LEVELS, getRiskLevel } = require("./vectors");
11
+ const { loadThresholds, getDecision } = require("./thresholds");
12
+ const { classifyFileDomain } = require("../reality/state");
13
+
14
+ /**
15
+ * @typedef {Object} RiskScore
16
+ * @property {number} total - Total risk score
17
+ * @property {string} level - Risk level (LOW, MEDIUM, HIGH, CRITICAL)
18
+ * @property {Object} vectors - Individual vector scores
19
+ * @property {string[]} reasons - Human-readable risk reasons
20
+ * @property {Object} decision - Decision based on thresholds
21
+ */
22
+
23
+ /**
24
+ * Build context object for risk calculation
25
+ * @param {Object} params - Score parameters
26
+ * @returns {Object} Risk calculation context
27
+ */
28
+ function buildContext(params) {
29
+ const {
30
+ files = [],
31
+ operations = [],
32
+ claims = [],
33
+ evidence = [],
34
+ intent = "",
35
+ assumptions = [],
36
+ proposalConfidence = 1,
37
+ policy = {},
38
+ } = params;
39
+
40
+ // Extract domains from files
41
+ const domains = new Set();
42
+ for (const file of files) {
43
+ const path = file.path || file;
44
+ const domain = classifyFileDomain(path);
45
+ domains.add(domain);
46
+ }
47
+
48
+ // Identify unresolved assumptions
49
+ const unresolvedAssumptions = [];
50
+ for (const assumption of assumptions) {
51
+ const evidenceForAssumption = evidence.find(e =>
52
+ e.claim?.key === assumption.key ||
53
+ e.claim?.type === assumption.type
54
+ );
55
+
56
+ if (!evidenceForAssumption || evidenceForAssumption.status === "UNPROVEN") {
57
+ unresolvedAssumptions.push(assumption);
58
+ }
59
+ }
60
+
61
+ // Detect new items
62
+ const newEnvVars = claims
63
+ .filter(c => c.type === "env" && !c.exists)
64
+ .map(c => c.key || c.value);
65
+
66
+ const newRoutes = claims
67
+ .filter(c => c.type === "route" && !c.exists)
68
+ .map(c => c.path || c.value);
69
+
70
+ const newDependencies = claims
71
+ .filter(c => c.type === "dependency" && !c.exists)
72
+ .map(c => c.name || c.value);
73
+
74
+ return {
75
+ files,
76
+ operations,
77
+ claims,
78
+ evidence,
79
+ intent,
80
+ assumptions,
81
+ proposalConfidence,
82
+ domains: Array.from(domains),
83
+ unresolvedAssumptions,
84
+ newEnvVars,
85
+ newRoutes,
86
+ newDependencies,
87
+ policy,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Calculate risk score for a change
93
+ * @param {Object} params - Score parameters
94
+ * @returns {RiskScore} Risk score result
95
+ */
96
+ function calculateRiskScore(params) {
97
+ const context = buildContext(params);
98
+ const policy = params.policy || {};
99
+ const thresholds = loadThresholds(policy);
100
+
101
+ // Calculate individual vector scores
102
+ const vectorScores = {};
103
+ const reasons = [];
104
+ let totalScore = 0;
105
+
106
+ for (const [key, vector] of Object.entries(RISK_VECTORS)) {
107
+ try {
108
+ // Get weight from policy or use default
109
+ const weight = policy.risk?.vectorWeights?.[vector.id] ?? vector.baseWeight;
110
+
111
+ // Skip disabled vectors
112
+ if (weight === 0) continue;
113
+
114
+ // Calculate raw score
115
+ const rawScore = vector.calculate(context);
116
+ const weightedScore = Math.round(rawScore * weight);
117
+
118
+ vectorScores[vector.id] = {
119
+ raw: rawScore,
120
+ weighted: weightedScore,
121
+ weight,
122
+ name: vector.name,
123
+ description: vector.description,
124
+ };
125
+
126
+ totalScore += weightedScore;
127
+
128
+ // Add reason if score is significant
129
+ if (weightedScore > 0) {
130
+ const threshold = thresholds.vectors?.[vector.id];
131
+ if (threshold) {
132
+ if (weightedScore >= threshold.block) {
133
+ reasons.push(`${vector.name}: ${weightedScore} (CRITICAL - exceeds block threshold)`);
134
+ } else if (weightedScore >= threshold.warn) {
135
+ reasons.push(`${vector.name}: ${weightedScore} (WARNING - exceeds warn threshold)`);
136
+ } else if (weightedScore >= 10) {
137
+ reasons.push(`${vector.name}: ${weightedScore}`);
138
+ }
139
+ } else if (weightedScore >= 15) {
140
+ reasons.push(`${vector.name}: ${weightedScore}`);
141
+ }
142
+ }
143
+ } catch (error) {
144
+ // Log but continue with other vectors
145
+ console.warn(`Error calculating ${vector.id} risk: ${error.message}`);
146
+ }
147
+ }
148
+
149
+ // Get risk level
150
+ const riskLevel = getRiskLevel(totalScore);
151
+
152
+ // Get decision based on thresholds
153
+ const decision = getDecision(totalScore, thresholds, context.domains);
154
+
155
+ // Build result
156
+ const result = {
157
+ total: totalScore,
158
+ level: riskLevel.label,
159
+ levelColor: riskLevel.color,
160
+ vectors: vectorScores,
161
+ reasons: reasons.length > 0 ? reasons : [`Total risk score: ${totalScore}`],
162
+ decision,
163
+ context: {
164
+ fileCount: context.files.length,
165
+ domains: context.domains,
166
+ unresolvedAssumptions: context.unresolvedAssumptions.length,
167
+ newEnvVars: context.newEnvVars.length,
168
+ newRoutes: context.newRoutes.length,
169
+ },
170
+ thresholds: {
171
+ autoAllow: thresholds.autoAllow,
172
+ requireConfirm: thresholds.requireConfirm,
173
+ autoBlock: thresholds.autoBlock,
174
+ },
175
+ };
176
+
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Quick risk assessment without full calculation
182
+ * @param {Object} params - Basic parameters
183
+ * @returns {Object} Quick assessment
184
+ */
185
+ function quickAssess(params) {
186
+ const { files = [], operations = [], domains = [] } = params;
187
+
188
+ // Quick checks
189
+ const hasDeletes = operations.some(op => op.type === "delete");
190
+ const hasMigrations = files.some(f => (f.path || f).includes("migration"));
191
+ const touchesAuth = domains.includes("auth") || files.some(f => (f.path || f).includes("auth"));
192
+ const touchesPayments = domains.includes("payments") || files.some(f =>
193
+ (f.path || f).includes("payment") || (f.path || f).includes("stripe")
194
+ );
195
+
196
+ // Estimate risk level
197
+ let estimatedLevel = "LOW";
198
+ const flags = [];
199
+
200
+ if (hasDeletes) {
201
+ flags.push("Contains deletions");
202
+ estimatedLevel = "MEDIUM";
203
+ }
204
+
205
+ if (hasMigrations) {
206
+ flags.push("Contains migrations");
207
+ estimatedLevel = "HIGH";
208
+ }
209
+
210
+ if (touchesAuth) {
211
+ flags.push("Touches auth");
212
+ estimatedLevel = estimatedLevel === "LOW" ? "MEDIUM" : estimatedLevel;
213
+ }
214
+
215
+ if (touchesPayments) {
216
+ flags.push("Touches payments");
217
+ estimatedLevel = "HIGH";
218
+ }
219
+
220
+ if (files.length > 10) {
221
+ flags.push("Large change (>10 files)");
222
+ estimatedLevel = estimatedLevel === "LOW" ? "MEDIUM" : estimatedLevel;
223
+ }
224
+
225
+ if (files.length > 20) {
226
+ estimatedLevel = "HIGH";
227
+ }
228
+
229
+ return {
230
+ estimatedLevel,
231
+ flags,
232
+ requiresFullAssessment: flags.length > 0 || files.length > 5,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Get risk breakdown by domain
238
+ * @param {RiskScore} riskScore - Calculated risk score
239
+ * @returns {Object} Domain breakdown
240
+ */
241
+ function getDomainBreakdown(riskScore) {
242
+ const breakdown = {};
243
+
244
+ for (const domain of riskScore.context?.domains || []) {
245
+ breakdown[domain] = {
246
+ files: 0,
247
+ contribution: 0,
248
+ };
249
+ }
250
+
251
+ // Estimate contribution based on domain vector
252
+ const domainVector = riskScore.vectors?.domain;
253
+ if (domainVector && riskScore.context?.domains) {
254
+ const totalDomains = riskScore.context.domains.length;
255
+ if (totalDomains > 0) {
256
+ const avgContribution = domainVector.weighted / totalDomains;
257
+ for (const domain of riskScore.context.domains) {
258
+ breakdown[domain].contribution = Math.round(avgContribution);
259
+ }
260
+ }
261
+ }
262
+
263
+ return breakdown;
264
+ }
265
+
266
+ /**
267
+ * Format risk score for display
268
+ * @param {RiskScore} riskScore - Risk score
269
+ * @returns {string} Formatted string
270
+ */
271
+ function formatRiskScore(riskScore) {
272
+ const lines = [
273
+ `Risk Score: ${riskScore.total} (${riskScore.level})`,
274
+ `Decision: ${riskScore.decision.decision}`,
275
+ "",
276
+ "Breakdown:",
277
+ ];
278
+
279
+ for (const [id, vector] of Object.entries(riskScore.vectors)) {
280
+ if (vector.weighted > 0) {
281
+ lines.push(` ${vector.name}: ${vector.weighted}`);
282
+ }
283
+ }
284
+
285
+ if (riskScore.reasons.length > 0) {
286
+ lines.push("", "Risk Factors:");
287
+ for (const reason of riskScore.reasons) {
288
+ lines.push(` - ${reason}`);
289
+ }
290
+ }
291
+
292
+ return lines.join("\n");
293
+ }
294
+
295
+ /**
296
+ * Compare two risk scores
297
+ * @param {RiskScore} a - First score
298
+ * @param {RiskScore} b - Second score
299
+ * @returns {Object} Comparison result
300
+ */
301
+ function compareScores(a, b) {
302
+ return {
303
+ difference: a.total - b.total,
304
+ percentChange: b.total > 0 ? ((a.total - b.total) / b.total) * 100 : 0,
305
+ levelChanged: a.level !== b.level,
306
+ oldLevel: b.level,
307
+ newLevel: a.level,
308
+ vectorChanges: Object.keys(a.vectors).reduce((acc, key) => {
309
+ const oldVal = b.vectors[key]?.weighted || 0;
310
+ const newVal = a.vectors[key]?.weighted || 0;
311
+ if (oldVal !== newVal) {
312
+ acc[key] = { old: oldVal, new: newVal, change: newVal - oldVal };
313
+ }
314
+ return acc;
315
+ }, {}),
316
+ };
317
+ }
318
+
319
+ module.exports = {
320
+ calculateRiskScore,
321
+ quickAssess,
322
+ buildContext,
323
+ getDomainBreakdown,
324
+ formatRiskScore,
325
+ compareScores,
326
+ RISK_VECTORS,
327
+ RISK_LEVELS,
328
+ };
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Risk Thresholds
3
+ *
4
+ * Configurable thresholds for risk-based decisions.
5
+ * These can be overridden in policy configuration.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ /**
11
+ * Default threshold configuration
12
+ *
13
+ * Tuned to reduce false positives while maintaining security.
14
+ * Single-file UI/component changes should typically auto-allow.
15
+ * Multi-file changes to core/auth/payments require confirmation.
16
+ * Only block truly dangerous patterns (migrations, mass deletes, etc.)
17
+ */
18
+ const DEFAULT_THRESHOLDS = {
19
+ /**
20
+ * Score thresholds for automatic decisions
21
+ * Raised to reduce noise for normal development
22
+ */
23
+ autoAllow: 30, // Auto-allow if score <= this (raised from 15)
24
+ requireConfirm: 70, // Require confirmation if score > this (raised from 50)
25
+ autoBlock: 100, // Auto-block if score >= this (raised from 80)
26
+
27
+ /**
28
+ * Vector-specific thresholds
29
+ */
30
+ vectors: {
31
+ surface_area: {
32
+ warn: 10,
33
+ block: 25,
34
+ },
35
+ blast_radius: {
36
+ warn: 30,
37
+ block: 60,
38
+ },
39
+ irreversibility: {
40
+ warn: 25,
41
+ block: 50,
42
+ },
43
+ confidence: {
44
+ warn: 20,
45
+ block: 60,
46
+ },
47
+ novelty: {
48
+ warn: 20,
49
+ block: 40,
50
+ },
51
+ domain: {
52
+ warn: 30,
53
+ block: 60,
54
+ },
55
+ side_effects: {
56
+ warn: 20,
57
+ block: 50,
58
+ },
59
+ },
60
+
61
+ /**
62
+ * Domain-specific thresholds
63
+ * Multipliers reduced to prevent over-penalization of normal changes
64
+ */
65
+ domains: {
66
+ auth: {
67
+ multiplier: 1.2, // Reduced from 1.5 - auth changes are common
68
+ requireConfirm: 50, // Raised from 30
69
+ autoBlock: 90, // Raised from 60
70
+ },
71
+ payments: {
72
+ multiplier: 1.3, // Reduced from 1.8 - payments needs care but not blocking
73
+ requireConfirm: 45, // Raised from 25
74
+ autoBlock: 85, // Raised from 50
75
+ },
76
+ database: {
77
+ multiplier: 1.1, // Reduced from 1.3 - DB changes are normal
78
+ requireConfirm: 55, // Raised from 40
79
+ autoBlock: 95, // Raised from 70
80
+ },
81
+ security: {
82
+ multiplier: 1.2, // Reduced from 1.6
83
+ requireConfirm: 50, // Raised from 25
84
+ autoBlock: 90, // Raised from 55
85
+ },
86
+ core: {
87
+ multiplier: 1.1, // Reduced from 1.2
88
+ requireConfirm: 60, // Raised from 45
89
+ autoBlock: 95, // Raised from 75
90
+ },
91
+ middleware: {
92
+ multiplier: 1.0, // Reduced from 1.1 - middleware is usually safe
93
+ requireConfirm: 65, // Raised from 50
94
+ autoBlock: 100, // Raised from 80
95
+ },
96
+ ui: {
97
+ multiplier: 0.7, // Reduced from 0.8 - UI is very safe
98
+ requireConfirm: 80, // Raised from 60
99
+ autoBlock: 120, // Raised from 90 - UI should almost never block
100
+ },
101
+ test: {
102
+ multiplier: 0.3, // Reduced from 0.5 - tests are safest
103
+ requireConfirm: 100, // Raised from 70
104
+ autoBlock: 150, // Raised from 95 - tests should never block
105
+ },
106
+ },
107
+
108
+ /**
109
+ * File count limits
110
+ */
111
+ fileLimits: {
112
+ warn: 5,
113
+ block: 15,
114
+ hardLimit: 50,
115
+ },
116
+
117
+ /**
118
+ * Line count limits
119
+ */
120
+ lineLimits: {
121
+ warn: 200,
122
+ block: 500,
123
+ hardLimit: 2000,
124
+ },
125
+ };
126
+
127
+ /**
128
+ * Profile presets
129
+ */
130
+ const THRESHOLD_PROFILES = {
131
+ /**
132
+ * Strict profile - very conservative
133
+ */
134
+ strict: {
135
+ autoAllow: 10,
136
+ requireConfirm: 30,
137
+ autoBlock: 60,
138
+ fileLimits: {
139
+ warn: 3,
140
+ block: 8,
141
+ hardLimit: 20,
142
+ },
143
+ lineLimits: {
144
+ warn: 100,
145
+ block: 300,
146
+ hardLimit: 1000,
147
+ },
148
+ },
149
+
150
+ /**
151
+ * Balanced profile - default
152
+ */
153
+ balanced: {
154
+ ...DEFAULT_THRESHOLDS,
155
+ },
156
+
157
+ /**
158
+ * Permissive profile - more lenient
159
+ */
160
+ permissive: {
161
+ autoAllow: 25,
162
+ requireConfirm: 70,
163
+ autoBlock: 95,
164
+ fileLimits: {
165
+ warn: 10,
166
+ block: 25,
167
+ hardLimit: 100,
168
+ },
169
+ lineLimits: {
170
+ warn: 500,
171
+ block: 1000,
172
+ hardLimit: 5000,
173
+ },
174
+ },
175
+
176
+ /**
177
+ * Repo-lock profile - most conservative
178
+ */
179
+ "repo-lock": {
180
+ autoAllow: 5,
181
+ requireConfirm: 15,
182
+ autoBlock: 40,
183
+ fileLimits: {
184
+ warn: 2,
185
+ block: 5,
186
+ hardLimit: 10,
187
+ },
188
+ lineLimits: {
189
+ warn: 50,
190
+ block: 150,
191
+ hardLimit: 500,
192
+ },
193
+ },
194
+ };
195
+
196
+ /**
197
+ * Load thresholds from policy
198
+ * @param {Object} policy - Policy configuration
199
+ * @returns {Object} Merged threshold configuration
200
+ */
201
+ function loadThresholds(policy = {}) {
202
+ // Start with default
203
+ let thresholds = { ...DEFAULT_THRESHOLDS };
204
+
205
+ // Apply profile if specified
206
+ const profile = policy.profile || "balanced";
207
+ if (THRESHOLD_PROFILES[profile]) {
208
+ thresholds = mergeDeep(thresholds, THRESHOLD_PROFILES[profile]);
209
+ }
210
+
211
+ // Apply custom thresholds from policy
212
+ if (policy.thresholds) {
213
+ thresholds = mergeDeep(thresholds, policy.thresholds);
214
+ }
215
+
216
+ // Apply risk configuration
217
+ if (policy.risk) {
218
+ if (policy.risk.autoAllow !== undefined) thresholds.autoAllow = policy.risk.autoAllow;
219
+ if (policy.risk.requireConfirm !== undefined) thresholds.requireConfirm = policy.risk.requireConfirm;
220
+ if (policy.risk.autoBlock !== undefined) thresholds.autoBlock = policy.risk.autoBlock;
221
+ }
222
+
223
+ return thresholds;
224
+ }
225
+
226
+ /**
227
+ * Deep merge objects
228
+ */
229
+ function mergeDeep(target, source) {
230
+ const output = { ...target };
231
+
232
+ for (const key of Object.keys(source)) {
233
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
234
+ output[key] = mergeDeep(output[key] || {}, source[key]);
235
+ } else {
236
+ output[key] = source[key];
237
+ }
238
+ }
239
+
240
+ return output;
241
+ }
242
+
243
+ /**
244
+ * Get decision based on score and thresholds
245
+ * @param {number} score - Risk score
246
+ * @param {Object} thresholds - Threshold configuration
247
+ * @param {string[]} domains - Affected domains
248
+ * @returns {Object} Decision object
249
+ */
250
+ function getDecision(score, thresholds, domains = []) {
251
+ // Check for domain-specific overrides
252
+ let effectiveThresholds = { ...thresholds };
253
+ let maxMultiplier = 1;
254
+
255
+ for (const domain of domains) {
256
+ const domainConfig = thresholds.domains?.[domain];
257
+ if (domainConfig) {
258
+ if (domainConfig.multiplier > maxMultiplier) {
259
+ maxMultiplier = domainConfig.multiplier;
260
+ }
261
+ // Use the most restrictive domain threshold
262
+ if (domainConfig.autoBlock < effectiveThresholds.autoBlock) {
263
+ effectiveThresholds.autoBlock = domainConfig.autoBlock;
264
+ }
265
+ if (domainConfig.requireConfirm < effectiveThresholds.requireConfirm) {
266
+ effectiveThresholds.requireConfirm = domainConfig.requireConfirm;
267
+ }
268
+ }
269
+ }
270
+
271
+ // Apply domain multiplier to score
272
+ const effectiveScore = Math.round(score * maxMultiplier);
273
+
274
+ // Determine decision
275
+ if (effectiveScore >= effectiveThresholds.autoBlock) {
276
+ return {
277
+ decision: "BLOCK",
278
+ reason: `Risk score ${effectiveScore} exceeds auto-block threshold ${effectiveThresholds.autoBlock}`,
279
+ score: effectiveScore,
280
+ multiplier: maxMultiplier,
281
+ thresholdUsed: effectiveThresholds.autoBlock,
282
+ };
283
+ }
284
+
285
+ if (effectiveScore > effectiveThresholds.requireConfirm) {
286
+ return {
287
+ decision: "REQUIRE_CONFIRMATION",
288
+ reason: `Risk score ${effectiveScore} exceeds confirmation threshold ${effectiveThresholds.requireConfirm}`,
289
+ score: effectiveScore,
290
+ multiplier: maxMultiplier,
291
+ thresholdUsed: effectiveThresholds.requireConfirm,
292
+ };
293
+ }
294
+
295
+ if (effectiveScore <= effectiveThresholds.autoAllow) {
296
+ return {
297
+ decision: "ALLOW",
298
+ reason: `Risk score ${effectiveScore} within auto-allow threshold ${effectiveThresholds.autoAllow}`,
299
+ score: effectiveScore,
300
+ multiplier: maxMultiplier,
301
+ thresholdUsed: effectiveThresholds.autoAllow,
302
+ };
303
+ }
304
+
305
+ // Default to allow with warning for scores in between
306
+ return {
307
+ decision: "ALLOW_WITH_WARNING",
308
+ reason: `Risk score ${effectiveScore} is elevated but within limits`,
309
+ score: effectiveScore,
310
+ multiplier: maxMultiplier,
311
+ thresholdUsed: effectiveThresholds.requireConfirm,
312
+ };
313
+ }
314
+
315
+ module.exports = {
316
+ DEFAULT_THRESHOLDS,
317
+ THRESHOLD_PROFILES,
318
+ loadThresholds,
319
+ getDecision,
320
+ mergeDeep,
321
+ };