@ai-sdk/policy-opa 1.0.18 → 1.0.19

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @ai-sdk/policy
2
2
 
3
+ ## 1.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - aad737d: Use own-property checks when resolving per-tool approvals so tool names and approval ids that match inherited object properties (e.g. `constructor`, `toString`, `valueOf`, `__proto__`) are treated as unconfigured/absent.
8
+
9
+ - `@ai-sdk/policy-opa`: `wrapMcpTools` builds its per-tool map with a null prototype and reads supplied approvals via an own-property check, and `shadow` guards its per-tool map lookup the same way.
10
+ - `ai`: tool and tool-context lookups keyed by a model- or client-supplied name now go through an own-property check (`getOwn`), so a name matching an inherited object property resolves to "no such tool"/"unconfigured" instead of a prototype value. This covers the approval path (per-tool approval resolution and replay re-validation) as well as tool-call parsing, execution, streaming callbacks, and UI message conversion/validation. The human-in-the-loop approval matching (`collectToolApprovals`) and streaming tool-name maps are built with a null prototype so a client-supplied id that matches an inherited property no longer slips past the "unknown approval" / "tool call not found" guards.
11
+
12
+ - 47bd0a6: `wrapMcpTools`: per-tool approval functions now fail closed. In the per-tool map form, a per-tool approval function that returns a "no opinion" result (`not-applicable` or `undefined`) is now forced through the configured fallback (`user-approval` by default), matching the generic-function form. Previously such a result passed through and the tool resolved to `not-applicable`, letting it run without an approval request. Static per-tool statuses the caller configured explicitly are unchanged.
13
+ - Updated dependencies [be7f05a]
14
+ - Updated dependencies [ee55a07]
15
+ - Updated dependencies [aad737d]
16
+ - Updated dependencies [0f93c57]
17
+ - ai@7.0.19
18
+ - @ai-sdk/provider@4.0.3
19
+ - @ai-sdk/provider-utils@5.0.7
20
+
3
21
  ## 1.0.18
4
22
 
5
23
  ### Patch Changes
package/dist/index.js CHANGED
@@ -34,7 +34,10 @@ async function evaluateApproval(approval, args) {
34
34
  return await approval(args);
35
35
  }
36
36
  const map = approval;
37
- const perTool = map[args.toolCall.toolName];
37
+ const perTool = Object.prototype.hasOwnProperty.call(
38
+ map,
39
+ args.toolCall.toolName
40
+ ) ? map[args.toolCall.toolName] : void 0;
38
41
  if (perTool == null) return void 0;
39
42
  if (typeof perTool === "function") {
40
43
  return await perTool(args.toolCall.input, {
@@ -62,7 +65,7 @@ function normalizePolicyDecision(status) {
62
65
 
63
66
  // src/wrap-mcp-tools.ts
64
67
  function wrapMcpTools(tools, approval, opts) {
65
- var _a, _b;
68
+ var _a;
66
69
  const fallback = (_a = opts == null ? void 0 : opts.default) != null ? _a : "user-approval";
67
70
  if (typeof approval === "function") {
68
71
  const wrapped = async (args) => {
@@ -71,9 +74,20 @@ function wrapMcpTools(tools, approval, opts) {
71
74
  };
72
75
  return { tools, toolApproval: wrapped };
73
76
  }
74
- const filled = {};
77
+ const filled = /* @__PURE__ */ Object.create(null);
75
78
  for (const name of Object.keys(tools)) {
76
- filled[name] = (_b = approval[name]) != null ? _b : fallback;
79
+ const configured = Object.prototype.hasOwnProperty.call(approval, name) ? approval[name] : void 0;
80
+ if (configured == null) {
81
+ filled[name] = fallback;
82
+ } else if (typeof configured === "function") {
83
+ const original = configured;
84
+ filled[name] = async (...args) => {
85
+ const status = await original(...args);
86
+ return isNotApplicable(status) ? fallback : status;
87
+ };
88
+ } else {
89
+ filled[name] = configured;
90
+ }
77
91
  }
78
92
  return {
79
93
  tools,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shadow.ts","../src/wrap-mcp-tools.ts","../src/opa/http-policy-client.ts","../src/opa/normalize-opa-decision.ts","../src/opa/evaluate-policy.ts","../src/opa/opa-capability-middleware.ts","../src/opa/opa-policy.ts","../src/opa/wasm-policy-client.ts"],"sourcesContent":["import type {\n Context,\n ModelMessage,\n Tool,\n ToolSet,\n} from '@ai-sdk/provider-utils';\nimport type { ToolApprovalConfiguration } from 'ai';\nimport type { PolicyDecision } from './policy-decision';\n\ninterface GenericApprovalArgs {\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n tools: unknown;\n toolsContext: unknown;\n runtimeContext: unknown;\n messages: ModelMessage[];\n}\n\n/**\n * Event emitted by {@link shadow} every time the wrapped policy is evaluated.\n *\n * Compare `decision` and `effective` to see what would have happened\n * differently if the policy were enforcing: any record where they disagree\n * is a tool call that shadow mode is letting through but enforce mode would\n * have blocked or escalated.\n */\nexport interface PolicyDecisionEvent {\n /** Identifying info from the tool call being evaluated. */\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n /** The decision the wrapped policy returned, normalized to the object form. */\n decision: PolicyDecision;\n /** Whether the SDK will act on `decision` (true) or override to allow (false). */\n enforced: boolean;\n /** The decision the SDK actually acts on. Equals `decision` when enforcing,\n * `{ type: 'approved' }` in shadow mode. */\n effective: PolicyDecision;\n /** ISO 8601 timestamp of the evaluation. */\n timestamp: string;\n}\n\n/**\n * Wrap a `toolApproval` in shadow mode so the policy is evaluated and the\n * decision is reported via `onDecision`, but the SDK is told the call is\n * approved regardless of what the policy said.\n *\n * Use this when you are rolling out a new policy and want to see what it\n * *would* deny before letting it actually deny anything in production. Wire\n * `onDecision` to your logger / metrics pipeline, run for a while, inspect\n * the events where `decision.type !== 'approved'`, fix the policy, then\n * flip `enforce: true` to graduate.\n *\n * @example\n * ```ts\n * import { shadow } from '@ai-sdk/policy-opa';\n * import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';\n *\n * const client = await wasmPolicyClient({ wasm });\n *\n * const toolApproval = shadow(\n * opaPolicy({ client, path: 'agent/call/decision' }),\n * {\n * enforce: process.env.ENFORCE_POLICY === 'true',\n * onDecision: (event) => {\n * logger.info('policy.decision', {\n * tool: event.toolCall.toolName,\n * decision: event.decision.type,\n * enforced: event.enforced,\n * wouldBlock: event.decision.type === 'denied',\n * });\n * },\n * },\n * );\n * ```\n */\nexport function shadow<\n TOOLS extends Record<string, Tool>,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(\n approval: ToolApprovalConfiguration<TOOLS & ToolSet, RUNTIME_CONTEXT>,\n opts: {\n /** When true, the SDK acts on the policy's decision. When false (default),\n * the SDK is told every call is approved. */\n enforce?: boolean;\n /** Invoked once per evaluated tool call with the captured decision. */\n onDecision?: (event: PolicyDecisionEvent) => void | Promise<void>;\n } = {},\n): ToolApprovalConfiguration<TOOLS & ToolSet, RUNTIME_CONTEXT> {\n const enforce = opts.enforce === true;\n const { onDecision } = opts;\n\n const wrapped = async (args: GenericApprovalArgs) => {\n const raw = await evaluateApproval(approval, args);\n const decision = normalizePolicyDecision(raw);\n\n const effective: PolicyDecision = enforce ? decision : { type: 'approved' };\n\n if (onDecision) {\n const event: PolicyDecisionEvent = {\n toolCall: {\n toolName: args.toolCall.toolName,\n toolCallId: args.toolCall.toolCallId,\n input: args.toolCall.input,\n },\n decision,\n enforced: enforce,\n effective,\n timestamp: new Date().toISOString(),\n };\n // Fire-and-forget so a slow or throwing logger does not block the model.\n void (async () => {\n try {\n await onDecision(event);\n } catch {\n // Swallow errors from telemetry; enforcement must not depend on it.\n }\n })();\n }\n\n return effective;\n };\n\n // `wrapped` is typed against the erased `GenericApprovalArgs`; TS can't prove\n // that matches the SDK's per-tool-set function arm, so bridge it here.\n return wrapped as unknown as ToolApprovalConfiguration<\n TOOLS & ToolSet,\n RUNTIME_CONTEXT\n >;\n}\n\nasync function evaluateApproval(\n approval: unknown,\n args: GenericApprovalArgs,\n): Promise<unknown> {\n if (typeof approval === 'function') {\n return await (\n approval as (a: GenericApprovalArgs) => Promise<unknown> | unknown\n )(args);\n }\n\n const map = approval as Record<string, unknown>;\n const perTool = map[args.toolCall.toolName];\n if (perTool == null) return undefined;\n if (typeof perTool === 'function') {\n return await (\n perTool as (\n input: unknown,\n options: {\n toolCallId: string;\n messages: ModelMessage[];\n toolContext: unknown;\n runtimeContext: unknown;\n },\n ) => Promise<unknown> | unknown\n )(args.toolCall.input, {\n toolCallId: args.toolCall.toolCallId,\n messages: args.messages,\n toolContext: undefined,\n runtimeContext: args.runtimeContext,\n });\n }\n return perTool;\n}\n\nfunction normalizePolicyDecision(status: unknown): PolicyDecision {\n if (status == null) return { type: 'not-applicable' };\n if (typeof status === 'string') {\n if (\n status === 'approved' ||\n status === 'denied' ||\n status === 'user-approval' ||\n status === 'not-applicable'\n ) {\n return { type: status };\n }\n return { type: 'not-applicable' };\n }\n if (typeof status === 'object' && 'type' in (status as object)) {\n return status as PolicyDecision;\n }\n return { type: 'not-applicable' };\n}\n","import type { Context, Tool } from '@ai-sdk/provider-utils';\nimport type { ToolApprovalConfiguration, ToolApprovalStatus } from 'ai';\n\ntype ApprovalLiteralStatus = Extract<ToolApprovalStatus, string>;\n\n/**\n * Result returned by {@link wrapMcpTools}: the original tool set plus a\n * `toolApproval` that falls back to a configurable default for any tool the\n * supplied approval does not explicitly handle.\n */\nexport interface WrappedMcpTools<\n TOOLS extends Record<string, Tool>,\n RUNTIME_CONTEXT extends Context | unknown | never,\n> {\n tools: TOOLS;\n toolApproval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>;\n}\n\n/**\n * Apply a fallback approval policy to a discovered tool set so the resulting\n * configuration is total: every tool in `tools` is gated either by the\n * supplied `approval` (when it has an opinion) or by `default` (otherwise).\n *\n * The motivating case is MCP. An MCP server hands you whatever tools it\n * exposes, and the agent's effective permissions become the union of the\n * full server surface. Without a fallback, any tool you forgot to write a\n * rule for is silently allowed. This helper closes that gap by forcing every\n * uncovered tool through `default` (which defaults to `user-approval` so the\n * human is in the loop for anything you didn't think about).\n *\n * Works for any tool set, not just MCP-discovered tools. The name reflects\n * the primary use case rather than a hard constraint.\n *\n * @example\n * ```ts\n * import { wrapMcpTools } from '@ai-sdk/policy-opa';\n * import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';\n *\n * const discovered = await mcpClient.tools();\n * const client = await wasmPolicyClient({ wasm });\n *\n * const { tools, toolApproval } = wrapMcpTools(\n * discovered,\n * opaPolicy({ client, path: 'agent/call/decision' }),\n * { default: 'user-approval' }, // anything OPA does not match needs a human\n * );\n *\n * await generateText({ model, tools, toolApproval, prompt });\n * ```\n */\nexport function wrapMcpTools<\n TOOLS extends Record<string, Tool>,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(\n tools: TOOLS,\n approval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>,\n opts?: { default?: ApprovalLiteralStatus },\n): WrappedMcpTools<TOOLS, RUNTIME_CONTEXT> {\n const fallback: ApprovalLiteralStatus = opts?.default ?? 'user-approval';\n\n if (typeof approval === 'function') {\n const wrapped: ToolApprovalConfiguration<\n TOOLS,\n RUNTIME_CONTEXT\n > = async args => {\n const status = await (\n approval as (a: typeof args) => Promise<unknown> | unknown\n )(args);\n return isNotApplicable(status) ? fallback : (status as never);\n };\n return { tools, toolApproval: wrapped };\n }\n\n // Per-tool map form: fill in any tool that the user did not list with the\n // fallback decision. We use `Object.keys(tools)` (not the approval keys) so\n // the result is total over the discovered surface.\n const filled = {} as Record<keyof TOOLS, unknown>;\n for (const name of Object.keys(tools) as Array<keyof TOOLS>) {\n filled[name] = approval[name] ?? fallback;\n }\n\n // `filled` is a plain per-tool map; cast to the SDK's map arm, which TS\n // can't infer from the erased `unknown` values.\n return {\n tools,\n toolApproval: filled as ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>,\n };\n}\n\nfunction isNotApplicable(status: unknown): boolean {\n if (status == null) return true;\n if (status === 'not-applicable') return true;\n if (typeof status === 'object' && status !== null) {\n return (status as { type?: unknown }).type === 'not-applicable';\n }\n return false;\n}\n","import type { PolicyClient } from '../policy-client';\n\n/**\n * Construct a {@link PolicyClient} that talks to a running OPA server over\n * HTTP using `@open-policy-agent/opa`.\n *\n * The `@open-policy-agent/opa` package is an optional peer dependency; install\n * it before using this client:\n *\n * ```sh\n * pnpm add @open-policy-agent/opa\n * ```\n *\n * The `url` typically points at `http://localhost:8181` for a locally running\n * OPA. `headers` is forwarded for Styra DAS / EOPA authentication.\n */\nexport function httpPolicyClient(opts: {\n url: string;\n headers?: Record<string, string>;\n}): PolicyClient {\n const { url, headers } = opts;\n type Underlying = {\n evaluate(path: string, input: unknown): Promise<unknown>;\n };\n type OPAModule = {\n OPAClient: new (\n url: string,\n opts?: { headers?: Record<string, string> },\n ) => Underlying;\n };\n\n let underlying: Underlying | undefined;\n\n async function getUnderlying(): Promise<Underlying> {\n if (underlying) return underlying;\n let mod: OPAModule;\n try {\n mod = (await import('@open-policy-agent/opa')) as unknown as OPAModule;\n } catch (cause) {\n throw Object.assign(\n new Error(\n 'Cannot import \"@open-policy-agent/opa\". Install it as a peer dependency to use httpPolicyClient().',\n ),\n { cause },\n );\n }\n underlying = new mod.OPAClient(url, headers ? { headers } : undefined);\n return underlying;\n }\n\n return {\n async evaluate(path, input) {\n const client = await getUnderlying();\n return (await client.evaluate(path, input)) as never;\n },\n };\n}\n","import type { PolicyDecision } from '../policy-decision';\n\n/**\n * Normalize an OPA evaluation result into the package's {@link PolicyDecision}\n * shape.\n *\n * Supports two Rego output conventions:\n *\n * - **Recommended (explicit):** `{ \"decision\": \"allow\" | \"deny\" | \"requires-approval\", \"reason\": string }`.\n * Maps to `approved` / `denied` / `user-approval` respectively.\n *\n * - **Legacy (boolean):** `{ \"allow\": boolean, \"reason\"?: string }`. `true`\n * maps to `approved`, `false` to `denied`.\n *\n * Unknown shapes and `undefined` are treated as `not-applicable` so that a\n * Rego rule that does not match any branch defaults to \"no opinion\" rather\n * than blocking.\n */\nexport function normalizeOpaDecision(result: unknown): PolicyDecision {\n if (result == null) {\n return { type: 'not-applicable' };\n }\n\n if (typeof result !== 'object') {\n return { type: 'not-applicable' };\n }\n\n const record = result as Record<string, unknown>;\n const reason = typeof record.reason === 'string' ? record.reason : undefined;\n\n if (typeof record.decision === 'string') {\n switch (record.decision) {\n case 'allow':\n return withReason('approved', reason);\n case 'deny':\n return withReason('denied', reason);\n case 'requires-approval':\n return { type: 'user-approval' };\n case 'not-applicable':\n return { type: 'not-applicable' };\n }\n }\n\n if (typeof record.allow === 'boolean') {\n return withReason(record.allow ? 'approved' : 'denied', reason);\n }\n\n return { type: 'not-applicable' };\n}\n\nfunction withReason(\n type: 'approved' | 'denied',\n reason: string | undefined,\n): PolicyDecision {\n return reason ? { type, reason } : { type };\n}\n","import type { PolicyClient } from '../policy-client';\n\nexport type EvaluateOutcome =\n | { ok: true; result: unknown }\n | { ok: false; error: unknown };\n\n/**\n * Evaluate a policy, capturing a backend error (OPA unreachable, WASM fault,\n * bad path) as a value instead of letting it throw. This is the package's\n * fail-closed invariant in one place: a thrown `client.evaluate` must never\n * escape into the SDK callback or middleware, where it would abort the run.\n * Each caller turns `ok: false` into its own safe fallback (deny / no tools).\n */\nexport async function evaluatePolicy(\n client: PolicyClient,\n path: string,\n input: unknown,\n): Promise<EvaluateOutcome> {\n try {\n return { ok: true, result: await client.evaluate(path, input) };\n } catch (error) {\n return { ok: false, error };\n }\n}\n","import type {\n LanguageModelV4CallOptions,\n LanguageModelV4Middleware,\n} from '@ai-sdk/provider';\nimport type { PolicyClient } from '../policy-client';\nimport { evaluatePolicy } from './evaluate-policy';\n\n/**\n * Default OPA input shape passed to the capability-scoping rule.\n * Override with `toInput` if your Rego expects a different schema.\n */\nexport interface DefaultOpaCapabilityInput {\n messages: LanguageModelV4CallOptions['prompt'];\n providerOptions: LanguageModelV4CallOptions['providerOptions'];\n}\n\n/**\n * Construct an experimental {@link LanguageModelV4Middleware} that narrows\n * the `tools` field on every model call to an allowlist returned by OPA.\n *\n * Why use this alongside the per-call `toolApproval` gate? Two reasons:\n *\n * 1. **Defense in depth.** If a bug or regression slips through the policy\n * used by `toolApproval`, the middleware still prevents the model from\n * seeing the disallowed tools in the first place.\n * 2. **Capability disclosure.** The model does not waste tokens describing\n * tools it cannot call, and jailbreak attempts get \"I don't have access\n * to that tool\" rather than \"[approval denied]\".\n *\n * The OPA rule at `path` is expected to return either a `string[]` of allowed\n * tool names, or an object `{ tools: string[] }`. Function tools whose `name`\n * matches are kept; provider tools whose dotted `id` or bare `name` matches are\n * kept; everything else is dropped from `params.tools`.\n *\n * On a malformed result (or an OPA error), the middleware **fails closed**:\n * `params.tools` is set to `undefined`, so the model is told it has no tools\n * available. Misconfiguration should not silently widen capabilities.\n *\n * @example\n * ```ts\n * import { wrapLanguageModel } from 'ai';\n * import { wasmPolicyClient, opaCapabilityMiddleware } from '@ai-sdk/policy-opa';\n *\n * const client = await wasmPolicyClient({ wasm });\n *\n * const wrappedModel = wrapLanguageModel({\n * model: anthropic('claude-sonnet-4-5'),\n * middleware: opaCapabilityMiddleware({\n * client,\n * path: 'agent/tools/allowed',\n * }),\n * });\n * ```\n */\nexport function opaCapabilityMiddleware(opts: {\n client: PolicyClient;\n path: string;\n toInput?: (args: {\n messages: LanguageModelV4CallOptions['prompt'];\n providerOptions: LanguageModelV4CallOptions['providerOptions'];\n }) => unknown;\n}): LanguageModelV4Middleware {\n const { client, path, toInput } = opts;\n\n return {\n specificationVersion: 'v4',\n async transformParams({ params }) {\n // No tools on the call: nothing to filter.\n if (params.tools == null || params.tools.length === 0) {\n return params;\n }\n\n const input =\n toInput?.({\n messages: params.prompt,\n providerOptions: params.providerOptions,\n }) ??\n ({\n messages: params.prompt,\n providerOptions: params.providerOptions,\n } satisfies DefaultOpaCapabilityInput);\n\n // Fail closed on evaluator error or a malformed allowlist: drop all\n // tools so a misconfiguration never silently widens capabilities.\n const outcome = await evaluatePolicy(client, path, input);\n if (!outcome.ok) {\n return { ...params, tools: undefined };\n }\n\n const allowed = extractAllowedNameSet(outcome.result);\n if (allowed == null) {\n return { ...params, tools: undefined };\n }\n\n let removed = false;\n const filtered = params.tools.filter(t => {\n // Function tools are keyed by `name`. Provider tools carry both a\n // dotted `id` (`<provider>.<tool>`) and a bare `name`; match either so\n // an allowlist authored with the bare name keeps the tool instead of\n // silently dropping it.\n const keep =\n t.type === 'function'\n ? allowed.has(t.name)\n : allowed.has(t.id) || allowed.has(t.name);\n if (!keep) removed = true;\n return keep;\n });\n\n // Preserve object identity when nothing was dropped so downstream\n // middleware can no-op on reference equality.\n if (!removed) return params;\n return { ...params, tools: filtered };\n },\n };\n}\n\nfunction extractAllowedNameSet(result: unknown): Set<string> | null {\n const list = Array.isArray(result)\n ? result\n : result != null && typeof result === 'object'\n ? (result as { tools?: unknown }).tools\n : undefined;\n if (!Array.isArray(list)) return null;\n const out = new Set<string>();\n for (const item of list) {\n if (typeof item !== 'string') return null;\n out.add(item);\n }\n return out;\n}\n","import type {\n Context,\n InferToolSetContext,\n ModelMessage,\n ToolSet,\n} from '@ai-sdk/provider-utils';\nimport type { ToolApprovalConfiguration } from 'ai';\nimport type { PolicyClient } from '../policy-client';\nimport { evaluatePolicy } from './evaluate-policy';\nimport { normalizeOpaDecision } from './normalize-opa-decision';\n\n/**\n * The default shape passed to the OPA rule as `input` when no `toInput` is\n * supplied. Rego rules can read `input.tool.name`, `input.args`, and so on.\n */\nexport interface DefaultOpaInput {\n tool: { name: string };\n args: unknown;\n messages: ReadonlyArray<ModelMessage>;\n runtimeContext: unknown;\n}\n\n/**\n * Construct a {@link ToolApprovalConfiguration} backed by an OPA policy.\n *\n * The returned generic approval function evaluates the supplied Rego entry\n * (`path`) for every tool call and maps the result to the SDK's approval\n * status via {@link normalizeOpaDecision}. Pass the result directly as\n * `toolApproval` on `generateText` / `streamText` / `ToolLoopAgent`.\n *\n * ```ts\n * import { wasmPolicyClient } from '@ai-sdk/policy-opa';\n * import { opaPolicy } from '@ai-sdk/policy-opa';\n *\n * const client = await wasmPolicyClient({ wasm });\n * const toolApproval = opaPolicy({ client, path: 'agent/call/decision' });\n *\n * await generateText({ model, tools, toolApproval, prompt });\n * ```\n *\n * @param opts.client The OPA client (HTTP or WASM).\n * @param opts.path The Rego entrypoint that returns the decision object.\n * @param opts.toInput Optional transformer to shape the OPA input.\n */\nexport function opaPolicy<\n TOOLS extends ToolSet = ToolSet,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(opts: {\n client: PolicyClient;\n path: string;\n toInput?: (args: {\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n tools: TOOLS | undefined;\n toolsContext: InferToolSetContext<TOOLS>;\n runtimeContext: RUNTIME_CONTEXT;\n messages: ModelMessage[];\n }) => unknown;\n}): ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> {\n const { client, path, toInput } = opts;\n\n return async ({\n toolCall,\n tools,\n toolsContext,\n runtimeContext,\n messages,\n }) => {\n const opaInput =\n toInput?.({ toolCall, tools, toolsContext, runtimeContext, messages }) ??\n ({\n tool: { name: toolCall.toolName },\n args: toolCall.input,\n messages,\n runtimeContext,\n } satisfies DefaultOpaInput);\n\n // Fail closed: a backend error must not be read as \"no opinion\". Deny so\n // the model sees a structured result it can reason about, rather than the\n // error rejecting out of the callback and aborting the run.\n const outcome = await evaluatePolicy(client, path, opaInput);\n if (!outcome.ok) {\n return {\n type: 'denied',\n reason: `policy evaluation failed: ${errorMessage(outcome.error)}`,\n };\n }\n\n return normalizeOpaDecision(outcome.result);\n };\n}\n\nfunction errorMessage(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'object' && cause !== null) {\n try {\n return JSON.stringify(cause);\n } catch {\n // Fall through for circular / non-serializable objects.\n }\n }\n return String(cause);\n}\n\n/**\n * Optional variant of {@link opaPolicy} that gracefully degrades when no\n * policy backend is available.\n *\n * Returns `undefined` when `client` is `undefined`. Pass that directly as\n * `toolApproval` and the SDK falls back to its default allow-all behavior.\n * When `client` is supplied, behaves exactly like {@link opaPolicy}.\n *\n * Use this when the policy file is configured per environment (e.g. loaded\n * in production, absent in local development):\n *\n * ```ts\n * import { readFile } from 'node:fs/promises';\n * import { optionalOpaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';\n *\n * const wasm = process.env.POLICY_WASM_PATH\n * ? await readFile(process.env.POLICY_WASM_PATH)\n * : undefined;\n * const client = wasm ? await wasmPolicyClient({ wasm }) : undefined;\n *\n * const toolApproval = optionalOpaPolicy({\n * client,\n * path: 'agent/call/decision',\n * });\n *\n * await generateText({ model, tools, toolApproval, prompt });\n * ```\n */\nexport function optionalOpaPolicy<\n TOOLS extends ToolSet = ToolSet,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(opts: {\n client: PolicyClient | undefined;\n path: string;\n toInput?: (args: {\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n tools: TOOLS | undefined;\n toolsContext: InferToolSetContext<TOOLS>;\n runtimeContext: RUNTIME_CONTEXT;\n messages: ModelMessage[];\n }) => unknown;\n}): ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> | undefined {\n if (opts.client == null) return undefined;\n return opaPolicy<TOOLS, RUNTIME_CONTEXT>({\n client: opts.client,\n path: opts.path,\n toInput: opts.toInput,\n });\n}\n","import type { PolicyClient } from '../policy-client';\n\n/**\n * Loaded OPA WASM bundle. Compiled offline with `opa build -t wasm` and\n * passed in as bytes.\n */\ntype LoadedPolicy = {\n evaluate(input: unknown): Array<{ result: unknown }>;\n /** Some versions expose `setData` for documents bundled at evaluation time. */\n setData?(data: unknown): void;\n};\n\n/**\n * Construct a {@link PolicyClient} that evaluates a compiled OPA WASM bundle\n * in-process using `@open-policy-agent/opa-wasm`.\n *\n * The `@open-policy-agent/opa-wasm` package is an optional peer dependency;\n * install it before using this client:\n *\n * ```sh\n * pnpm add @open-policy-agent/opa-wasm\n * ```\n *\n * The `path` argument to `evaluate(path, input)` is informational. The WASM\n * bundle is built around a fixed entrypoint at `opa build` time. The path is\n * recorded for audit logs but does not affect the evaluation.\n */\nexport async function wasmPolicyClient(opts: {\n wasm: Uint8Array | ArrayBuffer;\n /** Optional data document bundled into the policy (passed to `setData`). */\n data?: unknown;\n}): Promise<PolicyClient> {\n type WasmModule = {\n loadPolicy(wasm: Uint8Array | ArrayBuffer): Promise<LoadedPolicy>;\n };\n\n let mod: WasmModule;\n try {\n mod =\n (await import('@open-policy-agent/opa-wasm')) as unknown as WasmModule;\n } catch (cause) {\n throw Object.assign(\n new Error(\n 'Cannot import \"@open-policy-agent/opa-wasm\". Install it as a peer dependency to use wasmPolicyClient().',\n ),\n { cause },\n );\n }\n\n const policy = await mod.loadPolicy(opts.wasm);\n\n if (opts.data !== undefined && typeof policy.setData === 'function') {\n policy.setData(opts.data);\n }\n\n return {\n async evaluate(_path, input) {\n const results = policy.evaluate(input);\n // The WASM SDK returns an array of `{ result }` entries, one per\n // top-level expression in the entrypoint. A non-array or empty array\n // means the entrypoint produced no value (wrong entrypoint at\n // `opa build` time, an undefined decision rule, or a misbehaving\n // bundle). Throw rather than return undefined so the caller fails closed\n // instead of silently treating it as \"no opinion\".\n if (!Array.isArray(results) || results.length === 0) {\n throw Object.assign(\n new Error(\n 'OPA WASM policy produced no result. Check that the bundle was built with the correct entrypoint (`opa build -t wasm -e <path>`).',\n ),\n { input },\n );\n }\n // Return the first entry's `result`, matching the HTTP client's\n // single-decision shape.\n return results[0].result as never;\n },\n };\n}\n"],"mappings":";AAyEO,SAAS,OAId,UACA,OAMI,CAAC,GACwD;AAC7D,QAAM,UAAU,KAAK,YAAY;AACjC,QAAM,EAAE,WAAW,IAAI;AAEvB,QAAM,UAAU,OAAO,SAA8B;AACnD,UAAM,MAAM,MAAM,iBAAiB,UAAU,IAAI;AACjD,UAAM,WAAW,wBAAwB,GAAG;AAE5C,UAAM,YAA4B,UAAU,WAAW,EAAE,MAAM,WAAW;AAE1E,QAAI,YAAY;AACd,YAAM,QAA6B;AAAA,QACjC,UAAU;AAAA,UACR,UAAU,KAAK,SAAS;AAAA,UACxB,YAAY,KAAK,SAAS;AAAA,UAC1B,OAAO,KAAK,SAAS;AAAA,QACvB;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA,QACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC;AAEA,YAAM,YAAY;AAChB,YAAI;AACF,gBAAM,WAAW,KAAK;AAAA,QACxB,SAAQ;AAAA,QAER;AAAA,MACF,GAAG;AAAA,IACL;AAEA,WAAO;AAAA,EACT;AAIA,SAAO;AAIT;AAEA,eAAe,iBACb,UACA,MACkB;AAClB,MAAI,OAAO,aAAa,YAAY;AAClC,WAAO,MACL,SACA,IAAI;AAAA,EACR;AAEA,QAAM,MAAM;AACZ,QAAM,UAAU,IAAI,KAAK,SAAS,QAAQ;AAC1C,MAAI,WAAW,KAAM,QAAO;AAC5B,MAAI,OAAO,YAAY,YAAY;AACjC,WAAO,MACL,QASA,KAAK,SAAS,OAAO;AAAA,MACrB,YAAY,KAAK,SAAS;AAAA,MAC1B,UAAU,KAAK;AAAA,MACf,aAAa;AAAA,MACb,gBAAgB,KAAK;AAAA,IACvB,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,SAAS,wBAAwB,QAAiC;AAChE,MAAI,UAAU,KAAM,QAAO,EAAE,MAAM,iBAAiB;AACpD,MAAI,OAAO,WAAW,UAAU;AAC9B,QACE,WAAW,cACX,WAAW,YACX,WAAW,mBACX,WAAW,kBACX;AACA,aAAO,EAAE,MAAM,OAAO;AAAA,IACxB;AACA,WAAO,EAAE,MAAM,iBAAiB;AAAA,EAClC;AACA,MAAI,OAAO,WAAW,YAAY,UAAW,QAAmB;AAC9D,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,iBAAiB;AAClC;;;ACjIO,SAAS,aAId,OACA,UACA,MACyC;AAzD3C;AA0DE,QAAM,YAAkC,kCAAM,YAAN,YAAiB;AAEzD,MAAI,OAAO,aAAa,YAAY;AAClC,UAAM,UAGF,OAAM,SAAQ;AAChB,YAAM,SAAS,MACb,SACA,IAAI;AACN,aAAO,gBAAgB,MAAM,IAAI,WAAY;AAAA,IAC/C;AACA,WAAO,EAAE,OAAO,cAAc,QAAQ;AAAA,EACxC;AAKA,QAAM,SAAS,CAAC;AAChB,aAAW,QAAQ,OAAO,KAAK,KAAK,GAAyB;AAC3D,WAAO,IAAI,KAAI,cAAS,IAAI,MAAb,YAAkB;AAAA,EACnC;AAIA,SAAO;AAAA,IACL;AAAA,IACA,cAAc;AAAA,EAChB;AACF;AAEA,SAAS,gBAAgB,QAA0B;AACjD,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,WAAW,iBAAkB,QAAO;AACxC,MAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,WAAQ,OAA8B,SAAS;AAAA,EACjD;AACA,SAAO;AACT;;;AChFO,SAAS,iBAAiB,MAGhB;AACf,QAAM,EAAE,KAAK,QAAQ,IAAI;AAWzB,MAAI;AAEJ,iBAAe,gBAAqC;AAClD,QAAI,WAAY,QAAO;AACvB,QAAI;AACJ,QAAI;AACF,YAAO,MAAM,OAAO,wBAAwB;AAAA,IAC9C,SAAS,OAAO;AACd,YAAM,OAAO;AAAA,QACX,IAAI;AAAA,UACF;AAAA,QACF;AAAA,QACA,EAAE,MAAM;AAAA,MACV;AAAA,IACF;AACA,iBAAa,IAAI,IAAI,UAAU,KAAK,UAAU,EAAE,QAAQ,IAAI,MAAS;AACrE,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,SAAS,MAAM,OAAO;AAC1B,YAAM,SAAS,MAAM,cAAc;AACnC,aAAQ,MAAM,OAAO,SAAS,MAAM,KAAK;AAAA,IAC3C;AAAA,EACF;AACF;;;ACtCO,SAAS,qBAAqB,QAAiC;AACpE,MAAI,UAAU,MAAM;AAClB,WAAO,EAAE,MAAM,iBAAiB;AAAA,EAClC;AAEA,MAAI,OAAO,WAAW,UAAU;AAC9B,WAAO,EAAE,MAAM,iBAAiB;AAAA,EAClC;AAEA,QAAM,SAAS;AACf,QAAM,SAAS,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAEnE,MAAI,OAAO,OAAO,aAAa,UAAU;AACvC,YAAQ,OAAO,UAAU;AAAA,MACvB,KAAK;AACH,eAAO,WAAW,YAAY,MAAM;AAAA,MACtC,KAAK;AACH,eAAO,WAAW,UAAU,MAAM;AAAA,MACpC,KAAK;AACH,eAAO,EAAE,MAAM,gBAAgB;AAAA,MACjC,KAAK;AACH,eAAO,EAAE,MAAM,iBAAiB;AAAA,IACpC;AAAA,EACF;AAEA,MAAI,OAAO,OAAO,UAAU,WAAW;AACrC,WAAO,WAAW,OAAO,QAAQ,aAAa,UAAU,MAAM;AAAA,EAChE;AAEA,SAAO,EAAE,MAAM,iBAAiB;AAClC;AAEA,SAAS,WACP,MACA,QACgB;AAChB,SAAO,SAAS,EAAE,MAAM,OAAO,IAAI,EAAE,KAAK;AAC5C;;;AC1CA,eAAsB,eACpB,QACA,MACA,OAC0B;AAC1B,MAAI;AACF,WAAO,EAAE,IAAI,MAAM,QAAQ,MAAM,OAAO,SAAS,MAAM,KAAK,EAAE;AAAA,EAChE,SAAS,OAAO;AACd,WAAO,EAAE,IAAI,OAAO,MAAM;AAAA,EAC5B;AACF;;;AC+BO,SAAS,wBAAwB,MAOV;AAC5B,QAAM,EAAE,QAAQ,MAAM,QAAQ,IAAI;AAElC,SAAO;AAAA,IACL,sBAAsB;AAAA,IACtB,MAAM,gBAAgB,EAAE,OAAO,GAAG;AAlEtC;AAoEM,UAAI,OAAO,SAAS,QAAQ,OAAO,MAAM,WAAW,GAAG;AACrD,eAAO;AAAA,MACT;AAEA,YAAM,SACJ,wCAAU;AAAA,QACR,UAAU,OAAO;AAAA,QACjB,iBAAiB,OAAO;AAAA,MAC1B,OAHA,YAIC;AAAA,QACC,UAAU,OAAO;AAAA,QACjB,iBAAiB,OAAO;AAAA,MAC1B;AAIF,YAAM,UAAU,MAAM,eAAe,QAAQ,MAAM,KAAK;AACxD,UAAI,CAAC,QAAQ,IAAI;AACf,eAAO,EAAE,GAAG,QAAQ,OAAO,OAAU;AAAA,MACvC;AAEA,YAAM,UAAU,sBAAsB,QAAQ,MAAM;AACpD,UAAI,WAAW,MAAM;AACnB,eAAO,EAAE,GAAG,QAAQ,OAAO,OAAU;AAAA,MACvC;AAEA,UAAI,UAAU;AACd,YAAM,WAAW,OAAO,MAAM,OAAO,OAAK;AAKxC,cAAM,OACJ,EAAE,SAAS,aACP,QAAQ,IAAI,EAAE,IAAI,IAClB,QAAQ,IAAI,EAAE,EAAE,KAAK,QAAQ,IAAI,EAAE,IAAI;AAC7C,YAAI,CAAC,KAAM,WAAU;AACrB,eAAO;AAAA,MACT,CAAC;AAID,UAAI,CAAC,QAAS,QAAO;AACrB,aAAO,EAAE,GAAG,QAAQ,OAAO,SAAS;AAAA,IACtC;AAAA,EACF;AACF;AAEA,SAAS,sBAAsB,QAAqC;AAClE,QAAM,OAAO,MAAM,QAAQ,MAAM,IAC7B,SACA,UAAU,QAAQ,OAAO,WAAW,WACjC,OAA+B,QAChC;AACN,MAAI,CAAC,MAAM,QAAQ,IAAI,EAAG,QAAO;AACjC,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,QAAQ,MAAM;AACvB,QAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAI,IAAI,IAAI;AAAA,EACd;AACA,SAAO;AACT;;;ACrFO,SAAS,UAGd,MAUoD;AACpD,QAAM,EAAE,QAAQ,MAAM,QAAQ,IAAI;AAElC,SAAO,OAAO;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,MAAM;AAlER;AAmEI,UAAM,YACJ,wCAAU,EAAE,UAAU,OAAO,cAAc,gBAAgB,SAAS,OAApE,YACC;AAAA,MACC,MAAM,EAAE,MAAM,SAAS,SAAS;AAAA,MAChC,MAAM,SAAS;AAAA,MACf;AAAA,MACA;AAAA,IACF;AAKF,UAAM,UAAU,MAAM,eAAe,QAAQ,MAAM,QAAQ;AAC3D,QAAI,CAAC,QAAQ,IAAI;AACf,aAAO;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,6BAA6B,aAAa,QAAQ,KAAK,CAAC;AAAA,MAClE;AAAA,IACF;AAEA,WAAO,qBAAqB,QAAQ,MAAM;AAAA,EAC5C;AACF;AAEA,SAAS,aAAa,OAAwB;AAC5C,MAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,QAAI;AACF,aAAO,KAAK,UAAU,KAAK;AAAA,IAC7B,SAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,OAAO,KAAK;AACrB;AA8BO,SAAS,kBAGd,MAUgE;AAChE,MAAI,KAAK,UAAU,KAAM,QAAO;AAChC,SAAO,UAAkC;AAAA,IACvC,QAAQ,KAAK;AAAA,IACb,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,EAChB,CAAC;AACH;;;AC5HA,eAAsB,iBAAiB,MAIb;AAKxB,MAAI;AACJ,MAAI;AACF,UACG,MAAM,OAAO,6BAA6B;AAAA,EAC/C,SAAS,OAAO;AACd,UAAM,OAAO;AAAA,MACX,IAAI;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,IAAI,WAAW,KAAK,IAAI;AAE7C,MAAI,KAAK,SAAS,UAAa,OAAO,OAAO,YAAY,YAAY;AACnE,WAAO,QAAQ,KAAK,IAAI;AAAA,EAC1B;AAEA,SAAO;AAAA,IACL,MAAM,SAAS,OAAO,OAAO;AAC3B,YAAM,UAAU,OAAO,SAAS,KAAK;AAOrC,UAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,GAAG;AACnD,cAAM,OAAO;AAAA,UACX,IAAI;AAAA,YACF;AAAA,UACF;AAAA,UACA,EAAE,MAAM;AAAA,QACV;AAAA,MACF;AAGA,aAAO,QAAQ,CAAC,EAAE;AAAA,IACpB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/shadow.ts","../src/wrap-mcp-tools.ts","../src/opa/http-policy-client.ts","../src/opa/normalize-opa-decision.ts","../src/opa/evaluate-policy.ts","../src/opa/opa-capability-middleware.ts","../src/opa/opa-policy.ts","../src/opa/wasm-policy-client.ts"],"sourcesContent":["import type {\n Context,\n ModelMessage,\n Tool,\n ToolSet,\n} from '@ai-sdk/provider-utils';\nimport type { ToolApprovalConfiguration } from 'ai';\nimport type { PolicyDecision } from './policy-decision';\n\ninterface GenericApprovalArgs {\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n tools: unknown;\n toolsContext: unknown;\n runtimeContext: unknown;\n messages: ModelMessage[];\n}\n\n/**\n * Event emitted by {@link shadow} every time the wrapped policy is evaluated.\n *\n * Compare `decision` and `effective` to see what would have happened\n * differently if the policy were enforcing: any record where they disagree\n * is a tool call that shadow mode is letting through but enforce mode would\n * have blocked or escalated.\n */\nexport interface PolicyDecisionEvent {\n /** Identifying info from the tool call being evaluated. */\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n /** The decision the wrapped policy returned, normalized to the object form. */\n decision: PolicyDecision;\n /** Whether the SDK will act on `decision` (true) or override to allow (false). */\n enforced: boolean;\n /** The decision the SDK actually acts on. Equals `decision` when enforcing,\n * `{ type: 'approved' }` in shadow mode. */\n effective: PolicyDecision;\n /** ISO 8601 timestamp of the evaluation. */\n timestamp: string;\n}\n\n/**\n * Wrap a `toolApproval` in shadow mode so the policy is evaluated and the\n * decision is reported via `onDecision`, but the SDK is told the call is\n * approved regardless of what the policy said.\n *\n * Use this when you are rolling out a new policy and want to see what it\n * *would* deny before letting it actually deny anything in production. Wire\n * `onDecision` to your logger / metrics pipeline, run for a while, inspect\n * the events where `decision.type !== 'approved'`, fix the policy, then\n * flip `enforce: true` to graduate.\n *\n * @example\n * ```ts\n * import { shadow } from '@ai-sdk/policy-opa';\n * import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';\n *\n * const client = await wasmPolicyClient({ wasm });\n *\n * const toolApproval = shadow(\n * opaPolicy({ client, path: 'agent/call/decision' }),\n * {\n * enforce: process.env.ENFORCE_POLICY === 'true',\n * onDecision: (event) => {\n * logger.info('policy.decision', {\n * tool: event.toolCall.toolName,\n * decision: event.decision.type,\n * enforced: event.enforced,\n * wouldBlock: event.decision.type === 'denied',\n * });\n * },\n * },\n * );\n * ```\n */\nexport function shadow<\n TOOLS extends Record<string, Tool>,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(\n approval: ToolApprovalConfiguration<TOOLS & ToolSet, RUNTIME_CONTEXT>,\n opts: {\n /** When true, the SDK acts on the policy's decision. When false (default),\n * the SDK is told every call is approved. */\n enforce?: boolean;\n /** Invoked once per evaluated tool call with the captured decision. */\n onDecision?: (event: PolicyDecisionEvent) => void | Promise<void>;\n } = {},\n): ToolApprovalConfiguration<TOOLS & ToolSet, RUNTIME_CONTEXT> {\n const enforce = opts.enforce === true;\n const { onDecision } = opts;\n\n const wrapped = async (args: GenericApprovalArgs) => {\n const raw = await evaluateApproval(approval, args);\n const decision = normalizePolicyDecision(raw);\n\n const effective: PolicyDecision = enforce ? decision : { type: 'approved' };\n\n if (onDecision) {\n const event: PolicyDecisionEvent = {\n toolCall: {\n toolName: args.toolCall.toolName,\n toolCallId: args.toolCall.toolCallId,\n input: args.toolCall.input,\n },\n decision,\n enforced: enforce,\n effective,\n timestamp: new Date().toISOString(),\n };\n // Fire-and-forget so a slow or throwing logger does not block the model.\n void (async () => {\n try {\n await onDecision(event);\n } catch {\n // Swallow errors from telemetry; enforcement must not depend on it.\n }\n })();\n }\n\n return effective;\n };\n\n // `wrapped` is typed against the erased `GenericApprovalArgs`; TS can't prove\n // that matches the SDK's per-tool-set function arm, so bridge it here.\n return wrapped as unknown as ToolApprovalConfiguration<\n TOOLS & ToolSet,\n RUNTIME_CONTEXT\n >;\n}\n\nasync function evaluateApproval(\n approval: unknown,\n args: GenericApprovalArgs,\n): Promise<unknown> {\n if (typeof approval === 'function') {\n return await (\n approval as (a: GenericApprovalArgs) => Promise<unknown> | unknown\n )(args);\n }\n\n const map = approval as Record<string, unknown>;\n // Own-property check so a tool name that matches an inherited object\n // property (e.g. `constructor`, `toString`, `valueOf`) is treated as\n // unconfigured instead of resolving to a prototype value and being invoked\n // as if it were a per-tool approval callback.\n const perTool = Object.prototype.hasOwnProperty.call(\n map,\n args.toolCall.toolName,\n )\n ? map[args.toolCall.toolName]\n : undefined;\n if (perTool == null) return undefined;\n if (typeof perTool === 'function') {\n return await (\n perTool as (\n input: unknown,\n options: {\n toolCallId: string;\n messages: ModelMessage[];\n toolContext: unknown;\n runtimeContext: unknown;\n },\n ) => Promise<unknown> | unknown\n )(args.toolCall.input, {\n toolCallId: args.toolCall.toolCallId,\n messages: args.messages,\n toolContext: undefined,\n runtimeContext: args.runtimeContext,\n });\n }\n return perTool;\n}\n\nfunction normalizePolicyDecision(status: unknown): PolicyDecision {\n if (status == null) return { type: 'not-applicable' };\n if (typeof status === 'string') {\n if (\n status === 'approved' ||\n status === 'denied' ||\n status === 'user-approval' ||\n status === 'not-applicable'\n ) {\n return { type: status };\n }\n return { type: 'not-applicable' };\n }\n if (typeof status === 'object' && 'type' in (status as object)) {\n return status as PolicyDecision;\n }\n return { type: 'not-applicable' };\n}\n","import type { Context, Tool } from '@ai-sdk/provider-utils';\nimport type { ToolApprovalConfiguration, ToolApprovalStatus } from 'ai';\n\ntype ApprovalLiteralStatus = Extract<ToolApprovalStatus, string>;\n\n/**\n * Result returned by {@link wrapMcpTools}: the original tool set plus a\n * `toolApproval` that falls back to a configurable default for any tool the\n * supplied approval does not explicitly handle.\n */\nexport interface WrappedMcpTools<\n TOOLS extends Record<string, Tool>,\n RUNTIME_CONTEXT extends Context | unknown | never,\n> {\n tools: TOOLS;\n toolApproval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>;\n}\n\n/**\n * Apply a fallback approval policy to a discovered tool set so the resulting\n * configuration is total: every tool in `tools` is gated either by the\n * supplied `approval` (when it has an opinion) or by `default` (otherwise).\n *\n * The motivating case is MCP. An MCP server hands you whatever tools it\n * exposes, and the agent's effective permissions become the union of the\n * full server surface. Without a fallback, any tool you forgot to write a\n * rule for is silently allowed. This helper closes that gap by forcing every\n * uncovered tool through `default` (which defaults to `user-approval` so the\n * human is in the loop for anything you didn't think about).\n *\n * Works for any tool set, not just MCP-discovered tools. The name reflects\n * the primary use case rather than a hard constraint.\n *\n * @example\n * ```ts\n * import { wrapMcpTools } from '@ai-sdk/policy-opa';\n * import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';\n *\n * const discovered = await mcpClient.tools();\n * const client = await wasmPolicyClient({ wasm });\n *\n * const { tools, toolApproval } = wrapMcpTools(\n * discovered,\n * opaPolicy({ client, path: 'agent/call/decision' }),\n * { default: 'user-approval' }, // anything OPA does not match needs a human\n * );\n *\n * await generateText({ model, tools, toolApproval, prompt });\n * ```\n */\nexport function wrapMcpTools<\n TOOLS extends Record<string, Tool>,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(\n tools: TOOLS,\n approval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>,\n opts?: { default?: ApprovalLiteralStatus },\n): WrappedMcpTools<TOOLS, RUNTIME_CONTEXT> {\n const fallback: ApprovalLiteralStatus = opts?.default ?? 'user-approval';\n\n if (typeof approval === 'function') {\n const wrapped: ToolApprovalConfiguration<\n TOOLS,\n RUNTIME_CONTEXT\n > = async args => {\n const status = await (\n approval as (a: typeof args) => Promise<unknown> | unknown\n )(args);\n return isNotApplicable(status) ? fallback : (status as never);\n };\n return { tools, toolApproval: wrapped };\n }\n\n // Per-tool map form: fill in any tool that the user did not list with the\n // fallback decision. We use `Object.keys(tools)` (not the approval keys) so\n // the result is total over the discovered surface.\n // `Object.create(null)` gives the result no prototype, and we read the\n // supplied approval with an own-property check, so tool names that match\n // inherited object properties (e.g. `constructor`, `toString`, `valueOf`)\n // are treated as unconfigured and receive the fallback.\n const filled = Object.create(null) as Record<keyof TOOLS, unknown>;\n for (const name of Object.keys(tools) as Array<keyof TOOLS>) {\n const configured = Object.prototype.hasOwnProperty.call(approval, name)\n ? approval[name]\n : undefined;\n\n if (configured == null) {\n // Tool the user did not list: force it through the fallback.\n filled[name] = fallback;\n } else if (typeof configured === 'function') {\n // Per-tool approval function: mirror the generic-function form so a\n // \"no opinion\" result (`not-applicable`/`undefined`) is forced through\n // the fallback instead of letting the tool run unapproved.\n const original = configured as (\n ...args: unknown[]\n ) => Promise<unknown> | unknown;\n filled[name] = async (...args: unknown[]) => {\n const status = await original(...args);\n return isNotApplicable(status) ? fallback : status;\n };\n } else {\n // Static status the user explicitly configured: keep it as-is.\n filled[name] = configured;\n }\n }\n\n // `filled` is a plain per-tool map; cast to the SDK's map arm, which TS\n // can't infer from the erased `unknown` values.\n return {\n tools,\n toolApproval: filled as ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>,\n };\n}\n\nfunction isNotApplicable(status: unknown): boolean {\n if (status == null) return true;\n if (status === 'not-applicable') return true;\n if (typeof status === 'object' && status !== null) {\n return (status as { type?: unknown }).type === 'not-applicable';\n }\n return false;\n}\n","import type { PolicyClient } from '../policy-client';\n\n/**\n * Construct a {@link PolicyClient} that talks to a running OPA server over\n * HTTP using `@open-policy-agent/opa`.\n *\n * The `@open-policy-agent/opa` package is an optional peer dependency; install\n * it before using this client:\n *\n * ```sh\n * pnpm add @open-policy-agent/opa\n * ```\n *\n * The `url` typically points at `http://localhost:8181` for a locally running\n * OPA. `headers` is forwarded for Styra DAS / EOPA authentication.\n */\nexport function httpPolicyClient(opts: {\n url: string;\n headers?: Record<string, string>;\n}): PolicyClient {\n const { url, headers } = opts;\n type Underlying = {\n evaluate(path: string, input: unknown): Promise<unknown>;\n };\n type OPAModule = {\n OPAClient: new (\n url: string,\n opts?: { headers?: Record<string, string> },\n ) => Underlying;\n };\n\n let underlying: Underlying | undefined;\n\n async function getUnderlying(): Promise<Underlying> {\n if (underlying) return underlying;\n let mod: OPAModule;\n try {\n mod = (await import('@open-policy-agent/opa')) as unknown as OPAModule;\n } catch (cause) {\n throw Object.assign(\n new Error(\n 'Cannot import \"@open-policy-agent/opa\". Install it as a peer dependency to use httpPolicyClient().',\n ),\n { cause },\n );\n }\n underlying = new mod.OPAClient(url, headers ? { headers } : undefined);\n return underlying;\n }\n\n return {\n async evaluate(path, input) {\n const client = await getUnderlying();\n return (await client.evaluate(path, input)) as never;\n },\n };\n}\n","import type { PolicyDecision } from '../policy-decision';\n\n/**\n * Normalize an OPA evaluation result into the package's {@link PolicyDecision}\n * shape.\n *\n * Supports two Rego output conventions:\n *\n * - **Recommended (explicit):** `{ \"decision\": \"allow\" | \"deny\" | \"requires-approval\", \"reason\": string }`.\n * Maps to `approved` / `denied` / `user-approval` respectively.\n *\n * - **Legacy (boolean):** `{ \"allow\": boolean, \"reason\"?: string }`. `true`\n * maps to `approved`, `false` to `denied`.\n *\n * Unknown shapes and `undefined` are treated as `not-applicable` so that a\n * Rego rule that does not match any branch defaults to \"no opinion\" rather\n * than blocking.\n */\nexport function normalizeOpaDecision(result: unknown): PolicyDecision {\n if (result == null) {\n return { type: 'not-applicable' };\n }\n\n if (typeof result !== 'object') {\n return { type: 'not-applicable' };\n }\n\n const record = result as Record<string, unknown>;\n const reason = typeof record.reason === 'string' ? record.reason : undefined;\n\n if (typeof record.decision === 'string') {\n switch (record.decision) {\n case 'allow':\n return withReason('approved', reason);\n case 'deny':\n return withReason('denied', reason);\n case 'requires-approval':\n return { type: 'user-approval' };\n case 'not-applicable':\n return { type: 'not-applicable' };\n }\n }\n\n if (typeof record.allow === 'boolean') {\n return withReason(record.allow ? 'approved' : 'denied', reason);\n }\n\n return { type: 'not-applicable' };\n}\n\nfunction withReason(\n type: 'approved' | 'denied',\n reason: string | undefined,\n): PolicyDecision {\n return reason ? { type, reason } : { type };\n}\n","import type { PolicyClient } from '../policy-client';\n\nexport type EvaluateOutcome =\n | { ok: true; result: unknown }\n | { ok: false; error: unknown };\n\n/**\n * Evaluate a policy, capturing a backend error (OPA unreachable, WASM fault,\n * bad path) as a value instead of letting it throw. This is the package's\n * fail-closed invariant in one place: a thrown `client.evaluate` must never\n * escape into the SDK callback or middleware, where it would abort the run.\n * Each caller turns `ok: false` into its own safe fallback (deny / no tools).\n */\nexport async function evaluatePolicy(\n client: PolicyClient,\n path: string,\n input: unknown,\n): Promise<EvaluateOutcome> {\n try {\n return { ok: true, result: await client.evaluate(path, input) };\n } catch (error) {\n return { ok: false, error };\n }\n}\n","import type {\n LanguageModelV4CallOptions,\n LanguageModelV4Middleware,\n} from '@ai-sdk/provider';\nimport type { PolicyClient } from '../policy-client';\nimport { evaluatePolicy } from './evaluate-policy';\n\n/**\n * Default OPA input shape passed to the capability-scoping rule.\n * Override with `toInput` if your Rego expects a different schema.\n */\nexport interface DefaultOpaCapabilityInput {\n messages: LanguageModelV4CallOptions['prompt'];\n providerOptions: LanguageModelV4CallOptions['providerOptions'];\n}\n\n/**\n * Construct an experimental {@link LanguageModelV4Middleware} that narrows\n * the `tools` field on every model call to an allowlist returned by OPA.\n *\n * Why use this alongside the per-call `toolApproval` gate? Two reasons:\n *\n * 1. **Defense in depth.** If a bug or regression slips through the policy\n * used by `toolApproval`, the middleware still prevents the model from\n * seeing the disallowed tools in the first place.\n * 2. **Capability disclosure.** The model does not waste tokens describing\n * tools it cannot call, and jailbreak attempts get \"I don't have access\n * to that tool\" rather than \"[approval denied]\".\n *\n * The OPA rule at `path` is expected to return either a `string[]` of allowed\n * tool names, or an object `{ tools: string[] }`. Function tools whose `name`\n * matches are kept; provider tools whose dotted `id` or bare `name` matches are\n * kept; everything else is dropped from `params.tools`.\n *\n * On a malformed result (or an OPA error), the middleware **fails closed**:\n * `params.tools` is set to `undefined`, so the model is told it has no tools\n * available. Misconfiguration should not silently widen capabilities.\n *\n * @example\n * ```ts\n * import { wrapLanguageModel } from 'ai';\n * import { wasmPolicyClient, opaCapabilityMiddleware } from '@ai-sdk/policy-opa';\n *\n * const client = await wasmPolicyClient({ wasm });\n *\n * const wrappedModel = wrapLanguageModel({\n * model: anthropic('claude-sonnet-4-5'),\n * middleware: opaCapabilityMiddleware({\n * client,\n * path: 'agent/tools/allowed',\n * }),\n * });\n * ```\n */\nexport function opaCapabilityMiddleware(opts: {\n client: PolicyClient;\n path: string;\n toInput?: (args: {\n messages: LanguageModelV4CallOptions['prompt'];\n providerOptions: LanguageModelV4CallOptions['providerOptions'];\n }) => unknown;\n}): LanguageModelV4Middleware {\n const { client, path, toInput } = opts;\n\n return {\n specificationVersion: 'v4',\n async transformParams({ params }) {\n // No tools on the call: nothing to filter.\n if (params.tools == null || params.tools.length === 0) {\n return params;\n }\n\n const input =\n toInput?.({\n messages: params.prompt,\n providerOptions: params.providerOptions,\n }) ??\n ({\n messages: params.prompt,\n providerOptions: params.providerOptions,\n } satisfies DefaultOpaCapabilityInput);\n\n // Fail closed on evaluator error or a malformed allowlist: drop all\n // tools so a misconfiguration never silently widens capabilities.\n const outcome = await evaluatePolicy(client, path, input);\n if (!outcome.ok) {\n return { ...params, tools: undefined };\n }\n\n const allowed = extractAllowedNameSet(outcome.result);\n if (allowed == null) {\n return { ...params, tools: undefined };\n }\n\n let removed = false;\n const filtered = params.tools.filter(t => {\n // Function tools are keyed by `name`. Provider tools carry both a\n // dotted `id` (`<provider>.<tool>`) and a bare `name`; match either so\n // an allowlist authored with the bare name keeps the tool instead of\n // silently dropping it.\n const keep =\n t.type === 'function'\n ? allowed.has(t.name)\n : allowed.has(t.id) || allowed.has(t.name);\n if (!keep) removed = true;\n return keep;\n });\n\n // Preserve object identity when nothing was dropped so downstream\n // middleware can no-op on reference equality.\n if (!removed) return params;\n return { ...params, tools: filtered };\n },\n };\n}\n\nfunction extractAllowedNameSet(result: unknown): Set<string> | null {\n const list = Array.isArray(result)\n ? result\n : result != null && typeof result === 'object'\n ? (result as { tools?: unknown }).tools\n : undefined;\n if (!Array.isArray(list)) return null;\n const out = new Set<string>();\n for (const item of list) {\n if (typeof item !== 'string') return null;\n out.add(item);\n }\n return out;\n}\n","import type {\n Context,\n InferToolSetContext,\n ModelMessage,\n ToolSet,\n} from '@ai-sdk/provider-utils';\nimport type { ToolApprovalConfiguration } from 'ai';\nimport type { PolicyClient } from '../policy-client';\nimport { evaluatePolicy } from './evaluate-policy';\nimport { normalizeOpaDecision } from './normalize-opa-decision';\n\n/**\n * The default shape passed to the OPA rule as `input` when no `toInput` is\n * supplied. Rego rules can read `input.tool.name`, `input.args`, and so on.\n */\nexport interface DefaultOpaInput {\n tool: { name: string };\n args: unknown;\n messages: ReadonlyArray<ModelMessage>;\n runtimeContext: unknown;\n}\n\n/**\n * Construct a {@link ToolApprovalConfiguration} backed by an OPA policy.\n *\n * The returned generic approval function evaluates the supplied Rego entry\n * (`path`) for every tool call and maps the result to the SDK's approval\n * status via {@link normalizeOpaDecision}. Pass the result directly as\n * `toolApproval` on `generateText` / `streamText` / `ToolLoopAgent`.\n *\n * ```ts\n * import { wasmPolicyClient } from '@ai-sdk/policy-opa';\n * import { opaPolicy } from '@ai-sdk/policy-opa';\n *\n * const client = await wasmPolicyClient({ wasm });\n * const toolApproval = opaPolicy({ client, path: 'agent/call/decision' });\n *\n * await generateText({ model, tools, toolApproval, prompt });\n * ```\n *\n * @param opts.client The OPA client (HTTP or WASM).\n * @param opts.path The Rego entrypoint that returns the decision object.\n * @param opts.toInput Optional transformer to shape the OPA input.\n */\nexport function opaPolicy<\n TOOLS extends ToolSet = ToolSet,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(opts: {\n client: PolicyClient;\n path: string;\n toInput?: (args: {\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n tools: TOOLS | undefined;\n toolsContext: InferToolSetContext<TOOLS>;\n runtimeContext: RUNTIME_CONTEXT;\n messages: ModelMessage[];\n }) => unknown;\n}): ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> {\n const { client, path, toInput } = opts;\n\n return async ({\n toolCall,\n tools,\n toolsContext,\n runtimeContext,\n messages,\n }) => {\n const opaInput =\n toInput?.({ toolCall, tools, toolsContext, runtimeContext, messages }) ??\n ({\n tool: { name: toolCall.toolName },\n args: toolCall.input,\n messages,\n runtimeContext,\n } satisfies DefaultOpaInput);\n\n // Fail closed: a backend error must not be read as \"no opinion\". Deny so\n // the model sees a structured result it can reason about, rather than the\n // error rejecting out of the callback and aborting the run.\n const outcome = await evaluatePolicy(client, path, opaInput);\n if (!outcome.ok) {\n return {\n type: 'denied',\n reason: `policy evaluation failed: ${errorMessage(outcome.error)}`,\n };\n }\n\n return normalizeOpaDecision(outcome.result);\n };\n}\n\nfunction errorMessage(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'object' && cause !== null) {\n try {\n return JSON.stringify(cause);\n } catch {\n // Fall through for circular / non-serializable objects.\n }\n }\n return String(cause);\n}\n\n/**\n * Optional variant of {@link opaPolicy} that gracefully degrades when no\n * policy backend is available.\n *\n * Returns `undefined` when `client` is `undefined`. Pass that directly as\n * `toolApproval` and the SDK falls back to its default allow-all behavior.\n * When `client` is supplied, behaves exactly like {@link opaPolicy}.\n *\n * Use this when the policy file is configured per environment (e.g. loaded\n * in production, absent in local development):\n *\n * ```ts\n * import { readFile } from 'node:fs/promises';\n * import { optionalOpaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';\n *\n * const wasm = process.env.POLICY_WASM_PATH\n * ? await readFile(process.env.POLICY_WASM_PATH)\n * : undefined;\n * const client = wasm ? await wasmPolicyClient({ wasm }) : undefined;\n *\n * const toolApproval = optionalOpaPolicy({\n * client,\n * path: 'agent/call/decision',\n * });\n *\n * await generateText({ model, tools, toolApproval, prompt });\n * ```\n */\nexport function optionalOpaPolicy<\n TOOLS extends ToolSet = ToolSet,\n RUNTIME_CONTEXT extends Context | unknown | never = unknown,\n>(opts: {\n client: PolicyClient | undefined;\n path: string;\n toInput?: (args: {\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n tools: TOOLS | undefined;\n toolsContext: InferToolSetContext<TOOLS>;\n runtimeContext: RUNTIME_CONTEXT;\n messages: ModelMessage[];\n }) => unknown;\n}): ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> | undefined {\n if (opts.client == null) return undefined;\n return opaPolicy<TOOLS, RUNTIME_CONTEXT>({\n client: opts.client,\n path: opts.path,\n toInput: opts.toInput,\n });\n}\n","import type { PolicyClient } from '../policy-client';\n\n/**\n * Loaded OPA WASM bundle. Compiled offline with `opa build -t wasm` and\n * passed in as bytes.\n */\ntype LoadedPolicy = {\n evaluate(input: unknown): Array<{ result: unknown }>;\n /** Some versions expose `setData` for documents bundled at evaluation time. */\n setData?(data: unknown): void;\n};\n\n/**\n * Construct a {@link PolicyClient} that evaluates a compiled OPA WASM bundle\n * in-process using `@open-policy-agent/opa-wasm`.\n *\n * The `@open-policy-agent/opa-wasm` package is an optional peer dependency;\n * install it before using this client:\n *\n * ```sh\n * pnpm add @open-policy-agent/opa-wasm\n * ```\n *\n * The `path` argument to `evaluate(path, input)` is informational. The WASM\n * bundle is built around a fixed entrypoint at `opa build` time. The path is\n * recorded for audit logs but does not affect the evaluation.\n */\nexport async function wasmPolicyClient(opts: {\n wasm: Uint8Array | ArrayBuffer;\n /** Optional data document bundled into the policy (passed to `setData`). */\n data?: unknown;\n}): Promise<PolicyClient> {\n type WasmModule = {\n loadPolicy(wasm: Uint8Array | ArrayBuffer): Promise<LoadedPolicy>;\n };\n\n let mod: WasmModule;\n try {\n mod =\n (await import('@open-policy-agent/opa-wasm')) as unknown as WasmModule;\n } catch (cause) {\n throw Object.assign(\n new Error(\n 'Cannot import \"@open-policy-agent/opa-wasm\". Install it as a peer dependency to use wasmPolicyClient().',\n ),\n { cause },\n );\n }\n\n const policy = await mod.loadPolicy(opts.wasm);\n\n if (opts.data !== undefined && typeof policy.setData === 'function') {\n policy.setData(opts.data);\n }\n\n return {\n async evaluate(_path, input) {\n const results = policy.evaluate(input);\n // The WASM SDK returns an array of `{ result }` entries, one per\n // top-level expression in the entrypoint. A non-array or empty array\n // means the entrypoint produced no value (wrong entrypoint at\n // `opa build` time, an undefined decision rule, or a misbehaving\n // bundle). Throw rather than return undefined so the caller fails closed\n // instead of silently treating it as \"no opinion\".\n if (!Array.isArray(results) || results.length === 0) {\n throw Object.assign(\n new Error(\n 'OPA WASM policy produced no result. Check that the bundle was built with the correct entrypoint (`opa build -t wasm -e <path>`).',\n ),\n { input },\n );\n }\n // Return the first entry's `result`, matching the HTTP client's\n // single-decision shape.\n return results[0].result as never;\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/policy-opa",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -25,20 +25,20 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@ai-sdk/provider": "4.0.2",
29
- "@ai-sdk/provider-utils": "5.0.6"
28
+ "@ai-sdk/provider": "4.0.3",
29
+ "@ai-sdk/provider-utils": "5.0.7"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "22.19.19",
33
33
  "tsup": "^8.5.1",
34
34
  "typescript": "5.8.3",
35
35
  "@vercel/ai-tsconfig": "0.0.0",
36
- "ai": "7.0.18"
36
+ "ai": "7.0.19"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@open-policy-agent/opa": "^1.0.0",
40
40
  "@open-policy-agent/opa-wasm": "^1.0.0",
41
- "ai": "7.0.18"
41
+ "ai": "7.0.19"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@open-policy-agent/opa": {
package/src/shadow.ts CHANGED
@@ -137,7 +137,16 @@ async function evaluateApproval(
137
137
  }
138
138
 
139
139
  const map = approval as Record<string, unknown>;
140
- const perTool = map[args.toolCall.toolName];
140
+ // Own-property check so a tool name that matches an inherited object
141
+ // property (e.g. `constructor`, `toString`, `valueOf`) is treated as
142
+ // unconfigured instead of resolving to a prototype value and being invoked
143
+ // as if it were a per-tool approval callback.
144
+ const perTool = Object.prototype.hasOwnProperty.call(
145
+ map,
146
+ args.toolCall.toolName,
147
+ )
148
+ ? map[args.toolCall.toolName]
149
+ : undefined;
141
150
  if (perTool == null) return undefined;
142
151
  if (typeof perTool === 'function') {
143
152
  return await (
@@ -74,9 +74,34 @@ export function wrapMcpTools<
74
74
  // Per-tool map form: fill in any tool that the user did not list with the
75
75
  // fallback decision. We use `Object.keys(tools)` (not the approval keys) so
76
76
  // the result is total over the discovered surface.
77
- const filled = {} as Record<keyof TOOLS, unknown>;
77
+ // `Object.create(null)` gives the result no prototype, and we read the
78
+ // supplied approval with an own-property check, so tool names that match
79
+ // inherited object properties (e.g. `constructor`, `toString`, `valueOf`)
80
+ // are treated as unconfigured and receive the fallback.
81
+ const filled = Object.create(null) as Record<keyof TOOLS, unknown>;
78
82
  for (const name of Object.keys(tools) as Array<keyof TOOLS>) {
79
- filled[name] = approval[name] ?? fallback;
83
+ const configured = Object.prototype.hasOwnProperty.call(approval, name)
84
+ ? approval[name]
85
+ : undefined;
86
+
87
+ if (configured == null) {
88
+ // Tool the user did not list: force it through the fallback.
89
+ filled[name] = fallback;
90
+ } else if (typeof configured === 'function') {
91
+ // Per-tool approval function: mirror the generic-function form so a
92
+ // "no opinion" result (`not-applicable`/`undefined`) is forced through
93
+ // the fallback instead of letting the tool run unapproved.
94
+ const original = configured as (
95
+ ...args: unknown[]
96
+ ) => Promise<unknown> | unknown;
97
+ filled[name] = async (...args: unknown[]) => {
98
+ const status = await original(...args);
99
+ return isNotApplicable(status) ? fallback : status;
100
+ };
101
+ } else {
102
+ // Static status the user explicitly configured: keep it as-is.
103
+ filled[name] = configured;
104
+ }
80
105
  }
81
106
 
82
107
  // `filled` is a plain per-tool map; cast to the SDK's map arm, which TS