@sovr/engine 3.2.0 → 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.
- package/dist/index.d.mts +352 -1
- package/dist/index.d.ts +352 -1
- package/dist/index.js +672 -3
- package/dist/index.mjs +661 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,11 +20,672 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AdaptiveThresholdManager: () => AdaptiveThresholdManager,
|
|
23
24
|
DEFAULT_RULES: () => DEFAULT_RULES,
|
|
25
|
+
FeatureSwitchesManager: () => FeatureSwitchesManager,
|
|
24
26
|
PolicyEngine: () => PolicyEngine,
|
|
25
|
-
|
|
27
|
+
PricingRulesEngine: () => PricingRulesEngine,
|
|
28
|
+
SOVR_FEATURE_SWITCHES: () => SOVR_FEATURE_SWITCHES,
|
|
29
|
+
compileFromJSON: () => compileFromJSON,
|
|
30
|
+
compileRuleSet: () => compileRuleSet,
|
|
31
|
+
default: () => index_default,
|
|
32
|
+
evaluateRules: () => evaluateRules,
|
|
33
|
+
registerFunction: () => registerFunction
|
|
26
34
|
});
|
|
27
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/expressionTree.ts
|
|
38
|
+
function compileFromJSON(rule) {
|
|
39
|
+
const ast = parseNode(rule);
|
|
40
|
+
const variables = extractVariables(ast);
|
|
41
|
+
return {
|
|
42
|
+
ast,
|
|
43
|
+
evaluate: (ctx) => evaluateNode(ast, ctx),
|
|
44
|
+
toString: () => nodeToString(ast),
|
|
45
|
+
variables
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parseNode(rule) {
|
|
49
|
+
if (rule === null || rule === void 0) {
|
|
50
|
+
return { type: "LITERAL", value: false };
|
|
51
|
+
}
|
|
52
|
+
if (rule.and && Array.isArray(rule.and)) {
|
|
53
|
+
return {
|
|
54
|
+
type: "AND",
|
|
55
|
+
children: rule.and.map((child) => parseNode(child))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (rule.or && Array.isArray(rule.or)) {
|
|
59
|
+
return {
|
|
60
|
+
type: "OR",
|
|
61
|
+
children: rule.or.map((child) => parseNode(child))
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (rule.not) {
|
|
65
|
+
return {
|
|
66
|
+
type: "NOT",
|
|
67
|
+
child: parseNode(rule.not)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (rule.fn) {
|
|
71
|
+
return {
|
|
72
|
+
type: "COMPARE",
|
|
73
|
+
operator: rule.op || "==",
|
|
74
|
+
left: {
|
|
75
|
+
type: "FUNCTION",
|
|
76
|
+
name: rule.fn,
|
|
77
|
+
args: (rule.args || []).map(
|
|
78
|
+
(a) => typeof a === "string" ? { type: "VARIABLE", path: a } : { type: "LITERAL", value: a }
|
|
79
|
+
)
|
|
80
|
+
},
|
|
81
|
+
right: { type: "LITERAL", value: rule.value }
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (rule.field && rule.op !== void 0) {
|
|
85
|
+
return {
|
|
86
|
+
type: "COMPARE",
|
|
87
|
+
operator: rule.op,
|
|
88
|
+
left: { type: "VARIABLE", path: rule.field },
|
|
89
|
+
right: { type: "LITERAL", value: rule.value }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (typeof rule === "boolean") {
|
|
93
|
+
return { type: "LITERAL", value: rule };
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`[ExpressionTree] Invalid rule node: ${JSON.stringify(rule)}`);
|
|
96
|
+
}
|
|
97
|
+
function evaluateNode(node, ctx) {
|
|
98
|
+
switch (node.type) {
|
|
99
|
+
case "AND":
|
|
100
|
+
return node.children.every((child) => evaluateNode(child, ctx));
|
|
101
|
+
case "OR":
|
|
102
|
+
return node.children.some((child) => evaluateNode(child, ctx));
|
|
103
|
+
case "NOT":
|
|
104
|
+
return !evaluateNode(node.child, ctx);
|
|
105
|
+
case "COMPARE":
|
|
106
|
+
return evaluateComparison(node, ctx);
|
|
107
|
+
case "LITERAL":
|
|
108
|
+
return Boolean(node.value);
|
|
109
|
+
case "VARIABLE":
|
|
110
|
+
return Boolean(resolveVariable(node.path, ctx));
|
|
111
|
+
case "FUNCTION":
|
|
112
|
+
return Boolean(evaluateFunction(node, ctx));
|
|
113
|
+
default:
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function evaluateComparison(node, ctx) {
|
|
118
|
+
const leftVal = resolveNode(node.left, ctx);
|
|
119
|
+
const rightVal = resolveNode(node.right, ctx);
|
|
120
|
+
switch (node.operator) {
|
|
121
|
+
case "==":
|
|
122
|
+
return leftVal == rightVal;
|
|
123
|
+
case "!=":
|
|
124
|
+
return leftVal != rightVal;
|
|
125
|
+
case ">":
|
|
126
|
+
return Number(leftVal) > Number(rightVal);
|
|
127
|
+
case "<":
|
|
128
|
+
return Number(leftVal) < Number(rightVal);
|
|
129
|
+
case ">=":
|
|
130
|
+
return Number(leftVal) >= Number(rightVal);
|
|
131
|
+
case "<=":
|
|
132
|
+
return Number(leftVal) <= Number(rightVal);
|
|
133
|
+
case "in":
|
|
134
|
+
return Array.isArray(rightVal) ? rightVal.includes(leftVal) : false;
|
|
135
|
+
case "not_in":
|
|
136
|
+
return Array.isArray(rightVal) ? !rightVal.includes(leftVal) : true;
|
|
137
|
+
case "contains":
|
|
138
|
+
return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.includes(rightVal) : false;
|
|
139
|
+
case "starts_with":
|
|
140
|
+
return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.startsWith(rightVal) : false;
|
|
141
|
+
case "ends_with":
|
|
142
|
+
return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.endsWith(rightVal) : false;
|
|
143
|
+
case "matches":
|
|
144
|
+
try {
|
|
145
|
+
return typeof leftVal === "string" && typeof rightVal === "string" ? new RegExp(rightVal).test(leftVal) : false;
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function resolveNode(node, ctx) {
|
|
154
|
+
switch (node.type) {
|
|
155
|
+
case "LITERAL":
|
|
156
|
+
return node.value;
|
|
157
|
+
case "VARIABLE":
|
|
158
|
+
return resolveVariable(node.path, ctx);
|
|
159
|
+
case "FUNCTION":
|
|
160
|
+
return evaluateFunction(node, ctx);
|
|
161
|
+
default:
|
|
162
|
+
return evaluateNode(node, ctx);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function resolveVariable(path, ctx) {
|
|
166
|
+
const parts = path.split(".");
|
|
167
|
+
let current = ctx;
|
|
168
|
+
for (const part of parts) {
|
|
169
|
+
if (current === null || current === void 0) return void 0;
|
|
170
|
+
current = current[part];
|
|
171
|
+
}
|
|
172
|
+
return current;
|
|
173
|
+
}
|
|
174
|
+
var BUILT_IN_FUNCTIONS = {
|
|
175
|
+
now_hour: () => (/* @__PURE__ */ new Date()).getHours(),
|
|
176
|
+
now_day: () => (/* @__PURE__ */ new Date()).getDay(),
|
|
177
|
+
// 0=Sunday
|
|
178
|
+
now_timestamp: () => Date.now(),
|
|
179
|
+
len: (args) => {
|
|
180
|
+
const val = args[0];
|
|
181
|
+
return typeof val === "string" || Array.isArray(val) ? val.length : 0;
|
|
182
|
+
},
|
|
183
|
+
lower: (args) => String(args[0] || "").toLowerCase(),
|
|
184
|
+
upper: (args) => String(args[0] || "").toUpperCase(),
|
|
185
|
+
abs: (args) => Math.abs(Number(args[0] || 0)),
|
|
186
|
+
min: (args) => Math.min(...args.map(Number)),
|
|
187
|
+
max: (args) => Math.max(...args.map(Number)),
|
|
188
|
+
coalesce: (args) => args.find((a) => a !== null && a !== void 0),
|
|
189
|
+
risk_level: (args) => {
|
|
190
|
+
const score = Number(args[0] || 0);
|
|
191
|
+
if (score >= 0.9) return "critical";
|
|
192
|
+
if (score >= 0.7) return "high";
|
|
193
|
+
if (score >= 0.4) return "medium";
|
|
194
|
+
return "low";
|
|
195
|
+
},
|
|
196
|
+
is_business_hours: () => {
|
|
197
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
198
|
+
const day = (/* @__PURE__ */ new Date()).getDay();
|
|
199
|
+
return day >= 1 && day <= 5 && hour >= 9 && hour < 18;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function registerFunction(name, fn) {
|
|
203
|
+
BUILT_IN_FUNCTIONS[name] = fn;
|
|
204
|
+
}
|
|
205
|
+
function evaluateFunction(node, ctx) {
|
|
206
|
+
const fn = BUILT_IN_FUNCTIONS[node.name];
|
|
207
|
+
if (!fn) {
|
|
208
|
+
console.warn(`[ExpressionTree] Unknown function: ${node.name}`);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const resolvedArgs = node.args.map((arg) => resolveNode(arg, ctx));
|
|
212
|
+
return fn(resolvedArgs, ctx);
|
|
213
|
+
}
|
|
214
|
+
function extractVariables(node) {
|
|
215
|
+
const vars = /* @__PURE__ */ new Set();
|
|
216
|
+
function walk(n) {
|
|
217
|
+
switch (n.type) {
|
|
218
|
+
case "VARIABLE":
|
|
219
|
+
vars.add(n.path);
|
|
220
|
+
break;
|
|
221
|
+
case "AND":
|
|
222
|
+
case "OR":
|
|
223
|
+
n.children.forEach(walk);
|
|
224
|
+
break;
|
|
225
|
+
case "NOT":
|
|
226
|
+
walk(n.child);
|
|
227
|
+
break;
|
|
228
|
+
case "COMPARE":
|
|
229
|
+
walk(n.left);
|
|
230
|
+
walk(n.right);
|
|
231
|
+
break;
|
|
232
|
+
case "FUNCTION":
|
|
233
|
+
n.args.forEach(walk);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
walk(node);
|
|
238
|
+
return Array.from(vars);
|
|
239
|
+
}
|
|
240
|
+
function nodeToString(node, depth = 0) {
|
|
241
|
+
const indent = " ".repeat(depth);
|
|
242
|
+
switch (node.type) {
|
|
243
|
+
case "AND":
|
|
244
|
+
return `${indent}AND(
|
|
245
|
+
${node.children.map((c) => nodeToString(c, depth + 1)).join(",\n")}
|
|
246
|
+
${indent})`;
|
|
247
|
+
case "OR":
|
|
248
|
+
return `${indent}OR(
|
|
249
|
+
${node.children.map((c) => nodeToString(c, depth + 1)).join(",\n")}
|
|
250
|
+
${indent})`;
|
|
251
|
+
case "NOT":
|
|
252
|
+
return `${indent}NOT(
|
|
253
|
+
${nodeToString(node.child, depth + 1)}
|
|
254
|
+
${indent})`;
|
|
255
|
+
case "COMPARE":
|
|
256
|
+
return `${indent}${nodeToString(node.left, 0)} ${node.operator} ${nodeToString(node.right, 0)}`;
|
|
257
|
+
case "LITERAL":
|
|
258
|
+
return `${indent}${JSON.stringify(node.value)}`;
|
|
259
|
+
case "VARIABLE":
|
|
260
|
+
return `${indent}\${${node.path}}`;
|
|
261
|
+
case "FUNCTION":
|
|
262
|
+
return `${indent}${node.name}(${node.args.map((a) => nodeToString(a, 0)).join(", ")})`;
|
|
263
|
+
default:
|
|
264
|
+
return `${indent}???`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
var ruleCache = /* @__PURE__ */ new Map();
|
|
268
|
+
function compileRuleSet(rules) {
|
|
269
|
+
ruleCache.clear();
|
|
270
|
+
for (const rule of rules) {
|
|
271
|
+
try {
|
|
272
|
+
ruleCache.set(rule.id, compileFromJSON(rule.condition));
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error(`[ExpressionTree] Failed to compile rule ${rule.id}: ${rule.name}`, err);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function evaluateRules(rules, ctx) {
|
|
279
|
+
const sorted = [...rules].sort((a, b) => b.priority - a.priority);
|
|
280
|
+
const results = [];
|
|
281
|
+
let matched = null;
|
|
282
|
+
for (const rule of sorted) {
|
|
283
|
+
const start = performance.now();
|
|
284
|
+
const compiled = ruleCache.get(rule.id) || compileFromJSON(rule.condition);
|
|
285
|
+
let isMatch = false;
|
|
286
|
+
try {
|
|
287
|
+
isMatch = compiled.evaluate(ctx);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.warn(`[ExpressionTree] Rule ${rule.id} evaluation error`, err);
|
|
290
|
+
}
|
|
291
|
+
const elapsed = performance.now() - start;
|
|
292
|
+
results.push({
|
|
293
|
+
ruleId: rule.id,
|
|
294
|
+
ruleName: rule.name,
|
|
295
|
+
matched: isMatch,
|
|
296
|
+
action: rule.action,
|
|
297
|
+
evaluationTimeMs: elapsed
|
|
298
|
+
});
|
|
299
|
+
if (isMatch && !matched) {
|
|
300
|
+
matched = rule;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { matched, results };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/adaptiveThreshold.ts
|
|
307
|
+
var DEFAULT_THRESHOLDS = [
|
|
308
|
+
{
|
|
309
|
+
name: "risk_score",
|
|
310
|
+
currentValue: 0.7,
|
|
311
|
+
minValue: 0.3,
|
|
312
|
+
maxValue: 0.95,
|
|
313
|
+
alpha: 0.1,
|
|
314
|
+
maxStepPercent: 5,
|
|
315
|
+
lastAdjustedAt: 0,
|
|
316
|
+
directionLock: null,
|
|
317
|
+
consecutiveAdjustments: 0
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "cost_limit_usd",
|
|
321
|
+
currentValue: 100,
|
|
322
|
+
minValue: 10,
|
|
323
|
+
maxValue: 1e4,
|
|
324
|
+
alpha: 0.05,
|
|
325
|
+
maxStepPercent: 10,
|
|
326
|
+
lastAdjustedAt: 0,
|
|
327
|
+
directionLock: null,
|
|
328
|
+
consecutiveAdjustments: 0
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "latency_p99_ms",
|
|
332
|
+
currentValue: 5e3,
|
|
333
|
+
minValue: 1e3,
|
|
334
|
+
maxValue: 3e4,
|
|
335
|
+
alpha: 0.15,
|
|
336
|
+
maxStepPercent: 15,
|
|
337
|
+
lastAdjustedAt: 0,
|
|
338
|
+
directionLock: null,
|
|
339
|
+
consecutiveAdjustments: 0
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "hallucination_score",
|
|
343
|
+
currentValue: 0.5,
|
|
344
|
+
minValue: 0.1,
|
|
345
|
+
maxValue: 0.9,
|
|
346
|
+
alpha: 0.08,
|
|
347
|
+
maxStepPercent: 5,
|
|
348
|
+
lastAdjustedAt: 0,
|
|
349
|
+
directionLock: null,
|
|
350
|
+
consecutiveAdjustments: 0
|
|
351
|
+
}
|
|
352
|
+
];
|
|
353
|
+
var AdaptiveThresholdManager = class {
|
|
354
|
+
thresholds = /* @__PURE__ */ new Map();
|
|
355
|
+
feedbackBuffer = [];
|
|
356
|
+
maxBufferSize;
|
|
357
|
+
cooldownMs;
|
|
358
|
+
onAdjustment;
|
|
359
|
+
constructor(options = {}) {
|
|
360
|
+
this.maxBufferSize = options.maxBufferSize ?? 1e4;
|
|
361
|
+
this.cooldownMs = options.adjustmentCooldownMs ?? 5 * 60 * 1e3;
|
|
362
|
+
this.onAdjustment = options.onAdjustment;
|
|
363
|
+
const configs = options.thresholds ?? DEFAULT_THRESHOLDS;
|
|
364
|
+
for (const config of configs) {
|
|
365
|
+
this.thresholds.set(config.name, { ...config });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ========== Core API ==========
|
|
369
|
+
/** Get a threshold by name */
|
|
370
|
+
getThreshold(name) {
|
|
371
|
+
const t = this.thresholds.get(name);
|
|
372
|
+
return t ? { ...t } : void 0;
|
|
373
|
+
}
|
|
374
|
+
/** Get all thresholds */
|
|
375
|
+
getAllThresholds() {
|
|
376
|
+
return Array.from(this.thresholds.values()).map((t) => ({ ...t }));
|
|
377
|
+
}
|
|
378
|
+
/** Record a decision feedback sample */
|
|
379
|
+
recordFeedback(feedback) {
|
|
380
|
+
this.feedbackBuffer.push(feedback);
|
|
381
|
+
if (this.feedbackBuffer.length > this.maxBufferSize) {
|
|
382
|
+
this.feedbackBuffer.splice(0, this.feedbackBuffer.length - this.maxBufferSize);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Adjust a single threshold based on feedback (EWMA algorithm).
|
|
387
|
+
* Returns null if no adjustment is needed (cooldown, insufficient samples, etc.).
|
|
388
|
+
*/
|
|
389
|
+
async adjustThreshold(thresholdName, windowHours = 24) {
|
|
390
|
+
const config = this.thresholds.get(thresholdName);
|
|
391
|
+
if (!config) return null;
|
|
392
|
+
if (Date.now() - config.lastAdjustedAt < this.cooldownMs) return null;
|
|
393
|
+
const windowStart = Date.now() - windowHours * 60 * 60 * 1e3;
|
|
394
|
+
const windowFeedback = this.feedbackBuffer.filter(
|
|
395
|
+
(f) => f.thresholdName === thresholdName && f.timestamp >= windowStart
|
|
396
|
+
);
|
|
397
|
+
if (windowFeedback.length < 10) return null;
|
|
398
|
+
const fp = windowFeedback.filter((f) => f.outcome === "false_positive").length;
|
|
399
|
+
const fn = windowFeedback.filter((f) => f.outcome === "false_negative").length;
|
|
400
|
+
const total = windowFeedback.length;
|
|
401
|
+
const fpRate = fp / total;
|
|
402
|
+
const fnRate = fn / total;
|
|
403
|
+
let direction = "none";
|
|
404
|
+
let reason = "";
|
|
405
|
+
if (fpRate > 0.15 && fpRate > fnRate * 2) {
|
|
406
|
+
direction = "up";
|
|
407
|
+
reason = `False positive rate ${(fpRate * 100).toFixed(1)}% exceeds 15% threshold`;
|
|
408
|
+
} else if (fnRate > 0.1 && fnRate > fpRate * 2) {
|
|
409
|
+
direction = "down";
|
|
410
|
+
reason = `False negative rate ${(fnRate * 100).toFixed(1)}% exceeds 10% threshold`;
|
|
411
|
+
}
|
|
412
|
+
if (direction === "none") return null;
|
|
413
|
+
if (config.directionLock && config.directionLock !== direction && config.consecutiveAdjustments < 3) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const errorRate = direction === "up" ? fpRate : fnRate;
|
|
417
|
+
const ewmaAdjustment = config.alpha * errorRate;
|
|
418
|
+
const maxStep = config.currentValue * (config.maxStepPercent / 100);
|
|
419
|
+
const actualStep = Math.min(ewmaAdjustment * config.currentValue, maxStep);
|
|
420
|
+
const previousValue = config.currentValue;
|
|
421
|
+
let newValue = direction === "up" ? config.currentValue + actualStep : config.currentValue - actualStep;
|
|
422
|
+
newValue = Math.max(config.minValue, Math.min(config.maxValue, newValue));
|
|
423
|
+
config.currentValue = newValue;
|
|
424
|
+
config.lastAdjustedAt = Date.now();
|
|
425
|
+
config.consecutiveAdjustments = config.directionLock === direction ? config.consecutiveAdjustments + 1 : 1;
|
|
426
|
+
config.directionLock = direction;
|
|
427
|
+
const result = {
|
|
428
|
+
thresholdName,
|
|
429
|
+
previousValue,
|
|
430
|
+
newValue,
|
|
431
|
+
reason,
|
|
432
|
+
metrics: { falsePositiveRate: fpRate, falseNegativeRate: fnRate, totalSamples: total, windowHours }
|
|
433
|
+
};
|
|
434
|
+
if (this.onAdjustment) {
|
|
435
|
+
try {
|
|
436
|
+
await this.onAdjustment(result);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.warn("[AdaptiveThreshold] Audit callback failed", err);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
/** Adjust all thresholds */
|
|
444
|
+
async adjustAll(windowHours = 24) {
|
|
445
|
+
const results = [];
|
|
446
|
+
for (const [name] of this.thresholds) {
|
|
447
|
+
const result = await this.adjustThreshold(name, windowHours);
|
|
448
|
+
if (result) results.push(result);
|
|
449
|
+
}
|
|
450
|
+
return results;
|
|
451
|
+
}
|
|
452
|
+
/** Manually override a threshold value */
|
|
453
|
+
setThresholdManual(name, value) {
|
|
454
|
+
const config = this.thresholds.get(name);
|
|
455
|
+
if (!config) return false;
|
|
456
|
+
if (value < config.minValue || value > config.maxValue) return false;
|
|
457
|
+
config.currentValue = value;
|
|
458
|
+
config.lastAdjustedAt = Date.now();
|
|
459
|
+
config.directionLock = null;
|
|
460
|
+
config.consecutiveAdjustments = 0;
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
/** Get feedback statistics for a threshold (or all) */
|
|
464
|
+
getFeedbackStats(thresholdName, windowHours = 24) {
|
|
465
|
+
const windowStart = Date.now() - windowHours * 60 * 60 * 1e3;
|
|
466
|
+
const filtered = this.feedbackBuffer.filter(
|
|
467
|
+
(f) => f.timestamp >= windowStart && (!thresholdName || f.thresholdName === thresholdName)
|
|
468
|
+
);
|
|
469
|
+
const tp = filtered.filter((f) => f.outcome === "true_positive").length;
|
|
470
|
+
const tn = filtered.filter((f) => f.outcome === "true_negative").length;
|
|
471
|
+
const fp = filtered.filter((f) => f.outcome === "false_positive").length;
|
|
472
|
+
const fn = filtered.filter((f) => f.outcome === "false_negative").length;
|
|
473
|
+
const total = filtered.length;
|
|
474
|
+
return {
|
|
475
|
+
total,
|
|
476
|
+
truePositive: tp,
|
|
477
|
+
trueNegative: tn,
|
|
478
|
+
falsePositive: fp,
|
|
479
|
+
falseNegative: fn,
|
|
480
|
+
accuracy: total > 0 ? (tp + tn) / total : 0,
|
|
481
|
+
precision: tp + fp > 0 ? tp / (tp + fp) : 0,
|
|
482
|
+
recall: tp + fn > 0 ? tp / (tp + fn) : 0
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Clear the feedback buffer */
|
|
486
|
+
clearFeedback() {
|
|
487
|
+
this.feedbackBuffer.length = 0;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/featureSwitches.ts
|
|
492
|
+
var SOVR_FEATURE_SWITCHES = {
|
|
493
|
+
USE_NEW_RISK_ENGINE: {
|
|
494
|
+
key: "USE_NEW_RISK_ENGINE",
|
|
495
|
+
description: "Enable new risk engine (expression tree + adaptive threshold), replacing legacy static rule matching",
|
|
496
|
+
defaultValue: false,
|
|
497
|
+
category: "risk",
|
|
498
|
+
impactLevel: "high"
|
|
499
|
+
},
|
|
500
|
+
ENFORCE_COST_CAP: {
|
|
501
|
+
key: "ENFORCE_COST_CAP",
|
|
502
|
+
description: "Enforce cost cap \u2014 hard-reject execution when agent cost exceeds budget (not just warn)",
|
|
503
|
+
defaultValue: false,
|
|
504
|
+
category: "cost",
|
|
505
|
+
impactLevel: "high"
|
|
506
|
+
},
|
|
507
|
+
ENABLE_PROMPT_GUARD: {
|
|
508
|
+
key: "ENABLE_PROMPT_GUARD",
|
|
509
|
+
description: "Enable prompt injection guard \u2014 scan all LLM call prompts for injection attacks",
|
|
510
|
+
defaultValue: true,
|
|
511
|
+
category: "security",
|
|
512
|
+
impactLevel: "medium"
|
|
513
|
+
},
|
|
514
|
+
DISABLE_EVIDENCE_PAUSE: {
|
|
515
|
+
key: "DISABLE_EVIDENCE_PAUSE",
|
|
516
|
+
description: "Disable evidence pause \u2014 keep audit chain writing continuously even under high system load",
|
|
517
|
+
defaultValue: false,
|
|
518
|
+
category: "evidence",
|
|
519
|
+
impactLevel: "medium"
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
var FeatureSwitchesManager = class {
|
|
523
|
+
cache = /* @__PURE__ */ new Map();
|
|
524
|
+
cacheTtlMs;
|
|
525
|
+
onLoad;
|
|
526
|
+
onSave;
|
|
527
|
+
constructor(options = {}) {
|
|
528
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 3e4;
|
|
529
|
+
this.onLoad = options.onLoad;
|
|
530
|
+
this.onSave = options.onSave;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Get a switch value (with cache + optional external load).
|
|
534
|
+
*/
|
|
535
|
+
async getValue(key, tenantId = "default") {
|
|
536
|
+
const cacheKey = `${tenantId}:${key}`;
|
|
537
|
+
const cached = this.cache.get(cacheKey);
|
|
538
|
+
if (cached && Date.now() - cached.updatedAt < this.cacheTtlMs) {
|
|
539
|
+
return cached.value;
|
|
540
|
+
}
|
|
541
|
+
if (this.onLoad) {
|
|
542
|
+
try {
|
|
543
|
+
const loaded = await this.onLoad(key, tenantId);
|
|
544
|
+
if (loaded !== null) {
|
|
545
|
+
this.cache.set(cacheKey, { value: loaded, updatedAt: Date.now() });
|
|
546
|
+
return loaded;
|
|
547
|
+
}
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.warn(`[FeatureSwitch] Failed to load ${key}, using default`, err);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const def = SOVR_FEATURE_SWITCHES[key];
|
|
553
|
+
const defaultVal = def ? def.defaultValue : false;
|
|
554
|
+
this.cache.set(cacheKey, { value: defaultVal, updatedAt: Date.now() });
|
|
555
|
+
return defaultVal;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Set a switch value (updates cache + optional external save).
|
|
559
|
+
*/
|
|
560
|
+
async setValue(key, value, tenantId = "default") {
|
|
561
|
+
const def = SOVR_FEATURE_SWITCHES[key];
|
|
562
|
+
if (!def) {
|
|
563
|
+
throw new Error(`Unknown feature switch: ${key}`);
|
|
564
|
+
}
|
|
565
|
+
const cacheKey = `${tenantId}:${key}`;
|
|
566
|
+
if (this.onSave) {
|
|
567
|
+
await this.onSave(key, value, tenantId);
|
|
568
|
+
}
|
|
569
|
+
this.cache.set(cacheKey, { value, updatedAt: Date.now() });
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Get all switch states.
|
|
573
|
+
*/
|
|
574
|
+
async getAll(tenantId = "default") {
|
|
575
|
+
const result = {};
|
|
576
|
+
for (const [key, def] of Object.entries(SOVR_FEATURE_SWITCHES)) {
|
|
577
|
+
const value = await this.getValue(key, tenantId);
|
|
578
|
+
result[key] = {
|
|
579
|
+
key,
|
|
580
|
+
value,
|
|
581
|
+
description: def.description,
|
|
582
|
+
category: def.category,
|
|
583
|
+
impactLevel: def.impactLevel,
|
|
584
|
+
isDefault: value === def.defaultValue
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
/** Clear the cache (for testing or forced refresh) */
|
|
590
|
+
clearCache() {
|
|
591
|
+
this.cache.clear();
|
|
592
|
+
}
|
|
593
|
+
// ========== Convenience Checkers ==========
|
|
594
|
+
async isNewRiskEngineEnabled(tenantId) {
|
|
595
|
+
return this.getValue("USE_NEW_RISK_ENGINE", tenantId);
|
|
596
|
+
}
|
|
597
|
+
async isCostCapEnforced(tenantId) {
|
|
598
|
+
return this.getValue("ENFORCE_COST_CAP", tenantId);
|
|
599
|
+
}
|
|
600
|
+
async isPromptGuardEnabled(tenantId) {
|
|
601
|
+
return this.getValue("ENABLE_PROMPT_GUARD", tenantId);
|
|
602
|
+
}
|
|
603
|
+
async isEvidencePauseDisabled(tenantId) {
|
|
604
|
+
return this.getValue("DISABLE_EVIDENCE_PAUSE", tenantId);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/pricingRules.ts
|
|
609
|
+
var PricingRulesEngine = class {
|
|
610
|
+
rules = [];
|
|
611
|
+
constructor(rules) {
|
|
612
|
+
if (rules) {
|
|
613
|
+
this.loadRules(rules);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/** Load rules (sorted by priority descending) */
|
|
617
|
+
loadRules(rules) {
|
|
618
|
+
this.rules = [...rules].filter((r) => r.isActive).sort((a, b) => b.priority - a.priority);
|
|
619
|
+
}
|
|
620
|
+
/** Add a single rule */
|
|
621
|
+
addRule(rule) {
|
|
622
|
+
this.rules.push(rule);
|
|
623
|
+
this.rules.sort((a, b) => b.priority - a.priority);
|
|
624
|
+
}
|
|
625
|
+
/** Get all loaded rules */
|
|
626
|
+
getRules() {
|
|
627
|
+
return this.rules;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Evaluate pricing for an action.
|
|
631
|
+
* Returns the computed cost and breakdown.
|
|
632
|
+
*/
|
|
633
|
+
evaluate(request) {
|
|
634
|
+
const currentHour = request.currentHour ?? (/* @__PURE__ */ new Date()).getHours();
|
|
635
|
+
for (const rule of this.rules) {
|
|
636
|
+
if (rule.tier && rule.tier !== request.tier) continue;
|
|
637
|
+
if (!this.globMatch(rule.actionPattern, request.action)) continue;
|
|
638
|
+
let timeWindowActive = false;
|
|
639
|
+
if (rule.timeWindowStart >= 0 && rule.timeWindowEnd >= 0) {
|
|
640
|
+
if (rule.timeWindowStart <= rule.timeWindowEnd) {
|
|
641
|
+
timeWindowActive = currentHour >= rule.timeWindowStart && currentHour < rule.timeWindowEnd;
|
|
642
|
+
} else {
|
|
643
|
+
timeWindowActive = currentHour >= rule.timeWindowStart || currentHour < rule.timeWindowEnd;
|
|
644
|
+
}
|
|
645
|
+
if (!timeWindowActive) continue;
|
|
646
|
+
} else {
|
|
647
|
+
timeWindowActive = false;
|
|
648
|
+
}
|
|
649
|
+
let cost = rule.baseCostUsd * rule.multiplier;
|
|
650
|
+
let discountPercent = 0;
|
|
651
|
+
if (rule.volumeDiscountThreshold > 0 && request.currentVolume >= rule.volumeDiscountThreshold) {
|
|
652
|
+
discountPercent = rule.volumeDiscountPercent;
|
|
653
|
+
cost = cost * (1 - discountPercent / 100);
|
|
654
|
+
}
|
|
655
|
+
const parts = [`base=$${rule.baseCostUsd.toFixed(4)}`];
|
|
656
|
+
if (rule.multiplier !== 1) parts.push(`\xD7${rule.multiplier}`);
|
|
657
|
+
if (discountPercent > 0) parts.push(`-${discountPercent}% vol`);
|
|
658
|
+
if (timeWindowActive) parts.push(`[time:${rule.timeWindowStart}-${rule.timeWindowEnd}]`);
|
|
659
|
+
return {
|
|
660
|
+
costUsd: Math.max(0, cost),
|
|
661
|
+
baseCostUsd: rule.baseCostUsd,
|
|
662
|
+
multiplier: rule.multiplier,
|
|
663
|
+
volumeDiscountPercent: discountPercent,
|
|
664
|
+
timeWindowActive,
|
|
665
|
+
matchedRuleId: rule.id,
|
|
666
|
+
breakdown: parts.join(" ")
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
costUsd: 0,
|
|
671
|
+
baseCostUsd: 0,
|
|
672
|
+
multiplier: 1,
|
|
673
|
+
volumeDiscountPercent: 0,
|
|
674
|
+
timeWindowActive: false,
|
|
675
|
+
matchedRuleId: null,
|
|
676
|
+
breakdown: "no matching pricing rule"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
globMatch(pattern, value) {
|
|
680
|
+
if (pattern === "*") return true;
|
|
681
|
+
const regex = new RegExp(
|
|
682
|
+
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
|
|
683
|
+
);
|
|
684
|
+
return regex.test(value);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/index.ts
|
|
28
689
|
var DEFAULT_RULES = [
|
|
29
690
|
// --- HTTP Proxy: Dangerous outbound calls ---
|
|
30
691
|
{
|
|
@@ -258,7 +919,7 @@ var RISK_SCORES = {
|
|
|
258
919
|
high: 70,
|
|
259
920
|
critical: 95
|
|
260
921
|
};
|
|
261
|
-
var ENGINE_VERSION = "3.
|
|
922
|
+
var ENGINE_VERSION = "3.3.0";
|
|
262
923
|
var ENGINE_VERSION_CHECK_URL = "https://api.sovr.inc/api/sovr/v1/version/check";
|
|
263
924
|
var ENGINE_TIER_LIMITS = {
|
|
264
925
|
free: { evaluationsPerMonth: 50, irreversibleAllowsPerMonth: 0 },
|
|
@@ -513,6 +1174,14 @@ var PolicyEngine = class {
|
|
|
513
1174
|
var index_default = PolicyEngine;
|
|
514
1175
|
// Annotate the CommonJS export names for ESM import in node:
|
|
515
1176
|
0 && (module.exports = {
|
|
1177
|
+
AdaptiveThresholdManager,
|
|
516
1178
|
DEFAULT_RULES,
|
|
517
|
-
|
|
1179
|
+
FeatureSwitchesManager,
|
|
1180
|
+
PolicyEngine,
|
|
1181
|
+
PricingRulesEngine,
|
|
1182
|
+
SOVR_FEATURE_SWITCHES,
|
|
1183
|
+
compileFromJSON,
|
|
1184
|
+
compileRuleSet,
|
|
1185
|
+
evaluateRules,
|
|
1186
|
+
registerFunction
|
|
518
1187
|
});
|