Skip to main content

What it does

Before you ship a permission change, you want to know what it does. The simulator takes an agent (or a set of token claims), an action and a resource, and returns a decision with the reasoning behind it: allow, deny or needs_approval. It only reads. It does not write audit rows, bump rate limit counters, or record spend, so you can run it as often as you like against production data.

Decision parity with authorize

The simulator is not a second implementation of the rules. Resource and action matching (findMatchingPermission) and constraint evaluation (inspectConstraints) live in policy/abac.ts, and the real authorize() path calls the same functions. A test runs the same fixtures through theauth.authorize() and the simulator and checks that they agree. Three places differ on purpose:
  • A requireApproval rule is deny from authorize() but needs_approval from the simulator.
  • Budget is checked only by the simulator, and only when it can tell the action would not fit: a cost is supplied and it is larger than what is left. authorize() does not look at budgets.
  • An agent whose expiresAt has passed but whose status is still active gets a warning step. authorize() by agent id looks at status only, so the simulator matches that and flags it instead.

Run it

Calls return { success: true, data } or { success: false, error }, like the rest of the SDK. Without a stored agent, pass token claims instead:

What the trace shows

Each step has a stage, an outcome and a detail, plus structured data where it helps. If the agent’s own rules deny the action, the simulator then tries delegation chains, in the same order authorize() does.

What-if overrides

overrides changes the inputs for one run and never stores anything.
Supplied chains replace the stored ones. A chain whose depth is above its maxDepth, or that has expired, is listed in the trace and ignored.

Matrices and effective permissions

simulateMany refuses grids over 2000 cells.

HTTP route

The route is off until you add the plugin. Nobody is an admin by default, so you have to say who is.
POST /agents/:id/simulate needs a signed-in admin and accepts one of three bodies:
All three accept overrides. The single form returns { decision, allowed, reasons, trace }.

CLI

Sign in as an admin with theauth login, then:
simulate exits 0 on allow and 2 on anything else, so you can use it in scripts. permissions prints a table of resource, action, source (own or delegation:<chain>) and decision.

Limits

  • Time windows use the server’s local clock, the same as authorize(). Pass context.timestamp to test another moment.
  • ReBAC relations and the unified policy engine’s cache are not part of the simulation yet. It covers the direct permission path that theauth.authorize() uses.
Last modified on October 9, 2026