100% spot on. Relying on the LLM to supply or echo tenantId was a textbook design flaw. We completely overhauled this in v1.7.0 (docs/security/anti-idor.md):
- No
tenantId in Zod Schema: Tools now only expose business arguments (invoiceId). Exposing tenantId to the model triggers a security warning.
- Server-Side Session Context:
ctx.tenantId is strictly injected from the authenticated session and bound to DB queries (where: { id, tenantId: ctx.tenantId }).
- Post-Fetch Assertion (
assertTenant): Runs after execute() and before llmDto. If record.tenantId !== ctx.tenantId, it fails closed and throws ToolAccessDeniedError.
import { z } from "zod";
import { createTenantTool } from "avantgate/agent";
export const getInvoiceTool = createTenantTool({
id: "get_invoice",
parameters: z.object({ invoiceId: z.string() }), // LLM never touches tenantId
roles: ["billing"], // Native RBAC guard
assertTenant: (record, ctx) => record.tenantId === ctx.tenantId, // Post-fetch defense
execute: async ({ invoiceId }, ctx) => {
return await db.invoices.findUnique({
where: { id: invoiceId, tenantId: ctx.tenantId },
});
},
llmDto: (inv) => ({ id: inv.id, total: inv.total }),
});
Thanks for the sharp critique — it directly shaped our v1.7.0 multi-tenant hardening!
The Law module is billed as Anti-IDOR, but the sample tool doesn't quite deliver that. tenantId is a model-supplied argument in the zod schema, dataAccessGuard only checks that the model echoed the same tenantId as the session context, and execute() then calls db.invoices.findById(args.invoiceId) with no tenant filter at all. So the actual isolation rests entirely on the model always passing the right tenantId, not on the data layer refusing to return someone else's invoice. If invoiceId space is guessable or ever collides across tenants, that guard doesn't catch it. I'd derive tenantId from context server-side rather than accept it as a tool argument, and have execute() query by both invoiceId and tenantId, or at minimum check record.tenantId === context.tenantId after the fetch before it reaches llmDto.