Enforcer
An agent acting for a user is not the user. It needs its own identity, only the permissions it actually needs, and the ability to be switched off without switching off the person it works for.
The most common first attempt is an instruction in the system prompt: do not spend more than five hundred, do not email customers, do not delete anything. The model is stochastic, so its adherence to that instruction is stochastic too. A prompt injection can talk it past the rule faster than a user can. As one engineer put it, a promise from the thing you are trying to constrain is not a control, it is a hope.
The check has to sit outside the agent's reasoning loop, in the code that runs the tool, where the model cannot reach it.
Four properties, and most attempts miss at least one.
policy_id and your rule replaces that decision. Adding a tool that acts on someone else's data does not silently widen what the agent can do.| Approach | Where the check runs | Holds up under prompt injection | Cost |
|---|---|---|---|
| Rules in the system prompt | Inside the model | No | Free |
Hard-coded if statements | Your application | Yes | Rewritten in every service, drifts |
| Policy engine (OPA, Cedar, Casbin) | A separate service | Yes | You maintain a copy of your data in it |
| Relationship engine (OpenFGA, SpiceDB, WorkOS FGA) | A separate service | Yes | Dual writes; permissions drift from your database |
| Identity-aware authorization (Enforcer) | A service that already knows the caller | Yes | Nothing to sync |
Most fine-grained authorization products are a decision layer. You hand them a subject id and a resource, and they answer true or false. To answer at all, they need a copy of your data, so every time your application creates a project or adds a member you write it twice, once to your database and once to theirs. If the two disagree, your permissions are wrong and nobody finds out until somebody sees something they should not. This is the most common complaint about the whole category, and AuthZed's own CTO names it as the first pain point of the model.
Enforcer avoids it because it signed the user in. When a policy runs it is handed the resolved caller: role, tenant, group memberships, verification status. You write a rule over a real user object and pass the resource you already have in hand.
Before the agent issues a refund, the tool handler asks:
POST https://api.instruxi.dev/api/v1/enforcer/authz/check
{ "action": "issue",
"resource": { "type": "payment", "id": "pay_8f21", "owner_id": "...", "tenant_id": "..." },
"contexts": { "amount": 820 } }
-> { "allow": false,
"policy_id": "pol_01J7Q...",
"reason": "over 500, no approval on file" }
The caller's identity comes from the token, so it is never sent and the agent cannot claim to be someone else. Return reason to the agent and it can tell the user what would make the answer yes, instead of retrying a call that will always fail.
Create every rule inactive, then switch it on once the wording is agreed. An inactive rule is stored and not evaluated, so there is nothing to watch; log the returned reason and policy_id yourself. A rule that starts blocking the moment it exists is a rule nobody dares create, which is how teams end up with no rules at all.