@ai-sdk/policy-opa 0.0.0 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -0
- package/LICENSE +13 -0
- package/README.md +636 -0
- package/dist/index.d.ts +362 -0
- package/dist/index.js +295 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -1
- package/src/index.ts +19 -0
- package/src/opa/evaluate-policy.ts +24 -0
- package/src/opa/http-policy-client.ts +57 -0
- package/src/opa/normalize-opa-decision.ts +56 -0
- package/src/opa/opa-capability-middleware.ts +130 -0
- package/src/opa/opa-policy.ts +152 -0
- package/src/opa/test-helpers.ts +14 -0
- package/src/opa/wasm-policy-client.ts +78 -0
- package/src/policy-client.ts +20 -0
- package/src/policy-decision.ts +13 -0
- package/src/shadow.ts +180 -0
- package/src/wrap-mcp-tools.ts +97 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { Tool, Context, ToolSet, ModelMessage, InferToolSetContext } from '@ai-sdk/provider-utils';
|
|
2
|
+
import { ToolApprovalConfiguration, ToolApprovalStatus } from 'ai';
|
|
3
|
+
import { LanguageModelV4CallOptions, LanguageModelV4Middleware } from '@ai-sdk/provider';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A generic client for evaluating a policy decision against some external
|
|
7
|
+
* engine (OPA, Cedar, OpenFGA, a remote HTTP rule service, etc.).
|
|
8
|
+
*
|
|
9
|
+
* Each adapter in this package wraps a backend behind this interface so that
|
|
10
|
+
* the rest of the package (and user code) can switch engines without changing
|
|
11
|
+
* call sites.
|
|
12
|
+
*/
|
|
13
|
+
interface PolicyClient {
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate the given input against the policy identified by `path` and
|
|
16
|
+
* return the decision payload the engine emitted. Adapters are responsible
|
|
17
|
+
* for interpreting that payload. For OPA, `opaPolicy` normalizes it into
|
|
18
|
+
* the SDK's `ToolApprovalStatus` shape.
|
|
19
|
+
*/
|
|
20
|
+
evaluate<TInput = unknown, TResult = unknown>(path: string, input: TInput): Promise<TResult>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Narrowed object form of the SDK's `ToolApprovalStatus`.
|
|
25
|
+
*
|
|
26
|
+
* The public `ToolApprovalStatus` from `ai` allows both bare strings
|
|
27
|
+
* (`'approved'`) and the matching object form (`{ type: 'approved' }`). This
|
|
28
|
+
* package normalizes everything to the object form internally so callers like
|
|
29
|
+
* `shadow` can rely on a single discriminant.
|
|
30
|
+
*/
|
|
31
|
+
type PolicyDecision = {
|
|
32
|
+
type: 'approved';
|
|
33
|
+
reason?: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'denied';
|
|
36
|
+
reason?: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'user-approval';
|
|
39
|
+
} | {
|
|
40
|
+
type: 'not-applicable';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Event emitted by {@link shadow} every time the wrapped policy is evaluated.
|
|
45
|
+
*
|
|
46
|
+
* Compare `decision` and `effective` to see what would have happened
|
|
47
|
+
* differently if the policy were enforcing: any record where they disagree
|
|
48
|
+
* is a tool call that shadow mode is letting through but enforce mode would
|
|
49
|
+
* have blocked or escalated.
|
|
50
|
+
*/
|
|
51
|
+
interface PolicyDecisionEvent {
|
|
52
|
+
/** Identifying info from the tool call being evaluated. */
|
|
53
|
+
toolCall: {
|
|
54
|
+
toolName: string;
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
input: unknown;
|
|
57
|
+
};
|
|
58
|
+
/** The decision the wrapped policy returned, normalized to the object form. */
|
|
59
|
+
decision: PolicyDecision;
|
|
60
|
+
/** Whether the SDK will act on `decision` (true) or override to allow (false). */
|
|
61
|
+
enforced: boolean;
|
|
62
|
+
/** The decision the SDK actually acts on. Equals `decision` when enforcing,
|
|
63
|
+
* `{ type: 'approved' }` in shadow mode. */
|
|
64
|
+
effective: PolicyDecision;
|
|
65
|
+
/** ISO 8601 timestamp of the evaluation. */
|
|
66
|
+
timestamp: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Wrap a `toolApproval` in shadow mode so the policy is evaluated and the
|
|
70
|
+
* decision is reported via `onDecision`, but the SDK is told the call is
|
|
71
|
+
* approved regardless of what the policy said.
|
|
72
|
+
*
|
|
73
|
+
* Use this when you are rolling out a new policy and want to see what it
|
|
74
|
+
* *would* deny before letting it actually deny anything in production. Wire
|
|
75
|
+
* `onDecision` to your logger / metrics pipeline, run for a while, inspect
|
|
76
|
+
* the events where `decision.type !== 'approved'`, fix the policy, then
|
|
77
|
+
* flip `enforce: true` to graduate.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import { shadow } from '@ai-sdk/policy-opa';
|
|
82
|
+
* import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
|
|
83
|
+
*
|
|
84
|
+
* const client = await wasmPolicyClient({ wasm });
|
|
85
|
+
*
|
|
86
|
+
* const toolApproval = shadow(
|
|
87
|
+
* opaPolicy({ client, path: 'agent/call/decision' }),
|
|
88
|
+
* {
|
|
89
|
+
* enforce: process.env.ENFORCE_POLICY === 'true',
|
|
90
|
+
* onDecision: (event) => {
|
|
91
|
+
* logger.info('policy.decision', {
|
|
92
|
+
* tool: event.toolCall.toolName,
|
|
93
|
+
* decision: event.decision.type,
|
|
94
|
+
* enforced: event.enforced,
|
|
95
|
+
* wouldBlock: event.decision.type === 'denied',
|
|
96
|
+
* });
|
|
97
|
+
* },
|
|
98
|
+
* },
|
|
99
|
+
* );
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare function shadow<TOOLS extends Record<string, Tool>, RUNTIME_CONTEXT extends Context | unknown | never = unknown>(approval: ToolApprovalConfiguration<TOOLS & ToolSet, RUNTIME_CONTEXT>, opts?: {
|
|
103
|
+
/** When true, the SDK acts on the policy's decision. When false (default),
|
|
104
|
+
* the SDK is told every call is approved. */
|
|
105
|
+
enforce?: boolean;
|
|
106
|
+
/** Invoked once per evaluated tool call with the captured decision. */
|
|
107
|
+
onDecision?: (event: PolicyDecisionEvent) => void | Promise<void>;
|
|
108
|
+
}): ToolApprovalConfiguration<TOOLS & ToolSet, RUNTIME_CONTEXT>;
|
|
109
|
+
|
|
110
|
+
type ApprovalLiteralStatus = Extract<ToolApprovalStatus, string>;
|
|
111
|
+
/**
|
|
112
|
+
* Result returned by {@link wrapMcpTools}: the original tool set plus a
|
|
113
|
+
* `toolApproval` that falls back to a configurable default for any tool the
|
|
114
|
+
* supplied approval does not explicitly handle.
|
|
115
|
+
*/
|
|
116
|
+
interface WrappedMcpTools<TOOLS extends Record<string, Tool>, RUNTIME_CONTEXT extends Context | unknown | never> {
|
|
117
|
+
tools: TOOLS;
|
|
118
|
+
toolApproval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Apply a fallback approval policy to a discovered tool set so the resulting
|
|
122
|
+
* configuration is total: every tool in `tools` is gated either by the
|
|
123
|
+
* supplied `approval` (when it has an opinion) or by `default` (otherwise).
|
|
124
|
+
*
|
|
125
|
+
* The motivating case is MCP. An MCP server hands you whatever tools it
|
|
126
|
+
* exposes, and the agent's effective permissions become the union of the
|
|
127
|
+
* full server surface. Without a fallback, any tool you forgot to write a
|
|
128
|
+
* rule for is silently allowed. This helper closes that gap by forcing every
|
|
129
|
+
* uncovered tool through `default` (which defaults to `user-approval` so the
|
|
130
|
+
* human is in the loop for anything you didn't think about).
|
|
131
|
+
*
|
|
132
|
+
* Works for any tool set, not just MCP-discovered tools. The name reflects
|
|
133
|
+
* the primary use case rather than a hard constraint.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* import { wrapMcpTools } from '@ai-sdk/policy-opa';
|
|
138
|
+
* import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
|
|
139
|
+
*
|
|
140
|
+
* const discovered = await mcpClient.tools();
|
|
141
|
+
* const client = await wasmPolicyClient({ wasm });
|
|
142
|
+
*
|
|
143
|
+
* const { tools, toolApproval } = wrapMcpTools(
|
|
144
|
+
* discovered,
|
|
145
|
+
* opaPolicy({ client, path: 'agent/call/decision' }),
|
|
146
|
+
* { default: 'user-approval' }, // anything OPA does not match needs a human
|
|
147
|
+
* );
|
|
148
|
+
*
|
|
149
|
+
* await generateText({ model, tools, toolApproval, prompt });
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
declare function wrapMcpTools<TOOLS extends Record<string, Tool>, RUNTIME_CONTEXT extends Context | unknown | never = unknown>(tools: TOOLS, approval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>, opts?: {
|
|
153
|
+
default?: ApprovalLiteralStatus;
|
|
154
|
+
}): WrappedMcpTools<TOOLS, RUNTIME_CONTEXT>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Construct a {@link PolicyClient} that talks to a running OPA server over
|
|
158
|
+
* HTTP using `@open-policy-agent/opa`.
|
|
159
|
+
*
|
|
160
|
+
* The `@open-policy-agent/opa` package is an optional peer dependency; install
|
|
161
|
+
* it before using this client:
|
|
162
|
+
*
|
|
163
|
+
* ```sh
|
|
164
|
+
* pnpm add @open-policy-agent/opa
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* The `url` typically points at `http://localhost:8181` for a locally running
|
|
168
|
+
* OPA. `headers` is forwarded for Styra DAS / EOPA authentication.
|
|
169
|
+
*/
|
|
170
|
+
declare function httpPolicyClient(opts: {
|
|
171
|
+
url: string;
|
|
172
|
+
headers?: Record<string, string>;
|
|
173
|
+
}): PolicyClient;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Normalize an OPA evaluation result into the package's {@link PolicyDecision}
|
|
177
|
+
* shape.
|
|
178
|
+
*
|
|
179
|
+
* Supports two Rego output conventions:
|
|
180
|
+
*
|
|
181
|
+
* - **Recommended (explicit):** `{ "decision": "allow" | "deny" | "requires-approval", "reason": string }`.
|
|
182
|
+
* Maps to `approved` / `denied` / `user-approval` respectively.
|
|
183
|
+
*
|
|
184
|
+
* - **Legacy (boolean):** `{ "allow": boolean, "reason"?: string }`. `true`
|
|
185
|
+
* maps to `approved`, `false` to `denied`.
|
|
186
|
+
*
|
|
187
|
+
* Unknown shapes and `undefined` are treated as `not-applicable` so that a
|
|
188
|
+
* Rego rule that does not match any branch defaults to "no opinion" rather
|
|
189
|
+
* than blocking.
|
|
190
|
+
*/
|
|
191
|
+
declare function normalizeOpaDecision(result: unknown): PolicyDecision;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Default OPA input shape passed to the capability-scoping rule.
|
|
195
|
+
* Override with `toInput` if your Rego expects a different schema.
|
|
196
|
+
*/
|
|
197
|
+
interface DefaultOpaCapabilityInput {
|
|
198
|
+
messages: LanguageModelV4CallOptions['prompt'];
|
|
199
|
+
providerOptions: LanguageModelV4CallOptions['providerOptions'];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Construct an experimental {@link LanguageModelV4Middleware} that narrows
|
|
203
|
+
* the `tools` field on every model call to an allowlist returned by OPA.
|
|
204
|
+
*
|
|
205
|
+
* Why use this alongside the per-call `toolApproval` gate? Two reasons:
|
|
206
|
+
*
|
|
207
|
+
* 1. **Defense in depth.** If a bug or regression slips through the policy
|
|
208
|
+
* used by `toolApproval`, the middleware still prevents the model from
|
|
209
|
+
* seeing the disallowed tools in the first place.
|
|
210
|
+
* 2. **Capability disclosure.** The model does not waste tokens describing
|
|
211
|
+
* tools it cannot call, and jailbreak attempts get "I don't have access
|
|
212
|
+
* to that tool" rather than "[approval denied]".
|
|
213
|
+
*
|
|
214
|
+
* The OPA rule at `path` is expected to return either a `string[]` of allowed
|
|
215
|
+
* tool names, or an object `{ tools: string[] }`. Function tools whose `name`
|
|
216
|
+
* matches are kept; provider tools whose dotted `id` or bare `name` matches are
|
|
217
|
+
* kept; everything else is dropped from `params.tools`.
|
|
218
|
+
*
|
|
219
|
+
* On a malformed result (or an OPA error), the middleware **fails closed**:
|
|
220
|
+
* `params.tools` is set to `undefined`, so the model is told it has no tools
|
|
221
|
+
* available. Misconfiguration should not silently widen capabilities.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* import { wrapLanguageModel } from 'ai';
|
|
226
|
+
* import { wasmPolicyClient, opaCapabilityMiddleware } from '@ai-sdk/policy-opa';
|
|
227
|
+
*
|
|
228
|
+
* const client = await wasmPolicyClient({ wasm });
|
|
229
|
+
*
|
|
230
|
+
* const wrappedModel = wrapLanguageModel({
|
|
231
|
+
* model: anthropic('claude-sonnet-4-5'),
|
|
232
|
+
* middleware: opaCapabilityMiddleware({
|
|
233
|
+
* client,
|
|
234
|
+
* path: 'agent/tools/allowed',
|
|
235
|
+
* }),
|
|
236
|
+
* });
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
declare function opaCapabilityMiddleware(opts: {
|
|
240
|
+
client: PolicyClient;
|
|
241
|
+
path: string;
|
|
242
|
+
toInput?: (args: {
|
|
243
|
+
messages: LanguageModelV4CallOptions['prompt'];
|
|
244
|
+
providerOptions: LanguageModelV4CallOptions['providerOptions'];
|
|
245
|
+
}) => unknown;
|
|
246
|
+
}): LanguageModelV4Middleware;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The default shape passed to the OPA rule as `input` when no `toInput` is
|
|
250
|
+
* supplied. Rego rules can read `input.tool.name`, `input.args`, and so on.
|
|
251
|
+
*/
|
|
252
|
+
interface DefaultOpaInput {
|
|
253
|
+
tool: {
|
|
254
|
+
name: string;
|
|
255
|
+
};
|
|
256
|
+
args: unknown;
|
|
257
|
+
messages: ReadonlyArray<ModelMessage>;
|
|
258
|
+
runtimeContext: unknown;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Construct a {@link ToolApprovalConfiguration} backed by an OPA policy.
|
|
262
|
+
*
|
|
263
|
+
* The returned generic approval function evaluates the supplied Rego entry
|
|
264
|
+
* (`path`) for every tool call and maps the result to the SDK's approval
|
|
265
|
+
* status via {@link normalizeOpaDecision}. Pass the result directly as
|
|
266
|
+
* `toolApproval` on `generateText` / `streamText` / `ToolLoopAgent`.
|
|
267
|
+
*
|
|
268
|
+
* ```ts
|
|
269
|
+
* import { wasmPolicyClient } from '@ai-sdk/policy-opa';
|
|
270
|
+
* import { opaPolicy } from '@ai-sdk/policy-opa';
|
|
271
|
+
*
|
|
272
|
+
* const client = await wasmPolicyClient({ wasm });
|
|
273
|
+
* const toolApproval = opaPolicy({ client, path: 'agent/call/decision' });
|
|
274
|
+
*
|
|
275
|
+
* await generateText({ model, tools, toolApproval, prompt });
|
|
276
|
+
* ```
|
|
277
|
+
*
|
|
278
|
+
* @param opts.client The OPA client (HTTP or WASM).
|
|
279
|
+
* @param opts.path The Rego entrypoint that returns the decision object.
|
|
280
|
+
* @param opts.toInput Optional transformer to shape the OPA input.
|
|
281
|
+
*/
|
|
282
|
+
declare function opaPolicy<TOOLS extends ToolSet = ToolSet, RUNTIME_CONTEXT extends Context | unknown | never = unknown>(opts: {
|
|
283
|
+
client: PolicyClient;
|
|
284
|
+
path: string;
|
|
285
|
+
toInput?: (args: {
|
|
286
|
+
toolCall: {
|
|
287
|
+
toolName: string;
|
|
288
|
+
toolCallId: string;
|
|
289
|
+
input: unknown;
|
|
290
|
+
};
|
|
291
|
+
tools: TOOLS | undefined;
|
|
292
|
+
toolsContext: InferToolSetContext<TOOLS>;
|
|
293
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
294
|
+
messages: ModelMessage[];
|
|
295
|
+
}) => unknown;
|
|
296
|
+
}): ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>;
|
|
297
|
+
/**
|
|
298
|
+
* Optional variant of {@link opaPolicy} that gracefully degrades when no
|
|
299
|
+
* policy backend is available.
|
|
300
|
+
*
|
|
301
|
+
* Returns `undefined` when `client` is `undefined`. Pass that directly as
|
|
302
|
+
* `toolApproval` and the SDK falls back to its default allow-all behavior.
|
|
303
|
+
* When `client` is supplied, behaves exactly like {@link opaPolicy}.
|
|
304
|
+
*
|
|
305
|
+
* Use this when the policy file is configured per environment (e.g. loaded
|
|
306
|
+
* in production, absent in local development):
|
|
307
|
+
*
|
|
308
|
+
* ```ts
|
|
309
|
+
* import { readFile } from 'node:fs/promises';
|
|
310
|
+
* import { optionalOpaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
|
|
311
|
+
*
|
|
312
|
+
* const wasm = process.env.POLICY_WASM_PATH
|
|
313
|
+
* ? await readFile(process.env.POLICY_WASM_PATH)
|
|
314
|
+
* : undefined;
|
|
315
|
+
* const client = wasm ? await wasmPolicyClient({ wasm }) : undefined;
|
|
316
|
+
*
|
|
317
|
+
* const toolApproval = optionalOpaPolicy({
|
|
318
|
+
* client,
|
|
319
|
+
* path: 'agent/call/decision',
|
|
320
|
+
* });
|
|
321
|
+
*
|
|
322
|
+
* await generateText({ model, tools, toolApproval, prompt });
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
declare function optionalOpaPolicy<TOOLS extends ToolSet = ToolSet, RUNTIME_CONTEXT extends Context | unknown | never = unknown>(opts: {
|
|
326
|
+
client: PolicyClient | undefined;
|
|
327
|
+
path: string;
|
|
328
|
+
toInput?: (args: {
|
|
329
|
+
toolCall: {
|
|
330
|
+
toolName: string;
|
|
331
|
+
toolCallId: string;
|
|
332
|
+
input: unknown;
|
|
333
|
+
};
|
|
334
|
+
tools: TOOLS | undefined;
|
|
335
|
+
toolsContext: InferToolSetContext<TOOLS>;
|
|
336
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
337
|
+
messages: ModelMessage[];
|
|
338
|
+
}) => unknown;
|
|
339
|
+
}): ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> | undefined;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Construct a {@link PolicyClient} that evaluates a compiled OPA WASM bundle
|
|
343
|
+
* in-process using `@open-policy-agent/opa-wasm`.
|
|
344
|
+
*
|
|
345
|
+
* The `@open-policy-agent/opa-wasm` package is an optional peer dependency;
|
|
346
|
+
* install it before using this client:
|
|
347
|
+
*
|
|
348
|
+
* ```sh
|
|
349
|
+
* pnpm add @open-policy-agent/opa-wasm
|
|
350
|
+
* ```
|
|
351
|
+
*
|
|
352
|
+
* The `path` argument to `evaluate(path, input)` is informational. The WASM
|
|
353
|
+
* bundle is built around a fixed entrypoint at `opa build` time. The path is
|
|
354
|
+
* recorded for audit logs but does not affect the evaluation.
|
|
355
|
+
*/
|
|
356
|
+
declare function wasmPolicyClient(opts: {
|
|
357
|
+
wasm: Uint8Array | ArrayBuffer;
|
|
358
|
+
/** Optional data document bundled into the policy (passed to `setData`). */
|
|
359
|
+
data?: unknown;
|
|
360
|
+
}): Promise<PolicyClient>;
|
|
361
|
+
|
|
362
|
+
export { type DefaultOpaCapabilityInput, type DefaultOpaInput, type PolicyClient, type PolicyDecision, type PolicyDecisionEvent, type WrappedMcpTools, httpPolicyClient, normalizeOpaDecision, opaCapabilityMiddleware, opaPolicy, optionalOpaPolicy, shadow, wasmPolicyClient, wrapMcpTools };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// src/shadow.ts
|
|
2
|
+
function shadow(approval, opts = {}) {
|
|
3
|
+
const enforce = opts.enforce === true;
|
|
4
|
+
const { onDecision } = opts;
|
|
5
|
+
const wrapped = async (args) => {
|
|
6
|
+
const raw = await evaluateApproval(approval, args);
|
|
7
|
+
const decision = normalizePolicyDecision(raw);
|
|
8
|
+
const effective = enforce ? decision : { type: "approved" };
|
|
9
|
+
if (onDecision) {
|
|
10
|
+
const event = {
|
|
11
|
+
toolCall: {
|
|
12
|
+
toolName: args.toolCall.toolName,
|
|
13
|
+
toolCallId: args.toolCall.toolCallId,
|
|
14
|
+
input: args.toolCall.input
|
|
15
|
+
},
|
|
16
|
+
decision,
|
|
17
|
+
enforced: enforce,
|
|
18
|
+
effective,
|
|
19
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20
|
+
};
|
|
21
|
+
void (async () => {
|
|
22
|
+
try {
|
|
23
|
+
await onDecision(event);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
}
|
|
28
|
+
return effective;
|
|
29
|
+
};
|
|
30
|
+
return wrapped;
|
|
31
|
+
}
|
|
32
|
+
async function evaluateApproval(approval, args) {
|
|
33
|
+
if (typeof approval === "function") {
|
|
34
|
+
return await approval(args);
|
|
35
|
+
}
|
|
36
|
+
const map = approval;
|
|
37
|
+
const perTool = map[args.toolCall.toolName];
|
|
38
|
+
if (perTool == null) return void 0;
|
|
39
|
+
if (typeof perTool === "function") {
|
|
40
|
+
return await perTool(args.toolCall.input, {
|
|
41
|
+
toolCallId: args.toolCall.toolCallId,
|
|
42
|
+
messages: args.messages,
|
|
43
|
+
toolContext: void 0,
|
|
44
|
+
runtimeContext: args.runtimeContext
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return perTool;
|
|
48
|
+
}
|
|
49
|
+
function normalizePolicyDecision(status) {
|
|
50
|
+
if (status == null) return { type: "not-applicable" };
|
|
51
|
+
if (typeof status === "string") {
|
|
52
|
+
if (status === "approved" || status === "denied" || status === "user-approval" || status === "not-applicable") {
|
|
53
|
+
return { type: status };
|
|
54
|
+
}
|
|
55
|
+
return { type: "not-applicable" };
|
|
56
|
+
}
|
|
57
|
+
if (typeof status === "object" && "type" in status) {
|
|
58
|
+
return status;
|
|
59
|
+
}
|
|
60
|
+
return { type: "not-applicable" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/wrap-mcp-tools.ts
|
|
64
|
+
function wrapMcpTools(tools, approval, opts) {
|
|
65
|
+
var _a, _b;
|
|
66
|
+
const fallback = (_a = opts == null ? void 0 : opts.default) != null ? _a : "user-approval";
|
|
67
|
+
if (typeof approval === "function") {
|
|
68
|
+
const wrapped = async (args) => {
|
|
69
|
+
const status = await approval(args);
|
|
70
|
+
return isNotApplicable(status) ? fallback : status;
|
|
71
|
+
};
|
|
72
|
+
return { tools, toolApproval: wrapped };
|
|
73
|
+
}
|
|
74
|
+
const filled = {};
|
|
75
|
+
for (const name of Object.keys(tools)) {
|
|
76
|
+
filled[name] = (_b = approval[name]) != null ? _b : fallback;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
tools,
|
|
80
|
+
toolApproval: filled
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function isNotApplicable(status) {
|
|
84
|
+
if (status == null) return true;
|
|
85
|
+
if (status === "not-applicable") return true;
|
|
86
|
+
if (typeof status === "object" && status !== null) {
|
|
87
|
+
return status.type === "not-applicable";
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/opa/http-policy-client.ts
|
|
93
|
+
function httpPolicyClient(opts) {
|
|
94
|
+
const { url, headers } = opts;
|
|
95
|
+
let underlying;
|
|
96
|
+
async function getUnderlying() {
|
|
97
|
+
if (underlying) return underlying;
|
|
98
|
+
let mod;
|
|
99
|
+
try {
|
|
100
|
+
mod = await import("@open-policy-agent/opa");
|
|
101
|
+
} catch (cause) {
|
|
102
|
+
throw Object.assign(
|
|
103
|
+
new Error(
|
|
104
|
+
'Cannot import "@open-policy-agent/opa". Install it as a peer dependency to use httpPolicyClient().'
|
|
105
|
+
),
|
|
106
|
+
{ cause }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
underlying = new mod.OPAClient(url, headers ? { headers } : void 0);
|
|
110
|
+
return underlying;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
async evaluate(path, input) {
|
|
114
|
+
const client = await getUnderlying();
|
|
115
|
+
return await client.evaluate(path, input);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/opa/normalize-opa-decision.ts
|
|
121
|
+
function normalizeOpaDecision(result) {
|
|
122
|
+
if (result == null) {
|
|
123
|
+
return { type: "not-applicable" };
|
|
124
|
+
}
|
|
125
|
+
if (typeof result !== "object") {
|
|
126
|
+
return { type: "not-applicable" };
|
|
127
|
+
}
|
|
128
|
+
const record = result;
|
|
129
|
+
const reason = typeof record.reason === "string" ? record.reason : void 0;
|
|
130
|
+
if (typeof record.decision === "string") {
|
|
131
|
+
switch (record.decision) {
|
|
132
|
+
case "allow":
|
|
133
|
+
return withReason("approved", reason);
|
|
134
|
+
case "deny":
|
|
135
|
+
return withReason("denied", reason);
|
|
136
|
+
case "requires-approval":
|
|
137
|
+
return { type: "user-approval" };
|
|
138
|
+
case "not-applicable":
|
|
139
|
+
return { type: "not-applicable" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (typeof record.allow === "boolean") {
|
|
143
|
+
return withReason(record.allow ? "approved" : "denied", reason);
|
|
144
|
+
}
|
|
145
|
+
return { type: "not-applicable" };
|
|
146
|
+
}
|
|
147
|
+
function withReason(type, reason) {
|
|
148
|
+
return reason ? { type, reason } : { type };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/opa/evaluate-policy.ts
|
|
152
|
+
async function evaluatePolicy(client, path, input) {
|
|
153
|
+
try {
|
|
154
|
+
return { ok: true, result: await client.evaluate(path, input) };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { ok: false, error };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/opa/opa-capability-middleware.ts
|
|
161
|
+
function opaCapabilityMiddleware(opts) {
|
|
162
|
+
const { client, path, toInput } = opts;
|
|
163
|
+
return {
|
|
164
|
+
specificationVersion: "v4",
|
|
165
|
+
async transformParams({ params }) {
|
|
166
|
+
var _a;
|
|
167
|
+
if (params.tools == null || params.tools.length === 0) {
|
|
168
|
+
return params;
|
|
169
|
+
}
|
|
170
|
+
const input = (_a = toInput == null ? void 0 : toInput({
|
|
171
|
+
messages: params.prompt,
|
|
172
|
+
providerOptions: params.providerOptions
|
|
173
|
+
})) != null ? _a : {
|
|
174
|
+
messages: params.prompt,
|
|
175
|
+
providerOptions: params.providerOptions
|
|
176
|
+
};
|
|
177
|
+
const outcome = await evaluatePolicy(client, path, input);
|
|
178
|
+
if (!outcome.ok) {
|
|
179
|
+
return { ...params, tools: void 0 };
|
|
180
|
+
}
|
|
181
|
+
const allowed = extractAllowedNameSet(outcome.result);
|
|
182
|
+
if (allowed == null) {
|
|
183
|
+
return { ...params, tools: void 0 };
|
|
184
|
+
}
|
|
185
|
+
let removed = false;
|
|
186
|
+
const filtered = params.tools.filter((t) => {
|
|
187
|
+
const keep = t.type === "function" ? allowed.has(t.name) : allowed.has(t.id) || allowed.has(t.name);
|
|
188
|
+
if (!keep) removed = true;
|
|
189
|
+
return keep;
|
|
190
|
+
});
|
|
191
|
+
if (!removed) return params;
|
|
192
|
+
return { ...params, tools: filtered };
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function extractAllowedNameSet(result) {
|
|
197
|
+
const list = Array.isArray(result) ? result : result != null && typeof result === "object" ? result.tools : void 0;
|
|
198
|
+
if (!Array.isArray(list)) return null;
|
|
199
|
+
const out = /* @__PURE__ */ new Set();
|
|
200
|
+
for (const item of list) {
|
|
201
|
+
if (typeof item !== "string") return null;
|
|
202
|
+
out.add(item);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/opa/opa-policy.ts
|
|
208
|
+
function opaPolicy(opts) {
|
|
209
|
+
const { client, path, toInput } = opts;
|
|
210
|
+
return async ({
|
|
211
|
+
toolCall,
|
|
212
|
+
tools,
|
|
213
|
+
toolsContext,
|
|
214
|
+
runtimeContext,
|
|
215
|
+
messages
|
|
216
|
+
}) => {
|
|
217
|
+
var _a;
|
|
218
|
+
const opaInput = (_a = toInput == null ? void 0 : toInput({ toolCall, tools, toolsContext, runtimeContext, messages })) != null ? _a : {
|
|
219
|
+
tool: { name: toolCall.toolName },
|
|
220
|
+
args: toolCall.input,
|
|
221
|
+
messages,
|
|
222
|
+
runtimeContext
|
|
223
|
+
};
|
|
224
|
+
const outcome = await evaluatePolicy(client, path, opaInput);
|
|
225
|
+
if (!outcome.ok) {
|
|
226
|
+
return {
|
|
227
|
+
type: "denied",
|
|
228
|
+
reason: `policy evaluation failed: ${errorMessage(outcome.error)}`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return normalizeOpaDecision(outcome.result);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function errorMessage(cause) {
|
|
235
|
+
if (cause instanceof Error) return cause.message;
|
|
236
|
+
if (typeof cause === "object" && cause !== null) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.stringify(cause);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return String(cause);
|
|
243
|
+
}
|
|
244
|
+
function optionalOpaPolicy(opts) {
|
|
245
|
+
if (opts.client == null) return void 0;
|
|
246
|
+
return opaPolicy({
|
|
247
|
+
client: opts.client,
|
|
248
|
+
path: opts.path,
|
|
249
|
+
toInput: opts.toInput
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/opa/wasm-policy-client.ts
|
|
254
|
+
async function wasmPolicyClient(opts) {
|
|
255
|
+
let mod;
|
|
256
|
+
try {
|
|
257
|
+
mod = await import("@open-policy-agent/opa-wasm");
|
|
258
|
+
} catch (cause) {
|
|
259
|
+
throw Object.assign(
|
|
260
|
+
new Error(
|
|
261
|
+
'Cannot import "@open-policy-agent/opa-wasm". Install it as a peer dependency to use wasmPolicyClient().'
|
|
262
|
+
),
|
|
263
|
+
{ cause }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const policy = await mod.loadPolicy(opts.wasm);
|
|
267
|
+
if (opts.data !== void 0 && typeof policy.setData === "function") {
|
|
268
|
+
policy.setData(opts.data);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
async evaluate(_path, input) {
|
|
272
|
+
const results = policy.evaluate(input);
|
|
273
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
274
|
+
throw Object.assign(
|
|
275
|
+
new Error(
|
|
276
|
+
"OPA WASM policy produced no result. Check that the bundle was built with the correct entrypoint (`opa build -t wasm -e <path>`)."
|
|
277
|
+
),
|
|
278
|
+
{ input }
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return results[0].result;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
export {
|
|
286
|
+
httpPolicyClient,
|
|
287
|
+
normalizeOpaDecision,
|
|
288
|
+
opaCapabilityMiddleware,
|
|
289
|
+
opaPolicy,
|
|
290
|
+
optionalOpaPolicy,
|
|
291
|
+
shadow,
|
|
292
|
+
wasmPolicyClient,
|
|
293
|
+
wrapMcpTools
|
|
294
|
+
};
|
|
295
|
+
//# sourceMappingURL=index.js.map
|