@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.
@@ -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