@ai-sdk/policy-opa 1.0.122 → 1.0.123

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/dist/index.js CHANGED
@@ -1,312 +1,468 @@
1
- // src/shadow.ts
1
+ //#region src/shadow.ts
2
+ /**
3
+ * Wrap a `toolApproval` in shadow mode so the policy is evaluated and the
4
+ * decision is reported via `onDecision`, but the SDK is told the call is
5
+ * approved regardless of what the policy said.
6
+ *
7
+ * Use this when you are rolling out a new policy and want to see what it
8
+ * *would* deny before letting it actually deny anything in production. Wire
9
+ * `onDecision` to your logger / metrics pipeline, run for a while, inspect
10
+ * the events where `decision.type !== 'approved'`, fix the policy, then
11
+ * flip `enforce: true` to graduate.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { shadow } from '@ai-sdk/policy-opa';
16
+ * import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
17
+ *
18
+ * const client = await wasmPolicyClient({ wasm });
19
+ *
20
+ * const toolApproval = shadow(
21
+ * opaPolicy({ client, path: 'agent/call/decision' }),
22
+ * {
23
+ * enforce: process.env.ENFORCE_POLICY === 'true',
24
+ * onDecision: (event) => {
25
+ * logger.info('policy.decision', {
26
+ * tool: event.toolCall.toolName,
27
+ * decision: event.decision.type,
28
+ * enforced: event.enforced,
29
+ * wouldBlock: event.decision.type === 'denied',
30
+ * });
31
+ * },
32
+ * },
33
+ * );
34
+ * ```
35
+ */
2
36
  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 {
25
- }
26
- })();
27
- }
28
- return effective;
29
- };
30
- return wrapped;
37
+ const enforce = opts.enforce === true;
38
+ const { onDecision } = opts;
39
+ const wrapped = async (args) => {
40
+ const decision = normalizePolicyDecision(await evaluateApproval(approval, args));
41
+ const effective = enforce ? decision : { type: "approved" };
42
+ if (onDecision) {
43
+ const event = {
44
+ toolCall: {
45
+ toolName: args.toolCall.toolName,
46
+ toolCallId: args.toolCall.toolCallId,
47
+ input: args.toolCall.input
48
+ },
49
+ decision,
50
+ enforced: enforce,
51
+ effective,
52
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
53
+ };
54
+ (async () => {
55
+ try {
56
+ await onDecision(event);
57
+ } catch {}
58
+ })();
59
+ }
60
+ return effective;
61
+ };
62
+ return wrapped;
31
63
  }
32
64
  async function evaluateApproval(approval, args) {
33
- if (typeof approval === "function") {
34
- return await approval(args);
35
- }
36
- const map = approval;
37
- const perTool = Object.prototype.hasOwnProperty.call(
38
- map,
39
- args.toolCall.toolName
40
- ) ? map[args.toolCall.toolName] : void 0;
41
- if (perTool == null) return void 0;
42
- if (typeof perTool === "function") {
43
- return await perTool(args.toolCall.input, {
44
- toolCallId: args.toolCall.toolCallId,
45
- messages: args.messages,
46
- toolContext: void 0,
47
- runtimeContext: args.runtimeContext
48
- });
49
- }
50
- return perTool;
65
+ if (typeof approval === "function") return await approval(args);
66
+ const map = approval;
67
+ const perTool = Object.prototype.hasOwnProperty.call(map, args.toolCall.toolName) ? map[args.toolCall.toolName] : void 0;
68
+ if (perTool == null) return void 0;
69
+ if (typeof perTool === "function") return await perTool(args.toolCall.input, {
70
+ toolCallId: args.toolCall.toolCallId,
71
+ messages: args.messages,
72
+ toolContext: void 0,
73
+ runtimeContext: args.runtimeContext
74
+ });
75
+ return perTool;
51
76
  }
52
77
  function normalizePolicyDecision(status) {
53
- if (status == null) return { type: "not-applicable" };
54
- if (typeof status === "string") {
55
- if (status === "approved" || status === "denied" || status === "user-approval" || status === "not-applicable") {
56
- return { type: status };
57
- }
58
- return { type: "not-applicable" };
59
- }
60
- if (typeof status === "object" && "type" in status) {
61
- return status;
62
- }
63
- return { type: "not-applicable" };
78
+ if (status == null) return { type: "not-applicable" };
79
+ if (typeof status === "string") {
80
+ if (status === "approved" || status === "denied" || status === "user-approval" || status === "not-applicable") return { type: status };
81
+ return { type: "not-applicable" };
82
+ }
83
+ if (typeof status === "object" && "type" in status) return status;
84
+ return { type: "not-applicable" };
64
85
  }
65
-
66
- // src/wrap-mcp-tools.ts
86
+ //#endregion
87
+ //#region src/wrap-mcp-tools.ts
88
+ /**
89
+ * Apply a fallback approval policy to a discovered tool set so the resulting
90
+ * configuration is total: every tool in `tools` is gated either by the
91
+ * supplied `approval` (when it has an opinion) or by `default` (otherwise).
92
+ *
93
+ * The motivating case is MCP. An MCP server hands you whatever tools it
94
+ * exposes, and the agent's effective permissions become the union of the
95
+ * full server surface. Without a fallback, any tool you forgot to write a
96
+ * rule for is silently allowed. This helper closes that gap by forcing every
97
+ * uncovered tool through `default` (which defaults to `user-approval` so the
98
+ * human is in the loop for anything you didn't think about).
99
+ *
100
+ * Works for any tool set, not just MCP-discovered tools. The name reflects
101
+ * the primary use case rather than a hard constraint.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * import { wrapMcpTools } from '@ai-sdk/policy-opa';
106
+ * import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
107
+ *
108
+ * const discovered = await mcpClient.tools();
109
+ * const client = await wasmPolicyClient({ wasm });
110
+ *
111
+ * const { tools, toolApproval } = wrapMcpTools(
112
+ * discovered,
113
+ * opaPolicy({ client, path: 'agent/call/decision' }),
114
+ * { default: 'user-approval' }, // anything OPA does not match needs a human
115
+ * );
116
+ *
117
+ * await generateText({ model, tools, toolApproval, prompt });
118
+ * ```
119
+ */
67
120
  function wrapMcpTools(tools, approval, opts) {
68
- const fallback = opts?.default ?? "user-approval";
69
- if (typeof approval === "function") {
70
- const wrapped = async (args) => {
71
- const status = await approval(args);
72
- return isNotApplicable(status) ? fallback : status;
73
- };
74
- return { tools, toolApproval: wrapped };
75
- }
76
- const filled = /* @__PURE__ */ Object.create(null);
77
- for (const name of Object.keys(tools)) {
78
- const configured = Object.prototype.hasOwnProperty.call(approval, name) ? approval[name] : void 0;
79
- if (configured == null) {
80
- filled[name] = fallback;
81
- } else if (typeof configured === "function") {
82
- const original = configured;
83
- filled[name] = async (...args) => {
84
- const status = await original(...args);
85
- return isNotApplicable(status) ? fallback : status;
86
- };
87
- } else {
88
- filled[name] = configured;
89
- }
90
- }
91
- return {
92
- tools,
93
- toolApproval: filled
94
- };
121
+ const fallback = opts?.default ?? "user-approval";
122
+ if (typeof approval === "function") {
123
+ const wrapped = async (args) => {
124
+ const status = await approval(args);
125
+ return isNotApplicable(status) ? fallback : status;
126
+ };
127
+ return {
128
+ tools,
129
+ toolApproval: wrapped
130
+ };
131
+ }
132
+ const filled = Object.create(null);
133
+ for (const name of Object.keys(tools)) {
134
+ const configured = Object.prototype.hasOwnProperty.call(approval, name) ? approval[name] : void 0;
135
+ if (configured == null) filled[name] = fallback;
136
+ else if (typeof configured === "function") {
137
+ const original = configured;
138
+ filled[name] = async (...args) => {
139
+ const status = await original(...args);
140
+ return isNotApplicable(status) ? fallback : status;
141
+ };
142
+ } else filled[name] = configured;
143
+ }
144
+ return {
145
+ tools,
146
+ toolApproval: filled
147
+ };
95
148
  }
96
149
  function isNotApplicable(status) {
97
- if (status == null) return true;
98
- if (status === "not-applicable") return true;
99
- if (typeof status === "object" && status !== null) {
100
- return status.type === "not-applicable";
101
- }
102
- return false;
150
+ if (status == null) return true;
151
+ if (status === "not-applicable") return true;
152
+ if (typeof status === "object" && status !== null) return status.type === "not-applicable";
153
+ return false;
103
154
  }
104
-
105
- // src/opa/http-policy-client.ts
155
+ //#endregion
156
+ //#region src/opa/http-policy-client.ts
157
+ /**
158
+ * Construct a {@link PolicyClient} that talks to a running OPA server over
159
+ * HTTP using `@open-policy-agent/opa`.
160
+ *
161
+ * The `@open-policy-agent/opa` package is an optional peer dependency; install
162
+ * it before using this client:
163
+ *
164
+ * ```sh
165
+ * pnpm add @open-policy-agent/opa
166
+ * ```
167
+ *
168
+ * The `url` typically points at `http://localhost:8181` for a locally running
169
+ * OPA. `headers` is forwarded for Styra DAS / EOPA authentication.
170
+ */
106
171
  function httpPolicyClient(opts) {
107
- const { url, headers } = opts;
108
- let underlying;
109
- async function getUnderlying() {
110
- if (underlying) return underlying;
111
- let mod;
112
- try {
113
- mod = await import("@open-policy-agent/opa");
114
- } catch (cause) {
115
- throw Object.assign(
116
- new Error(
117
- 'Cannot import "@open-policy-agent/opa". Install it as a peer dependency to use httpPolicyClient().'
118
- ),
119
- { cause }
120
- );
121
- }
122
- underlying = new mod.OPAClient(url, headers ? { headers } : void 0);
123
- return underlying;
124
- }
125
- return {
126
- async evaluate(path, input) {
127
- const client = await getUnderlying();
128
- return await client.evaluate(path, input);
129
- }
130
- };
172
+ const { url, headers } = opts;
173
+ let underlying;
174
+ async function getUnderlying() {
175
+ if (underlying) return underlying;
176
+ let mod;
177
+ try {
178
+ mod = await import("@open-policy-agent/opa");
179
+ } catch (cause) {
180
+ throw Object.assign(/* @__PURE__ */ new Error("Cannot import \"@open-policy-agent/opa\". Install it as a peer dependency to use httpPolicyClient()."), { cause });
181
+ }
182
+ underlying = new mod.OPAClient(url, headers ? { headers } : void 0);
183
+ return underlying;
184
+ }
185
+ return { async evaluate(path, input) {
186
+ return await (await getUnderlying()).evaluate(path, input);
187
+ } };
131
188
  }
132
-
133
- // src/opa/normalize-opa-decision.ts
189
+ //#endregion
190
+ //#region src/opa/normalize-opa-decision.ts
191
+ /**
192
+ * Normalize an OPA evaluation result into the package's {@link PolicyDecision}
193
+ * shape.
194
+ *
195
+ * Supports two Rego output conventions:
196
+ *
197
+ * - **Recommended (explicit):** `{ "decision": "allow" | "deny" | "requires-approval", "reason": string }`.
198
+ * Maps to `approved` / `denied` / `user-approval` respectively.
199
+ *
200
+ * - **Legacy (boolean):** `{ "allow": boolean, "reason"?: string }`. `true`
201
+ * maps to `approved`, `false` to `denied`.
202
+ *
203
+ * `null` and `undefined` are treated as `not-applicable` so that a Rego rule
204
+ * that does not match any branch defaults to "no opinion" rather than
205
+ * blocking. Any other unrecognized result is denied so malformed policy
206
+ * output cannot silently bypass the approval gate.
207
+ */
134
208
  function normalizeOpaDecision(result) {
135
- if (result == null) {
136
- return { type: "not-applicable" };
137
- }
138
- if (typeof result !== "object") {
139
- return unrecognizedDecision();
140
- }
141
- const record = result;
142
- const reason = typeof record.reason === "string" ? record.reason : void 0;
143
- if (typeof record.decision === "string") {
144
- switch (record.decision) {
145
- case "allow":
146
- return withReason("approved", reason);
147
- case "deny":
148
- return withReason("denied", reason);
149
- case "requires-approval":
150
- return withReason("user-approval", reason);
151
- case "not-applicable":
152
- return { type: "not-applicable" };
153
- }
154
- }
155
- if (typeof record.allow === "boolean") {
156
- return withReason(record.allow ? "approved" : "denied", reason);
157
- }
158
- return unrecognizedDecision();
209
+ if (result == null) return { type: "not-applicable" };
210
+ if (typeof result !== "object") return unrecognizedDecision();
211
+ const record = result;
212
+ const reason = typeof record.reason === "string" ? record.reason : void 0;
213
+ if (typeof record.decision === "string") switch (record.decision) {
214
+ case "allow": return withReason("approved", reason);
215
+ case "deny": return withReason("denied", reason);
216
+ case "requires-approval": return withReason("user-approval", reason);
217
+ case "not-applicable": return { type: "not-applicable" };
218
+ }
219
+ if (typeof record.allow === "boolean") return withReason(record.allow ? "approved" : "denied", reason);
220
+ return unrecognizedDecision();
159
221
  }
160
222
  function unrecognizedDecision() {
161
- return {
162
- type: "denied",
163
- reason: "unrecognized OPA policy decision"
164
- };
223
+ return {
224
+ type: "denied",
225
+ reason: "unrecognized OPA policy decision"
226
+ };
165
227
  }
166
228
  function withReason(type, reason) {
167
- return reason ? { type, reason } : { type };
229
+ return reason ? {
230
+ type,
231
+ reason
232
+ } : { type };
168
233
  }
169
-
170
- // src/opa/evaluate-policy.ts
234
+ //#endregion
235
+ //#region src/opa/evaluate-policy.ts
236
+ /**
237
+ * Evaluate a policy, capturing a backend error (OPA unreachable, WASM fault,
238
+ * bad path) as a value instead of letting it throw. This is the package's
239
+ * fail-closed invariant in one place: a thrown `client.evaluate` must never
240
+ * escape into the SDK callback or middleware, where it would abort the run.
241
+ * Each caller turns `ok: false` into its own safe fallback (deny / no tools).
242
+ */
171
243
  async function evaluatePolicy(client, path, input) {
172
- try {
173
- return { ok: true, result: await client.evaluate(path, input) };
174
- } catch (error) {
175
- return { ok: false, error };
176
- }
244
+ try {
245
+ return {
246
+ ok: true,
247
+ result: await client.evaluate(path, input)
248
+ };
249
+ } catch (error) {
250
+ return {
251
+ ok: false,
252
+ error
253
+ };
254
+ }
177
255
  }
178
-
179
- // src/opa/opa-capability-middleware.ts
256
+ //#endregion
257
+ //#region src/opa/opa-capability-middleware.ts
258
+ /**
259
+ * Construct an experimental {@link LanguageModelV4Middleware} that narrows
260
+ * the `tools` field on every model call to an allowlist returned by OPA.
261
+ *
262
+ * Why use this alongside the per-call `toolApproval` gate? Two reasons:
263
+ *
264
+ * 1. **Defense in depth.** If a bug or regression slips through the policy
265
+ * used by `toolApproval`, the middleware still prevents the model from
266
+ * seeing the disallowed tools in the first place.
267
+ * 2. **Capability disclosure.** The model does not waste tokens describing
268
+ * tools it cannot call, and jailbreak attempts get "I don't have access
269
+ * to that tool" rather than "[approval denied]".
270
+ *
271
+ * The OPA rule at `path` is expected to return either a `string[]` of allowed
272
+ * tool names, or an object `{ tools: string[] }`. Function tools whose `name`
273
+ * matches are kept; provider tools whose dotted `id` or bare `name` matches are
274
+ * kept; everything else is dropped from `params.tools`.
275
+ *
276
+ * On a malformed result (or an OPA error), the middleware **fails closed**:
277
+ * `params.tools` is set to `undefined`, so the model is told it has no tools
278
+ * available. Misconfiguration should not silently widen capabilities.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * import { wrapLanguageModel } from 'ai';
283
+ * import { wasmPolicyClient, opaCapabilityMiddleware } from '@ai-sdk/policy-opa';
284
+ *
285
+ * const client = await wasmPolicyClient({ wasm });
286
+ *
287
+ * const wrappedModel = wrapLanguageModel({
288
+ * model: anthropic('claude-sonnet-4-5'),
289
+ * middleware: opaCapabilityMiddleware({
290
+ * client,
291
+ * path: 'agent/tools/allowed',
292
+ * }),
293
+ * });
294
+ * ```
295
+ */
180
296
  function opaCapabilityMiddleware(opts) {
181
- const { client, path, toInput } = opts;
182
- return {
183
- specificationVersion: "v4",
184
- async transformParams({ params }) {
185
- if (params.tools == null || params.tools.length === 0) {
186
- return params;
187
- }
188
- const input = toInput?.({
189
- messages: params.prompt,
190
- providerOptions: params.providerOptions
191
- }) ?? {
192
- messages: params.prompt,
193
- providerOptions: params.providerOptions
194
- };
195
- const outcome = await evaluatePolicy(client, path, input);
196
- if (!outcome.ok) {
197
- return { ...params, tools: void 0 };
198
- }
199
- const allowed = extractAllowedNameSet(outcome.result);
200
- if (allowed == null) {
201
- return { ...params, tools: void 0 };
202
- }
203
- let removed = false;
204
- const filtered = params.tools.filter((t) => {
205
- const keep = t.type === "function" ? allowed.has(t.name) : allowed.has(t.id) || allowed.has(t.name);
206
- if (!keep) removed = true;
207
- return keep;
208
- });
209
- if (!removed) return params;
210
- return { ...params, tools: filtered };
211
- }
212
- };
297
+ const { client, path, toInput } = opts;
298
+ return {
299
+ specificationVersion: "v4",
300
+ async transformParams({ params }) {
301
+ if (params.tools == null || params.tools.length === 0) return params;
302
+ const input = toInput?.({
303
+ messages: params.prompt,
304
+ providerOptions: params.providerOptions
305
+ }) ?? {
306
+ messages: params.prompt,
307
+ providerOptions: params.providerOptions
308
+ };
309
+ const outcome = await evaluatePolicy(client, path, input);
310
+ if (!outcome.ok) return {
311
+ ...params,
312
+ tools: void 0
313
+ };
314
+ const allowed = extractAllowedNameSet(outcome.result);
315
+ if (allowed == null) return {
316
+ ...params,
317
+ tools: void 0
318
+ };
319
+ let removed = false;
320
+ const filtered = params.tools.filter((t) => {
321
+ const keep = t.type === "function" ? allowed.has(t.name) : allowed.has(t.id) || allowed.has(t.name);
322
+ if (!keep) removed = true;
323
+ return keep;
324
+ });
325
+ if (!removed) return params;
326
+ return {
327
+ ...params,
328
+ tools: filtered
329
+ };
330
+ }
331
+ };
213
332
  }
214
333
  function extractAllowedNameSet(result) {
215
- const list = Array.isArray(result) ? result : result != null && typeof result === "object" ? result.tools : void 0;
216
- if (!Array.isArray(list)) return null;
217
- const out = /* @__PURE__ */ new Set();
218
- for (const item of list) {
219
- if (typeof item !== "string") return null;
220
- out.add(item);
221
- }
222
- return out;
334
+ const list = Array.isArray(result) ? result : result != null && typeof result === "object" ? result.tools : void 0;
335
+ if (!Array.isArray(list)) return null;
336
+ const out = /* @__PURE__ */ new Set();
337
+ for (const item of list) {
338
+ if (typeof item !== "string") return null;
339
+ out.add(item);
340
+ }
341
+ return out;
223
342
  }
224
-
225
- // src/opa/opa-policy.ts
343
+ //#endregion
344
+ //#region src/opa/opa-policy.ts
345
+ /**
346
+ * Construct a {@link ToolApprovalConfiguration} backed by an OPA policy.
347
+ *
348
+ * The returned generic approval function evaluates the supplied Rego entry
349
+ * (`path`) for every tool call and maps the result to the SDK's approval
350
+ * status via {@link normalizeOpaDecision}. Pass the result directly as
351
+ * `toolApproval` on `generateText` / `streamText` / `ToolLoopAgent`.
352
+ *
353
+ * ```ts
354
+ * import { wasmPolicyClient } from '@ai-sdk/policy-opa';
355
+ * import { opaPolicy } from '@ai-sdk/policy-opa';
356
+ *
357
+ * const client = await wasmPolicyClient({ wasm });
358
+ * const toolApproval = opaPolicy({ client, path: 'agent/call/decision' });
359
+ *
360
+ * await generateText({ model, tools, toolApproval, prompt });
361
+ * ```
362
+ *
363
+ * @param opts.client The OPA client (HTTP or WASM).
364
+ * @param opts.path The Rego entrypoint that returns the decision object.
365
+ * @param opts.toInput Optional transformer to shape the OPA input.
366
+ */
226
367
  function opaPolicy(opts) {
227
- const { client, path, toInput } = opts;
228
- return async ({
229
- toolCall,
230
- tools,
231
- toolsContext,
232
- runtimeContext,
233
- messages
234
- }) => {
235
- const opaInput = toInput?.({ toolCall, tools, toolsContext, runtimeContext, messages }) ?? {
236
- tool: { name: toolCall.toolName },
237
- args: toolCall.input,
238
- messages,
239
- runtimeContext
240
- };
241
- const outcome = await evaluatePolicy(client, path, opaInput);
242
- if (!outcome.ok) {
243
- return {
244
- type: "denied",
245
- reason: `policy evaluation failed: ${errorMessage(outcome.error)}`
246
- };
247
- }
248
- return normalizeOpaDecision(outcome.result);
249
- };
368
+ const { client, path, toInput } = opts;
369
+ return async ({ toolCall, tools, toolsContext, runtimeContext, messages }) => {
370
+ const opaInput = toInput?.({
371
+ toolCall,
372
+ tools,
373
+ toolsContext,
374
+ runtimeContext,
375
+ messages
376
+ }) ?? {
377
+ tool: { name: toolCall.toolName },
378
+ args: toolCall.input,
379
+ messages,
380
+ runtimeContext
381
+ };
382
+ const outcome = await evaluatePolicy(client, path, opaInput);
383
+ if (!outcome.ok) return {
384
+ type: "denied",
385
+ reason: `policy evaluation failed: ${errorMessage(outcome.error)}`
386
+ };
387
+ return normalizeOpaDecision(outcome.result);
388
+ };
250
389
  }
251
390
  function errorMessage(cause) {
252
- if (cause instanceof Error) return cause.message;
253
- if (typeof cause === "object" && cause !== null) {
254
- try {
255
- return JSON.stringify(cause);
256
- } catch {
257
- }
258
- }
259
- return String(cause);
391
+ if (cause instanceof Error) return cause.message;
392
+ if (typeof cause === "object" && cause !== null) try {
393
+ return JSON.stringify(cause);
394
+ } catch {}
395
+ return String(cause);
260
396
  }
397
+ /**
398
+ * Optional variant of {@link opaPolicy} that gracefully degrades when no
399
+ * policy backend is available.
400
+ *
401
+ * Returns `undefined` when `client` is `undefined`. Pass that directly as
402
+ * `toolApproval` and the SDK falls back to its default allow-all behavior.
403
+ * When `client` is supplied, behaves exactly like {@link opaPolicy}.
404
+ *
405
+ * Use this when the policy file is configured per environment (e.g. loaded
406
+ * in production, absent in local development):
407
+ *
408
+ * ```ts
409
+ * import { readFile } from 'node:fs/promises';
410
+ * import { optionalOpaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
411
+ *
412
+ * const wasm = process.env.POLICY_WASM_PATH
413
+ * ? await readFile(process.env.POLICY_WASM_PATH)
414
+ * : undefined;
415
+ * const client = wasm ? await wasmPolicyClient({ wasm }) : undefined;
416
+ *
417
+ * const toolApproval = optionalOpaPolicy({
418
+ * client,
419
+ * path: 'agent/call/decision',
420
+ * });
421
+ *
422
+ * await generateText({ model, tools, toolApproval, prompt });
423
+ * ```
424
+ */
261
425
  function optionalOpaPolicy(opts) {
262
- if (opts.client == null) return void 0;
263
- return opaPolicy({
264
- client: opts.client,
265
- path: opts.path,
266
- toInput: opts.toInput
267
- });
426
+ if (opts.client == null) return void 0;
427
+ return opaPolicy({
428
+ client: opts.client,
429
+ path: opts.path,
430
+ toInput: opts.toInput
431
+ });
268
432
  }
269
-
270
- // src/opa/wasm-policy-client.ts
433
+ //#endregion
434
+ //#region src/opa/wasm-policy-client.ts
435
+ /**
436
+ * Construct a {@link PolicyClient} that evaluates a compiled OPA WASM bundle
437
+ * in-process using `@open-policy-agent/opa-wasm`.
438
+ *
439
+ * The `@open-policy-agent/opa-wasm` package is an optional peer dependency;
440
+ * install it before using this client:
441
+ *
442
+ * ```sh
443
+ * pnpm add @open-policy-agent/opa-wasm
444
+ * ```
445
+ *
446
+ * The `path` argument to `evaluate(path, input)` is informational. The WASM
447
+ * bundle is built around a fixed entrypoint at `opa build` time. The path is
448
+ * recorded for audit logs but does not affect the evaluation.
449
+ */
271
450
  async function wasmPolicyClient(opts) {
272
- let mod;
273
- try {
274
- mod = await import("@open-policy-agent/opa-wasm");
275
- } catch (cause) {
276
- throw Object.assign(
277
- new Error(
278
- 'Cannot import "@open-policy-agent/opa-wasm". Install it as a peer dependency to use wasmPolicyClient().'
279
- ),
280
- { cause }
281
- );
282
- }
283
- const policy = await mod.loadPolicy(opts.wasm);
284
- if (opts.data !== void 0 && typeof policy.setData === "function") {
285
- policy.setData(opts.data);
286
- }
287
- return {
288
- async evaluate(_path, input) {
289
- const results = policy.evaluate(input);
290
- if (!Array.isArray(results) || results.length === 0) {
291
- throw Object.assign(
292
- new Error(
293
- "OPA WASM policy produced no result. Check that the bundle was built with the correct entrypoint (`opa build -t wasm -e <path>`)."
294
- ),
295
- { input }
296
- );
297
- }
298
- return results[0].result;
299
- }
300
- };
451
+ let mod;
452
+ try {
453
+ mod = await import("@open-policy-agent/opa-wasm");
454
+ } catch (cause) {
455
+ throw Object.assign(/* @__PURE__ */ new Error("Cannot import \"@open-policy-agent/opa-wasm\". Install it as a peer dependency to use wasmPolicyClient()."), { cause });
456
+ }
457
+ const policy = await mod.loadPolicy(opts.wasm);
458
+ if (opts.data !== void 0 && typeof policy.setData === "function") policy.setData(opts.data);
459
+ return { async evaluate(_path, input) {
460
+ const results = policy.evaluate(input);
461
+ if (!Array.isArray(results) || results.length === 0) throw Object.assign(/* @__PURE__ */ new Error("OPA WASM policy produced no result. Check that the bundle was built with the correct entrypoint (`opa build -t wasm -e <path>`)."), { input });
462
+ return results[0].result;
463
+ } };
301
464
  }
302
- export {
303
- httpPolicyClient,
304
- normalizeOpaDecision,
305
- opaCapabilityMiddleware,
306
- opaPolicy,
307
- optionalOpaPolicy,
308
- shadow,
309
- wasmPolicyClient,
310
- wrapMcpTools
311
- };
465
+ //#endregion
466
+ export { httpPolicyClient, normalizeOpaDecision, opaCapabilityMiddleware, opaPolicy, optionalOpaPolicy, shadow, wasmPolicyClient, wrapMcpTools };
467
+
312
468
  //# sourceMappingURL=index.js.map