Workers
A worker reservation gives a remote caller its own short-lived machine to
run chats on. You ask the hub for a worker from a pool the operator defined, a
fresh aya-helper boots and enrolls, you drive chats on it through the
Remote API, and when you release it the machine is destroyed and
every credential it held stops working.
It is off unless the operator turns it on (see
Run your own). With it off, the routes below
answer 404 {"error":"workers unavailable"}. The full reference, with every
field and status, is docs/api/workers.md.
Examples use hub.example.test for the hub and issuer.example.test for its
identity provider.
What it is
- A pool is something the operator defines: which harnesses a worker runs, who may reserve from it, how many can run at once, how long they may live, and how machines are created.
- A reservation is your claim on one worker from one pool. It has a state
(
queued,provisioning,enrolled,active,releasing,destroyed, and so on) and a time-to-live. - A worker is an
aya-helperon the machine behind it. It has its own identity on the hub, created when it enrolls and disabled when the reservation ends. - Only you can use your reservation. Its chats run on a reserved root,
res-<reservation_id>.<harness>, which no one else can address and which the browser UI never reaches.
Get a token
Reserving uses the same kind of token as the rest of the Remote API, with a new
scope. Ask for workers:reserve, plus chat:read and chat:drive to run and
read chats:
TOKEN=$(curl -s -X POST "$TOKEN_ENDPOINT" \
-d grant_type=client_credentials \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" \
-d resource="$RESOURCE" \
-d scope="workers:reserve chat:read chat:drive" | jq -r .access_token)
Your client’s sub must be on the operator’s REMOTE_ALLOWED_SUBJECTS list and
on the pool’s access list. See Authentication for the scopes.
GET /api/remote/v1/auth-info still lists only chat:read and chat:drive.
Reserve a worker
HUB=https://hub.example.test
RSV=$(curl -s -X POST "$HUB/api/remote/v1/reservations" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"pool":"dev","request_id":"build-001","kind":"job"}' | jq -r .reservation_id)
kind is job or checkout. request_id makes the call safe to retry: the
same id with the same input returns the same reservation, and different input is
a 409. You may also give ttl_secs (it can only shorten the pool’s maximum),
a subset of the pool’s harnesses, labels the worker must carry, and a
source the pool’s schema accepts.
Poll GET /api/remote/v1/reservations/$RSV and read state. It moves through
queued and provisioning to enrolled, and to active once the worker has
sent its first heartbeat. 429 means a pool limit or budget was hit and carries
retry_after_secs. A failed or rejected reservation never becomes usable;
reserve again.
Chat on it
Start a chat on the reservation with reservation_id instead of root_id
(name exactly one of the two):
curl -s -X POST "$HUB/api/remote/v1/chats" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"request_id":"chat-001","provider":"claude","reservation_id":"'"$RSV"'","text":"Run the tests."}'
# 202 {"turn_id":7,"replayed":false,"warning":null}
Then poll /turns/{id}/events exactly as in Remote API. A reserved
root takes neither tier nor working_directory, and provider must be one of
the reservation’s harnesses. If the reservation belongs to someone else you get
403 not your reservation, and once it has ended you get 410 reservation ended.
You can also bring an existing session along with
POST /api/remote/v1/sessions/{pk}/move (needs workers:reserve and
chat:drive). seed is native to continue the session itself on the worker
(the worker must support cross-host resume), or summary to start a new chat
seeded from the transcript.
Release it
curl -s -X DELETE "$HUB/api/remote/v1/reservations/$RSV" \
-H "Authorization: Bearer $TOKEN"
Releasing is safe to repeat. The hub revokes the worker’s tokens in the same
step, ends its pending turns and destroys the machine. From then on the worker’s
own calls answer 401, and a new chat on the reservation answers 410.
How a worker enrolls
You do not do this part; the operator’s pool setup does. It is here so you can
tell what is happening while a reservation is provisioning.
- The pool’s backend creates or assigns a machine. A
staticpool hands out a fixed set of registered machines; awebhookpool asks the operator’s own provisioning service to create one. - The machine receives a single-use enrollment token and presents it to the hub together with an Ed25519 public key it generated in memory, and proof (an attestation) that it is the machine the pool expected. The private key never leaves the machine and no shared secret is stored on it.
- The hub checks the proof and the pool’s requirements, creates the worker
identity, binds the reserved roots to it, and moves the reservation to
enrolled. A replayed token, or one for a reservation in the wrong state, gets one constant refusal. - The worker signs short assertions with its key to obtain short-lived access
tokens from the hub’s own token endpoint, then heartbeats, which makes the
reservation
active. It keeps a wake long-poll open so it learns about your chat at once instead of waiting out its idle poll.
If the hub is unreachable for too long, or the reservation ends, the helper stops and kills every process it started. It is not restartable: the enrollment token is single-use.
Pointing a worker’s Claude at the AYA gateway
An option for the operator of a worker machine; the helper never sets these
itself. When the helper process has both of these set, every claude it
starts for a turn gets ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN from them,
and nothing else changes:
| Helper variable | Becomes, in the claude child |
|---|---|
AYA_WORKER_ANTHROPIC_BASE_URL | ANTHROPIC_BASE_URL |
AYA_WORKER_ANTHROPIC_AUTH_TOKEN | ANTHROPIC_AUTH_TOKEN |
With neither set, claude runs exactly as before, on its own credentials. With
only one set (or one blank), nothing is passed and the helper logs a warning
naming the two variables. The token is never logged.
Fresh checkouts for work runs
When a work run’s profile on the hub sets checkout: "fresh", the helper that
claims it gives the run its own working copy instead of the profile’s working
directory: a new run-… folder under work_runs_dir in the helper’s config
(default <data_dir>/runs, created the first time a run needs it), holding a
clone of the profile’s repository (or a worktree of a local mirror) on the run’s
branch. When the turn ends, after its claude process tree has exited, the
helper posts a work checkout: notice on the turn with the branch, its head
commit, the commits the run made and whether anything was left uncommitted,
then removes the folder. Commits that were not pushed (clone mode) go with it;
the notice still lists them. Work runs whose profile keeps the default
checkout: "workdir" run exactly as before. Fresh checkouts are for Claude
turns only.
Security model
- Separate scopes.
workers:reserveandworkers:adminare not implied bychat:readorchat:drive, and do not imply them. - Your reservation is yours. The hub compares your identity with the
reservation’s requester on every chat route. Someone else’s reservation and one
that does not exist return the same
403. - Workers cannot be impersonated. The hub is a second OIDC issuer for worker
tokens. A hub-issued token is judged only by the hub’s own, revocation-aware
verifier. A token from the external issuer whose
substartsclient-w-is refused, because that prefix belongs to the workers the hub enrolls. - Workers only get their own work. A reserved worker claims only its reservation’s turns and never the shared pool; every other worker never sees a reserved turn.
- Short-lived, narrow tokens. Worker tokens last minutes (10 by default) and carry only the scopes the pool granted.
- A worker can only narrow its claims. Its self-description can drop capabilities and labels declared at enrollment, never add them.
- Release is final. Release revokes the worker’s tokens and disables its identity in one transaction.
- You stay read-only by default. Reserved chats follow the same permission
rules as any remote chat:
read-onlyunless you ask forallow-edits.
For the operator’s side (pools, backends, signing keys, rotation and rate limits) see Run your own.