# Manage AgentOS users and roles from your own database

> Agno AgentOS now enforces roles stored in your own database, with an `/authz` admin API for changes at runtime and an audit log of every allow and deny.

- Published: 2026-10-01
- Author: Samuel Jupe
- Categories: Changelog
- Canonical: https://www.agno.com/blog/manage-agentos-users-and-roles-from-your-own-database
- Markdown: https://www.agno.com/blog/manage-agentos-users-and-roles-from-your-own-database.md

Agno's new `Authorization` object, passed as `AgentOS(authorization=...)`, verifies each caller's JWT and then checks the route against roles that live in the AgentOS database. A role is a named list of scopes like `agents:*:read`, and a request passes only when one of the caller's roles grants the scope that route requires.

<Video
  src="/videos/changelog-authorization-terminal.mp4"
  controls
  preload="metadata"
  aria-label="A terminal recording of AgentOS authorization: carol, a viewer, reads the research agent with a 200, gets a 403 when she tries to run it, and the admin reads both decisions back from /authz/decisions"
/>

```python
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS, Authorization

db = SqliteDb(db_file="agentos.db")

authz = Authorization(
    verification_keys=[JWT_SECRET],
    algorithm="HS256",
    audience="acme-os",
    audit=True,
)
authz.define_role("viewer", ["agents:*:read"], default=True)
authz.define_role("analyst", ["agents:*:read", "agents:research:run"])
authz.define_role("admin", ["agent_os:admin"])
authz.seed(admin="ops@acme.com")
authz.assign("carol@acme.com", "viewer")

research = Agent(id="research", model=OpenAIResponses(id="gpt-5.5"), db=db)
agent_os = AgentOS(id="acme-os", db=db, agents=[research], authorization=authz)
app = agent_os.get_app()
```

`Authorization` borrows the `db` you hand to `AgentOS`, so you configure the database once. `define_role`, `seed` and `assign` only fill in what is missing, so running them on every boot never overwrites a change an admin made through the API. The native engine reads grants fresh from the database on each request, which means a promotion or a revocation applies to the very next call on every replica, with the same token.

![How AgentOS authorizes POST /agents/research/runs for a viewer, in five steps. Identity: the auth middleware verifies the JWT and reads sub, carol@acme.com. Role: from stored assignments or the token's roles_claim, viewer. Scope: the route requires agents:run. Engine decision: the native engine allows only when a grant matches and no deny does, so it denies with a 403. Audit: one access.denied row in authz_decisions, read at GET /authz/decisions.](https://www.agno.com/images/v3-1-0-authz-request-flow.png)

We ran that setup on SQLite with no model key. Carol, a viewer, can read the agent and gets a 403 when she tries to run it. The admin then reads both decisions back from the audit log:

```text
carol GET /agents/research 200
carol POST /agents/research/runs 403 {"detail":"Insufficient permissions. Required scope(s): agents:run"}

GET /authz/decisions (as ops@acme.com)
{"actor": "carol@acme.com", "action": "access.denied", "target": "POST /agents/research/runs", "metadata": {"required": ["agents:run"], "token": "2080c4f7d870", "scopes": []}}
{"actor": "carol@acme.com", "action": "access.allowed", "target": "GET /agents/research", "metadata": {"required": ["agents:read"], "token": "2080c4f7d870", "scopes": []}}
```

### The admin API and the audit trail

AgentOS mounts `/authz` whenever the object has roles. Admins list, create, patch and delete roles, replace or diff a role's scopes with allow and deny entries, and assign or revoke roles per subject. Every route requires admin, so a non-admin token gets `403 Admin privileges required`. `audit=True` adds `GET /authz/audit` for role and assignment changes, each with the actor and the before and after state, and `GET /authz/decisions` for every allow and deny. To ship either trail to a SIEM, pass your own `AuditSink`.

Add `AgentOS(user_directory=True)` to serve `/users`, a directory of people with a `disabled` switch that locks a user out even while their token is still valid, plus `GET /users/metrics` for directory size and users per role.

### Bring your own engine

Teams already on WorkOS, Auth0 or Okta set `roles_claim="role"` so the token names the caller's role and `define_role` still decides what that role may do. `engine=` swaps the native engine for any `PolicyEngine` you write and keeps `/authz` and the audit trail on top. For permissions that depend on relationships, such as "alice may run this workflow because she owns its folder", `FGAAuthorizationProvider` asks an FGA store for one `check` per resource and one `list_objects` per list. It ships with an OpenFGA adapter behind `agno[fga]` and pairs with `ScopeAuthorizationProvider`, which gates routes like `/config` that have no single object.

See the [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/05_agent_os/26_authorization/README.md), and learn more about [authorization](https://docs.agno.com/agent-os/security/authorization/overview) and [scopes](https://docs.agno.com/agent-os/security/authorization/scopes) in the documentation.
