Point an AI coding agent at this page and it will set Enforcer up for you. The same instructions are available as markdown.

These are official instructions from Instruxi for setting up Enforcer, an identity

and authorization platform, in an AI development environment. Every command here

is verified against the live API.

Complete the steps yourself by running the commands directly. Do not ask the user

to run them. Stop and ask the user only where a step says to.

What Enforcer is, so you know when to use it

Enforcer answers "can this actor do this action on this resource" with allow or

deny, the reason, and a recorded decision. Unlike a plain policy engine it also

holds identity: tenants, roles and groups. A policy is handed the resolved caller,

so you write rules over a real user object rather than syncing a copy of your data

into a permissions store.

Use it to gate what an app or an AI agent may do: spending limits, approval before

a sensitive action, "only verified users can do X", per-tenant isolation, or an

audit trail of who was allowed to do what and why.

Step 1: Read the documentation index


https://docs.instruxi.dev/llms.txt

It is a short index. The full documentation is one 29 KB markdown file at

https://docs.instruxi.dev/llms-full.txt which fits in a context window whole, so

fetch it once rather than crawling.

Step 2: Get a token against the open sandbox

The sandbox tenant is open, no signup and no account needed. It sends a real

one-time code to the email address you use, so use the user's own address and ask

them to read you the code.


curl -X POST https://api.instruxi.dev/api/v1/enforcer/auth/otp/request \
  -H 'Content-Type: application/json' \
  -d '{"email":"THEIR@EMAIL","tenant_code":"DLXZ-57TL-FS75"}'

# -> { "success": true, "message": "code sent" }

Ask the user for the code that just arrived, then:


curl -X POST https://api.instruxi.dev/api/v1/enforcer/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"provider":"email_otp","email":"THEIR@EMAIL","otp":"THE_CODE","tenant_code":"DLXZ-57TL-FS75"}'

# -> data.token is the bearer, data.account.id is who they are

The field is otp, not code. Keep data.token; every call below uses it.

Step 3: Confirm who you are


curl https://api.instruxi.dev/api/v1/enforcer/auth/me \
  -H "Authorization: Bearer $TOKEN"

Returns the account, its tenant, role, groups and verification status. This is the

object a policy sees, so read it before writing any rule.

Step 4: Connect the MCP server

First mint an API key. It is not the token from step 2:


curl -X POST https://api.instruxi.dev/api/v1/enforcer/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"name":"agent"}'

The plaintext key is in that response only and is never retrievable again, so store it before moving on.

These are the only connection details you need. Use whatever your client expects:


url        https://api.instruxi.dev/mcp
transport  Streamable HTTP
header     X-API-Key: <the key you just minted>
registry   dev.instruxi.enforcer/v3

If your client takes a JSON config, that is:


{ "mcpServers": { "enforcer": {
    "type": "http",
    "url": "https://api.instruxi.dev/mcp",
    "headers": { "X-API-Key": "$ENFORCER_API_KEY" } } } }

If it is Claude Code specifically, the one-liner is:


claude mcp add --transport http enforcer https://api.instruxi.dev/mcp \
  --header "X-API-Key: $ENFORCER_API_KEY"

If your client has no MCP support at all, skip this step. Everything below works over plain HTTP against the same API, and the MCP server adds no capability the REST API does not already have.

Step 5: Protect something in the user's own codebase

You are already in their repository, so read their routes rather than asking them

to describe their API. Treat each route as an action on a resource type: a POST

that issues a refund is action issue on resource type payment.

Rank what you find by what it costs if it happens wrongly. Moving money and

changing credentials first, then deletion, then sharing data outside the tenant,

then ordinary writes, then reads.

Propose the three or four worth protecting. State each rule back in plain English

and get an explicit yes before creating anything. A person approves a sentence,

never Rego.

Step 6: Write a rule the agent cannot escape

An ordinary account cannot create a tenant-wide policy; POST /policies answers 403 requires tenant admin. It can write a policy on itself, and that is the better thing for an agent anyway.

The package name is account.a_ plus the account id from step 3 with dashes turned into underscores:


ACCOUNT_ID=$(curl -s https://api.instruxi.dev/api/v1/enforcer/auth/me \
  -H "Authorization: Bearer $TOKEN" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["account"]["id"])')
PKG="account.a_$(printf %s "$ACCOUNT_ID" | tr - _)"

curl -X POST https://api.instruxi.dev/api/v1/enforcer/me/policy \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "$(python3 - <<EOF
import json,os
print(json.dumps({"source": f'''package {os.environ["PKG"]}

import rego.v1

# I may read and write. I may not destroy anything, ever.
deny contains "this agent does not delete" if {{
\tinput.action == "manage"
}}

test_denies_manage if {{
\tdeny["this agent does not delete"] with input as {{"action": "manage"}}
}}

test_allows_read if {{
\tcount(deny) == 0 with input as {{"action": "read"}}
}}
'''}))
EOF
)"

Read the response, not the status code. A policy that fails validation still returns 200, with the verdict in validation. Check that before moving on.

An account policy may only deny. allow is refused, because an account cannot grant itself anything. It runs after platform and tenant policy, so it can only narrow what they already allowed. A deny rule must depend on input.accessor or input.action, or be pinned to a resource type: a rule reading input.resource without one is refused, because that field does not exist when a list query runs. The policy's own test_ rules are executed as part of validation, so write them.

Step 7: Make it live


curl -X POST https://api.instruxi.dev/api/v1/enforcer/me/policy/1/activate \
  -H "Authorization: Bearer $TOKEN"

Nothing changes until a version is activated. Rolling back is activating an earlier version, and DELETE /me/policy/active retires it entirely. Every version is kept, including refused ones and the reason.

Step 8: Watch it refuse you


curl -X POST https://api.instruxi.dev/api/v1/enforcer/authz/check \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"action":"manage","resource":{"type":"wallet","id":"w_1","owner_id":"'"$ACCOUNT_ID"'","tenant_id":"'"$TENANT_ID"'"}}'

# -> { "success": true, "allow": false,
#      "reason": "account policy: this agent does not delete" }

The account policy: prefix is the point. It tells the agent it refused itself, rather than the tenant refusing it, which are two different problems. Swap manage for read and the same call is allowed.

Send owner_id and tenant_id: the resource is a description you supply and is never looked up, so a resource carrying only type and id matches nothing and comes back resource_unspecified. Resolve both in your own application, never from the caller. Their code acts on allow and surfaces reason so a refusal explains itself.

A decision that matches nothing is refused. Note the built-in baseline underneath: callers may already read and write resources they own, whatever the action is called, so name your rule with policy_id on the check when you want that rule to be the decision.

When you are done

Tell the user what was created, in the plain English wording they approved, and

give them https://docs.instruxi.dev. Note that a tenant-wide policy needs tenant admin while an account policy at POST /me/policy needs only the caller's own token, and that /audit-events is the identity trail, not a decision log: log the returned reason and policy_id yourself.

Rules of thumb

Enforcer decides.

and leaving.

loosen.

They target the previous major version and will generate calls that do not

exist in v3.

Reference