Skip to content

flue-guardGovernance layer for Flue tools

Per-call authorization, idempotency, and a tamper-evident audit trail for agent tools, in-process.

Thirty seconds โ€‹

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.

ts
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:

ts
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.