@intutic/mcp-governance-proxy 2.2.0 → 2.4.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 (137) hide show
  1. package/README.md +8 -7
  2. package/dist/agentTools.d.ts +83 -0
  3. package/dist/agentTools.d.ts.map +1 -0
  4. package/dist/agentTools.js +170 -0
  5. package/dist/agentTools.js.map +1 -0
  6. package/dist/anomaly/detectors.js +1 -1
  7. package/dist/anomaly/detectors.js.map +1 -1
  8. package/dist/approvalHold.d.ts +8 -7
  9. package/dist/approvalHold.d.ts.map +1 -1
  10. package/dist/approvalHold.js +7 -6
  11. package/dist/approvalHold.js.map +1 -1
  12. package/dist/budget.d.ts +133 -0
  13. package/dist/budget.d.ts.map +1 -0
  14. package/dist/budget.js +191 -0
  15. package/dist/budget.js.map +1 -0
  16. package/dist/config.d.ts +1 -1
  17. package/dist/config.js +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/daemon/healthMonitor.d.ts.map +1 -1
  20. package/dist/daemon/healthMonitor.js +0 -1
  21. package/dist/daemon/healthMonitor.js.map +1 -1
  22. package/dist/daemon/index.d.ts +0 -2
  23. package/dist/daemon/index.d.ts.map +1 -1
  24. package/dist/daemon/index.js +0 -2
  25. package/dist/daemon/index.js.map +1 -1
  26. package/dist/daemon/policyCache.d.ts +22 -3
  27. package/dist/daemon/policyCache.d.ts.map +1 -1
  28. package/dist/daemon/policyCache.js +38 -15
  29. package/dist/daemon/policyCache.js.map +1 -1
  30. package/dist/daemon/socketServer.d.ts +0 -1
  31. package/dist/daemon/socketServer.d.ts.map +1 -1
  32. package/dist/daemon/socketServer.js +0 -1
  33. package/dist/daemon/socketServer.js.map +1 -1
  34. package/dist/daemon/statusReporter.d.ts.map +1 -1
  35. package/dist/daemon/statusReporter.js +0 -1
  36. package/dist/daemon/statusReporter.js.map +1 -1
  37. package/dist/daemon/telemetryBatcher.d.ts.map +1 -1
  38. package/dist/daemon/telemetryBatcher.js +0 -1
  39. package/dist/daemon/telemetryBatcher.js.map +1 -1
  40. package/dist/daemonClient.d.ts.map +1 -1
  41. package/dist/daemonClient.js +0 -1
  42. package/dist/daemonClient.js.map +1 -1
  43. package/dist/dlp.d.ts +32 -3
  44. package/dist/dlp.d.ts.map +1 -1
  45. package/dist/dlp.js +116 -18
  46. package/dist/dlp.js.map +1 -1
  47. package/dist/dlpPii.d.ts +66 -0
  48. package/dist/dlpPii.d.ts.map +1 -0
  49. package/dist/dlpPii.js +235 -0
  50. package/dist/dlpPii.js.map +1 -0
  51. package/dist/emitter.d.ts +65 -3
  52. package/dist/emitter.d.ts.map +1 -1
  53. package/dist/emitter.js +13 -1
  54. package/dist/emitter.js.map +1 -1
  55. package/dist/guardedValkey.d.ts +40 -0
  56. package/dist/guardedValkey.d.ts.map +1 -0
  57. package/dist/guardedValkey.js +84 -0
  58. package/dist/guardedValkey.js.map +1 -0
  59. package/dist/httpJson.d.ts +7 -0
  60. package/dist/httpJson.d.ts.map +1 -1
  61. package/dist/httpJson.js +39 -0
  62. package/dist/httpJson.js.map +1 -1
  63. package/dist/injection.js +1 -1
  64. package/dist/interceptor.d.ts +65 -10
  65. package/dist/interceptor.d.ts.map +1 -1
  66. package/dist/interceptor.js +190 -69
  67. package/dist/interceptor.js.map +1 -1
  68. package/dist/policy.d.ts +37 -1
  69. package/dist/policy.d.ts.map +1 -1
  70. package/dist/policy.js +62 -3
  71. package/dist/policy.js.map +1 -1
  72. package/dist/proxy.d.ts +30 -9
  73. package/dist/proxy.d.ts.map +1 -1
  74. package/dist/proxy.js +127 -57
  75. package/dist/proxy.js.map +1 -1
  76. package/dist/refusals.d.ts +26 -0
  77. package/dist/refusals.d.ts.map +1 -0
  78. package/dist/refusals.js +43 -0
  79. package/dist/refusals.js.map +1 -0
  80. package/dist/registryObserver.d.ts +9 -5
  81. package/dist/registryObserver.d.ts.map +1 -1
  82. package/dist/registryObserver.js +19 -8
  83. package/dist/registryObserver.js.map +1 -1
  84. package/dist/session.d.ts +1 -1
  85. package/dist/session.js +1 -1
  86. package/dist/sessionScope.d.ts +1 -1
  87. package/dist/sessionScope.js +1 -1
  88. package/dist/sessionStore.d.ts +10 -23
  89. package/dist/sessionStore.d.ts.map +1 -1
  90. package/dist/sessionStore.js +17 -76
  91. package/dist/sessionStore.js.map +1 -1
  92. package/dist/wasm/cloudRules.d.ts +138 -0
  93. package/dist/wasm/cloudRules.d.ts.map +1 -0
  94. package/dist/wasm/cloudRules.js +242 -0
  95. package/dist/wasm/cloudRules.js.map +1 -0
  96. package/dist/wasm/context.d.ts +10 -0
  97. package/dist/wasm/context.d.ts.map +1 -1
  98. package/dist/wasm/context.js +27 -0
  99. package/dist/wasm/context.js.map +1 -1
  100. package/dist/wasm/hostImports.d.ts +3 -3
  101. package/dist/wasm/hostImports.d.ts.map +1 -1
  102. package/dist/wasm/hostImports.js +8 -7
  103. package/dist/wasm/hostImports.js.map +1 -1
  104. package/dist/wasm/loader.d.ts +10 -1
  105. package/dist/wasm/loader.d.ts.map +1 -1
  106. package/dist/wasm/loader.js +2 -0
  107. package/dist/wasm/loader.js.map +1 -1
  108. package/dist/wasm/referencedFiles.d.ts +29 -4
  109. package/dist/wasm/referencedFiles.d.ts.map +1 -1
  110. package/dist/wasm/referencedFiles.js +48 -21
  111. package/dist/wasm/referencedFiles.js.map +1 -1
  112. package/dist/wasm/runner.d.ts +169 -31
  113. package/dist/wasm/runner.d.ts.map +1 -1
  114. package/dist/wasm/runner.js +298 -83
  115. package/dist/wasm/runner.js.map +1 -1
  116. package/dist/wasm/watch.js +1 -1
  117. package/dist/wasm/worker.d.ts +10 -6
  118. package/dist/wasm/worker.d.ts.map +1 -1
  119. package/dist/wasm/worker.js +93 -9
  120. package/dist/wasm/worker.js.map +1 -1
  121. package/package.json +5 -4
  122. package/dist/toolPoison.d.ts +0 -32
  123. package/dist/toolPoison.d.ts.map +0 -1
  124. package/dist/toolPoison.js +0 -81
  125. package/dist/toolPoison.js.map +0 -1
  126. package/dist/wasm/fuel.d.ts +0 -35
  127. package/dist/wasm/fuel.d.ts.map +0 -1
  128. package/dist/wasm/fuel.js +0 -299
  129. package/dist/wasm/fuel.js.map +0 -1
  130. package/dist/wasm/memoryCap.d.ts +0 -21
  131. package/dist/wasm/memoryCap.d.ts.map +0 -1
  132. package/dist/wasm/memoryCap.js +0 -54
  133. package/dist/wasm/memoryCap.js.map +0 -1
  134. package/dist/wasm/wasmBinary.d.ts +0 -25
  135. package/dist/wasm/wasmBinary.d.ts.map +0 -1
  136. package/dist/wasm/wasmBinary.js +0 -100
  137. package/dist/wasm/wasmBinary.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"httpJson.js","sourceRoot":"","sources":["../src/httpJson.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"httpJson.js","sourceRoot":"","sources":["../src/httpJson.ts"],"names":[],"mappings":"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"}
package/dist/injection.js CHANGED
@@ -65,7 +65,7 @@ const PATTERNS = [
65
65
  */
66
66
  const KILL_THRESHOLD = 2;
67
67
  /**
68
- * Workspace-supplied patterns, compiled (TD-436). Same contract as
68
+ * Workspace-supplied patterns, compiled. Same contract as
69
69
  * `dlp.ts`'s `setDynamicPatterns`: sources come from the control plane's
70
70
  * `mcpInjectionPatterns`, an invalid one is dropped and counted, and the
71
71
  * hardcoded floor above applies regardless — an unreachable control plane
@@ -5,6 +5,8 @@
5
5
  * 0. The MCP server registry, server/tool allowlists and SSO group clearance
6
6
  * 1. DLP scan (credential / destructive pattern detection)
7
7
  * 2. SOP policy rules (fetched from control plane via PolicyClient)
8
+ * 3–5. Prompt injection, anomaly detectors, WASM rules
9
+ * 6. MCP call budgets — last, so only a call that would otherwise run is counted
8
10
  *
9
11
  * Returns an allow / block / redact decision.
10
12
  *
@@ -16,25 +18,38 @@ import type { WasmRunner } from './wasm/runner.js';
16
18
  import type { PolicyClient } from './policy.js';
17
19
  import { type GovernanceEmitter } from './emitter.js';
18
20
  import type { ApprovalHolds } from './approvalHold.js';
19
- export type Decision = {
20
- action: 'allow';
21
- } | {
21
+ import { type McpBudgetEnforcer } from './budget.js';
22
+ import type { McpRefusalCode } from './refusals.js';
23
+ /**
24
+ * A refusal: what the agent reads (`reason`), and what a client reads — the
25
+ * stable `code`, the `ruleId` that decided, and any `detail` the code carries
26
+ * (refusals.ts). All three reach the JSON-RPC frame's `error.data`.
27
+ */
28
+ export interface Block {
22
29
  action: 'block';
23
30
  reason: string;
24
- } | {
31
+ code: Exclude<McpRefusalCode, 'HELD'>;
32
+ ruleId: string;
33
+ detail?: Record<string, unknown>;
34
+ }
35
+ export type Decision = {
36
+ action: 'allow';
37
+ } | Block | {
25
38
  action: 'redact';
26
39
  reason: string;
27
40
  redactedInput: unknown;
28
41
  }
29
42
  /**
30
43
  * Refused for now, pending a person's approval (`require_approval`). Not a
31
- * block: the agent is told the hold id and that an identical retry passes
32
- * once approved — see approvalHold.ts.
44
+ * block: the agent is told the hold id and to retry once it is approved,
45
+ * which passes only while the workspace's review-hold bypass is on — see
46
+ * approvalHold.ts. `holdId` is empty when the hold could not be recorded.
33
47
  */
34
48
  | {
35
49
  action: 'hold';
36
50
  reason: string;
37
51
  holdId: string;
52
+ ruleId: string;
38
53
  };
39
54
  export declare class ToolCallInterceptor {
40
55
  private readonly policy;
@@ -111,6 +126,13 @@ export declare class ToolCallInterceptor {
111
126
  * refusal — never an allow.
112
127
  */
113
128
  private readonly holds;
129
+ /**
130
+ * Counts each call against the workspace's MCP call budgets (budget.ts).
131
+ * `undefined` (construction sites that predate budgets) skips them; the
132
+ * proxy always passes one, with no store when it has no Valkey, so a
133
+ * covered call then resolves through the fail setting.
134
+ */
135
+ private readonly budgets;
114
136
  constructor(policy: PolicyClient, emitter: GovernanceEmitter,
115
137
  /**
116
138
  * The local fail setting (`INTUTIC_MCP_FAIL_OPEN`, config.ts). The
@@ -183,7 +205,16 @@ export declare class ToolCallInterceptor {
183
205
  * holds) leaves such a call held with nothing recorded, which is still a
184
206
  * refusal — never an allow.
185
207
  */
186
- holds?: ApprovalHolds | undefined);
208
+ holds?: ApprovalHolds | undefined,
209
+ /**
210
+ * Counts each call against the workspace's MCP call budgets (budget.ts).
211
+ * `undefined` (construction sites that predate budgets) skips them; the
212
+ * proxy always passes one, with no store when it has no Valkey, so a
213
+ * covered call then resolves through the fail setting.
214
+ */
215
+ budgets?: McpBudgetEnforcer | undefined);
216
+ /** Set once this process has said that budgets went unchecked under fail-open, so the log says it once. */
217
+ private budgetsUncheckedWarned;
187
218
  /**
188
219
  * Whether a governance check that cannot complete lets the call through:
189
220
  * the workspace's `mcpProxyFailBehavior` when the control plane has sent
@@ -203,9 +234,9 @@ export declare class ToolCallInterceptor {
203
234
  /**
204
235
  * A `require_approval` rule matched. Returns the hold decision, or `null`
205
236
  * when an approved bypass for this exact call lets it continue. Every hold
206
- * reason names the hold id and the command that approves it, the same
207
- * wording the hook gates print, so a person reading the agent's transcript
208
- * knows what to run.
237
+ * reason names the hold id, who may approve it and when a retry passes, in
238
+ * the words the hook gates print (`holdApprovalHint`), so a person reading
239
+ * the agent's transcript knows what to do.
209
240
  */
210
241
  private hold;
211
242
  /**
@@ -226,6 +257,18 @@ export declare class ToolCallInterceptor {
226
257
  * from an `allow` one without the registry.
227
258
  */
228
259
  private checkRegistry;
260
+ /**
261
+ * Brings the workspace's control-plane rules up to date with the policy, and
262
+ * refuses the call when they have never loaded and the proxy fails closed.
263
+ *
264
+ * Never loaded means no policy from the control plane yet, or one whose rule
265
+ * binaries could not be fetched: whether one of those rules refuses this
266
+ * call is unknown. Fail-open judges the call by the rules that are loaded —
267
+ * this machine's — as a workspace with no uploaded rules would be;
268
+ * fail-closed refuses it, as it does an unknown registry. Once loaded, the
269
+ * set is kept for as long as the process runs, through any later outage.
270
+ */
271
+ private checkCloudRulesLoaded;
229
272
  /**
230
273
  * The workspace's SSO group policy, applied to the member this proxy's API
231
274
  * key resolves to, by `evaluateSsoGroupClearance` — the function the
@@ -248,5 +291,17 @@ export declare class ToolCallInterceptor {
248
291
  * @returns Decision: allow, block, or redact
249
292
  */
250
293
  decide(toolName: string, toolInput: unknown): Promise<Decision>;
294
+ /**
295
+ * Counts the call against every budget that covers it, or refuses it when
296
+ * one is used up. The threshold and exceeded events go out once per budget
297
+ * per period (the store claims them), each as a `budget_breach` finding; a
298
+ * refusal also sends `tool_blocked`, as every refusal does, carrying the
299
+ * budget — the control plane files one incident per budget per period from
300
+ * those and counts the rest on it.
301
+ *
302
+ * When the count cannot be checked, the fail setting decides, as for every
303
+ * other governance check that cannot complete.
304
+ */
305
+ private checkBudgets;
251
306
  }
252
307
  //# sourceMappingURL=interceptor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,aAAa,CAAA;AACxD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAItD,MAAM,MAAM,QAAQ,GAChB;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE;AAC9D;;;;GAIG;GACD;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtD,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IACvC;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,uBAAuB;IACxC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAzEL,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,iBAAiB;IAC3C;;;;OAIG;IACc,aAAa,GAAE,OAAc;IAC9C;;;;;;OAMG;IACc,UAAU,GAAE,MAAkB;IAC/C;;;;;;;OAOG;IACc,sBAAsB,GAAE,MAAM,GAAG,OAAgB;IAClE;;;;;;;;OAQG;IACc,OAAO,GAAE,YAAiC;IAC3D;;;;;;;OAOG;IACc,kBAAkB,GAAE,WAAuB;IAC5D;;;;;;OAMG;IACc,uBAAuB,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAAC,CAAM;IAC5F;;;;;OAKG;IACc,UAAU,GAAE,UAAU,GAAG,SAAqB;IAC/D;;;;;;OAMG;IACc,WAAW,GAAE,MAAkB;IAChD;;;;;OAKG;IACc,KAAK,GAAE,aAAa,GAAG,SAAqB;IAG/D;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;;;;;OAOG;YACW,gBAAgB;IAmB9B;;;;;;OAMG;YACW,IAAI;IAwBlB;;;;;;;;;;;;;;;;OAgBG;YACW,aAAa;IAmC3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;;;;;OAMG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;CA8RtE"}
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAChE,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,aAAa,CAAA;AACxD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,EAAuE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACzH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAInD;;;;GAIG;AACH,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,OAAO,CAAC,cAAc,EAAE,MAAM,CAAC,CAAA;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,MAAM,QAAQ,GAChB;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB,KAAK,GACL;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE;AAC9D;;;;;GAKG;GACD;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAatE,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IACvC;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,uBAAuB;IACxC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAhFP,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,iBAAiB;IAC3C;;;;OAIG;IACc,aAAa,GAAE,OAAc;IAC9C;;;;;;OAMG;IACc,UAAU,GAAE,MAAkB;IAC/C;;;;;;;OAOG;IACc,sBAAsB,GAAE,MAAM,GAAG,OAAgB;IAClE;;;;;;;;OAQG;IACc,OAAO,GAAE,YAAiC;IAC3D;;;;;;;OAOG;IACc,kBAAkB,GAAE,WAAuB;IAC5D;;;;;;OAMG;IACc,uBAAuB,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAAC,CAAM;IAC5F;;;;;OAKG;IACc,UAAU,GAAE,UAAU,GAAG,SAAqB;IAC/D;;;;;;OAMG;IACc,WAAW,GAAE,MAAkB;IAChD;;;;;OAKG;IACc,KAAK,GAAE,aAAa,GAAG,SAAqB;IAC7D;;;;;OAKG;IACc,OAAO,GAAE,iBAAiB,GAAG,SAAqB;IAGrE,2GAA2G;IAC3G,OAAO,CAAC,sBAAsB,CAAQ;IAEtC;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;;;;;OAOG;YACW,gBAAgB;IAmB9B;;;;;;OAMG;YACW,IAAI;IAuBlB;;;;;;;;;;;;;;;;OAgBG;YACW,aAAa;IAyB3B;;;;;;;;;;OAUG;YACW,qBAAqB;IAYnC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;;;;;OAMG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqUrE;;;;;;;;;;OAUG;YACW,YAAY;CAwD3B"}
@@ -5,6 +5,8 @@
5
5
  * 0. The MCP server registry, server/tool allowlists and SSO group clearance
6
6
  * 1. DLP scan (credential / destructive pattern detection)
7
7
  * 2. SOP policy rules (fetched from control plane via PolicyClient)
8
+ * 3–5. Prompt injection, anomaly detectors, WASM rules
9
+ * 6. MCP call budgets — last, so only a call that would otherwise run is counted
8
10
  *
9
11
  * Returns an allow / block / redact decision.
10
12
  *
@@ -12,13 +14,22 @@
12
14
  */
13
15
  import * as node_crypto from 'node:crypto';
14
16
  import { createStderrLogger as createLogger } from './stderrLog.js';
15
- import { evaluateSsoGroupClearance } from '@intutic/shared-types';
16
- import { scanToolInput, formatDlpBlockReason, setDynamicPatterns } from './dlp.js';
17
+ import { evaluateMcpRegistry, evaluateSsoGroupClearance, holdApprovalHint } from '@intutic/shared-types';
18
+ import { scanToolInput, formatDlpBlockReason, setDynamicPatterns, setWorkspacePii } from './dlp.js';
17
19
  import { scanText, injectionSeverity, setDynamicInjectionPatterns } from './injection.js';
18
20
  import { evaluateSequenceDetectors, resolveEffectiveDisposition, REASK_MAX_ATTEMPTS } from './anomaly/index.js';
19
21
  import { SessionState } from './session.js';
20
22
  import { detectionFinding } from './emitter.js';
23
+ import { budgetEventDetail, exceededReason, unavailableReason, warningReason } from './budget.js';
21
24
  const log = createLogger('mcp-proxy-interceptor');
25
+ /** The decision for a refusal. */
26
+ function block(code, ruleId, reason, detail) {
27
+ return detail ? { action: 'block', reason, code, ruleId, detail } : { action: 'block', reason, code, ruleId };
28
+ }
29
+ /** Every governance check that cannot complete refuses with this, when the proxy fails closed. */
30
+ const FAIL_CLOSED_REASON = 'Governance check failed — Intutic control plane unreachable. ' +
31
+ 'Tool call blocked by workspace policy (fail-closed mode). ' +
32
+ 'Contact your administrator or update mcpProxyFailBehavior to open.';
22
33
  export class ToolCallInterceptor {
23
34
  policy;
24
35
  emitter;
@@ -31,6 +42,7 @@ export class ToolCallInterceptor {
31
42
  wasmRunner;
32
43
  workspaceId;
33
44
  holds;
45
+ budgets;
34
46
  constructor(policy, emitter,
35
47
  /**
36
48
  * The local fail setting (`INTUTIC_MCP_FAIL_OPEN`, config.ts). The
@@ -103,7 +115,14 @@ export class ToolCallInterceptor {
103
115
  * holds) leaves such a call held with nothing recorded, which is still a
104
116
  * refusal — never an allow.
105
117
  */
106
- holds = undefined) {
118
+ holds = undefined,
119
+ /**
120
+ * Counts each call against the workspace's MCP call budgets (budget.ts).
121
+ * `undefined` (construction sites that predate budgets) skips them; the
122
+ * proxy always passes one, with no store when it has no Valkey, so a
123
+ * covered call then resolves through the fail setting.
124
+ */
125
+ budgets = undefined) {
107
126
  this.policy = policy;
108
127
  this.emitter = emitter;
109
128
  this.localFailOpen = localFailOpen;
@@ -115,7 +134,10 @@ export class ToolCallInterceptor {
115
134
  this.wasmRunner = wasmRunner;
116
135
  this.workspaceId = workspaceId;
117
136
  this.holds = holds;
137
+ this.budgets = budgets;
118
138
  }
139
+ /** Set once this process has said that budgets went unchecked under fail-open, so the log says it once. */
140
+ budgetsUncheckedWarned = false;
119
141
  /**
120
142
  * Whether a governance check that cannot complete lets the call through:
121
143
  * the workspace's `mcpProxyFailBehavior` when the control plane has sent
@@ -142,19 +164,19 @@ export class ToolCallInterceptor {
142
164
  `attempts with no correction.`;
143
165
  log.warn({ action: 'reask_hardened', toolName, key, attempts }, 'Reask attempts exhausted — hardening to a hard block');
144
166
  this.emitter.emit('tool_blocked', toolName, toolInput, hardenedReason);
145
- return { action: 'block', reason: hardenedReason };
167
+ return block('REASK_EXHAUSTED', key, hardenedReason);
146
168
  }
147
169
  const reaskReason = `${baseReason} (attempt ${attempts}/${REASK_MAX_ATTEMPTS} — will become an unconditional ` +
148
170
  `block if this keeps tripping)`;
149
171
  this.emitter.emit('tool_blocked', toolName, toolInput, reaskReason);
150
- return { action: 'block', reason: reaskReason };
172
+ return block('REASK', key, reaskReason);
151
173
  }
152
174
  /**
153
175
  * A `require_approval` rule matched. Returns the hold decision, or `null`
154
176
  * when an approved bypass for this exact call lets it continue. Every hold
155
- * reason names the hold id and the command that approves it, the same
156
- * wording the hook gates print, so a person reading the agent's transcript
157
- * knows what to run.
177
+ * reason names the hold id, who may approve it and when a retry passes, in
178
+ * the words the hook gates print (`holdApprovalHint`), so a person reading
179
+ * the agent's transcript knows what to do.
158
180
  */
159
181
  async hold(rule, toolName, toolInput) {
160
182
  const outcome = this.holds
@@ -168,14 +190,13 @@ export class ToolCallInterceptor {
168
190
  }
169
191
  const reason = outcome.recorded
170
192
  ? `HELD for approval: ${rule.reason} [${rule.id}]. Hold id: ${outcome.holdId}. ` +
171
- `An approver can run: intutic decision approve ${outcome.holdId} (or reject it). ` +
172
- `Retry this exact call after it is approved.`
193
+ holdApprovalHint(outcome.holdId)
173
194
  : `HELD for approval: ${rule.reason} [${rule.id}], but the hold could not be recorded ` +
174
195
  `(Intutic control plane unreachable), so there is nothing to approve yet. Retry once the ` +
175
196
  `control plane is reachable to request approval.`;
176
197
  log.warn({ action: 'tool_held', toolName, ruleId: rule.id, holdId: outcome.holdId, recorded: outcome.recorded }, reason);
177
198
  this.emitter.emit('tool_held', toolName, toolInput, `${rule.reason} [${rule.id}]`);
178
- return { action: 'hold', reason, holdId: outcome.holdId };
199
+ return { action: 'hold', reason, holdId: outcome.recorded ? outcome.holdId : '', ruleId: rule.id };
179
200
  }
180
201
  /**
181
202
  * Refuses the call when the registry says this server — or this tool on it —
@@ -205,30 +226,40 @@ export class ToolCallInterceptor {
205
226
  `Tool call blocked (fail-closed mode: mcpProxyFailBehavior or INTUTIC_MCP_FAIL_OPEN=false).`;
206
227
  log.warn({ action: 'registry_unknown_block', serverName: this.serverName, toolName }, reason);
207
228
  this.emitter.emit('tool_blocked', toolName, toolInput, reason);
208
- return { action: 'block', reason };
209
- }
210
- let reason = null;
211
- if (registry.blockedServers.includes(this.serverName)) {
212
- reason =
213
- `MCP server "${this.serverName}" is blocked in this workspace's MCP server registry. ` +
214
- `An owner or admin can change that on the MCP Servers page.`;
215
- }
216
- else if (registry.defaultPolicy === 'deny' && !registry.approvedServers.includes(this.serverName)) {
217
- reason =
218
- `MCP server "${this.serverName}" is not approved in this workspace's MCP server registry, ` +
219
- `and the workspace refuses unapproved servers (mcpDefaultPolicy: deny). It is waiting in ` +
220
- `the approval queue on the MCP Servers page for an owner or admin.`;
229
+ return block('REGISTRY_UNAVAILABLE', 'mcpProxyFailBehavior', reason);
221
230
  }
222
- else if ((registry.disabledTools[this.serverName] ?? []).includes(toolName)) {
223
- reason =
224
- `Tool "${toolName}" is disabled on MCP server "${this.serverName}" in this workspace's ` +
225
- `MCP server registry. An owner or admin can re-enable it on the MCP Servers page.`;
226
- }
227
- if (!reason)
231
+ // The decision every gate makes (`@intutic/shared-types` mcpRegistryRecord.ts):
232
+ // the harness hook gates apply the same function, from the policy
233
+ // snapshot, to the servers no proxy fronts.
234
+ const decision = evaluateMcpRegistry(registry, this.serverName, toolName);
235
+ const refusal = decision ? block(decision.code, decision.ruleId, decision.reason) : null;
236
+ if (!refusal)
228
237
  return null;
229
- log.warn({ action: 'registry_block', serverName: this.serverName, toolName }, reason);
238
+ log.warn({ action: 'registry_block', serverName: this.serverName, toolName }, refusal.reason);
239
+ this.emitter.emit('tool_blocked', toolName, toolInput, refusal.reason);
240
+ return refusal;
241
+ }
242
+ /**
243
+ * Brings the workspace's control-plane rules up to date with the policy, and
244
+ * refuses the call when they have never loaded and the proxy fails closed.
245
+ *
246
+ * Never loaded means no policy from the control plane yet, or one whose rule
247
+ * binaries could not be fetched: whether one of those rules refuses this
248
+ * call is unknown. Fail-open judges the call by the rules that are loaded —
249
+ * this machine's — as a workspace with no uploaded rules would be;
250
+ * fail-closed refuses it, as it does an unknown registry. Once loaded, the
251
+ * set is kept for as long as the process runs, through any later outage.
252
+ */
253
+ async checkCloudRulesLoaded(runner, toolName, toolInput) {
254
+ await runner.syncCloudRules(this.policy.getWasmRules());
255
+ if (runner.cloudRulesLoaded() || this.failOpen)
256
+ return null;
257
+ const reason = `This workspace's custom rules have not loaded (Intutic control plane unreachable since this ` +
258
+ `proxy started), so whether one of them refuses this call is unknown. Tool call blocked ` +
259
+ `(fail-closed mode: mcpProxyFailBehavior or INTUTIC_MCP_FAIL_OPEN=false).`;
260
+ log.warn({ action: 'wasm_cloud_unknown_block', toolName }, reason);
230
261
  this.emitter.emit('tool_blocked', toolName, toolInput, reason);
231
- return { action: 'block', reason };
262
+ return block('GOVERNANCE_UNAVAILABLE', 'mcpProxyFailBehavior', reason);
232
263
  }
233
264
  /**
234
265
  * The workspace's SSO group policy, applied to the member this proxy's API
@@ -256,7 +287,7 @@ export class ToolCallInterceptor {
256
287
  const reason = `${decision.reason} [${decision.ruleId}]`;
257
288
  log.warn({ action: 'sso_group_block', toolName, memberId: principal?.memberId ?? null, ruleId: decision.ruleId }, reason);
258
289
  this.emitter.emit('tool_blocked', toolName, toolInput, reason);
259
- return { action: 'block', reason };
290
+ return block('SSO_GROUP', decision.ruleId ?? 'sso_group', reason);
260
291
  }
261
292
  /**
262
293
  * Evaluate a tools/call request and return a governance decision.
@@ -299,7 +330,7 @@ export class ToolCallInterceptor {
299
330
  `allowlist in workspace settings (mcpAllowedServers).`;
300
331
  log.warn({ action: 'server_allowlist_block', serverName: this.serverName, toolName }, reason);
301
332
  this.emitter.emit('tool_blocked', toolName, toolInput, reason);
302
- return { action: 'block', reason };
333
+ return block('SERVER_NOT_ALLOWED', 'mcpAllowedServers', reason);
303
334
  }
304
335
  // 0. Additive tool scoping. When the workspace declares an allowlist,
305
336
  // ONLY those tools may be called — the inverse of every other rule here,
@@ -314,7 +345,7 @@ export class ToolCallInterceptor {
314
345
  `allowlist in workspace settings (mcpAllowedTools).`;
315
346
  log.warn({ action: 'allowlist_block', toolName }, reason);
316
347
  this.emitter.emit('tool_blocked', toolName, toolInput, reason);
317
- return { action: 'block', reason };
348
+ return block('TOOL_NOT_ALLOWED', 'mcpAllowedTools', reason);
318
349
  }
319
350
  // 0.5. SSO group clearance, after the scoping checks and before DLP.
320
351
  const clearance = this.checkSsoGroupClearance(toolName, toolInput);
@@ -328,6 +359,21 @@ export class ToolCallInterceptor {
328
359
  catch {
329
360
  // Pattern delivery must never take the scanner down; the floor stands.
330
361
  }
362
+ // The workspace's PII detector actions, the baseline this machine's
363
+ // INTUTIC_MCP_DLP_DETECTORS may only tighten. When the control plane
364
+ // could not read them, which detectors the workspace requires is
365
+ // unknown: fail-closed refuses the call, as the LLM proxy refuses the
366
+ // request; fail-open scans with the local config alone.
367
+ const piiDetectors = this.policy.getPiiDetectors();
368
+ if (piiDetectors.kind === 'unreadable' && !this.failOpen) {
369
+ const reason = `This workspace's PII detector actions could not be read (${piiDetectors.reason}), so which ` +
370
+ `detectors apply is unknown. Tool call blocked (fail-closed mode: mcpProxyFailBehavior or ` +
371
+ `INTUTIC_MCP_FAIL_OPEN=false).`;
372
+ log.warn({ action: 'pii_detectors_unreadable_block', toolName }, reason);
373
+ this.emitter.emit('tool_blocked', toolName, toolInput, reason);
374
+ return block('GOVERNANCE_UNAVAILABLE', 'piiDetectors', reason);
375
+ }
376
+ setWorkspacePii(piiDetectors);
331
377
  try {
332
378
  const dlp = scanToolInput(toolInput);
333
379
  // Captured regardless of outcome — by pipeline position, a non-empty
@@ -339,19 +385,13 @@ export class ToolCallInterceptor {
339
385
  const reason = formatDlpBlockReason(dlp.findings);
340
386
  log.warn({ action: 'dlp_block', toolName, findings: dlp.findings }, 'DLP block');
341
387
  this.emitter.emit('tool_blocked', toolName, toolInput, reason);
342
- return { action: 'block', reason };
388
+ return block('DLP', `dlp.${dlp.findings[0]?.pattern ?? 'input'}`, reason);
343
389
  }
344
390
  }
345
391
  catch (err) {
346
392
  log.error({ action: 'dlp_error', err: err.message }, 'DLP scan error — skipping');
347
- if (!this.failOpen) {
348
- return {
349
- action: 'block',
350
- reason: 'Governance check failed — Intutic control plane unreachable. ' +
351
- 'Tool call blocked by workspace policy (fail-closed mode). ' +
352
- 'Contact your administrator or update mcpProxyFailBehavior to open.',
353
- };
354
- }
393
+ if (!this.failOpen)
394
+ return block('GOVERNANCE_UNAVAILABLE', 'mcpProxyFailBehavior', FAIL_CLOSED_REASON);
355
395
  }
356
396
  // 2. SOP policy rule match
357
397
  try {
@@ -361,13 +401,13 @@ export class ToolCallInterceptor {
361
401
  if (rule.action === 'block') {
362
402
  log.warn({ action: 'policy_block', toolName, ruleId: rule.id, reason: rule.reason }, 'Policy block');
363
403
  this.emitter.emit('tool_blocked', toolName, toolInput, rule.reason);
364
- return { action: 'block', reason: rule.reason };
404
+ return block('SOP_RULE', rule.id, rule.reason);
365
405
  }
366
406
  if (rule.action === 'warn') {
367
407
  log.warn({ action: 'policy_warn', toolName, ruleId: rule.id, reason: rule.reason }, 'Policy warning (allowing)');
368
408
  // Fall through to allow. Reported as `tool_flagged` with the rule id
369
409
  // in the reason — the same shape the harness gates use — so a
370
- // SHADOW guardrail's evidence (LLD #71) counts this proxy's traffic.
410
+ // SHADOW guardrail's evidence counts this proxy's traffic.
371
411
  this.emitter.emit('tool_flagged', toolName, toolInput, `${rule.reason} [${rule.id}]`);
372
412
  }
373
413
  if (rule.action === 'require_approval') {
@@ -380,14 +420,8 @@ export class ToolCallInterceptor {
380
420
  }
381
421
  catch (err) {
382
422
  log.error({ action: 'policy_error', err: err.message }, 'Policy evaluation error');
383
- if (!this.failOpen) {
384
- return {
385
- action: 'block',
386
- reason: 'Governance check failed — Intutic control plane unreachable. ' +
387
- 'Tool call blocked by workspace policy (fail-closed mode). ' +
388
- 'Contact your administrator or update mcpProxyFailBehavior to open.',
389
- };
390
- }
423
+ if (!this.failOpen)
424
+ return block('GOVERNANCE_UNAVAILABLE', 'mcpProxyFailBehavior', FAIL_CLOSED_REASON);
391
425
  }
392
426
  // 3. Prompt-injection scan (request direction) — the `toolInput` a
393
427
  // MISBEHAVING or COMPROMISED calling agent sends, e.g. an agent that
@@ -416,7 +450,7 @@ export class ToolCallInterceptor {
416
450
  });
417
451
  if (injectionAction === 'block') {
418
452
  this.emitter.emit('tool_blocked', toolName, toolInput, reason);
419
- return { action: 'block', reason };
453
+ return block('INJECTION', 'injection.tool_input', reason);
420
454
  }
421
455
  // 'warn' (default): report only, fall through to allow.
422
456
  }
@@ -441,7 +475,7 @@ export class ToolCallInterceptor {
441
475
  // built) and allow.
442
476
  const anomalyMode = this.policy.getAnomalyMode() ?? this.anomalyModeDefault;
443
477
  // The session window — the shared one across this session's sibling
444
- // proxy processes when a store is configured (Wave 5.3, TD-437), the
478
+ // proxy processes when a store is configured (Wave 5.3), the
445
479
  // per-process one otherwise. One round trip at most per tools/call, and
446
480
  // none when nothing below would read it.
447
481
  const window = anomalyMode !== 'off' || this.wasmRunner ? await this.session.loadWindow(toolName) : undefined;
@@ -467,7 +501,7 @@ export class ToolCallInterceptor {
467
501
  this.emitter.emit('anomaly_detected', toolName, toolInput, finding.reason, detectionFinding(finding, effective, severity));
468
502
  if (effective === 'kill') {
469
503
  this.emitter.emit('tool_blocked', toolName, toolInput, finding.reason);
470
- return { action: 'block', reason: finding.reason };
504
+ return block('ANOMALY', finding.detectorId, finding.reason);
471
505
  }
472
506
  if (effective === 'reask') {
473
507
  // Keyed per-detector-id, independent of every other detector's
@@ -488,14 +522,19 @@ export class ToolCallInterceptor {
488
522
  log.error({ action: 'anomaly_detection_error', err: err.message }, 'Anomaly detection error — skipping');
489
523
  }
490
524
  }
491
- // 5. WASM custom rules (Phase 3) — operator-authored, compiled
492
- // AssemblyScript rules dropped into `~/.intutic/wasm/`. Position: after
493
- // every built-in check, immediately before allow — this is deliberately
494
- // the LAST gate, so a WASM rule's `RequestContext` carries
525
+ // 5. WASM custom rules (Phase 3) — operator-authored rules, native or
526
+ // Rego: the workspace's, uploaded to the control plane, and this
527
+ // machine's, dropped into `~/.intutic/wasm/`. Position: after every
528
+ // built-in check, immediately before allow — this is deliberately the
529
+ // LAST gate, so a WASM rule's `RequestContext` carries
495
530
  // `injection_findings`/`corroborating_detectors`/etc. already populated
496
531
  // by the steps above it (see `wasm/context.ts`'s module doc).
497
532
  if (this.wasmRunner) {
533
+ const unloaded = await this.checkCloudRulesLoaded(this.wasmRunner, toolName, toolInput);
534
+ if (unloaded)
535
+ return unloaded;
498
536
  try {
537
+ const shadow = [];
499
538
  const verdict = await this.wasmRunner.evaluate({
500
539
  sessionId: this.session.sessionId,
501
540
  workspaceId: this.workspaceId,
@@ -510,30 +549,112 @@ export class ToolCallInterceptor {
510
549
  injectionSources: injectionSourcesForContext,
511
550
  corroboratingDetectors: corroboratingDetectorsForContext,
512
551
  toolContractChanged: this.session.getToolContractChanged(),
513
- });
552
+ serverName: this.serverName,
553
+ }, shadow);
554
+ // Whatever the call's outcome: the shadowed rules' evaluations are
555
+ // promotion evidence whether or not another rule refused the call.
556
+ if (shadow.length > 0) {
557
+ this.emitter.emit('wasm_shadow_evaluated', toolName, toolInput, undefined, undefined, undefined, { wasmShadowReports: shadow });
558
+ }
559
+ if (verdict.code === 'unavailable') {
560
+ // A rule reached no verdict: refused whatever `failOpen` says (see
561
+ // `WasmRunner.evaluate`). Reported like any refusal, except a
562
+ // quarantined rule's: the call that quarantined it was reported
563
+ // already, and one event per refused call after it would file an
564
+ // incident per retry.
565
+ log.warn({ action: 'wasm_unavailable_block', toolName, ruleId: verdict.ruleId, stop: verdict.stop }, verdict.reason);
566
+ if (verdict.stop !== 'quarantined')
567
+ this.emitter.emit('tool_blocked', toolName, toolInput, verdict.reason);
568
+ return block('GOVERNANCE_UNAVAILABLE', `wasm:${verdict.ruleId}`, verdict.reason);
569
+ }
514
570
  if (verdict.code === 'block') {
515
571
  log.warn({ action: 'wasm_block', toolName, ruleId: verdict.ruleId }, 'Tool call blocked by WASM governance rule');
516
572
  this.emitter.emit('tool_blocked', toolName, toolInput, verdict.reason);
517
- return { action: 'block', reason: verdict.reason };
573
+ return block('WASM_RULE', `wasm:${verdict.ruleId}`, verdict.reason);
518
574
  }
519
575
  if (verdict.code === 'reask') {
520
576
  // Keyed per-rule-id, independent of every anomaly detector's own
521
577
  // counter — see applyReaskLadder's doc comment.
522
578
  return await this.applyReaskLadder(`wasm:${verdict.ruleId}`, verdict.reason, toolName, toolInput);
523
579
  }
580
+ if (verdict.code === 'hold') {
581
+ // A Rego rule's hold takes the `require_approval` path: the same
582
+ // approved-bypass lookup and hold record, keyed on the rule id.
583
+ const held = await this.hold({ id: verdict.ruleId, reason: verdict.reason }, toolName, toolInput);
584
+ if (held)
585
+ return held;
586
+ // An approved bypass: the remaining checks still apply.
587
+ }
524
588
  // 'allow': fall through.
525
589
  }
526
590
  catch (err) {
527
- // Fail-open, matching every other governance-check catch in this
528
- // method — a WASM runner failure (not to be confused with a single
529
- // rule's own timeout/trap, which `WasmRunner.evaluate` already
530
- // absorbs internally) must not take the whole proxy down.
531
- log.error({ action: 'wasm_evaluate_error', err: err.message }, 'WASM rule evaluation error — skipping');
591
+ // The runner itself failed (not one rule's timeout or trap, which
592
+ // `WasmRunner.evaluate` turns into a verdict): no rule judged the
593
+ // call, so it is refused like a rule that reached no verdict, whatever
594
+ // the fail setting.
595
+ log.error({ action: 'wasm_evaluate_error', err: err.message }, 'WASM rule evaluation error');
596
+ return block('GOVERNANCE_UNAVAILABLE', 'wasm', `Custom rules could not be evaluated: ${err.message}. Tool call blocked: a rule ` +
597
+ `that cannot decide never allows.`);
532
598
  }
533
599
  }
534
- // 6. Allow — emit telemetry event
600
+ // 6. MCP call budgets — after every other check, so a call another check
601
+ // refuses never spends allowance.
602
+ const budgetDecision = await this.checkBudgets(toolName, toolInput);
603
+ if (budgetDecision)
604
+ return budgetDecision;
605
+ // 7. Allow — emit telemetry event
535
606
  this.emitter.emit('tool_allowed', toolName, toolInput);
536
607
  return { action: 'allow' };
537
608
  }
609
+ /**
610
+ * Counts the call against every budget that covers it, or refuses it when
611
+ * one is used up. The threshold and exceeded events go out once per budget
612
+ * per period (the store claims them), each as a `budget_breach` finding; a
613
+ * refusal also sends `tool_blocked`, as every refusal does, carrying the
614
+ * budget — the control plane files one incident per budget per period from
615
+ * those and counts the rest on it.
616
+ *
617
+ * When the count cannot be checked, the fail setting decides, as for every
618
+ * other governance check that cannot complete.
619
+ */
620
+ async checkBudgets(toolName, toolInput) {
621
+ if (!this.budgets)
622
+ return null;
623
+ const policy = this.policy.getMcpBudgets();
624
+ const verdict = await this.budgets.check(policy, toolName, this.policy.getPrincipal()?.memberId ?? null);
625
+ if (verdict.kind === 'unlimited')
626
+ return null;
627
+ if (verdict.kind === 'allowed') {
628
+ for (const standing of verdict.warnings) {
629
+ const reason = warningReason(standing, policy.warnAtPct);
630
+ log.warn({ action: 'mcp_budget_threshold', toolName, budgetId: standing.budget.id, used: standing.used }, reason);
631
+ this.emitter.emit('mcp_budget_threshold', toolName, undefined, reason, { detectorId: 'budget', kind: 'budget_breach', disposition: 'steer', severity: 'low', confidence: 1 }, budgetEventDetail(standing));
632
+ }
633
+ return null;
634
+ }
635
+ if (verdict.kind === 'exceeded') {
636
+ const reason = exceededReason(verdict.standing);
637
+ log.warn({ action: 'mcp_budget_exceeded', toolName, budgetId: verdict.standing.budget.id, notify: verdict.notify }, reason);
638
+ if (verdict.notify) {
639
+ this.emitter.emit('mcp_budget_exceeded', toolName, undefined, reason, { detectorId: 'budget', kind: 'budget_breach', disposition: 'kill', severity: 'medium', confidence: 1 }, budgetEventDetail(verdict.standing));
640
+ }
641
+ const detail = budgetEventDetail(verdict.standing);
642
+ this.emitter.emit('tool_blocked', toolName, toolInput, reason, undefined, detail);
643
+ return block('BUDGET_EXCEEDED', detail.budgetId, reason, { ...detail });
644
+ }
645
+ // Unavailable: no Valkey configured, or it did not answer.
646
+ if (this.failOpen) {
647
+ if (!this.budgetsUncheckedWarned) {
648
+ this.budgetsUncheckedWarned = true;
649
+ log.warn({ action: 'mcp_budget_unchecked', budgets: verdict.budgets.map((b) => b.id) }, 'MCP call budgets cover calls through this proxy but could not be checked (no Valkey, or unreachable); ' +
650
+ 'allowing them uncounted (fail-open)');
651
+ }
652
+ return null;
653
+ }
654
+ const reason = unavailableReason(verdict.budgets);
655
+ log.warn({ action: 'mcp_budget_unchecked_block', toolName }, reason);
656
+ this.emitter.emit('tool_blocked', toolName, toolInput, reason);
657
+ return block('BUDGET_UNAVAILABLE', 'mcpProxyFailBehavior', reason, { budgetIds: verdict.budgets.map((b) => b.id) });
658
+ }
538
659
  }
539
660
  //# sourceMappingURL=interceptor.js.map