Worker Auth Reference
3 auth modes, JWT claims, and rehydration.
All three modes are implemented and in production. The legacy opaque key mode remains only as a transitional path and is scheduled for removal in Phase 1b cleanup.
The worker protocol uses a three-mode authentication system. Every request to a worker-protocol endpoint (/v1/daemon/*, /api/workers/*, /api/sessions/*) must carry a credential in the Authorization: Bearer header that satisfies one of these modes.
Auth mode overview
Prop
Type
Mode 1: Runtime JWT (preferred)
The runtime JWT is minted by the platform at registration time and returned in the runtimeJwt (daemon path) or runtimeToken (AF-compatible path) field. Use it for all subsequent calls:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ1dWlkIiwic3ViIjoid2tyXy4uLiIsInByb2oiOiJwcm9qXy4uLiIsIm9yZyI6Im9yZ18uLi4iLCJyZWciOiJ1dWlkIiwic2NvcGUiOlsid29ya2VyOnBvbGwiXX0.signatureJWT discriminator: the platform distinguishes a runtime JWT from a legacy key by checking whether the bearer value contains exactly two . characters (three segments). Legacy keys are opaque hex strings with no dots.
JWT claims
interface RuntimeJwtClaims {
jti: string // JWT ID - unique identifier for tracing and revocation
sub: string // Worker ID (e.g. "wkr_a1b2c3d4e5f6g7h8")
proj: string // Rensei project ID - authoritative scoping boundary
org: string // Rensei org ID
reg: string // Registration token ID the JWT was minted from
scope: string[] // Granted scopes (e.g. ["worker:poll", "worker:heartbeat"])
}Note the on-wire claim is singular scope (an array). The decoded WorkerAuthContext exposes it as scopes.
WorkerAuthContext (runtime_jwt variant)
When a runtime JWT is validated, the platform produces this auth context:
type WorkerAuthContext = {
mode: 'runtime_jwt'
jti: string // JWT ID for tracing
projectId: string // Rensei project ID
orgId: string // Rensei org ID
workerId: string // Extracted from sub claim
registrationTokenId: string // Ties back to the registration key
scopes: string[] // Granted scopes
claims: RuntimeJwtClaims // Full decoded claims
}Verification
- Algorithm: HS256; the signing secret is configured by the platform operator.
- JWT sub claim must match the
{workerId}URL path parameter on allGET /api/workers/{id}/...andPOST /api/sessions/{id}/...endpoints. A JWT for worker A cannot be used to poll worker B, even within the same project.
Token refresh
POST /api/workers/{workerId}/refresh-token
Authorization: Bearer <current runtimeJwt>Response:
{
"runtimeToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"runtimeTokenExpiresAt": "2026-06-03T12:00:00.000Z"
}Refresh proactively (5 minutes before expiry) rather than waiting for a 401.
Mode 2: Registration token
Registration tokens (rsk_live_* or rsp_live_*) are long-lived project-scoped API keys stored in the api_keys table. They are accepted only on the two registration endpoints:
POST /v1/daemon/register- token in request body (registrationTokenfield)POST /api/workers/register- token inAuthorization: Bearerheader
The platform validates the token, checks it is not revoked, confirms it has keyType = worker_registration and at least one projectId, then mints a runtime JWT for use in all subsequent calls.
WorkerAuthContext (registration_token variant)
type WorkerAuthContext = {
mode: 'registration_token'
registrationTokenId: string // api_keys.id
projectId: string // First projectId from the token's projectIds[]
orgId: string // Org the project belongs to
scopes: string[] // Scopes granted on the registration token
}Registration tokens are long-lived credentials. Treat them like passwords: store them in your secrets manager, rotate them periodically, and revoke tokens immediately if a host is decommissioned.
Mode 3: Legacy opaque key (deprecated)
The legacy mode accepts a global opaque token (configured by the platform operator) as a bearer token, verified with a constant-time comparison. No project or org scoping is applied - the request is treated as single-tenant global traffic.
Token shape: opaque string (typically 64 hex characters) with no . separators.
Authorization: Bearer a1b2c3d4e5f6...WorkerAuthContext (legacy variant)
type WorkerAuthContext = {
mode: 'legacy'
projectId: null // No scoping applied
orgId: null
}The legacy mode is scheduled for removal in Phase 1b cleanup. Migrate to runtime JWT as soon as possible. Self-hosted workers running af daemon v0.9.0+ already use runtime JWT by default.
Which modes each endpoint accepts
| Endpoint | registration_token | runtime_jwt | legacy |
|---|---|---|---|
POST /v1/daemon/register | Yes (body) | - | - |
POST /api/workers/register | Yes (header) | - | - |
POST /v1/daemon/heartbeat | - | Yes | - |
GET /api/workers/{id}/poll | Yes | Yes | Yes |
POST /api/workers/{id}/heartbeat | - | Yes | Yes |
POST /api/workers/{id}/refresh-token | - | Yes | - |
POST /api/sessions/{id}/status | - | Yes | Yes |
POST /api/sessions/{id}/activity | - | Yes | Yes |
POST /api/sessions/{id}/files/reserve | Yes | Yes | Yes |
| All other session endpoints | - | Yes | Yes |
POST /api/daemon/credentials/snapshot | - | - | rsk_* (bearer) |
GET /api/daemon/credentials/rotate-stream | - | - | rsk_* (bearer) |
The credentials endpoints (/api/daemon/credentials/*) use getCliOrSessionAuth (bearer rsk_* API key) rather than the worker protocol auth chain. This is because they are daemon-authenticated management endpoints, not worker-runtime endpoints.
Scoping and cross-project isolation
When a worker authenticates with a runtime JWT, every read and write is scoped to the projectId and orgId in the JWT claims:
- Session reads: a runtime-JWT worker can only see sessions belonging to its project. Attempting to read a session from another project returns
404 Not Found(not403) to avoid leaking the existence of cross-project resources. - Worker reads: the JWT's
subclaim (workerId) must match the{id}path parameter. A JWT for workerwkr_Acannot poll workerwkr_B. ScopingViolationError: internally thrown byassertSessionBelongsToCallerandassertWorkerBelongsToCallerwhen a scoping check fails. Route handlers catch this and return 404.
Legacy mode applies no scoping. Registration token mode applies project-scoping at the point of JWT issuance; the minted JWT then enforces it on all downstream calls.
Worker rehydration
The platform stores worker state in two places:
- SQL
workerstable - authoritative scoping record (used byassertWorkerBelongsToCaller). - Redis
work:worker:{id}- live state used by poll, heartbeat, and work dispatch.
These two writes are not transactional. If Redis state is evicted (TTL expiry, restart, memory pressure), a worker with a valid runtime JWT will receive 404 Worker not found from poll and heartbeat. Rather than requiring full re-registration, the platform runs rehydrateWorkerFromSql transparently:
- Poll handler receives a
runtime_jwtorregistration_tokenauthenticated request for an unknown worker ID. - Platform queries the SQL workers row to verify the record exists and belongs to the caller's project.
- Platform reconstructs the Redis
WorkerDatablob from the SQL row (withactiveCount = 0,status = 'active',lastHeartbeat = now). - Platform retries the Redis lookup. The worker's next poll and heartbeat succeed without any re-registration round-trip.
Self-hosted workers running af daemon do not need to handle this explicitly. Custom workers should implement a simple retry on 404 from poll before concluding that full re-registration is needed.
Session hash (public viewer access)
A separate, read-only authentication mechanism exists for unauthenticated public viewers:
// platform/src/lib/worker-protocol/session-hash.ts
// Format: SHA-256("session:{sessionId}").slice(0, 32 hex chars)This is the public session hash used by GET /api/public/sessions/{id}?hash=<32chars>. It is distinct from the 16-character internal session hash used in some worker-auth contexts. The two are not interchangeable.
Related pages
- Worker Registration - how to register and obtain a runtime JWT
- Poll & Heartbeat - using the runtime JWT in the work loop
- Session Lifecycle - authenticated session state transitions
- API Keys - creating and managing
rsk_live_*keys