AgentX docs

Authentication

AgentX ships with optional, single-user authentication (Phase 17). It is disabled by default — a fresh install needs no login. Enable it when exposing a server beyond your local machine.

Model

  • One root user. AgentX is single-tenant: one server = one user. There is a single root account, no registration.
  • Password: hashed with bcrypt (cost 12) and stored in the PostgreSQL agentx_auth table (api/agentx_ai/auth/service.py, schema in alembic/baseline.sql).
  • Sessions: a 32-byte URL-safe token stored in Redis under agentx:session:{token}. The session carries user_id, username, created_at, last_active, and the client IP/UA.
  • TTL: AGENTX_SESSION_TTL (default 86400, 24h). The TTL is extended on every validated request, so active sessions don’t expire mid-use.

Enabling auth

AGENTX_AUTH_ENABLED=true     # gate all /api/* routes (the built-in default)
AGENTX_SESSION_TTL=86400     # session lifetime in seconds (default 24h)

Auth is on by default: an instance started with no env at all requires it. The dev .env.example in the repo opts out explicitly (AGENTX_AUTH_ENABLED=false) for local hacking; deployment templates keep it on.

Then set the root password:

task auth:setup          # interactive prompt
task auth:setup:force    # reset an existing password (invalidates all sessions)
task auth:check          # report whether setup is required

In containerized deployments use the wrapper cluster:auth:setup CLUSTER=<name> (see Clusters), which runs setup_auth inside the cluster’s API container.

How requests are gated

When auth is enabled, AgentXAuthMiddleware (auth/middleware.py) requires a valid session token on every /api/* route, sent in the X-Auth-Token header. Missing or invalid tokens get 401.

A few routes stay public so a client can bootstrap: /api/health, /api/version, /api/auth/login, /api/auth/status, and /api/auth/setup (only while setup is required).

The client IP that feeds this check comes from `X-Forwarded-For` **only when
`AGENTX_TRUST_PROXY=true`** — set it exclusively behind a proxy that overwrites the header
(the Nginx gateway does). Otherwise the TCP peer address is used, so a spoofed
`X-Forwarded-For: 127.0.0.1` from a direct connection can't trigger the bypass.

On the client, the token is stored per-server in localStorage (agentx:server:{id}:authToken) and attached to every request by the API layer (client/src/lib/api/core.ts). AuthContext drives login/logout/setup and surfaces authRequired / setupRequired state.

Endpoints

Full request/response bodies are in the API reference → Authentication. In brief:

EndpointMethodPurpose
/api/auth/statusGET / POSTIs auth enabled? Is setup complete?
/api/auth/setupPOSTSet the initial root password
/api/auth/loginPOSTExchange the password for a session token
/api/auth/logoutPOSTDestroy the current session
/api/auth/sessionGETValidate / inspect the current session
/api/auth/change-passwordPOSTChange the password (invalidates other sessions)

Encrypted log archives

Setting a password does more than gate the API: it also encrypts your durable logs at rest. The daily log archive (data/logs/agentx-YYYY-MM-DD.log.gz) is sealed with AES-256-GCM so on-disk history is unreadable without your password — defense-in-depth on top of the secret-redaction that already happens when each line is captured.

It uses envelope encryption: a random data key (DEK) encrypts the archives, and your password only derives a key that wraps that DEK in data/logs/keyring.json. The practical consequences:

  • The hot path never needs your password. The current day is written as a redacted plaintext gzip; completed days are sealed the moment you log in (the DEK is unwrapped and cached in server memory). Days that roll while no one is logged in stay redacted-plaintext until the next login or a manual task logs:seal.
  • Changing your password is instant. change-password just re-wraps the small key — no archive is rewritten. Use task logs:rotate-keys:deep for a full re-encrypt under a brand-new data key (e.g. if you believe the old one leaked).
  • Lost password ⇒ unrecoverable archives, by design.
  • Auth disabled (the default) ⇒ no keyring, so archives stay redacted-plaintext gzip exactly as before. Encryption activates only once a password exists. (AGENTX_LOG_ARCHIVE_ENCRYPT=false forces it off even with auth on.)

Downloading a sealed segment from the Log panel decrypts it on the fly; if the server was restarted and no one has logged back in, the vault is locked and the download returns 423 until you re-authenticate. Retention defaults to 30 days (AGENTX_LOG_ARCHIVE_RETENTION_DAYS).

task logs:keys:status        # keyring present? unlocked? sealed/pending counts
task logs:seal               # seal any pending plaintext days now (prompts for password)
task logs:rotate-keys        # re-wrap the key under a new password (O(1))
task logs:rotate-keys:deep   # deep rotation: new data key + re-encrypt every segment

See the API reference → Logs archive for the segment/status endpoints.

Version Compatibility

versions.yaml is the single source of truth for the API/client versions and the wire protocol version:

api:
  version: "0.20.0"
  protocol_version: 1
  min_client_version: "0.20.0"
client:
  version: "0.20.0"

GET /api/version (public) returns {version, protocol_version, min_client_version} — the same fields are also embedded in /api/health. At startup the client (AuthContext + lib/api/version.ts) probes /api/version and checks two things:

  1. Protocolprotocol_version must match the client’s exactly. protocol_version is bumped only on breaking API changes.
  2. Minimum client — the client’s semver must be >= min_client_version.

If either check fails the app shows VersionMismatchPage (with a retry) instead of connecting, so an out-of-date client never talks to an incompatible server.