Rensei docs
Worker Protocol

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.signature

JWT 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 all GET /api/workers/{id}/... and POST /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 (registrationToken field)
  • POST /api/workers/register - token in Authorization: Bearer header

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

Endpointregistration_tokenruntime_jwtlegacy
POST /v1/daemon/registerYes (body)--
POST /api/workers/registerYes (header)--
POST /v1/daemon/heartbeat-Yes-
GET /api/workers/{id}/pollYesYesYes
POST /api/workers/{id}/heartbeat-YesYes
POST /api/workers/{id}/refresh-token-Yes-
POST /api/sessions/{id}/status-YesYes
POST /api/sessions/{id}/activity-YesYes
POST /api/sessions/{id}/files/reserveYesYesYes
All other session endpoints-YesYes
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 (not 403) to avoid leaking the existence of cross-project resources.
  • Worker reads: the JWT's sub claim (workerId) must match the {id} path parameter. A JWT for worker wkr_A cannot poll worker wkr_B.
  • ScopingViolationError: internally thrown by assertSessionBelongsToCaller and assertWorkerBelongsToCaller when 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:

  1. SQL workers table - authoritative scoping record (used by assertWorkerBelongsToCaller).
  2. 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:

  1. Poll handler receives a runtime_jwt or registration_token authenticated request for an unknown worker ID.
  2. Platform queries the SQL workers row to verify the record exists and belongs to the caller's project.
  3. Platform reconstructs the Redis WorkerData blob from the SQL row (with activeCount = 0, status = 'active', lastHeartbeat = now).
  4. 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.


On this page