Tool approvals
Require a trusted decision before a tool performs a sensitive action.
Require approval before a tool performs an action that needs the user’s decision, such as cancelling an order. The model may propose the action; it must not approve itself.
Native LangChain middleware and tools may interrupt execution, but they do not replace RELKIT’s tool policy. Use the policies and trusted invocation decisions below for function-backed tools.
Choose a policy
defineTool.approval and .asTool({ approval }) accept exactly three policy strings:
The examples below are alternatives: choose the policy that fits each tool.
approval |
Behavior |
|---|---|
never |
No separate approval decision is required. |
on-write |
Require approval for write and external actions, but not none or read. |
always |
Require approval for every call, including reads. |
Declare sideEffect accurately. A write incorrectly marked as a read can skip an intended on-write approval.
No approval for a read
Use never when the operation may run without a separate confirmation:
/** Exposes the hello function as a read-only greeting tool with its existing two-second timeout. */
import hello from "@app/services/hello/functions/hello.function.js";
export default hello.asTool({
id: "hello.lookup",
description: "Read a greeting for a supplied name",
sideEffect: "read",
approval: "never",
timeoutMs: 2_000,
});
Approval for writes
This defineTool example requires confirmation because its side effect is write:
import { defineTool } from "@relkit/app/tools";
import deleteOrder from "../../src/services/orders/functions/delete-order.function.js";
export const cancelOrder = defineTool({
id: "orders.cancel-order-on-write",
target: deleteOrder,
description: "Cancel an order after confirmation",
sideEffect: "write",
approval: "on-write",
timeoutMs: 2_000,
mcp: false,
});
It uses the existing order-deletion function. mcp: false keeps this approval-required tool out of MCP.
Approval on every call
This order-cancellation tool requires approval on every call:
/** Exposes the order deletion function as a write tool requiring explicit approval before invocation. */
import deleteOrder from "../functions/delete-order.function.js";
const cancelOrder = deleteOrder.asTool({
id: "orders.cancel-order",
description: "Cancel one order after a human approves the write",
sideEffect: "write",
approval: "always",
timeoutMs: 2_000,
});
export default cancelOrder;
The function must still check whether the caller may cancel that order. Approval does not replace authorization.
Use an approval callback
The tool’s approval policy is not a function. The callback goes in the second argument of tool.invoke(), where your application supplies the decision for that call.
The callback may return true or "approved" to allow execution, and false or "denied" to stop it. It may also return a Promise of any of those values. It receives { toolId, sideEffect, policy }; capture the specific action’s arguments from your calling code.
Wait for a trusted confirmation
For the on-write tool above, call your own confirmation flow before cancelling:
/** Invokes cancellation only after the trusted confirmation flow approves the exact order.
* @param orderId - Order identity shown to confirmation and passed to the tool.
* @param confirm - Trusted asynchronous decision callback.
* @returns The native checked deletion result when approval succeeds.
* @throws The original invocation failure when confirmation denies or fails. */
export async function cancelOrderWithApproval(
orderId: string,
confirm: (message: string) => Promise<boolean>,
): Promise<{ orderId: string; deleted: boolean }> {
return cancelOrder.invoke(
{ orderId },
{
// Ask your trusted confirmation flow about this specific order.
approval: async () => {
const confirmed = await confirm(`Cancel order ${orderId}?`);
return confirmed ? "approved" : "denied";
},
},
);
}
Your app supplies confirm from a trusted UI or approval service. Bind the decision to the authenticated user and this specific order; do not take approval from model-generated arguments or an unchecked request field. Setting the tool policy alone does not create a confirmation screen.
Test synchronous decisions
These unit-test examples show every supported decision value. The unconditional callbacks are test stubs, not a production approval flow:
await expect(
cancelOrder.invoke({ orderId: "order-123" }, { approval: () => true }),
).resolves.toEqual({ orderId: "order-123", deleted: true });
await expect(
cancelOrder.invoke({ orderId: "order-123" }, { approval: () => "approved" }),
).resolves.toEqual({ orderId: "order-123", deleted: true });
await expect(
cancelOrder.invoke({ orderId: "order-123" }, { approval: () => false }),
).rejects.toMatchObject({ code: "RELKIT_APPROVAL_DENIED" });
await expect(
cancelOrder.invoke({ orderId: "order-123" }, { approval: () => "denied" }),
).rejects.toMatchObject({ code: "RELKIT_APPROVAL_DENIED" });
A missing callback on an approval-required call fails with RELKIT_APPROVAL_REQUIRED; a denied decision fails with RELKIT_APPROVAL_DENIED. A callback that throws also stops execution. A callback is not called when the policy does not require approval.
Agent and MCP behavior
A client-exposed agent that declares the approve control publishes approval waiting state through its
durable thread. The typed hook exposes approve(approvalId, { threadId }) and
deny(approvalId, { threadId }); the caller must use the same authorized business thread. The
agent test helper can supply scripted approvals for deterministic tests.
Over MCP, approval-required calls are currently rejected; there is no MCP approval-and-resume flow. Keep these tools out of MCP with mcp: false if clients cannot use them.