veto-sdk 1.17.0 → 2.2.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/README.md +69 -203
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/types.d.ts +17 -1
- package/dist/browser/types.d.ts.map +1 -1
- package/dist/browser/veto.d.ts +10 -0
- package/dist/browser/veto.d.ts.map +1 -1
- package/dist/browser/veto.js +69 -5
- package/dist/browser/veto.js.map +1 -1
- package/dist/cli/bin.js +0 -0
- package/dist/cli/compile.d.ts +22 -1
- package/dist/cli/compile.d.ts.map +1 -1
- package/dist/cli/compile.js +100 -21
- package/dist/cli/compile.js.map +1 -1
- package/dist/cli/diff.d.ts +2 -25
- package/dist/cli/diff.d.ts.map +1 -1
- package/dist/cli/diff.js +5 -327
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/headless.js +1 -1
- package/dist/cli/headless.js.map +1 -1
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl-generate.d.ts.map +1 -1
- package/dist/cli/repl-generate.js +346 -17
- package/dist/cli/repl-generate.js.map +1 -1
- package/dist/cli/replay-engine.d.ts +77 -0
- package/dist/cli/replay-engine.d.ts.map +1 -0
- package/dist/cli/replay-engine.js +379 -0
- package/dist/cli/replay-engine.js.map +1 -0
- package/dist/cli/replay.d.ts +57 -0
- package/dist/cli/replay.d.ts.map +1 -0
- package/dist/cli/replay.js +202 -0
- package/dist/cli/replay.js.map +1 -0
- package/dist/cli/runner.d.ts.map +1 -1
- package/dist/cli/runner.js +20 -0
- package/dist/cli/runner.js.map +1 -1
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +1 -1
- package/dist/cloud/client.d.ts.map +1 -1
- package/dist/cloud/client.js +6 -1
- package/dist/cloud/client.js.map +1 -1
- package/dist/cloud/types.d.ts +33 -2
- package/dist/cloud/types.d.ts.map +1 -1
- package/dist/core/events.d.ts +11 -1
- package/dist/core/events.d.ts.map +1 -1
- package/dist/core/events.js +4 -0
- package/dist/core/events.js.map +1 -1
- package/dist/core/interceptor.d.ts +20 -1
- package/dist/core/interceptor.d.ts.map +1 -1
- package/dist/core/interceptor.js +47 -3
- package/dist/core/interceptor.js.map +1 -1
- package/dist/core/output-validator.d.ts +11 -0
- package/dist/core/output-validator.d.ts.map +1 -1
- package/dist/core/output-validator.js +88 -15
- package/dist/core/output-validator.js.map +1 -1
- package/dist/core/protect.d.ts +3 -1
- package/dist/core/protect.d.ts.map +1 -1
- package/dist/core/protect.js +14 -4
- package/dist/core/protect.js.map +1 -1
- package/dist/core/veto.d.ts +54 -1
- package/dist/core/veto.d.ts.map +1 -1
- package/dist/core/veto.js +471 -91
- package/dist/core/veto.js.map +1 -1
- package/dist/deterministic/types.d.ts +103 -0
- package/dist/deterministic/types.d.ts.map +1 -1
- package/dist/economic/budget-engine.d.ts +29 -0
- package/dist/economic/budget-engine.d.ts.map +1 -0
- package/dist/economic/budget-engine.js +146 -0
- package/dist/economic/budget-engine.js.map +1 -0
- package/dist/economic/connectors/ap2.d.ts +51 -0
- package/dist/economic/connectors/ap2.d.ts.map +1 -0
- package/dist/economic/connectors/ap2.js +133 -0
- package/dist/economic/connectors/ap2.js.map +1 -0
- package/dist/economic/connectors/index.d.ts +8 -0
- package/dist/economic/connectors/index.d.ts.map +1 -0
- package/dist/economic/connectors/index.js +8 -0
- package/dist/economic/connectors/index.js.map +1 -0
- package/dist/economic/connectors/mpp.d.ts +41 -0
- package/dist/economic/connectors/mpp.d.ts.map +1 -0
- package/dist/economic/connectors/mpp.js +97 -0
- package/dist/economic/connectors/mpp.js.map +1 -0
- package/dist/economic/connectors/x402.d.ts +20 -0
- package/dist/economic/connectors/x402.d.ts.map +1 -0
- package/dist/economic/connectors/x402.js +142 -0
- package/dist/economic/connectors/x402.js.map +1 -0
- package/dist/economic/evaluator.d.ts +77 -0
- package/dist/economic/evaluator.d.ts.map +1 -0
- package/dist/economic/evaluator.js +231 -0
- package/dist/economic/evaluator.js.map +1 -0
- package/dist/economic/index.d.ts +13 -0
- package/dist/economic/index.d.ts.map +1 -0
- package/dist/economic/index.js +15 -0
- package/dist/economic/index.js.map +1 -0
- package/dist/economic/types.d.ts +188 -0
- package/dist/economic/types.d.ts.map +1 -0
- package/dist/economic/types.js +10 -0
- package/dist/economic/types.js.map +1 -0
- package/dist/extractors/content.d.ts +42 -0
- package/dist/extractors/content.d.ts.map +1 -0
- package/dist/extractors/content.js +154 -0
- package/dist/extractors/content.js.map +1 -0
- package/dist/extractors/index.d.ts +7 -0
- package/dist/extractors/index.d.ts.map +1 -0
- package/dist/extractors/index.js +7 -0
- package/dist/extractors/index.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/policy/generator.d.ts +110 -0
- package/dist/policy/generator.d.ts.map +1 -0
- package/dist/policy/generator.js +463 -0
- package/dist/policy/generator.js.map +1 -0
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +7 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/providers/adapters.d.ts +27 -0
- package/dist/providers/adapters.d.ts.map +1 -1
- package/dist/providers/adapters.js +58 -0
- package/dist/providers/adapters.js.map +1 -1
- package/dist/rules/condition-evaluator.d.ts +2 -1
- package/dist/rules/condition-evaluator.d.ts.map +1 -1
- package/dist/rules/condition-evaluator.js +97 -7
- package/dist/rules/condition-evaluator.js.map +1 -1
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +1 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/local-evaluator.d.ts +69 -0
- package/dist/rules/local-evaluator.d.ts.map +1 -0
- package/dist/rules/local-evaluator.js +217 -0
- package/dist/rules/local-evaluator.js.map +1 -0
- package/dist/rules/policy-ir-schema.d.ts +132 -1
- package/dist/rules/policy-ir-schema.d.ts.map +1 -1
- package/dist/rules/policy-ir-schema.js +114 -0
- package/dist/rules/policy-ir-schema.js.map +1 -1
- package/dist/rules/policy-packs.d.ts.map +1 -1
- package/dist/rules/policy-packs.js +1 -0
- package/dist/rules/policy-packs.js.map +1 -1
- package/dist/rules/types.d.ts +3 -1
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/types/config.d.ts +2 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/utils/logger.d.ts +38 -2
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +231 -26
- package/dist/utils/logger.js.map +1 -1
- package/package.json +9 -1
- package/packs/economic-agent.yaml +62 -0
package/dist/core/veto.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module core/veto
|
|
8
8
|
*/
|
|
9
|
-
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
import { createLogger, } from '../utils/logger.js';
|
|
10
10
|
import { generateId, generateToolCallId } from '../utils/id.js';
|
|
11
11
|
import { ValidationEngine } from './validator.js';
|
|
12
12
|
import { HistoryTracker } from './history.js';
|
|
13
13
|
import { BudgetTracker, BudgetExceededError } from './budget.js';
|
|
14
14
|
import { Interceptor, ToolCallDeniedError } from './interceptor.js';
|
|
15
|
+
import { EconomicEvaluator } from '../economic/evaluator.js';
|
|
16
|
+
import { LocalBudgetEngine } from '../economic/budget-engine.js';
|
|
15
17
|
import { compile, evaluate } from '../compiler/index.js';
|
|
16
18
|
import { evaluateConditionCollections } from '../rules/condition-evaluator.js';
|
|
17
19
|
import { VetoCloudClient, ApprovalTimeoutError } from '../cloud/client.js';
|
|
@@ -19,6 +21,7 @@ import { PolicyCache } from '../cloud/policy-cache.js';
|
|
|
19
21
|
import { validateDeterministic } from '../deterministic/validator.js';
|
|
20
22
|
import { OutputValidator } from './output-validator.js';
|
|
21
23
|
import { EventWebhookEmitter, resolveEventWebhookConfig, } from './events.js';
|
|
24
|
+
const RESERVED_LOCAL_CONTEXT_KEYS = new Set(['market', 'budget', 'portfolio']);
|
|
22
25
|
class LocalApprovalTimeoutError extends Error {
|
|
23
26
|
constructor(timeoutMs) {
|
|
24
27
|
super(`Approval callback timed out after ${timeoutMs}ms`);
|
|
@@ -46,11 +49,13 @@ class LocalApprovalTimeoutError extends Error {
|
|
|
46
49
|
* ```
|
|
47
50
|
*/
|
|
48
51
|
export class Veto {
|
|
49
|
-
static DEFAULT_CLOUD_BASE_URL = 'https://api.
|
|
52
|
+
static DEFAULT_CLOUD_BASE_URL = 'https://api.veto.so';
|
|
50
53
|
logger;
|
|
51
54
|
validationEngine;
|
|
52
55
|
historyTracker;
|
|
53
56
|
budgetTracker;
|
|
57
|
+
economicEvaluator;
|
|
58
|
+
economicBudgetEngine;
|
|
54
59
|
interceptor;
|
|
55
60
|
outputValidator;
|
|
56
61
|
eventWebhookEmitter;
|
|
@@ -81,28 +86,44 @@ export class Veto {
|
|
|
81
86
|
approvalPollOptions;
|
|
82
87
|
localApprovalConfig;
|
|
83
88
|
onApprovalRequired;
|
|
89
|
+
onDecisionMade;
|
|
84
90
|
// Approval preference cache: tool name -> 'approve_all' | 'deny_all'
|
|
85
91
|
approvalPreferences = new Map();
|
|
86
92
|
// Client-side deterministic validation cache
|
|
87
93
|
policyCache = null;
|
|
94
|
+
remoteOutputRulesByTool = new Map();
|
|
88
95
|
// Loaded rules
|
|
89
96
|
rulesState;
|
|
97
|
+
localSourceFiles = [];
|
|
98
|
+
localRulesDir;
|
|
99
|
+
localRulesRecursive;
|
|
90
100
|
browserMode;
|
|
91
101
|
compiledExpressionCache = new Map();
|
|
92
102
|
refreshIntervalId = null;
|
|
93
|
-
constructor(options, config, rules, logger, browserMode = false) {
|
|
103
|
+
constructor(options, config, rules, logger, browserMode = false, localRulesSource) {
|
|
94
104
|
this.logger = logger;
|
|
95
105
|
this.configDir = options.configDir ?? './veto';
|
|
96
106
|
this.rulesState = rules;
|
|
107
|
+
this.localSourceFiles = localRulesSource?.sourceFiles ?? [];
|
|
108
|
+
this.localRulesDir = localRulesSource?.dir;
|
|
109
|
+
this.localRulesRecursive = localRulesSource?.recursive ?? true;
|
|
97
110
|
this.browserMode = browserMode;
|
|
98
111
|
const envMode = this.browserMode
|
|
99
112
|
? undefined
|
|
100
113
|
: Veto.parseEnvMode(process.env.VETO_MODE);
|
|
101
114
|
this.mode = options.mode ?? config.mode ?? envMode ?? 'strict';
|
|
115
|
+
const envLogSetting = this.browserMode
|
|
116
|
+
? undefined
|
|
117
|
+
: Veto.parseEnvLogSetting(process.env.VETO_LOG);
|
|
102
118
|
const envLogLevel = this.browserMode
|
|
103
119
|
? undefined
|
|
104
120
|
: Veto.parseEnvLogLevel(process.env.VETO_LOG_LEVEL);
|
|
105
|
-
|
|
121
|
+
const explicitLogLevel = Veto.resolveExplicitLogLevel(options);
|
|
122
|
+
this.resolvedLogLevel = explicitLogLevel
|
|
123
|
+
?? envLogSetting?.level
|
|
124
|
+
?? envLogLevel
|
|
125
|
+
?? config.logging?.level
|
|
126
|
+
?? 'info';
|
|
106
127
|
const explicitValidationMode = config.validation?.mode;
|
|
107
128
|
const cloudApiKey = options.apiKey
|
|
108
129
|
?? config.cloud?.apiKey
|
|
@@ -230,8 +251,9 @@ export class Veto {
|
|
|
230
251
|
reasonField: config.approval?.responseSchema?.reasonField ?? 'reason',
|
|
231
252
|
},
|
|
232
253
|
};
|
|
233
|
-
//
|
|
254
|
+
// Hooks
|
|
234
255
|
this.onApprovalRequired = options.onApprovalRequired;
|
|
256
|
+
this.onDecisionMade = options.onDecisionMade;
|
|
235
257
|
this.eventWebhookEmitter = new EventWebhookEmitter(resolveEventWebhookConfig(config.events?.webhook, this.logger), this.logger);
|
|
236
258
|
// Resolve tracking options
|
|
237
259
|
const envSessionId = this.browserMode ? undefined : process.env.VETO_SESSION_ID;
|
|
@@ -319,6 +341,28 @@ export class Veto {
|
|
|
319
341
|
else {
|
|
320
342
|
this.budgetTracker = null;
|
|
321
343
|
}
|
|
344
|
+
// Initialize economic evaluator (if configured)
|
|
345
|
+
if (config.economic?.budgets?.length) {
|
|
346
|
+
const localBudgetEngine = new LocalBudgetEngine({
|
|
347
|
+
budgets: config.economic.budgets,
|
|
348
|
+
logger: this.logger,
|
|
349
|
+
});
|
|
350
|
+
this.economicBudgetEngine = localBudgetEngine;
|
|
351
|
+
this.economicEvaluator = new EconomicEvaluator({
|
|
352
|
+
policy: config.economic,
|
|
353
|
+
budgetEngine: localBudgetEngine,
|
|
354
|
+
logger: this.logger,
|
|
355
|
+
});
|
|
356
|
+
this.logger.info('Economic authorization enabled', {
|
|
357
|
+
budgets: config.economic.budgets.length,
|
|
358
|
+
protocols: ['x402', 'mpp', 'ap2'],
|
|
359
|
+
payer_required: config.economic.payer?.required ?? false,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
this.economicEvaluator = null;
|
|
364
|
+
this.economicBudgetEngine = null;
|
|
365
|
+
}
|
|
322
366
|
// Initialize interceptor
|
|
323
367
|
this.outputValidator = new OutputValidator({
|
|
324
368
|
logger: this.logger,
|
|
@@ -367,17 +411,28 @@ export class Veto {
|
|
|
367
411
|
const parseYaml = await Veto.loadYamlParser();
|
|
368
412
|
const configDir = pathModule.resolve(options.configDir ?? './veto');
|
|
369
413
|
// Determine log level
|
|
370
|
-
const
|
|
371
|
-
|
|
414
|
+
const envLogSetting = Veto.parseEnvLogSetting(process.env.VETO_LOG);
|
|
415
|
+
const envLogLevel = Veto.parseEnvLogLevel(process.env.VETO_LOG_LEVEL);
|
|
416
|
+
const explicitLogLevel = Veto.resolveExplicitLogLevel(options);
|
|
417
|
+
let logLevel = explicitLogLevel ?? envLogSetting?.level ?? envLogLevel ?? 'info';
|
|
418
|
+
let streamMode = options.streamMode ?? envLogSetting?.streamMode ?? 'compact';
|
|
372
419
|
// Load config file
|
|
373
420
|
const configPath = pathModule.join(configDir, 'veto.config.yaml');
|
|
374
421
|
let config = {};
|
|
375
422
|
if (fsModule.existsSync(configPath)) {
|
|
376
423
|
const configContent = fsModule.readFileSync(configPath, 'utf-8');
|
|
377
424
|
config = parseYaml(configContent);
|
|
378
|
-
logLevel =
|
|
379
|
-
|
|
380
|
-
|
|
425
|
+
logLevel = explicitLogLevel
|
|
426
|
+
?? envLogSetting?.level
|
|
427
|
+
?? envLogLevel
|
|
428
|
+
?? config.logging?.level
|
|
429
|
+
?? 'info';
|
|
430
|
+
streamMode = options.streamMode
|
|
431
|
+
?? envLogSetting?.streamMode
|
|
432
|
+
?? config.logging?.streamMode
|
|
433
|
+
?? 'compact';
|
|
434
|
+
}
|
|
435
|
+
const logger = createLogger(logLevel, streamMode);
|
|
381
436
|
if (!fsModule.existsSync(configPath)) {
|
|
382
437
|
logger.warn('Veto config not found. Run "npx veto init" to initialize.', {
|
|
383
438
|
expected: configPath,
|
|
@@ -387,11 +442,20 @@ export class Veto {
|
|
|
387
442
|
const rulesDir = pathModule.resolve(configDir, config.rules?.directory ?? './rules');
|
|
388
443
|
const recursive = config.rules?.recursive ?? true;
|
|
389
444
|
const rules = await Veto.loadRules(rulesDir, recursive, logger, fsModule, pathModule, parseYaml);
|
|
390
|
-
return new Veto(options, config, rules, logger
|
|
445
|
+
return new Veto({ ...options, configDir }, config, rules.state, logger, false, {
|
|
446
|
+
dir: rulesDir,
|
|
447
|
+
recursive,
|
|
448
|
+
sourceFiles: rules.sourceFiles,
|
|
449
|
+
});
|
|
391
450
|
}
|
|
392
451
|
static fromRules(options) {
|
|
393
|
-
const
|
|
394
|
-
const
|
|
452
|
+
const envLogSetting = Veto.parseEnvLogSetting(typeof process === 'undefined' ? undefined : process.env.VETO_LOG);
|
|
453
|
+
const logLevel = Veto.resolveExplicitLogLevel(options)
|
|
454
|
+
?? envLogSetting?.level
|
|
455
|
+
?? Veto.parseEnvLogLevel(typeof process === 'undefined' ? undefined : process.env.VETO_LOG_LEVEL)
|
|
456
|
+
?? 'warn';
|
|
457
|
+
const streamMode = options.streamMode ?? envLogSetting?.streamMode ?? 'compact';
|
|
458
|
+
const logger = createLogger(logLevel, streamMode);
|
|
395
459
|
const envMode = Veto.parseEnvMode(typeof process === 'undefined' ? undefined : process.env.VETO_MODE);
|
|
396
460
|
const resolvedMode = options.mode ?? envMode;
|
|
397
461
|
const cloudClient = options.cloudClient ?? (options.apiKey
|
|
@@ -412,7 +476,7 @@ export class Veto {
|
|
|
412
476
|
baseUrl: options.endpoint,
|
|
413
477
|
}
|
|
414
478
|
: undefined,
|
|
415
|
-
logging: { level: logLevel },
|
|
479
|
+
logging: { level: logLevel, streamMode },
|
|
416
480
|
budget: options.budget,
|
|
417
481
|
costs: options.costs,
|
|
418
482
|
approval: options.approval,
|
|
@@ -422,6 +486,7 @@ export class Veto {
|
|
|
422
486
|
const vetoOptions = {
|
|
423
487
|
mode: resolvedMode,
|
|
424
488
|
logLevel,
|
|
489
|
+
streamMode,
|
|
425
490
|
sessionId: options.sessionId,
|
|
426
491
|
agentId: options.agentId,
|
|
427
492
|
userId: options.userId,
|
|
@@ -431,11 +496,17 @@ export class Veto {
|
|
|
431
496
|
endpoint: undefined,
|
|
432
497
|
cloudClient,
|
|
433
498
|
onApprovalRequired: options.onApprovalRequired,
|
|
499
|
+
onDecisionMade: options.onDecisionMade,
|
|
434
500
|
};
|
|
435
501
|
return new Veto(vetoOptions, config, rules, logger, true);
|
|
436
502
|
}
|
|
437
503
|
static async fromCloud(options) {
|
|
438
|
-
const
|
|
504
|
+
const envLogSetting = Veto.parseEnvLogSetting(typeof process === 'undefined' ? undefined : process.env.VETO_LOG);
|
|
505
|
+
const logLevel = envLogSetting?.level
|
|
506
|
+
?? Veto.parseEnvLogLevel(typeof process === 'undefined' ? undefined : process.env.VETO_LOG_LEVEL)
|
|
507
|
+
?? 'warn';
|
|
508
|
+
const streamMode = envLogSetting?.streamMode ?? 'compact';
|
|
509
|
+
const logger = createLogger(logLevel, streamMode);
|
|
439
510
|
const cloudClient = new VetoCloudClient({
|
|
440
511
|
config: {
|
|
441
512
|
apiKey: options.apiKey,
|
|
@@ -460,9 +531,29 @@ export class Veto {
|
|
|
460
531
|
}
|
|
461
532
|
return undefined;
|
|
462
533
|
}
|
|
534
|
+
static parseEnvLogSetting(value) {
|
|
535
|
+
if (!value) {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
const normalized = value.trim().toLowerCase();
|
|
539
|
+
if (normalized === 'stream') {
|
|
540
|
+
return { level: 'stream', streamMode: 'compact' };
|
|
541
|
+
}
|
|
542
|
+
if (normalized === 'stream:verbose') {
|
|
543
|
+
return { level: 'stream', streamMode: 'verbose' };
|
|
544
|
+
}
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
static resolveExplicitLogLevel(options) {
|
|
548
|
+
if (options.stream) {
|
|
549
|
+
return 'stream';
|
|
550
|
+
}
|
|
551
|
+
return options.logLevel;
|
|
552
|
+
}
|
|
463
553
|
static parseEnvLogLevel(level) {
|
|
464
554
|
if (level === 'debug'
|
|
465
555
|
|| level === 'info'
|
|
556
|
+
|| level === 'stream'
|
|
466
557
|
|| level === 'warn'
|
|
467
558
|
|| level === 'error'
|
|
468
559
|
|| level === 'silent') {
|
|
@@ -546,7 +637,10 @@ export class Veto {
|
|
|
546
637
|
const state = Veto.createEmptyRulesState();
|
|
547
638
|
if (!fsModule.existsSync(rulesDir)) {
|
|
548
639
|
logger.debug('Rules directory not found', { path: rulesDir });
|
|
549
|
-
return
|
|
640
|
+
return {
|
|
641
|
+
state,
|
|
642
|
+
sourceFiles: [],
|
|
643
|
+
};
|
|
550
644
|
}
|
|
551
645
|
const yamlFiles = Veto.findYamlFiles(rulesDir, recursive, fsModule, pathModule);
|
|
552
646
|
logger.debug('Found rule files', { count: yamlFiles.length });
|
|
@@ -609,7 +703,10 @@ export class Veto {
|
|
|
609
703
|
outputGlobal: state.globalOutputRules.length,
|
|
610
704
|
outputToolSpecific: state.outputRulesByTool.size,
|
|
611
705
|
});
|
|
612
|
-
return
|
|
706
|
+
return {
|
|
707
|
+
state,
|
|
708
|
+
sourceFiles: yamlFiles,
|
|
709
|
+
};
|
|
613
710
|
}
|
|
614
711
|
/**
|
|
615
712
|
* Find YAML files in a directory.
|
|
@@ -646,7 +743,16 @@ export class Veto {
|
|
|
646
743
|
}
|
|
647
744
|
getOutputRulesForTool(toolName) {
|
|
648
745
|
const toolSpecific = this.rulesState.outputRulesByTool.get(toolName) ?? [];
|
|
649
|
-
|
|
746
|
+
const remote = this.remoteOutputRulesByTool.get(toolName) ?? [];
|
|
747
|
+
return [...this.rulesState.globalOutputRules, ...toolSpecific, ...remote]
|
|
748
|
+
.filter((rule) => rule.enabled !== false);
|
|
749
|
+
}
|
|
750
|
+
cacheRemoteOutputRules(toolName, outputRules) {
|
|
751
|
+
if (!outputRules || outputRules.length === 0) {
|
|
752
|
+
this.remoteOutputRulesByTool.delete(toolName);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
this.remoteOutputRulesByTool.set(toolName, outputRules);
|
|
650
756
|
}
|
|
651
757
|
isGuardEvaluation(context) {
|
|
652
758
|
return context.source === 'guard';
|
|
@@ -917,100 +1023,133 @@ export class Veto {
|
|
|
917
1023
|
}
|
|
918
1024
|
const localContext = this.buildLocalEvaluationContext(context);
|
|
919
1025
|
let firstAllowRule = null;
|
|
1026
|
+
let firstApprovalRule = null;
|
|
1027
|
+
let firstBlockRule = null;
|
|
1028
|
+
let firstNonBlockingRule = null;
|
|
920
1029
|
for (const rule of rules) {
|
|
921
1030
|
if (!this.matchesLocalRule(rule, context, localContext)) {
|
|
922
1031
|
continue;
|
|
923
1032
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (this.shouldApplyLogModeOverride(context)) {
|
|
928
|
-
if (this.mode === 'shadow') {
|
|
929
|
-
return this.applyShadowModeOverride(context, 'require_approval', reason, metadata, rule.id);
|
|
930
|
-
}
|
|
931
|
-
this.logger.warn('Tool call would require approval locally (log mode)', {
|
|
932
|
-
tool: context.toolName,
|
|
933
|
-
ruleId: rule.id,
|
|
934
|
-
reason,
|
|
935
|
-
});
|
|
936
|
-
return {
|
|
937
|
-
decision: 'allow',
|
|
938
|
-
reason: `[LOG MODE] Would require approval: ${reason}`,
|
|
939
|
-
metadata: {
|
|
940
|
-
blocked_in_strict_mode: true,
|
|
941
|
-
...metadata,
|
|
942
|
-
},
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
if (this.isGuardEvaluation(context)) {
|
|
946
|
-
return {
|
|
947
|
-
decision: 'require_approval',
|
|
948
|
-
reason,
|
|
949
|
-
metadata,
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
return this.handleLocalApprovalFlow(context, rule, reason);
|
|
1033
|
+
if (rule.action === 'block' && !firstBlockRule) {
|
|
1034
|
+
firstBlockRule = rule;
|
|
1035
|
+
continue;
|
|
953
1036
|
}
|
|
954
|
-
if (rule.action === '
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1037
|
+
if (rule.action === 'require_approval' && !firstApprovalRule) {
|
|
1038
|
+
firstApprovalRule = rule;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (rule.action === 'allow' && !firstAllowRule) {
|
|
1042
|
+
firstAllowRule = rule;
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if ((rule.action === 'warn' || rule.action === 'log') && !firstNonBlockingRule) {
|
|
1046
|
+
firstNonBlockingRule = rule;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const decisiveRule = firstBlockRule ?? firstApprovalRule ?? firstAllowRule;
|
|
1050
|
+
if (!decisiveRule) {
|
|
1051
|
+
if (firstNonBlockingRule) {
|
|
1052
|
+
this.logger.warn('Local rule matched with non-blocking action', {
|
|
1053
|
+
tool: context.toolName,
|
|
1054
|
+
action: firstNonBlockingRule.action,
|
|
1055
|
+
ruleId: firstNonBlockingRule.id,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return { decision: 'allow' };
|
|
1059
|
+
}
|
|
1060
|
+
const reason = decisiveRule.description ?? `Matched rule: ${decisiveRule.name}`;
|
|
1061
|
+
const metadata = this.toLocalRuleMetadata(decisiveRule);
|
|
1062
|
+
if (decisiveRule.action === 'require_approval') {
|
|
1063
|
+
if (this.shouldApplyLogModeOverride(context)) {
|
|
1064
|
+
if (this.mode === 'shadow') {
|
|
1065
|
+
return this.applyShadowModeOverride(context, 'require_approval', reason, metadata, decisiveRule.id);
|
|
972
1066
|
}
|
|
973
|
-
this.logger.warn('Tool call
|
|
1067
|
+
this.logger.warn('Tool call would require approval locally (log mode)', {
|
|
974
1068
|
tool: context.toolName,
|
|
975
|
-
ruleId:
|
|
1069
|
+
ruleId: decisiveRule.id,
|
|
976
1070
|
reason,
|
|
977
1071
|
});
|
|
978
1072
|
return {
|
|
979
|
-
decision: '
|
|
1073
|
+
decision: 'allow',
|
|
1074
|
+
reason: `[LOG MODE] Would require approval: ${reason}`,
|
|
1075
|
+
metadata: {
|
|
1076
|
+
blocked_in_strict_mode: true,
|
|
1077
|
+
...metadata,
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
if (this.isGuardEvaluation(context)) {
|
|
1082
|
+
return {
|
|
1083
|
+
decision: 'require_approval',
|
|
980
1084
|
reason,
|
|
981
1085
|
metadata,
|
|
982
1086
|
};
|
|
983
1087
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if (
|
|
988
|
-
this.
|
|
1088
|
+
return this.handleLocalApprovalFlow(context, decisiveRule, reason);
|
|
1089
|
+
}
|
|
1090
|
+
if (decisiveRule.action === 'block') {
|
|
1091
|
+
if (this.shouldApplyLogModeOverride(context)) {
|
|
1092
|
+
if (this.mode === 'shadow') {
|
|
1093
|
+
return this.applyShadowModeOverride(context, 'deny', reason, metadata, decisiveRule.id);
|
|
1094
|
+
}
|
|
1095
|
+
this.logger.warn('Tool call would be blocked locally (log mode)', {
|
|
989
1096
|
tool: context.toolName,
|
|
990
|
-
|
|
991
|
-
|
|
1097
|
+
ruleId: decisiveRule.id,
|
|
1098
|
+
reason,
|
|
992
1099
|
});
|
|
1100
|
+
return {
|
|
1101
|
+
decision: 'allow',
|
|
1102
|
+
reason: `[LOG MODE] Would block: ${reason}`,
|
|
1103
|
+
metadata: {
|
|
1104
|
+
blocked_in_strict_mode: true,
|
|
1105
|
+
...metadata,
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
993
1108
|
}
|
|
1109
|
+
this.logger.warn('Tool call blocked by local rule', {
|
|
1110
|
+
tool: context.toolName,
|
|
1111
|
+
ruleId: decisiveRule.id,
|
|
1112
|
+
reason,
|
|
1113
|
+
});
|
|
1114
|
+
return {
|
|
1115
|
+
decision: 'deny',
|
|
1116
|
+
reason,
|
|
1117
|
+
metadata,
|
|
1118
|
+
};
|
|
994
1119
|
}
|
|
995
|
-
if (
|
|
1120
|
+
if (decisiveRule.action === 'allow') {
|
|
996
1121
|
return {
|
|
997
1122
|
decision: 'allow',
|
|
998
|
-
reason:
|
|
999
|
-
metadata
|
|
1123
|
+
reason: decisiveRule.description ?? `Allowed by rule: ${decisiveRule.name}`,
|
|
1124
|
+
metadata,
|
|
1000
1125
|
};
|
|
1001
1126
|
}
|
|
1002
1127
|
return { decision: 'allow' };
|
|
1003
1128
|
}
|
|
1004
1129
|
buildLocalEvaluationContext(context) {
|
|
1130
|
+
const customContext = context.custom ?? {};
|
|
1131
|
+
const localArguments = Object.fromEntries(Object.entries(context.arguments).filter(([key]) => !RESERVED_LOCAL_CONTEXT_KEYS.has(key)));
|
|
1132
|
+
const marketContext = customContext.market && typeof customContext.market === 'object' && !Array.isArray(customContext.market)
|
|
1133
|
+
? customContext.market
|
|
1134
|
+
: undefined;
|
|
1135
|
+
const budgetContext = customContext.budget && typeof customContext.budget === 'object' && !Array.isArray(customContext.budget)
|
|
1136
|
+
? customContext.budget
|
|
1137
|
+
: undefined;
|
|
1138
|
+
const portfolioContext = customContext.portfolio && typeof customContext.portfolio === 'object' && !Array.isArray(customContext.portfolio)
|
|
1139
|
+
? customContext.portfolio
|
|
1140
|
+
: undefined;
|
|
1005
1141
|
return {
|
|
1006
|
-
...
|
|
1142
|
+
...localArguments,
|
|
1007
1143
|
tool_name: context.toolName,
|
|
1008
1144
|
arguments: context.arguments,
|
|
1009
1145
|
session_id: this.resolveSessionId(context),
|
|
1010
1146
|
agent_id: this.resolveAgentId(context),
|
|
1011
1147
|
user_id: this.resolveUserId(context),
|
|
1012
1148
|
role: this.resolveRole(context),
|
|
1013
|
-
|
|
1149
|
+
...(marketContext ? { market: marketContext } : {}),
|
|
1150
|
+
...(budgetContext ? { budget: budgetContext } : {}),
|
|
1151
|
+
...(portfolioContext ? { portfolio: portfolioContext } : {}),
|
|
1152
|
+
custom: customContext,
|
|
1014
1153
|
};
|
|
1015
1154
|
}
|
|
1016
1155
|
matchesLocalRule(rule, validationContext, localContext) {
|
|
@@ -1459,6 +1598,7 @@ export class Veto {
|
|
|
1459
1598
|
}
|
|
1460
1599
|
try {
|
|
1461
1600
|
const response = await client.validate(context.toolName, context.arguments, apiContext);
|
|
1601
|
+
this.cacheRemoteOutputRules(context.toolName, response.outputRules);
|
|
1462
1602
|
const metadata = {};
|
|
1463
1603
|
if (response.failed_constraints) {
|
|
1464
1604
|
metadata.failed_constraints = response.failed_constraints;
|
|
@@ -1469,6 +1609,9 @@ export class Veto {
|
|
|
1469
1609
|
// Handle require_approval decision
|
|
1470
1610
|
if (response.decision === 'require_approval') {
|
|
1471
1611
|
const approvalReason = response.reason ?? 'Approval required';
|
|
1612
|
+
if (response.denial) {
|
|
1613
|
+
metadata.denial = response.denial;
|
|
1614
|
+
}
|
|
1472
1615
|
const metadataWithApproval = response.approval_id
|
|
1473
1616
|
? { ...metadata, approvalId: response.approval_id }
|
|
1474
1617
|
: metadata;
|
|
@@ -1539,6 +1682,9 @@ export class Veto {
|
|
|
1539
1682
|
tool: context.toolName,
|
|
1540
1683
|
reason: response.reason,
|
|
1541
1684
|
});
|
|
1685
|
+
if (response.denial) {
|
|
1686
|
+
metadata.denial = response.denial;
|
|
1687
|
+
}
|
|
1542
1688
|
return {
|
|
1543
1689
|
decision: 'deny',
|
|
1544
1690
|
reason: response.reason,
|
|
@@ -1959,6 +2105,61 @@ export class Veto {
|
|
|
1959
2105
|
};
|
|
1960
2106
|
this.eventWebhookEmitter.emit(event);
|
|
1961
2107
|
}
|
|
2108
|
+
notifyDecisionMade(result, toolName) {
|
|
2109
|
+
try {
|
|
2110
|
+
const maybePromise = this.onDecisionMade?.({ ...result, toolName });
|
|
2111
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
2112
|
+
maybePromise.catch(() => { });
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
catch {
|
|
2116
|
+
// swallow — callback errors must not break guard flow
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Emit a webhook event for economic authorization outcomes.
|
|
2121
|
+
*
|
|
2122
|
+
* Maps economic evaluation results to the appropriate webhook event type:
|
|
2123
|
+
* - budget_exceeded → 'budget_exceeded'
|
|
2124
|
+
* - approval_required → 'approval_triggered'
|
|
2125
|
+
* - budget_warning (>80% utilization on allow) → 'budget_warning'
|
|
2126
|
+
* - spend_committed (successful reservation) → 'spend_committed'
|
|
2127
|
+
*/
|
|
2128
|
+
emitEconomicEvent(toolName, args, econResult, economicContext, forceType) {
|
|
2129
|
+
let eventType;
|
|
2130
|
+
if (forceType) {
|
|
2131
|
+
eventType = forceType;
|
|
2132
|
+
}
|
|
2133
|
+
else if (econResult.denial?.reason === 'budget_exceeded') {
|
|
2134
|
+
eventType = 'budget_exceeded';
|
|
2135
|
+
}
|
|
2136
|
+
else if (econResult.denial?.reason === 'approval_required') {
|
|
2137
|
+
eventType = 'approval_triggered';
|
|
2138
|
+
}
|
|
2139
|
+
else {
|
|
2140
|
+
eventType = 'deny';
|
|
2141
|
+
}
|
|
2142
|
+
const event = {
|
|
2143
|
+
eventType,
|
|
2144
|
+
toolName,
|
|
2145
|
+
arguments: args,
|
|
2146
|
+
decision: econResult.decision,
|
|
2147
|
+
reason: econResult.denial?.reason,
|
|
2148
|
+
severity: eventType === 'budget_exceeded' ? 'high' : 'medium',
|
|
2149
|
+
timestamp: new Date().toISOString(),
|
|
2150
|
+
shadow: this.mode === 'shadow' ? true : undefined,
|
|
2151
|
+
economic: {
|
|
2152
|
+
cost: economicContext.cost,
|
|
2153
|
+
currency: economicContext.currency,
|
|
2154
|
+
protocol: economicContext.protocol,
|
|
2155
|
+
payer: economicContext.payer,
|
|
2156
|
+
budget_spent: econResult.denial?.budget_spent,
|
|
2157
|
+
budget_limit: econResult.denial?.budget_limit,
|
|
2158
|
+
budget_remaining: econResult.denial?.budget_remaining,
|
|
2159
|
+
},
|
|
2160
|
+
};
|
|
2161
|
+
this.eventWebhookEmitter.emit(event);
|
|
2162
|
+
}
|
|
1962
2163
|
toGuardResult(result) {
|
|
1963
2164
|
const metadata = result.metadata;
|
|
1964
2165
|
const ruleId = this.extractMetadataString(metadata, ['ruleId', 'rule_id']);
|
|
@@ -2027,9 +2228,42 @@ export class Veto {
|
|
|
2027
2228
|
delay(ms) {
|
|
2028
2229
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2029
2230
|
}
|
|
2030
|
-
|
|
2231
|
+
logOutputValidation(toolName, args, outputResult, latencyMs) {
|
|
2232
|
+
if (!this.cloudClient) {
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (outputResult.decision !== 'block' && outputResult.trace.length === 0) {
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
this.getCloudClient().logDecision({
|
|
2239
|
+
tool_name: toolName,
|
|
2240
|
+
arguments: args,
|
|
2241
|
+
decision: outputResult.decision === 'block' ? 'deny' : 'allow',
|
|
2242
|
+
reason: outputResult.reason,
|
|
2243
|
+
mode: 'deterministic',
|
|
2244
|
+
latency_ms: latencyMs,
|
|
2245
|
+
source: 'client',
|
|
2246
|
+
context: {
|
|
2247
|
+
output_validation: true,
|
|
2248
|
+
},
|
|
2249
|
+
redactions: outputResult.trace,
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
validateOutputOrThrow(toolName, args, output) {
|
|
2253
|
+
const startedAt = Date.now();
|
|
2031
2254
|
const outputResult = this.validateOutput(toolName, output);
|
|
2255
|
+
const latencyMs = Date.now() - startedAt;
|
|
2256
|
+
this.logOutputValidation(toolName, args, outputResult, latencyMs);
|
|
2032
2257
|
if (outputResult.decision === 'block') {
|
|
2258
|
+
if (this.mode === 'log' || this.mode === 'shadow') {
|
|
2259
|
+
this.logger.warn(this.mode === 'shadow'
|
|
2260
|
+
? '[shadow] Tool output would be blocked'
|
|
2261
|
+
: 'Tool output would be blocked (log mode)', {
|
|
2262
|
+
tool: toolName,
|
|
2263
|
+
reason: outputResult.reason,
|
|
2264
|
+
});
|
|
2265
|
+
return output;
|
|
2266
|
+
}
|
|
2033
2267
|
throw new Error(outputResult.reason ?? `Tool output blocked for ${toolName}`);
|
|
2034
2268
|
}
|
|
2035
2269
|
return outputResult.output;
|
|
@@ -2095,12 +2329,13 @@ export class Veto {
|
|
|
2095
2329
|
arguments: input,
|
|
2096
2330
|
});
|
|
2097
2331
|
if (!result.allowed) {
|
|
2098
|
-
|
|
2332
|
+
const denial = result.validationResult.metadata?.denial;
|
|
2333
|
+
throw new ToolCallDeniedError(toolName, result.originalCall.id || '', result.validationResult, denial);
|
|
2099
2334
|
}
|
|
2100
2335
|
// Execute the original function with potentially modified arguments
|
|
2101
2336
|
const finalArgs = result.finalArguments ?? input;
|
|
2102
2337
|
const executionResult = await originalFunc.call(tool, finalArgs);
|
|
2103
|
-
return veto.validateOutputOrThrow(toolName, executionResult);
|
|
2338
|
+
return veto.validateOutputOrThrow(toolName, finalArgs, executionResult);
|
|
2104
2339
|
};
|
|
2105
2340
|
// Replace func
|
|
2106
2341
|
wrapped.func = wrappedFunc;
|
|
@@ -2115,12 +2350,13 @@ export class Veto {
|
|
|
2115
2350
|
arguments: input,
|
|
2116
2351
|
});
|
|
2117
2352
|
if (!result.allowed) {
|
|
2118
|
-
|
|
2353
|
+
const denial = result.validationResult.metadata?.denial;
|
|
2354
|
+
throw new ToolCallDeniedError(toolName, result.originalCall.id || '', result.validationResult, denial);
|
|
2119
2355
|
}
|
|
2120
2356
|
// Call original invoke with potentially modified arguments
|
|
2121
2357
|
const finalArgs = result.finalArguments ?? input;
|
|
2122
2358
|
const executionResult = await originalInvoke.call(tool, finalArgs, ...rest);
|
|
2123
|
-
return veto.validateOutputOrThrow(toolName, executionResult);
|
|
2359
|
+
return veto.validateOutputOrThrow(toolName, finalArgs, executionResult);
|
|
2124
2360
|
};
|
|
2125
2361
|
}
|
|
2126
2362
|
veto.logger.debug('Tool wrapped', { name: toolName });
|
|
@@ -2147,15 +2383,16 @@ export class Veto {
|
|
|
2147
2383
|
arguments: callArgs,
|
|
2148
2384
|
});
|
|
2149
2385
|
if (!result.allowed) {
|
|
2150
|
-
|
|
2386
|
+
const denial = result.validationResult.metadata?.denial;
|
|
2387
|
+
throw new ToolCallDeniedError(toolName, result.originalCall.id || '', result.validationResult, denial);
|
|
2151
2388
|
}
|
|
2152
2389
|
const finalArgs = result.finalArguments ?? callArgs;
|
|
2153
2390
|
if (args.length === 1 && typeof args[0] === 'object') {
|
|
2154
2391
|
const executionResult = await originalFunc.call(tool, finalArgs);
|
|
2155
|
-
return veto.validateOutputOrThrow(toolName, executionResult);
|
|
2392
|
+
return veto.validateOutputOrThrow(toolName, finalArgs, executionResult);
|
|
2156
2393
|
}
|
|
2157
2394
|
const executionResult = await originalFunc.apply(tool, args);
|
|
2158
|
-
return veto.validateOutputOrThrow(toolName, executionResult);
|
|
2395
|
+
return veto.validateOutputOrThrow(toolName, finalArgs, executionResult);
|
|
2159
2396
|
};
|
|
2160
2397
|
wrapped[key] = wrappedFunc;
|
|
2161
2398
|
veto.logger.debug('Tool wrapped', { name: toolName });
|
|
@@ -2221,11 +2458,12 @@ export class Veto {
|
|
|
2221
2458
|
arguments: callArgs,
|
|
2222
2459
|
});
|
|
2223
2460
|
if (!result.allowed) {
|
|
2224
|
-
|
|
2461
|
+
const denial = result.validationResult.metadata?.denial;
|
|
2462
|
+
throw new ToolCallDeniedError(args.name, result.originalCall.id || '', result.validationResult, denial);
|
|
2225
2463
|
}
|
|
2226
2464
|
const finalArgs = result.finalArguments ?? callArgs;
|
|
2227
2465
|
const executionResult = await serverClient.callTool({ name: args.name, arguments: finalArgs });
|
|
2228
|
-
return veto.validateOutputOrThrow(args.name, executionResult);
|
|
2466
|
+
return veto.validateOutputOrThrow(args.name, finalArgs, executionResult);
|
|
2229
2467
|
};
|
|
2230
2468
|
this.logger.debug('MCP tools wrapped', { count: tools.length });
|
|
2231
2469
|
return { tools, callTool };
|
|
@@ -2272,6 +2510,88 @@ export class Veto {
|
|
|
2272
2510
|
* Unlike interceptor execution, this returns raw validation outcomes in log/shadow mode.
|
|
2273
2511
|
*/
|
|
2274
2512
|
async guard(toolName, args, context = {}) {
|
|
2513
|
+
// Economic pre-checks: payer validation and cost validation run BEFORE
|
|
2514
|
+
// behavioral rules. Budget reservation happens AFTER behavioral rules
|
|
2515
|
+
// to avoid the TOCTOU double-spend (check then reserve race).
|
|
2516
|
+
if (this.economicEvaluator && context.economic) {
|
|
2517
|
+
const econResult = this.economicEvaluator.evaluate(context.economic);
|
|
2518
|
+
if (econResult.decision !== 'allow') {
|
|
2519
|
+
this.logger.warn('Economic authorization denied', {
|
|
2520
|
+
toolName,
|
|
2521
|
+
decision: econResult.decision,
|
|
2522
|
+
reason: econResult.denial?.reason,
|
|
2523
|
+
cost: context.economic.cost,
|
|
2524
|
+
protocol: context.economic.protocol,
|
|
2525
|
+
});
|
|
2526
|
+
this.emitEconomicEvent(toolName, args, econResult, context.economic);
|
|
2527
|
+
const result = {
|
|
2528
|
+
decision: this.mode === 'shadow' ? 'allow' : econResult.decision,
|
|
2529
|
+
reason: econResult.denial
|
|
2530
|
+
? `Economic: ${econResult.denial.reason}`
|
|
2531
|
+
: 'Economic authorization denied',
|
|
2532
|
+
economicDenial: econResult.denial,
|
|
2533
|
+
shadow: this.mode === 'shadow' ? true : undefined,
|
|
2534
|
+
shadowDecision: this.mode === 'shadow' ? econResult.decision : undefined,
|
|
2535
|
+
};
|
|
2536
|
+
this.notifyDecisionMade(result, toolName);
|
|
2537
|
+
return result;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
// If economic evaluator is configured and can resolve cost from args
|
|
2541
|
+
// (no explicit EconomicContext provided, but cost_extraction is configured)
|
|
2542
|
+
let implicitEconomicContext;
|
|
2543
|
+
if (this.economicEvaluator && !context.economic) {
|
|
2544
|
+
const resolvedCost = this.economicEvaluator.resolveCost(toolName, args);
|
|
2545
|
+
if (resolvedCost !== undefined && resolvedCost > 0) {
|
|
2546
|
+
implicitEconomicContext = {
|
|
2547
|
+
cost: resolvedCost,
|
|
2548
|
+
currency: 'USD', // Default currency for implicit extraction
|
|
2549
|
+
protocol: 'custom',
|
|
2550
|
+
};
|
|
2551
|
+
const econResult = this.economicEvaluator.evaluate(implicitEconomicContext);
|
|
2552
|
+
if (econResult.decision !== 'allow') {
|
|
2553
|
+
this.logger.warn('Economic authorization denied (implicit cost)', {
|
|
2554
|
+
toolName,
|
|
2555
|
+
decision: econResult.decision,
|
|
2556
|
+
reason: econResult.denial?.reason,
|
|
2557
|
+
cost: resolvedCost,
|
|
2558
|
+
});
|
|
2559
|
+
this.emitEconomicEvent(toolName, args, econResult, implicitEconomicContext);
|
|
2560
|
+
const result = {
|
|
2561
|
+
decision: this.mode === 'shadow' ? 'allow' : econResult.decision,
|
|
2562
|
+
reason: econResult.denial
|
|
2563
|
+
? `Economic: ${econResult.denial.reason}`
|
|
2564
|
+
: 'Economic authorization denied',
|
|
2565
|
+
economicDenial: econResult.denial,
|
|
2566
|
+
shadow: this.mode === 'shadow' ? true : undefined,
|
|
2567
|
+
shadowDecision: this.mode === 'shadow' ? econResult.decision : undefined,
|
|
2568
|
+
};
|
|
2569
|
+
this.notifyDecisionMade(result, toolName);
|
|
2570
|
+
return result;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
const customContext = {
|
|
2575
|
+
...(context.custom ?? {}),
|
|
2576
|
+
};
|
|
2577
|
+
if (context.market) {
|
|
2578
|
+
if (customContext.market !== undefined) {
|
|
2579
|
+
this.logger.debug('Guard context override applied', { key: 'market' });
|
|
2580
|
+
}
|
|
2581
|
+
customContext.market = context.market;
|
|
2582
|
+
}
|
|
2583
|
+
if (context.budget) {
|
|
2584
|
+
if (customContext.budget !== undefined) {
|
|
2585
|
+
this.logger.debug('Guard context override applied', { key: 'budget' });
|
|
2586
|
+
}
|
|
2587
|
+
customContext.budget = context.budget;
|
|
2588
|
+
}
|
|
2589
|
+
if (context.portfolio) {
|
|
2590
|
+
if (customContext.portfolio !== undefined) {
|
|
2591
|
+
this.logger.debug('Guard context override applied', { key: 'portfolio' });
|
|
2592
|
+
}
|
|
2593
|
+
customContext.portfolio = context.portfolio;
|
|
2594
|
+
}
|
|
2275
2595
|
const validationContext = {
|
|
2276
2596
|
toolName,
|
|
2277
2597
|
arguments: args,
|
|
@@ -2282,6 +2602,7 @@ export class Veto {
|
|
|
2282
2602
|
agentId: context.agentId ?? this.agentId,
|
|
2283
2603
|
userId: context.userId ?? this.userId,
|
|
2284
2604
|
role: context.role ?? this.role,
|
|
2605
|
+
custom: Object.keys(customContext).length > 0 ? customContext : undefined,
|
|
2285
2606
|
source: 'guard',
|
|
2286
2607
|
};
|
|
2287
2608
|
const aggregatedResult = await this.validationEngine.validate(validationContext);
|
|
@@ -2289,7 +2610,34 @@ export class Veto {
|
|
|
2289
2610
|
this.historyTracker.record(toolName, args, validationResult, aggregatedResult.totalDurationMs);
|
|
2290
2611
|
this.emitDecisionEvent(validationContext, validationResult);
|
|
2291
2612
|
this.logClientDecision(validationContext, validationResult, aggregatedResult.totalDurationMs);
|
|
2292
|
-
|
|
2613
|
+
// If behavioral rules allow and economic context exists, reserve budget.
|
|
2614
|
+
// Skip reservation in shadow mode — shadow should never deduct real budget.
|
|
2615
|
+
const behavioralResult = this.toGuardResult(validationResult);
|
|
2616
|
+
const effectiveEconomic = context.economic ?? implicitEconomicContext;
|
|
2617
|
+
if (behavioralResult.decision === 'allow'
|
|
2618
|
+
&& this.economicEvaluator
|
|
2619
|
+
&& effectiveEconomic
|
|
2620
|
+
&& effectiveEconomic.cost > 0
|
|
2621
|
+
&& this.mode !== 'shadow') {
|
|
2622
|
+
const reserveResult = this.economicEvaluator.reserveBudget(effectiveEconomic.cost, effectiveEconomic.currency);
|
|
2623
|
+
if (reserveResult.decision !== 'allow') {
|
|
2624
|
+
this.emitEconomicEvent(toolName, args, reserveResult, effectiveEconomic);
|
|
2625
|
+
const result = {
|
|
2626
|
+
...behavioralResult,
|
|
2627
|
+
decision: reserveResult.decision,
|
|
2628
|
+
reason: reserveResult.denial
|
|
2629
|
+
? `Economic: ${reserveResult.denial.reason}`
|
|
2630
|
+
: 'Budget reservation failed',
|
|
2631
|
+
economicDenial: reserveResult.denial,
|
|
2632
|
+
};
|
|
2633
|
+
this.notifyDecisionMade(result, toolName);
|
|
2634
|
+
return result;
|
|
2635
|
+
}
|
|
2636
|
+
// Emit spend_committed event on successful reservation
|
|
2637
|
+
this.emitEconomicEvent(toolName, args, reserveResult, effectiveEconomic, 'spend_committed');
|
|
2638
|
+
}
|
|
2639
|
+
this.notifyDecisionMade(behavioralResult, toolName);
|
|
2640
|
+
return behavioralResult;
|
|
2293
2641
|
}
|
|
2294
2642
|
/**
|
|
2295
2643
|
* Cache an approval preference for a tool.
|
|
@@ -2352,6 +2700,25 @@ export class Veto {
|
|
|
2352
2700
|
outputRulesLoaded: this.rulesState.allOutputRules.length,
|
|
2353
2701
|
});
|
|
2354
2702
|
}
|
|
2703
|
+
async reloadLocalRules() {
|
|
2704
|
+
if (this.validationMode !== 'local' || !this.localRulesDir) {
|
|
2705
|
+
throw new Error('No local rules configured');
|
|
2706
|
+
}
|
|
2707
|
+
const [fsModule, pathModule, parseYaml] = await Promise.all([
|
|
2708
|
+
Veto.loadNodeFsModule(),
|
|
2709
|
+
Veto.loadNodePathModule(),
|
|
2710
|
+
Veto.loadYamlParser(),
|
|
2711
|
+
]);
|
|
2712
|
+
const rules = await Veto.loadRules(this.localRulesDir, this.localRulesRecursive, this.logger, fsModule, pathModule, parseYaml);
|
|
2713
|
+
this.rulesState = rules.state;
|
|
2714
|
+
this.localSourceFiles = rules.sourceFiles;
|
|
2715
|
+
this.compiledExpressionCache.clear();
|
|
2716
|
+
this.logger.info('Local rules reloaded', {
|
|
2717
|
+
rulesLoaded: this.rulesState.allRules.length,
|
|
2718
|
+
outputRulesLoaded: this.rulesState.allOutputRules.length,
|
|
2719
|
+
sourceFiles: this.localSourceFiles.length,
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2355
2722
|
setRefreshInterval(refreshIntervalMs) {
|
|
2356
2723
|
if (this.refreshIntervalId) {
|
|
2357
2724
|
clearInterval(this.refreshIntervalId);
|
|
@@ -2392,6 +2759,19 @@ export class Veto {
|
|
|
2392
2759
|
resetBudget() {
|
|
2393
2760
|
this.budgetTracker?.reset();
|
|
2394
2761
|
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Get current economic budget status for a scope.
|
|
2764
|
+
* Returns null if no economic policy is configured.
|
|
2765
|
+
*/
|
|
2766
|
+
getEconomicBudgetStatus(scope = 'session') {
|
|
2767
|
+
return this.economicBudgetEngine?.getStatus(scope) ?? null;
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Reset economic budget for a scope.
|
|
2771
|
+
*/
|
|
2772
|
+
resetEconomicBudget(scope = 'session') {
|
|
2773
|
+
this.economicBudgetEngine?.reset(scope);
|
|
2774
|
+
}
|
|
2395
2775
|
dispose() {
|
|
2396
2776
|
if (this.refreshIntervalId) {
|
|
2397
2777
|
clearInterval(this.refreshIntervalId);
|