Per-User Data Isolation
Scope user-owned AgentOS data and resources to the caller's user ID.
Authorization controls which operations a caller can perform. Per-user data isolation controls access to user-owned rows in built-in AgentOS routes. Opt in with user_isolation=True:
from agno.os import AgentOS
from agno.os.config import AuthorizationConfig
agent_os = AgentOS(
id="my-agent-os",
agents=[agent],
authorization=True,
authorization_config=AuthorizationConfig(
verification_keys=["your-jwt-verification-key"],
algorithm="RS256",
user_isolation=True,
),
)When enabled, AgentOS uses the JWT sub claim as the user_id for every non-admin caller:
| Operation | Behavior with user_isolation=True |
|---|---|
| User-owned reads | Scoped to the caller's user_id. Other users' rows are not returned. |
| User-owned writes | user_id is coerced to the caller's sub. A caller cannot persist rows attributed to another user. |
| Cancel / resume / continue routes | Require session_id and verify the caller owns the run. |
| WebSocket reconnect | Requires session_id (and workflow_id) for non-admins. |
A caller holding admin_scope (default agent_os:admin) bypasses isolation and sees all data. Set a custom override with admin_scope="ops:admin".
Some domains expose shared rows with no user_id, including shared knowledge and workspace service-account metadata. A scoped non-admin may read these rows but cannot mutate them. This does not grant access to another user's private rows.
Scoped operations require a usable JWT sub; otherwise they return 403. Custom routes must apply Agno's user-scope helpers explicitly. Enabling isolation does not automatically filter arbitrary database queries in your own handlers.
Isolation is off by default. JWT and scope checks still apply when user_isolation=False, but routes operate on the unscoped database and add no per-user ownership gates on top of authorization. Per-user isolation requires a database that records user_id (PostgreSQL recommended for production).
Service Accounts
Service account principals (sa:<name>) are always scoped to themselves, regardless of the user_isolation flag. The scoping decision runs in this order:
- A caller holding the admin scope is never scoped. This is checked first, so an admin service account reads across users.
- A service account is scoped to its own
sa:<name>principal, even whenuser_isolation=False. - JWT callers are scoped to their
subonly whenuser_isolation=True.
A machine token stamps its sessions and memories with its own principal, so an unscoped default would read every user's history. If you need a token that reads across users, mint one with the admin scope.
Service-account ownership is separate from execution identity. The account's creator owns its metadata; requests made with its token execute as sa:<name>. Scoped callers can see shared workspace accounts but cannot revoke those unowned accounts.
Next Steps
| Task | Guide |
|---|---|
Issue tokens with the sub claim | Tokens |
| Mint machine tokens | Service Accounts |
| See the user isolation cookbook example | user_isolation.py |