Fail-closed authorization
A side-effecting tool won't even define without a gate. Every call checks the untrusted arguments against the trusted caller context before the handler runs.
Per-call authorization, idempotency, and a tamper-evident audit trail for agent tools, in-process.
Flue's own guidance: "a tool's parameters are model-selected inputs, not an authorization boundary." flue-guard is that boundary. The model controls the arguments; your application controls the context. Every call is decided by comparing the two, then written to a hash-chained receipt.
import * as v from "valibot";
import { govern, caller } from "flue-guard";
declare const accounts: {
ownedBy(accountId: string, actorId: string): Promise<boolean>;
sendResetLink(accountId: string): Promise<void>;
};
const gov = govern({ audit: "audit.jsonl" }); // hash-chained JSONL receipt
export const resetPassword = gov.tool({
name: "reset_password",
description: "Send a password reset link.",
parameters: v.object({ accountId: v.string() }),
sideEffect: true,
// The check that was missing in the Meta incident:
authorize: caller(
(a: { accountId: string }, ctx) => accounts.ownedBy(a.accountId, ctx.actor.id),
),
idempotency: { key: (a) => `reset:${a.accountId}` }, // a retry won't send twice
execute: async (a) => {
await accounts.sendResetLink(a.accountId);
return "Sent.";
},
});Bind the caller once per run, from your auth. The model can never read or set it:
import { govern, type GovernedToolkit } from "flue-guard";
declare const gov: GovernedToolkit;
declare const session: { prompt(text: string): Promise<unknown> };
await gov.run(
{ actor: { id: "user-7", roles: ["account_holder"] }, tenantId: "acme" },
() => session.prompt("I'm locked out, reset my password"),
);Start with the tutorial: from npm i to a denied call and a verified audit line in under five minutes.