Examples
Permissioned Tools
Tools that expose product capabilities while keeping authorization, validation, audit, and output safety in application code.
Permissioned tools are the model’s narrow interface to your application. A tool schema validates model arguments, but authorization must come from request scope and service checks, not from values the model supplies.
Scenario
A support agent can read orders, create tickets, and request refunds. The model can ask for those actions, but the app must enforce user, tenant, role, idempotency, approval, audit, and safe return shapes.
Approval Policy
import { createTool, type ToolApprovalPolicy } from "@anvia/core";
import { z } from "zod";
type RefundArgs = {
orderId: string;
amountCents: number;
reason: string;
};
const refundApproval = {
when(ctx) {
return ctx.args.amountCents > 10_000;
},
reason(ctx) {
return `Refund ${ctx.args.amountCents} cents for order ${ctx.args.orderId}.`;
},
rejectMessage: "The refund was not approved.",
} satisfies ToolApprovalPolicy<RefundArgs>;
Tool Catalog
export function createSupportTools(scope: SupportToolScope) {
return [
createLookupOrderTool(scope),
createTicketTool(scope),
createRequestRefundTool(scope),
];
}
Read Tool
function createLookupOrderTool(scope: SupportToolScope) {
return createTool({
name: "lookup_order",
description: "Look up one order owned by the current customer.",
input: z.object({
orderId: z.string(),
}),
output: z.object({
id: z.string(),
status: z.string(),
canChangeAddress: z.boolean(),
}),
async execute({ orderId }) {
const order = await scope.services.orders.lookupForUser({
orderId,
userId: scope.user.id,
tenantId: scope.user.tenantId,
});
return {
id: order.id,
status: order.status,
canChangeAddress: order.status === "processing",
};
},
});
}
Ticket Write
function createTicketTool(scope: SupportToolScope) {
return createTool({
name: "create_ticket",
description: "Create a support ticket for the current customer.",
input: z.object({
subject: z.string().min(1),
body: z.string().min(1),
orderId: z.string().optional(),
}),
output: z.object({
ticketId: z.string(),
status: z.enum(["created", "not_allowed"]),
message: z.string(),
}),
async execute(input) {
await scope.authz.require("tickets:create");
const ticket = await scope.services.tickets.createForUser({
...input,
userId: scope.user.id,
tenantId: scope.user.tenantId,
});
await scope.auditLog.record({
actorId: scope.user.id,
action: "ticket.create",
targetId: ticket.id,
metadata: { orderId: input.orderId },
});
return {
ticketId: ticket.id,
status: "created",
message: "Ticket created.",
};
},
});
}
Refund Write
function createRequestRefundTool(scope: SupportToolScope) {
return createTool({
name: "request_refund",
description: "Request a refund for an eligible order.",
input: z.object({
orderId: z.string(),
amountCents: z.number().int().positive(),
reason: z.string().min(1),
}),
output: z.object({
refundId: z.string().optional(),
status: z.enum(["queued", "not_allowed"]),
message: z.string(),
}),
approval: refundApproval,
async execute(args) {
await scope.authz.require("orders:refund");
const refund = await scope.services.refunds.requestForUser({
...args,
userId: scope.user.id,
tenantId: scope.user.tenantId,
idempotencyKey: scope.idempotencyKey,
});
await scope.auditLog.record({
actorId: scope.user.id,
action: "refund.request",
targetId: args.orderId,
metadata: { refundId: refund.id, amountCents: args.amountCents },
});
return {
refundId: refund.id,
status: "queued",
message: "Refund request queued.",
};
},
});
}
The model sees tool names, descriptions, schemas, and safe outputs. It does not get to choose the authenticated user, tenant, permission set, service transaction, or idempotency key.
Permission Layers
| Layer | Responsibility |
|---|---|
| request scope | authenticated user, tenant, roles, feature flags |
| tool input schema | shape of model-supplied arguments |
| service authorization | whether the user can access or mutate the target record |
| business validation | whether the action is currently allowed |
| approval policy | whether a human or external policy must approve |
| audit log | who requested what, when, and with which product ids |
| output schema | safe result shape returned to the model |
Failure Modes
- The tool accepts
userId,tenantId, or role from the model. - Prompt text says “only access this user’s orders” but services do not enforce it.
- The tool returns full database records when the model only needs status fields.
- Expected denials throw raw service errors instead of returning safe typed results.
- Side-effect tools lack idempotency keys and audit records.
