A common mistake when designing tools for agents: thinking of them as if the agent were "root". If the tool is invoked, it runs. No checks.
In production that's a giant hole. The agent invokes on behalf of a user, and that user has limited permissions. The agent deciding to call delete_user doesn't mean the user talking to the agent has permission to delete users.
Rule of thumb:
Tools check authorization on their own. NEVER trust that "the agent will only invoke what it has permission to invoke". The agent does NOT know permissions. You do.
By user role. The user has a set of roles (crewmate, shift_lead, admin), and the tool requires one or more to run. Example: delete_user requires admin.
By ownership. The user can operate on the resources they own. delete_booking allows deleting your own bookings. This is resource-level scope.
Combined. Allow if owner OR if has a role that authorizes. The most common form: "owners AND managers can delete".
async function handle({ booking_id }) {
const booking = await db.bookings.get(booking_id);
if (!booking) {
return { ok: false, error: { code: "not_found", message: "..." } };
}
const user = currentUser();
const isOwner = user.alias === booking.crewmate_alias;
const isLead = user.roles.includes("shift_lead");
if (!isOwner && !isLead) {
logger.warn("auth.denied", {
tool: "delete_booking",
user: user.alias,
booking_id,
reason: "not_owner_not_lead",
});
return {
ok: false,
error: {
code: "permission_denied",
message: "You don't have permission to delete this booking.",
},
};
}
await db.bookings.delete(booking_id);
return { ok: true };
}When denying, log the attempt. Without that log you don't know:
logger.warn("auth.denied", { tool, user, resource_id, reason });On the right you have delete_booking. The starter deletes without checking. Implement combined authorization (owner OR shift_lead), return a structured error on denial, and log every denial.
Every tool that writes state must check authorization. No exceptions, even if it looks "internal". In six months that "internal tool" will be exposed at a public endpoint and you'll thank yourself for protecting it.