AI SDK
permdock/ai-sdk turns PermDock decisions into Vercel AI SDK 7 tool approvals, capability middleware and WorkflowAgent suspensions, fail-closed by construction.
Status: planned Phase: 1
permdock/ai-sdk connects the Decision model to the approval vocabulary of the Vercel AI SDK 7: toolApproval on generateText, streamText and ToolLoopAgent, a language-model middleware that narrows the tool list before the model sees it, and needsApproval for WorkflowAgent, the one place where that option is not deprecated.
Purpose
AI SDK 7 moved tool approval out of individual tools and into a single toolApproval callback that returns approved, denied, user-approval or not-applicable (Tool Approvals, deprecation commit). Vercel's reference policy adapter, @ai-sdk/policy-opa (Policy-Based Tool Approvals), evaluates Rego and includes opaCapabilityMiddleware to trim the tool list; it also fails open when a decision is unrecognised (vercel/ai#19978). permdock/ai-sdk offers the same three integration points with typed permission references and a fail-closed mapping: every path ends in approved, denied or user-approval, never not-applicable.
API
import { createPermDock } from 'permdock/ai-sdk'
const { toolApproval, capabilityMiddleware, needsApproval } = createPermDock(policy, {
subject: ({ runtimeContext }) => runtimeContext.user,
actor: ({ runtimeContext }) => ({ id: runtimeContext.agentId, kind: 'ai-sdk' }),
tools: {
delete_post: { permission: permissions.post.delete, data: (args) => loadPost(args.id) },
list_posts: { permission: permissions.post.list },
},
})
generateText({ model, tools, toolApproval })
// granted → 'approved'; denied → 'denied' (reason + alternatives); approval-required → 'user-approval'
wrapLanguageModel({ model, middleware: capabilityMiddleware })
// narrows `tools` to what this subject may call before the model sees them
tool({ ..., needsApproval: needsApproval(permissions.post.delete) })
// WorkflowAgent: durable suspend until a human answerstoolsmaps AI SDK tool names to a permission reference and, for instance-level actions, adataresolver that loads the resource from the tool arguments.subjectandactorread fromruntimeContext(or any per-call context the caller provides), so one configuration serves many tenants when returned fromprepareCall.toolApprovalis a plain function compatible with the AI SDK signature;capabilityMiddlewareis aLanguageModelMiddleware;needsApproval(permission)returns the predicateWorkflowAgentexpects.- Tool names not present in
toolsare treated as unmapped and denied (see fail-closed rules below).
Request lifecycle
- Before the model call,
capabilityMiddlewarebuilds a request-scopedPermDockfromruntimeContextand removes tools whose permission has no grant for this subject. The model cannot plan with tools it may not use. - The model emits a tool call. AI SDK invokes
toolApprovalwith the tool name and arguments. - The adapter validates the arguments against the resource schema when
datais declared (boundary validation), loads the resource, and callspermdock.decide(permission, data). - The Decision maps to the AI SDK vocabulary:
| Decision outcome | toolApproval result | Notes |
|---|---|---|
granted | approved | tool executes |
denied | denied | reason carries denials and alternatives for the model |
approval-required | user-approval | UI or workflow asks a human; Decision.token attached |
| unmapped tool, validation error, thrown error | denied | fail closed |
- On
user-approval, the human's answer returns as an approval response. The adapter re-runsdecideand comparesDecision.token(hash of permission key, resource id, subject, actor) with the token issued in step 4; a mismatch is denied. This is the problemexperimental_toolApprovalSecretaddresses by signing approval payloads; PermDock adds the check that the approved call is the same call. - Every step emits
on('decision')so audit logging andpermdock/otelsee the same events as any other adapter.
WorkflowAgent follows the same path but suspends the durable workflow at step 5 instead of returning to the caller; on resume, the token check runs before the tool executes.
What it validates
- Tool arguments against the resource's Standard Schema when a
dataresolver exists (validate: 'boundary'); invalid arguments are a denial with a validation reason, not an exception in the agent loop. - The subject comes from
runtimeContext, never from model output or tool arguments. - Approval replays: token equality on resume; expired or foreign tokens are denied.
- Tool coverage: in development, the adapter warns when
toolspassed togenerateTextinclude names missing from thetoolsmap, because those calls will be denied at runtime.
How denials surface
deniedresults include areasonstring built from the Decision: the failing role reasons and thealternativeslist (permitted permissions on the same resource), so a model can choose a permitted action instead of retrying the denied one.user-approvalresults carryDecision.tokenand a human-readable summary for the approval UI.- Nothing ever maps to
not-applicable. If the adapter cannot decide (unknown tool, thrown resolver, malformed context) it returnsdeniedand logs the cause. This is the intentional contrast with@ai-sdk/policy-opa, where unrecognised decisions execute the tool (vercel/ai#19978). capabilityMiddlewaredenials are silent by design: the tool is absent from the request. SetonDecisionon the instance to log them.
Example app
apps/examples/ai-sdk-agent: a ToolLoopAgent over post tools with two users (member, admin), a WorkflowAgent variant that suspends on delete_post, and tests asserting that a removed tool is never offered, a denied tool returns alternatives, and a tampered approval replay is refused.
Related standards
- Approvals:
approval: 'human', replay-safetoken, surfaces per runtime. - Delegation: the
actorhalf of the subject for agent runs. - OWASP Agentic Top 10: ASI02 Tool Misuse mitigations via per-tool permissions and argument validation.
- MCP adapter and Claude Agent adapter: the same Decision mapped to other runtimes.
Open questions
- Whether unmapped tools should be denied (current default) or passed through with a loud development warning; passing through would re-introduce fail-open behaviour.
- How much of the Decision to expose in the
reasonstring versus a structured field, given the AI SDK result type is a string. - Whether
capabilityMiddlewareshould also rewrite tool descriptions to mention conditions (for instance, "only your own posts"). - Whether
simulate()should be offered as a plan pre-flight hook before the agent loop starts.
MCP
permdock/mcp guards MCP tools with typed permissions, scope step-up challenges, per-caller tool lists, boundary-validated arguments and model-readable refusals.
Claude Agent SDK
permdock/claude-agent answers Claude Agent SDK canUseTool callbacks and PermissionRequest hooks from PermDock decisions, mapping built-in tools such as Bash to typed permissions.