veto-sdk 2.0.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/browser/index.d.ts +1 -1
  2. package/dist/browser/index.d.ts.map +1 -1
  3. package/dist/browser/index.js.map +1 -1
  4. package/dist/browser/protect.d.ts.map +1 -1
  5. package/dist/browser/protect.js +9 -1
  6. package/dist/browser/protect.js.map +1 -1
  7. package/dist/browser/types.d.ts +20 -1
  8. package/dist/browser/types.d.ts.map +1 -1
  9. package/dist/browser/veto.d.ts +10 -0
  10. package/dist/browser/veto.d.ts.map +1 -1
  11. package/dist/browser/veto.js +77 -4
  12. package/dist/browser/veto.js.map +1 -1
  13. package/dist/cloud/types.d.ts +17 -2
  14. package/dist/cloud/types.d.ts.map +1 -1
  15. package/dist/compiler/evaluator.d.ts.map +1 -1
  16. package/dist/compiler/evaluator.js +6 -0
  17. package/dist/compiler/evaluator.js.map +1 -1
  18. package/dist/core/events.d.ts +13 -1
  19. package/dist/core/events.d.ts.map +1 -1
  20. package/dist/core/events.js +37 -4
  21. package/dist/core/events.js.map +1 -1
  22. package/dist/core/protect.d.ts +7 -1
  23. package/dist/core/protect.d.ts.map +1 -1
  24. package/dist/core/protect.js +23 -5
  25. package/dist/core/protect.js.map +1 -1
  26. package/dist/core/veto.d.ts +43 -1
  27. package/dist/core/veto.d.ts.map +1 -1
  28. package/dist/core/veto.js +256 -13
  29. package/dist/core/veto.js.map +1 -1
  30. package/dist/deterministic/regex-safety.d.ts.map +1 -1
  31. package/dist/deterministic/regex-safety.js +42 -1
  32. package/dist/deterministic/regex-safety.js.map +1 -1
  33. package/dist/deterministic/types.d.ts +103 -0
  34. package/dist/deterministic/types.d.ts.map +1 -1
  35. package/dist/economic/budget-engine.d.ts +29 -0
  36. package/dist/economic/budget-engine.d.ts.map +1 -0
  37. package/dist/economic/budget-engine.js +146 -0
  38. package/dist/economic/budget-engine.js.map +1 -0
  39. package/dist/economic/connectors/ap2.d.ts +51 -0
  40. package/dist/economic/connectors/ap2.d.ts.map +1 -0
  41. package/dist/economic/connectors/ap2.js +133 -0
  42. package/dist/economic/connectors/ap2.js.map +1 -0
  43. package/dist/economic/connectors/index.d.ts +8 -0
  44. package/dist/economic/connectors/index.d.ts.map +1 -0
  45. package/dist/economic/connectors/index.js +8 -0
  46. package/dist/economic/connectors/index.js.map +1 -0
  47. package/dist/economic/connectors/mpp.d.ts +41 -0
  48. package/dist/economic/connectors/mpp.d.ts.map +1 -0
  49. package/dist/economic/connectors/mpp.js +97 -0
  50. package/dist/economic/connectors/mpp.js.map +1 -0
  51. package/dist/economic/connectors/x402.d.ts +20 -0
  52. package/dist/economic/connectors/x402.d.ts.map +1 -0
  53. package/dist/economic/connectors/x402.js +142 -0
  54. package/dist/economic/connectors/x402.js.map +1 -0
  55. package/dist/economic/evaluator.d.ts +77 -0
  56. package/dist/economic/evaluator.d.ts.map +1 -0
  57. package/dist/economic/evaluator.js +231 -0
  58. package/dist/economic/evaluator.js.map +1 -0
  59. package/dist/economic/index.d.ts +13 -0
  60. package/dist/economic/index.d.ts.map +1 -0
  61. package/dist/economic/index.js +15 -0
  62. package/dist/economic/index.js.map +1 -0
  63. package/dist/economic/types.d.ts +188 -0
  64. package/dist/economic/types.d.ts.map +1 -0
  65. package/dist/economic/types.js +10 -0
  66. package/dist/economic/types.js.map +1 -0
  67. package/dist/extractors/content.d.ts +42 -0
  68. package/dist/extractors/content.d.ts.map +1 -0
  69. package/dist/extractors/content.js +171 -0
  70. package/dist/extractors/content.js.map +1 -0
  71. package/dist/extractors/index.d.ts +7 -0
  72. package/dist/extractors/index.d.ts.map +1 -0
  73. package/dist/extractors/index.js +7 -0
  74. package/dist/extractors/index.js.map +1 -0
  75. package/dist/index.d.ts +9 -3
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +10 -2
  78. package/dist/index.js.map +1 -1
  79. package/dist/policy/generator.d.ts +110 -0
  80. package/dist/policy/generator.d.ts.map +1 -0
  81. package/dist/policy/generator.js +465 -0
  82. package/dist/policy/generator.js.map +1 -0
  83. package/dist/policy/index.d.ts +7 -0
  84. package/dist/policy/index.d.ts.map +1 -0
  85. package/dist/policy/index.js +7 -0
  86. package/dist/policy/index.js.map +1 -0
  87. package/dist/providers/adapters.d.ts +27 -0
  88. package/dist/providers/adapters.d.ts.map +1 -1
  89. package/dist/providers/adapters.js +58 -0
  90. package/dist/providers/adapters.js.map +1 -1
  91. package/dist/rules/condition-evaluator.d.ts +6 -0
  92. package/dist/rules/condition-evaluator.d.ts.map +1 -1
  93. package/dist/rules/condition-evaluator.js +60 -18
  94. package/dist/rules/condition-evaluator.js.map +1 -1
  95. package/dist/rules/expression-validator.d.ts.map +1 -1
  96. package/dist/rules/expression-validator.js +5 -0
  97. package/dist/rules/expression-validator.js.map +1 -1
  98. package/dist/rules/index.d.ts +1 -0
  99. package/dist/rules/index.d.ts.map +1 -1
  100. package/dist/rules/index.js +1 -0
  101. package/dist/rules/index.js.map +1 -1
  102. package/dist/rules/local-evaluator.d.ts +65 -0
  103. package/dist/rules/local-evaluator.d.ts.map +1 -0
  104. package/dist/rules/local-evaluator.js +250 -0
  105. package/dist/rules/local-evaluator.js.map +1 -0
  106. package/dist/rules/policy-ir-schema.d.ts +109 -0
  107. package/dist/rules/policy-ir-schema.d.ts.map +1 -1
  108. package/dist/rules/policy-ir-schema.js +90 -0
  109. package/dist/rules/policy-ir-schema.js.map +1 -1
  110. package/dist/rules/policy-packs.d.ts.map +1 -1
  111. package/dist/rules/policy-packs.js +1 -0
  112. package/dist/rules/policy-packs.js.map +1 -1
  113. package/dist/types/config.d.ts +2 -1
  114. package/dist/types/config.d.ts.map +1 -1
  115. package/dist/types/config.js.map +1 -1
  116. package/dist/utils/logger.d.ts +38 -2
  117. package/dist/utils/logger.d.ts.map +1 -1
  118. package/dist/utils/logger.js +231 -26
  119. package/dist/utils/logger.js.map +1 -1
  120. package/package.json +27 -12
  121. 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';
@@ -52,6 +54,8 @@ export class Veto {
52
54
  validationEngine;
53
55
  historyTracker;
54
56
  budgetTracker;
57
+ economicEvaluator;
58
+ economicBudgetEngine;
55
59
  interceptor;
56
60
  outputValidator;
57
61
  eventWebhookEmitter;
@@ -82,6 +86,7 @@ export class Veto {
82
86
  approvalPollOptions;
83
87
  localApprovalConfig;
84
88
  onApprovalRequired;
89
+ onDecisionMade;
85
90
  // Approval preference cache: tool name -> 'approve_all' | 'deny_all'
86
91
  approvalPreferences = new Map();
87
92
  // Client-side deterministic validation cache
@@ -107,10 +112,18 @@ export class Veto {
107
112
  ? undefined
108
113
  : Veto.parseEnvMode(process.env.VETO_MODE);
109
114
  this.mode = options.mode ?? config.mode ?? envMode ?? 'strict';
115
+ const envLogSetting = this.browserMode
116
+ ? undefined
117
+ : Veto.parseEnvLogSetting(process.env.VETO_LOG);
110
118
  const envLogLevel = this.browserMode
111
119
  ? undefined
112
120
  : Veto.parseEnvLogLevel(process.env.VETO_LOG_LEVEL);
113
- 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';
114
127
  const explicitValidationMode = config.validation?.mode;
115
128
  const cloudApiKey = options.apiKey
116
129
  ?? config.cloud?.apiKey
@@ -238,8 +251,9 @@ export class Veto {
238
251
  reasonField: config.approval?.responseSchema?.reasonField ?? 'reason',
239
252
  },
240
253
  };
241
- // Approval hook
254
+ // Hooks
242
255
  this.onApprovalRequired = options.onApprovalRequired;
256
+ this.onDecisionMade = options.onDecisionMade;
243
257
  this.eventWebhookEmitter = new EventWebhookEmitter(resolveEventWebhookConfig(config.events?.webhook, this.logger), this.logger);
244
258
  // Resolve tracking options
245
259
  const envSessionId = this.browserMode ? undefined : process.env.VETO_SESSION_ID;
@@ -327,6 +341,28 @@ export class Veto {
327
341
  else {
328
342
  this.budgetTracker = null;
329
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
+ }
330
366
  // Initialize interceptor
331
367
  this.outputValidator = new OutputValidator({
332
368
  logger: this.logger,
@@ -375,17 +411,28 @@ export class Veto {
375
411
  const parseYaml = await Veto.loadYamlParser();
376
412
  const configDir = pathModule.resolve(options.configDir ?? './veto');
377
413
  // Determine log level
378
- const envLogLevel = process.env.VETO_LOG_LEVEL;
379
- 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';
380
419
  // Load config file
381
420
  const configPath = pathModule.join(configDir, 'veto.config.yaml');
382
421
  let config = {};
383
422
  if (fsModule.existsSync(configPath)) {
384
423
  const configContent = fsModule.readFileSync(configPath, 'utf-8');
385
424
  config = parseYaml(configContent);
386
- logLevel = options.logLevel ?? envLogLevel ?? config.logging?.level ?? 'info';
387
- }
388
- 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);
389
436
  if (!fsModule.existsSync(configPath)) {
390
437
  logger.warn('Veto config not found. Run "npx veto init" to initialize.', {
391
438
  expected: configPath,
@@ -402,8 +449,13 @@ export class Veto {
402
449
  });
403
450
  }
404
451
  static fromRules(options) {
405
- const logLevel = options.logLevel ?? 'warn';
406
- 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);
407
459
  const envMode = Veto.parseEnvMode(typeof process === 'undefined' ? undefined : process.env.VETO_MODE);
408
460
  const resolvedMode = options.mode ?? envMode;
409
461
  const cloudClient = options.cloudClient ?? (options.apiKey
@@ -424,7 +476,7 @@ export class Veto {
424
476
  baseUrl: options.endpoint,
425
477
  }
426
478
  : undefined,
427
- logging: { level: logLevel },
479
+ logging: { level: logLevel, streamMode },
428
480
  budget: options.budget,
429
481
  costs: options.costs,
430
482
  approval: options.approval,
@@ -434,6 +486,7 @@ export class Veto {
434
486
  const vetoOptions = {
435
487
  mode: resolvedMode,
436
488
  logLevel,
489
+ streamMode,
437
490
  sessionId: options.sessionId,
438
491
  agentId: options.agentId,
439
492
  userId: options.userId,
@@ -443,11 +496,17 @@ export class Veto {
443
496
  endpoint: undefined,
444
497
  cloudClient,
445
498
  onApprovalRequired: options.onApprovalRequired,
499
+ onDecisionMade: options.onDecisionMade,
446
500
  };
447
501
  return new Veto(vetoOptions, config, rules, logger, true);
448
502
  }
449
503
  static async fromCloud(options) {
450
- 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);
451
510
  const cloudClient = new VetoCloudClient({
452
511
  config: {
453
512
  apiKey: options.apiKey,
@@ -472,9 +531,29 @@ export class Veto {
472
531
  }
473
532
  return undefined;
474
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
+ }
475
553
  static parseEnvLogLevel(level) {
476
554
  if (level === 'debug'
477
555
  || level === 'info'
556
+ || level === 'stream'
478
557
  || level === 'warn'
479
558
  || level === 'error'
480
559
  || level === 'silent') {
@@ -1149,6 +1228,11 @@ export class Veto {
1149
1228
  if (!ast) {
1150
1229
  try {
1151
1230
  ast = compile(expression);
1231
+ if (this.compiledExpressionCache.size >= 10_000) {
1232
+ const firstKey = this.compiledExpressionCache.keys().next().value;
1233
+ if (firstKey !== undefined)
1234
+ this.compiledExpressionCache.delete(firstKey);
1235
+ }
1152
1236
  this.compiledExpressionCache.set(expression, ast);
1153
1237
  }
1154
1238
  catch (error) {
@@ -2026,6 +2110,61 @@ export class Veto {
2026
2110
  };
2027
2111
  this.eventWebhookEmitter.emit(event);
2028
2112
  }
2113
+ notifyDecisionMade(result, toolName) {
2114
+ try {
2115
+ const maybePromise = this.onDecisionMade?.({ ...result, toolName });
2116
+ if (maybePromise && typeof maybePromise.catch === 'function') {
2117
+ maybePromise.catch(() => { });
2118
+ }
2119
+ }
2120
+ catch {
2121
+ // swallow — callback errors must not break guard flow
2122
+ }
2123
+ }
2124
+ /**
2125
+ * Emit a webhook event for economic authorization outcomes.
2126
+ *
2127
+ * Maps economic evaluation results to the appropriate webhook event type:
2128
+ * - budget_exceeded → 'budget_exceeded'
2129
+ * - approval_required → 'approval_triggered'
2130
+ * - budget_warning (>80% utilization on allow) → 'budget_warning'
2131
+ * - spend_committed (successful reservation) → 'spend_committed'
2132
+ */
2133
+ emitEconomicEvent(toolName, args, econResult, economicContext, forceType) {
2134
+ let eventType;
2135
+ if (forceType) {
2136
+ eventType = forceType;
2137
+ }
2138
+ else if (econResult.denial?.reason === 'budget_exceeded') {
2139
+ eventType = 'budget_exceeded';
2140
+ }
2141
+ else if (econResult.denial?.reason === 'approval_required') {
2142
+ eventType = 'approval_triggered';
2143
+ }
2144
+ else {
2145
+ eventType = 'deny';
2146
+ }
2147
+ const event = {
2148
+ eventType,
2149
+ toolName,
2150
+ arguments: args,
2151
+ decision: econResult.decision,
2152
+ reason: econResult.denial?.reason,
2153
+ severity: eventType === 'budget_exceeded' ? 'high' : 'medium',
2154
+ timestamp: new Date().toISOString(),
2155
+ shadow: this.mode === 'shadow' ? true : undefined,
2156
+ economic: {
2157
+ cost: economicContext.cost,
2158
+ currency: economicContext.currency,
2159
+ protocol: economicContext.protocol,
2160
+ payer: economicContext.payer,
2161
+ budget_spent: econResult.denial?.budget_spent,
2162
+ budget_limit: econResult.denial?.budget_limit,
2163
+ budget_remaining: econResult.denial?.budget_remaining,
2164
+ },
2165
+ };
2166
+ this.eventWebhookEmitter.emit(event);
2167
+ }
2029
2168
  toGuardResult(result) {
2030
2169
  const metadata = result.metadata;
2031
2170
  const ruleId = this.extractMetadataString(metadata, ['ruleId', 'rule_id']);
@@ -2376,6 +2515,67 @@ export class Veto {
2376
2515
  * Unlike interceptor execution, this returns raw validation outcomes in log/shadow mode.
2377
2516
  */
2378
2517
  async guard(toolName, args, context = {}) {
2518
+ // Economic pre-checks: payer validation and cost validation run BEFORE
2519
+ // behavioral rules. Budget reservation happens AFTER behavioral rules
2520
+ // to avoid the TOCTOU double-spend (check then reserve race).
2521
+ if (this.economicEvaluator && context.economic) {
2522
+ const econResult = this.economicEvaluator.evaluate(context.economic);
2523
+ if (econResult.decision !== 'allow') {
2524
+ this.logger.warn('Economic authorization denied', {
2525
+ toolName,
2526
+ decision: econResult.decision,
2527
+ reason: econResult.denial?.reason,
2528
+ cost: context.economic.cost,
2529
+ protocol: context.economic.protocol,
2530
+ });
2531
+ this.emitEconomicEvent(toolName, args, econResult, context.economic);
2532
+ const result = {
2533
+ decision: this.mode === 'shadow' ? 'allow' : econResult.decision,
2534
+ reason: econResult.denial
2535
+ ? `Economic: ${econResult.denial.reason}`
2536
+ : 'Economic authorization denied',
2537
+ economicDenial: econResult.denial,
2538
+ shadow: this.mode === 'shadow' ? true : undefined,
2539
+ shadowDecision: this.mode === 'shadow' ? econResult.decision : undefined,
2540
+ };
2541
+ this.notifyDecisionMade(result, toolName);
2542
+ return result;
2543
+ }
2544
+ }
2545
+ // If economic evaluator is configured and can resolve cost from args
2546
+ // (no explicit EconomicContext provided, but cost_extraction is configured)
2547
+ let implicitEconomicContext;
2548
+ if (this.economicEvaluator && !context.economic) {
2549
+ const resolvedCost = this.economicEvaluator.resolveCost(toolName, args);
2550
+ if (resolvedCost !== undefined && resolvedCost > 0) {
2551
+ implicitEconomicContext = {
2552
+ cost: resolvedCost,
2553
+ currency: 'USD', // Default currency for implicit extraction
2554
+ protocol: 'custom',
2555
+ };
2556
+ const econResult = this.economicEvaluator.evaluate(implicitEconomicContext);
2557
+ if (econResult.decision !== 'allow') {
2558
+ this.logger.warn('Economic authorization denied (implicit cost)', {
2559
+ toolName,
2560
+ decision: econResult.decision,
2561
+ reason: econResult.denial?.reason,
2562
+ cost: resolvedCost,
2563
+ });
2564
+ this.emitEconomicEvent(toolName, args, econResult, implicitEconomicContext);
2565
+ const result = {
2566
+ decision: this.mode === 'shadow' ? 'allow' : econResult.decision,
2567
+ reason: econResult.denial
2568
+ ? `Economic: ${econResult.denial.reason}`
2569
+ : 'Economic authorization denied',
2570
+ economicDenial: econResult.denial,
2571
+ shadow: this.mode === 'shadow' ? true : undefined,
2572
+ shadowDecision: this.mode === 'shadow' ? econResult.decision : undefined,
2573
+ };
2574
+ this.notifyDecisionMade(result, toolName);
2575
+ return result;
2576
+ }
2577
+ }
2578
+ }
2379
2579
  const customContext = {
2380
2580
  ...(context.custom ?? {}),
2381
2581
  };
@@ -2415,7 +2615,34 @@ export class Veto {
2415
2615
  this.historyTracker.record(toolName, args, validationResult, aggregatedResult.totalDurationMs);
2416
2616
  this.emitDecisionEvent(validationContext, validationResult);
2417
2617
  this.logClientDecision(validationContext, validationResult, aggregatedResult.totalDurationMs);
2418
- return this.toGuardResult(validationResult);
2618
+ // If behavioral rules allow and economic context exists, reserve budget.
2619
+ // Skip reservation in shadow mode — shadow should never deduct real budget.
2620
+ const behavioralResult = this.toGuardResult(validationResult);
2621
+ const effectiveEconomic = context.economic ?? implicitEconomicContext;
2622
+ if (behavioralResult.decision === 'allow'
2623
+ && this.economicEvaluator
2624
+ && effectiveEconomic
2625
+ && effectiveEconomic.cost > 0
2626
+ && this.mode !== 'shadow') {
2627
+ const reserveResult = this.economicEvaluator.reserveBudget(effectiveEconomic.cost, effectiveEconomic.currency);
2628
+ if (reserveResult.decision !== 'allow') {
2629
+ this.emitEconomicEvent(toolName, args, reserveResult, effectiveEconomic);
2630
+ const result = {
2631
+ ...behavioralResult,
2632
+ decision: reserveResult.decision,
2633
+ reason: reserveResult.denial
2634
+ ? `Economic: ${reserveResult.denial.reason}`
2635
+ : 'Budget reservation failed',
2636
+ economicDenial: reserveResult.denial,
2637
+ };
2638
+ this.notifyDecisionMade(result, toolName);
2639
+ return result;
2640
+ }
2641
+ // Emit spend_committed event on successful reservation
2642
+ this.emitEconomicEvent(toolName, args, reserveResult, effectiveEconomic, 'spend_committed');
2643
+ }
2644
+ this.notifyDecisionMade(behavioralResult, toolName);
2645
+ return behavioralResult;
2419
2646
  }
2420
2647
  /**
2421
2648
  * Cache an approval preference for a tool.
@@ -2512,6 +2739,9 @@ export class Veto {
2512
2739
  });
2513
2740
  });
2514
2741
  }, refreshIntervalMs);
2742
+ if (typeof this.refreshIntervalId === 'object' && this.refreshIntervalId !== null && 'unref' in this.refreshIntervalId) {
2743
+ this.refreshIntervalId.unref();
2744
+ }
2515
2745
  }
2516
2746
  /**
2517
2747
  * Get history statistics.
@@ -2537,6 +2767,19 @@ export class Veto {
2537
2767
  resetBudget() {
2538
2768
  this.budgetTracker?.reset();
2539
2769
  }
2770
+ /**
2771
+ * Get current economic budget status for a scope.
2772
+ * Returns null if no economic policy is configured.
2773
+ */
2774
+ getEconomicBudgetStatus(scope = 'session') {
2775
+ return this.economicBudgetEngine?.getStatus(scope) ?? null;
2776
+ }
2777
+ /**
2778
+ * Reset economic budget for a scope.
2779
+ */
2780
+ resetEconomicBudget(scope = 'session') {
2781
+ this.economicBudgetEngine?.reset(scope);
2782
+ }
2540
2783
  dispose() {
2541
2784
  if (this.refreshIntervalId) {
2542
2785
  clearInterval(this.refreshIntervalId);