Skip to content

Agents API

The agents API is project-scoped: list, inspect health, mint and revoke. There is no token rotation endpoint and no rotate-in-place operation. Replacing a credential means revoking the old agent and minting a new row, ID, and token.

Revocation uses the DELETE route but retains the agent row as a durable tombstone. It sets revoked_at and overwrites the token hash. The database change commits before the brain makes a best-effort attempt to kick active sockets locally and across replicas. Subsequent authentication with the old token fails even if a kick is delayed or lost. Worker control happens through the commands API (restart-worker, pool-resize, etc.).

Archiving the agent's project (DELETE /api/v1/projects/{slug}) disconnects its agents through the same kick, with the same 4003 close, but leaves the agent rows and tokens intact. While the project is archived a hello from one of its agents is refused with 4401 and the long-poll routes answer 403 with error: project_inactive; once the project is active again the agents reconnect with their existing tokens.

GET /api/v1/projects/{slug}/agents

Role: viewer. Returns a list of AgentPublic:

[
{
"id": "...",
"project_id": "...",
"name": "web-01",
"state": "online",
"protocol_version": "2",
"framework_adapter": "django",
"engine_adapters": ["celery"],
"scheduler_adapters": ["celery-beat"],
"capabilities": {},
"last_seen_at": "...",
"last_connect_at": "...",
"created_at": "...",
"is_outdated": false,
"host_name": "web-01.internal",
"agent_version": "...",
"version_status": "current"
}
]

is_outdated is true when the agent connected at least once and its last advertised protocol_version is older than the brain's CURRENT_PROTOCOL. Never-connected agents report false.

host_name is the operator-supplied label the agent sends in its hello frame (Z4J_AGENT_NAME), distinct from the mint-time name; null when the agent never set one. agent_version is the z4j-core version the agent advertised in its hello frame; null when it has never connected or did not report one. version_status compares agent_version with the brain's versions snapshot: current, outdated (older, same major), newer_than_known (the brain's snapshot is stale; refresh from Settings, Check for updates), incompatible (major version mismatch) or unknown (no version reported, or the package is missing from the snapshot); null when agent_version is null.

POST /api/v1/projects/{slug}/agents

Role: admin. CSRF-protected and requires a fresh MFA verification. The fresh MFA gate makes minting browser-session-only; an API key is rejected.

{"name": "billing-worker-02"}

(project_id, name) is unique among live agents; a duplicate returns 409 with "error": "conflict". Response (shown once -- save both):

{
"agent": { /* AgentPublic */ },
"token": "<43 URL-safe base64 characters; no prefix>",
"hmac_secret": "<urlsafe-base64, 32 raw bytes>"
}

The hmac_secret is the per-project signing key. It is HMAC-derived from the current brain master secret and is not persisted by the brain. Consequently, rotating Z4J_SECRET requires re-credentialing every agent. Adding the old master to Z4J_PREVIOUS_SECRETS keeps the old bearer token acceptable during the rotation window, but does not make an old hmac_secret valid: the handshake can succeed and the first signed data frame then fails HMAC. Operators paste both values into the agent configuration; the agent refuses to start without hmac_secret.

DELETE /api/v1/projects/{slug}/agents/{agent_id}

Role: admin. CSRF-protected and requires fresh MFA. Soft-revokes the token and retains the agent row because events.agent_id is non-null and uses ON DELETE RESTRICT; tasks have no agent foreign key. The tombstone is hidden from the agent list and cannot reconnect or receive new work. Revocation keeps the original name initially. Minting the same name later moves that tombstone into a reserved namespace under its row lock, then inserts a new agent row. The replacement does not reuse the revoked row or its ID.

GET /api/v1/projects/{slug}/agents/{agent_id}/health

Role: viewer. Returns at most 100 retained status samples, newest first, with id, captured_at, worker_id and telemetry_loss. captured_at is the time the agent signed the sample for sending, not the time its counters were read, so a sample buffered during an outage carries its delivery time. Revoked or foreign agents return 404. telemetry_loss: null means that sample does not provide supported loss accounting. An empty list is not a zero-loss measurement.

Counters are cumulative within their buffer_id or runtime_id scope. Do not sum samples or treat the bounded history as a complete fleet inventory. See agent telemetry loss for counter units, retention and operational interpretation.

GET /api/v1/projects/{slug}/agent-workers

Role: viewer. Lists the z4j agent processes (web, task, scheduler, beat) registered through the WebSocket handshake; this is distinct from the engine-native workers at GET /api/v1/projects/{slug}/workers, and long-poll agents do not register here. Query: ?state= (online or offline; default all; other values fail request validation with 422), ?role= (web, task, scheduler, beat or other; default all; other values fail request validation with 422) and ?limit= (default 200, 1 to 500). Rows come online before offline, then by last_seen_at descending. This route is session-cookie only: its tag has no API-key scope mapping, so every API key gets 403, even one granted admin:*. Returns a list of AgentWorkerPublic:

[
{
"id": "...",
"agent_id": "...",
"project_id": "...",
"worker_id": "...",
"role": "task",
"framework": "django",
"pid": 12400,
"started_at": "...",
"state": "online",
"last_seen_at": "...",
"last_connect_at": "...",
"created_at": "...",
"updated_at": "..."
}
]

worker_id, role, framework, pid, started_at, last_seen_at and last_connect_at can be null. id stays stable while the same (agent_id, worker_id) pair is refreshed; a regenerated worker_id creates a new row and the previous row remains as historical state.