Skip to main content
Service accounts are machine identities for AgentOS. Coding agents, chat apps, and CI pipelines authenticate with opaque agno_pat_... tokens instead of JWTs. Mint a token with a credential that can create service accounts: a JWT holding service_accounts:write (or the admin scope), or the OS security key:
curl -X POST http://localhost:7777/service-accounts \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{"name": "claude-code"}'
import httpx

response = httpx.post(
    "http://localhost:7777/service-accounts",
    headers={"Authorization": f"Bearer {admin_jwt}"},
    json={"name": "claude-code"},
)
token = response.json()["token"]
agno tokens create claude-code
The 201 response is the only time you’ll see the plaintext token, so store it somewhere safe. There’s no way to get it back later. agno tokens wraps this API for the terminal. The machine sends the token as a standard bearer header:
curl -X POST http://localhost:7777/agents/my-agent/runs \
  -H "Authorization: Bearer agno_pat_..." \
  -d "message=hello" -d "stream=false"
Service accounts require a database on your AgentOS (AgentOS(db=...)). Tokens are stored there, next to sessions and memories. Minting always requires a real credential, so anonymous requests on an open instance get a 401.

Token Properties

PropertyBehavior
Formatagno_pat_<base62>. The fixed prefix makes leaked tokens easy for secret scanners to find.
StorageSHA-256 hash only. The plaintext is returned once, at creation.
NameLowercase slug (letters, digits, _, -; max 63 chars), e.g. claude-code.
Expiry90 days by default. Set expires_in_days (1 to 3650) or never_expires: true.
RotationNames are unique among active accounts. Revoke, then mint again under the same name.

Scopes

A token minted without scopes gets run and read access:
agents:run, teams:run, workflows:run, sessions:read, config:read
config:read lets the token discover what it can run (GET /config). Custom scopes are passed as {scope, effect} objects. Token scopes are grants, so only effect: "allow" is accepted:
curl -X POST http://localhost:7777/service-accounts \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-runner",
    "scopes": [
      {"scope": "agents:run", "effect": "allow"},
      {"scope": "sessions:write", "effect": "allow"}
    ],
    "allow_privileged_scopes": true,
    "expires_in_days": 30
  }'
agno tokens create ci-runner -s agents:run -s sessions:write --privileged --expires 30d

Privileged Scopes

Some scopes require allow_privileged_scopes: true at mint time, so a privileged token is always deliberate:
Scope classExamples
Any write or delete actionsessions:write, knowledge:delete
The admin scopeagent_os:admin
Any service_accounts scopeservice_accounts:write (tokens that mint tokens)
A scoped caller can only grant scopes it already holds. A caller with service_accounts:write but without knowledge:delete cannot mint a token carrying knowledge:delete, so minting never escalates privileges. Admin callers and the OS security key (an unscoped root) can grant anything.

List and Revoke

curl http://localhost:7777/service-accounts \
  -H "Authorization: Bearer $ADMIN_JWT"

curl -X DELETE http://localhost:7777/service-accounts/<id> \
  -H "Authorization: Bearer $ADMIN_JWT"
import httpx

headers = {"Authorization": f"Bearer {admin_jwt}"}
base = "http://localhost:7777"

accounts = httpx.get(f"{base}/service-accounts", headers=headers).json()["data"]
httpx.delete(f"{base}/service-accounts/{accounts[0]['id']}", headers=headers)
agno tokens list
agno tokens revoke claude-code
Listing is paginated (limit, page, sort_by, sort_order, include_revoked) and returns metadata plus a display prefix (token_prefix) only. Neither the hash nor the plaintext is ever returned. Revocation is one-way and idempotent. A revoked name can be reused by minting a new account.

Revocation Timing

Successful verifications are cached in-process, so token auth does not hit the database on every request. Revocation takes effect immediately on the worker that processes the DELETE, and within service_account_cache_ttl_seconds (default 30) on other workers. Token expiry is always honored, even on a cache hit. For strict instant revocation, disable the cache. Every request then verifies against the database:
from agno.os import AgentOS
from agno.os.settings import AgnoAPISettings

agent_os = AgentOS(
    agents=[agent],
    db=db,
    settings=AgnoAPISettings(service_account_cache_ttl_seconds=0),
)

Attribution and Data Access

Requests authenticated with a token run as the principal sa:<name>. Sessions, memories, and traces created through a claude-code token show sa:claude-code as the user. sa: is a reserved namespace. A JWT whose sub claims an sa: identity is rejected with 401, so a human token can never impersonate a machine identity. Service accounts always self-scope: they read and write only data attributed to their own principal, even when user isolation is off. For a cross-user debugging token, grant the admin scope; agent_os:admin bypasses self-scoping.

Enforcement Surfaces

Tokens pass through the same auth layer as JWTs and cover the full AgentOS surface:
SurfaceBehavior
REST APIEvery route is checked against the token’s scopes.
MCP (/mcp)Tool calls are checked against the scopes of their equivalent REST route.
Interfaces (A2A)A2A routes map to agents/teams/workflows scopes. See Scopes.
WebSocketsSame verification and scope enforcement as REST.
Service account scopes are ACL data stored in your database, so AgentOS enforces them in every authentication mode, including security_key and none. JWT scopes are enforced only when authorization is enabled.

Failure Modes

StatusCause
401Unknown, expired, or revoked token. Also: minting without a credential, sending a token to an AgentOS without a database, or a JWT claiming an sa: subject.
403The token verified but lacks a scope the route requires (the detail lists the required scopes). Also: minting scopes the caller does not hold.
400Invalid scope strings, or privileged scopes without allow_privileged_scopes: true.
409An active account with that name already exists. Revoke it to rotate.
429Too many failed token lookups from one client address.
503The database is unreachable or does not support service accounts.
On an instance with no auth configured, a token that cannot be verified is ignored and the request proceeds anonymously. A token that verifies always attributes the request, and its scopes always apply.

Next Steps

TaskGuide
Mint tokens from the terminalagno tokens
See the full scope referenceScopes
Understand per-user data scopingUser Isolation
Run the cookbook exampleagent_os_with_service_accounts.py