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.
Files changed (156) hide show
  1. package/README.md +69 -203
  2. package/dist/browser/index.d.ts +1 -1
  3. package/dist/browser/index.d.ts.map +1 -1
  4. package/dist/browser/index.js.map +1 -1
  5. package/dist/browser/types.d.ts +17 -1
  6. package/dist/browser/types.d.ts.map +1 -1
  7. package/dist/browser/veto.d.ts +10 -0
  8. package/dist/browser/veto.d.ts.map +1 -1
  9. package/dist/browser/veto.js +69 -5
  10. package/dist/browser/veto.js.map +1 -1
  11. package/dist/cli/bin.js +0 -0
  12. package/dist/cli/compile.d.ts +22 -1
  13. package/dist/cli/compile.d.ts.map +1 -1
  14. package/dist/cli/compile.js +100 -21
  15. package/dist/cli/compile.js.map +1 -1
  16. package/dist/cli/diff.d.ts +2 -25
  17. package/dist/cli/diff.d.ts.map +1 -1
  18. package/dist/cli/diff.js +5 -327
  19. package/dist/cli/diff.js.map +1 -1
  20. package/dist/cli/headless.js +1 -1
  21. package/dist/cli/headless.js.map +1 -1
  22. package/dist/cli/index.d.ts +3 -1
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +2 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/cli/repl-generate.d.ts.map +1 -1
  27. package/dist/cli/repl-generate.js +346 -17
  28. package/dist/cli/repl-generate.js.map +1 -1
  29. package/dist/cli/replay-engine.d.ts +77 -0
  30. package/dist/cli/replay-engine.d.ts.map +1 -0
  31. package/dist/cli/replay-engine.js +379 -0
  32. package/dist/cli/replay-engine.js.map +1 -0
  33. package/dist/cli/replay.d.ts +57 -0
  34. package/dist/cli/replay.d.ts.map +1 -0
  35. package/dist/cli/replay.js +202 -0
  36. package/dist/cli/replay.js.map +1 -0
  37. package/dist/cli/runner.d.ts.map +1 -1
  38. package/dist/cli/runner.js +20 -0
  39. package/dist/cli/runner.js.map +1 -1
  40. package/dist/cli/templates.d.ts +1 -1
  41. package/dist/cli/templates.d.ts.map +1 -1
  42. package/dist/cli/templates.js +1 -1
  43. package/dist/cloud/client.d.ts.map +1 -1
  44. package/dist/cloud/client.js +6 -1
  45. package/dist/cloud/client.js.map +1 -1
  46. package/dist/cloud/types.d.ts +33 -2
  47. package/dist/cloud/types.d.ts.map +1 -1
  48. package/dist/core/events.d.ts +11 -1
  49. package/dist/core/events.d.ts.map +1 -1
  50. package/dist/core/events.js +4 -0
  51. package/dist/core/events.js.map +1 -1
  52. package/dist/core/interceptor.d.ts +20 -1
  53. package/dist/core/interceptor.d.ts.map +1 -1
  54. package/dist/core/interceptor.js +47 -3
  55. package/dist/core/interceptor.js.map +1 -1
  56. package/dist/core/output-validator.d.ts +11 -0
  57. package/dist/core/output-validator.d.ts.map +1 -1
  58. package/dist/core/output-validator.js +88 -15
  59. package/dist/core/output-validator.js.map +1 -1
  60. package/dist/core/protect.d.ts +3 -1
  61. package/dist/core/protect.d.ts.map +1 -1
  62. package/dist/core/protect.js +14 -4
  63. package/dist/core/protect.js.map +1 -1
  64. package/dist/core/veto.d.ts +54 -1
  65. package/dist/core/veto.d.ts.map +1 -1
  66. package/dist/core/veto.js +471 -91
  67. package/dist/core/veto.js.map +1 -1
  68. package/dist/deterministic/types.d.ts +103 -0
  69. package/dist/deterministic/types.d.ts.map +1 -1
  70. package/dist/economic/budget-engine.d.ts +29 -0
  71. package/dist/economic/budget-engine.d.ts.map +1 -0
  72. package/dist/economic/budget-engine.js +146 -0
  73. package/dist/economic/budget-engine.js.map +1 -0
  74. package/dist/economic/connectors/ap2.d.ts +51 -0
  75. package/dist/economic/connectors/ap2.d.ts.map +1 -0
  76. package/dist/economic/connectors/ap2.js +133 -0
  77. package/dist/economic/connectors/ap2.js.map +1 -0
  78. package/dist/economic/connectors/index.d.ts +8 -0
  79. package/dist/economic/connectors/index.d.ts.map +1 -0
  80. package/dist/economic/connectors/index.js +8 -0
  81. package/dist/economic/connectors/index.js.map +1 -0
  82. package/dist/economic/connectors/mpp.d.ts +41 -0
  83. package/dist/economic/connectors/mpp.d.ts.map +1 -0
  84. package/dist/economic/connectors/mpp.js +97 -0
  85. package/dist/economic/connectors/mpp.js.map +1 -0
  86. package/dist/economic/connectors/x402.d.ts +20 -0
  87. package/dist/economic/connectors/x402.d.ts.map +1 -0
  88. package/dist/economic/connectors/x402.js +142 -0
  89. package/dist/economic/connectors/x402.js.map +1 -0
  90. package/dist/economic/evaluator.d.ts +77 -0
  91. package/dist/economic/evaluator.d.ts.map +1 -0
  92. package/dist/economic/evaluator.js +231 -0
  93. package/dist/economic/evaluator.js.map +1 -0
  94. package/dist/economic/index.d.ts +13 -0
  95. package/dist/economic/index.d.ts.map +1 -0
  96. package/dist/economic/index.js +15 -0
  97. package/dist/economic/index.js.map +1 -0
  98. package/dist/economic/types.d.ts +188 -0
  99. package/dist/economic/types.d.ts.map +1 -0
  100. package/dist/economic/types.js +10 -0
  101. package/dist/economic/types.js.map +1 -0
  102. package/dist/extractors/content.d.ts +42 -0
  103. package/dist/extractors/content.d.ts.map +1 -0
  104. package/dist/extractors/content.js +154 -0
  105. package/dist/extractors/content.js.map +1 -0
  106. package/dist/extractors/index.d.ts +7 -0
  107. package/dist/extractors/index.d.ts.map +1 -0
  108. package/dist/extractors/index.js +7 -0
  109. package/dist/extractors/index.js.map +1 -0
  110. package/dist/index.d.ts +10 -4
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +10 -2
  113. package/dist/index.js.map +1 -1
  114. package/dist/policy/generator.d.ts +110 -0
  115. package/dist/policy/generator.d.ts.map +1 -0
  116. package/dist/policy/generator.js +463 -0
  117. package/dist/policy/generator.js.map +1 -0
  118. package/dist/policy/index.d.ts +7 -0
  119. package/dist/policy/index.d.ts.map +1 -0
  120. package/dist/policy/index.js +7 -0
  121. package/dist/policy/index.js.map +1 -0
  122. package/dist/providers/adapters.d.ts +27 -0
  123. package/dist/providers/adapters.d.ts.map +1 -1
  124. package/dist/providers/adapters.js +58 -0
  125. package/dist/providers/adapters.js.map +1 -1
  126. package/dist/rules/condition-evaluator.d.ts +2 -1
  127. package/dist/rules/condition-evaluator.d.ts.map +1 -1
  128. package/dist/rules/condition-evaluator.js +97 -7
  129. package/dist/rules/condition-evaluator.js.map +1 -1
  130. package/dist/rules/index.d.ts +1 -0
  131. package/dist/rules/index.d.ts.map +1 -1
  132. package/dist/rules/index.js +1 -0
  133. package/dist/rules/index.js.map +1 -1
  134. package/dist/rules/local-evaluator.d.ts +69 -0
  135. package/dist/rules/local-evaluator.d.ts.map +1 -0
  136. package/dist/rules/local-evaluator.js +217 -0
  137. package/dist/rules/local-evaluator.js.map +1 -0
  138. package/dist/rules/policy-ir-schema.d.ts +132 -1
  139. package/dist/rules/policy-ir-schema.d.ts.map +1 -1
  140. package/dist/rules/policy-ir-schema.js +114 -0
  141. package/dist/rules/policy-ir-schema.js.map +1 -1
  142. package/dist/rules/policy-packs.d.ts.map +1 -1
  143. package/dist/rules/policy-packs.js +1 -0
  144. package/dist/rules/policy-packs.js.map +1 -1
  145. package/dist/rules/types.d.ts +3 -1
  146. package/dist/rules/types.d.ts.map +1 -1
  147. package/dist/rules/types.js.map +1 -1
  148. package/dist/types/config.d.ts +2 -1
  149. package/dist/types/config.d.ts.map +1 -1
  150. package/dist/types/config.js.map +1 -1
  151. package/dist/utils/logger.d.ts +38 -2
  152. package/dist/utils/logger.d.ts.map +1 -1
  153. package/dist/utils/logger.js +231 -26
  154. package/dist/utils/logger.js.map +1 -1
  155. package/package.json +9 -1
  156. 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.runveto.com';
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
- this.resolvedLogLevel = options.logLevel ?? envLogLevel ?? config.logging?.level ?? 'info';
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
- // Approval hook
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 envLogLevel = process.env.VETO_LOG_LEVEL;
371
- let logLevel = options.logLevel ?? envLogLevel ?? 'info';
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 = options.logLevel ?? envLogLevel ?? config.logging?.level ?? 'info';
379
- }
380
- const logger = createLogger(logLevel);
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 logLevel = options.logLevel ?? 'warn';
394
- const logger = createLogger(logLevel);
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 logger = createLogger('warn');
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 state;
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 state;
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
- return [...this.rulesState.globalOutputRules, ...toolSpecific];
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
- const reason = rule.description ?? `Matched rule: ${rule.name}`;
925
- const metadata = this.toLocalRuleMetadata(rule);
926
- if (rule.action === 'require_approval') {
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 === 'block') {
955
- if (this.shouldApplyLogModeOverride(context)) {
956
- if (this.mode === 'shadow') {
957
- return this.applyShadowModeOverride(context, 'deny', reason, metadata, rule.id);
958
- }
959
- this.logger.warn('Tool call would be blocked locally (log mode)', {
960
- tool: context.toolName,
961
- ruleId: rule.id,
962
- reason,
963
- });
964
- return {
965
- decision: 'allow',
966
- reason: `[LOG MODE] Would block: ${reason}`,
967
- metadata: {
968
- blocked_in_strict_mode: true,
969
- ...metadata,
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 blocked by local rule', {
1067
+ this.logger.warn('Tool call would require approval locally (log mode)', {
974
1068
  tool: context.toolName,
975
- ruleId: rule.id,
1069
+ ruleId: decisiveRule.id,
976
1070
  reason,
977
1071
  });
978
1072
  return {
979
- decision: 'deny',
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
- if (rule.action === 'allow' && !firstAllowRule) {
985
- firstAllowRule = rule;
986
- }
987
- if (rule.action === 'warn' || rule.action === 'log') {
988
- this.logger.warn('Local rule matched with non-blocking action', {
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
- action: rule.action,
991
- ruleId: rule.id,
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 (firstAllowRule) {
1120
+ if (decisiveRule.action === 'allow') {
996
1121
  return {
997
1122
  decision: 'allow',
998
- reason: firstAllowRule.description ?? `Allowed by rule: ${firstAllowRule.name}`,
999
- metadata: this.toLocalRuleMetadata(firstAllowRule),
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
- ...context.arguments,
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
- custom: context.custom,
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
- validateOutputOrThrow(toolName, output) {
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
- throw new ToolCallDeniedError(toolName, result.originalCall.id || '', result.validationResult);
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
- throw new ToolCallDeniedError(toolName, result.originalCall.id || '', result.validationResult);
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
- throw new ToolCallDeniedError(toolName, result.originalCall.id || '', result.validationResult);
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
- throw new ToolCallDeniedError(args.name, result.originalCall.id || '', result.validationResult);
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
- return this.toGuardResult(validationResult);
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);