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
rootaccount, no registration. - Password: hashed with bcrypt (cost 12) and stored in the PostgreSQL
agentx_authtable (api/agentx_ai/auth/service.py, schema inalembic/baseline.sql). - Sessions: a 32-byte URL-safe token stored in Redis under
agentx:session:{token}. The session carriesuser_id,username,created_at,last_active, and the client IP/UA. - TTL:
AGENTX_SESSION_TTL(default86400, 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:
| Endpoint | Method | Purpose |
|---|---|---|
/api/auth/status | GET / POST | Is auth enabled? Is setup complete? |
/api/auth/setup | POST | Set the initial root password |
/api/auth/login | POST | Exchange the password for a session token |
/api/auth/logout | POST | Destroy the current session |
/api/auth/session | GET | Validate / inspect the current session |
/api/auth/change-password | POST | Change 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-passwordjust re-wraps the small key — no archive is rewritten. Usetask logs:rotate-keys:deepfor 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=falseforces 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:
- Protocol —
protocol_versionmust match the client’s exactly.protocol_versionis bumped only on breaking API changes. - 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.