Skip to content
Changelog

Manage AgentOS users and roles from your own database

October 1, 20262 min read

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.

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.

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:

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, and learn more about authorization and scopes in the documentation.

Frequently asked questions

Build an Authorization from agno.os with your JWT verification settings, call define_role for each role, and pass the object as AgentOS(authorization=...). AgentOS stores the roles in its own database and mounts the /authz admin API for you.

No. The native engine reads roles and grants from the database on every request and keeps no cache, so the next request from the same token sees the change, on every replica.

With audit=True, AgentOS keeps two trails. authz_audit records role and assignment changes with the actor and the before and after state. authz_decisions records each access.allowed or access.denied with the caller, the route, the required scopes and a token reference that is never the token itself.

Yes. Pass a custom PolicyEngine as engine= to keep the /authz API on your own backend, or pass FGAAuthorizationProvider in authorization_provider= for relationship checks against OpenFGA, WorkOS FGA or any store that answers check and list_objects.

Shipped around the same time