Short answer: filter the tools an agent can see for coarse capability control, use LangChain’s human-in-the-loop middleware when a proposed call needs review, and enforce the final user and resource permission inside the tool callback before the side effect starts.
LangChain’s current JavaScript docs cover tools and runtime context and human-in-the-loop middleware. Both are useful. Neither makes a model-supplied user or tenant ID trustworthy. The official adapter is nominee-langchain.
Tool filtering can be the whole requirement
const tools = session.role === 'admin'
? [readInvoice, refundPayment]
: [readInvoice]
const agent = createAgent({ model, tools })
This is enough when each run has trusted server-side role data, hidden tools cannot be called through another route, and the permission is limited to “which tool names may this session use?” Add LangChain’s interrupt-on policy when the selected call also needs a human review.
Filtering gets weaker when one tool can touch many resources. Seeing refundPayment does not prove that the user owns payment pay_123, belongs to its tenant, or may refund the proposed amount. That decision needs the exact parsed input and trusted application context.
Put the enforcement point inside the tool
Use nomineeTool from nominee-langchain so the side effect runs only after a capability is issued:
import { nomineeTool } from 'nominee-langchain'
import { Nominee, ask } from 'nominee'
import { z } from 'zod'
const nominee = new Nominee({
policy: {
rules: [ask('payments.refund')],
fallback: 'deny',
},
authorizer: ({ user, action, resource, tenant }) =>
permissions.can({ user, action, resource, tenant }),
})
export const refundTool = nomineeTool({
name: 'payments_refund',
description: 'Refund a payment',
schema: z.object({
transactionId: z.string(),
cents: z.number().int().positive(),
}),
nominee,
action: 'payments.refund',
user: session.userId,
tenant: session.tenantId,
resource: ({ input }) => 'payment:' + input.transactionId,
execute: async (input) =>
payments.refund({ ...input, tenantId: session.tenantId }),
})
The policy receives parsed tool arguments. The user and tenant come from the authenticated session. A denial exits before payments.refund receives control. With a named resource, the authorizer is checked during planning and again immediately before execution after an approval pause.
For an ask rule without an inline approval handler, preserve the resulting pending action and resume it after the human decision. Do not catch the pending result and call the side effect directly.
Use each control for its own job
- Tool filtering limits which capabilities enter a model run.
- LangChain human-in-the-loop pauses selected calls for review and edit, approve, or reject decisions.
- Action authorization enforces the trusted user, tenant, resource, input, and budget decision at execution.
If your permission service already makes the resource decision, keep it. FGA and OPA decide; nominee executes explains how to put that decision next to the tool side effect.
See the boundary run
Approve one tool call and inspect the receipt chain.