Workers and trust
What a worker is, which tasks it may run, and how the cell decides the trust tier of a job.
A worker
A worker is one static binary, ciresk-ci-worker, that connects outbound to your cell, leases one task at a time per slot, runs it, streams the log and reports the result. It accepts no inbound connections (its health endpoint and package cache listen on localhost only), needs no inbound rule, and belongs to exactly one organization. A Linux box in an office, a PC under WSL2, an Azure VM and an EC2 instance are the same code path; see Run your own worker.
Your own worker changes where tasks run, not where their data lives: the tree a task runs on, its log, its artifacts and its cache entries travel through presigned URLs into the cell's own object store in the EU.
A worker belongs to a factory: static for a host you run, or a cloud pool that starts and stops instances on queue depth. A static host stays up.
Where a task runs
A task goes only to a worker that matches its labels and trust tier and has room for its resources: free slots, CPU, memory, disk and scratch space. The disk a worker offers counts the idle build output it would evict for the task, so old target/ directories never keep a task waiting. Among the workers that qualify, the queue prefers, in this order:
| Preference | What it does |
|---|---|
| The worker that holds the repository warm | A trusted_interactive task goes to a worker with a warm workspace for its repository first. |
| The worker that last ran the same task | Other workers leave a warm task for up to 20 s while that worker is online and has room, because its slot still holds the task's build output. |
| The busiest member of a cloud pool | A pool member leaves a task for up to 5 s to a member of the same pool with fewer free slots, so work fills the busiest members and the idle ones stop sooner. A static host never waits. |
A preference never outranks labels, trust or room, and a task waits for a preferred worker no longer than the time above.
Labels
A worker advertises labels; the queue matches a task's os and arch against them. A task without os or arch takes any worker.
| Label | Source | Used for |
|---|---|---|
os, arch | Detected (linux, x86_64 or aarch64) | Manifest labels and the cache key |
vcpus, memory_bytes | Detected | Sizing |
toolchains | Always mise | Tasks that need a provisioned tool |
workspace_warm | warm_workspaces = true (the default) | Warm workspaces, which only trusted_interactive tasks get |
isolation | process unless you set isolation=vm on a host that gives each job a fresh instance | untrusted tasks run only on vm or disposable workers |
disposable | Set by you on a static host you re-image yourself | The way a static host runs untrusted tasks |
key=value | A [labels] table in worker.toml or CIRESK_CI_LABELS=gpu=a10,site=office | Shown with the worker; tasks are not routed on them yet |
Trust tiers
Trust is derived in the cell from who submitted, which ref and your organization's policy; the client never raises it. It is stored on the job and carried in the task's cache token.
| Tier | Who | Workspace | Isolation | Cache |
|---|---|---|---|---|
trusted_interactive | Your developers and their local agents, on your own branches and working trees | Warm, persistent | Unprivileged user, cgroup caps, egress proxy in your policy's mode (audit by default) | Reads its own tier and trusted_ci; writes its namespace |
trusted_ci | Protected branches (refs/heads/main by default) from a service token or a key marked for CI, CI systems through OIDC exchange | Ephemeral | Unprivileged user, cgroup caps, egress proxy in your policy's mode (audit by default) | Reads and writes; the only writer other tiers read |
untrusted | Fork PRs and refs your repository does not own, policy-marked refs and principals, service tokens off protected branches, agent jobs when your policy says so | Ephemeral | Unprivileged user, cgroup caps, its own network namespace whose only exit is the egress proxy in block mode | Reads trusted_ci entries; writes to an isolated namespace per job |
ciresk ci run prints the tier. A job started inside Claude Code or Codex is attributed to agent:claude-code or agent:codex and runs at your own tier: a coding agent under your key is your tool. An organization that wants agent jobs isolated turns on the agent_branches_untrusted policy, which runs every agent:* job as untrusted.
A static host (an office box, a PC under WSL2) accepts trusted_ci and trusted_interactive work only; it runs untrusted tasks only when labelled disposable.
What a task gets on the worker
| Control | Mechanism |
|---|---|
| User | Tasks run as an unprivileged account (ciresk-run), no sudo, private home and temp directory. |
| CPU and memory | On a root worker with systemd, systemd-run --scope with CPUQuota, MemoryMax, TasksMax, capped by the manifest's resources and the worker's own limits; the whole scope is killed on timeout, cancel or lease loss. |
| Toolchains | Provisioned with mise into a directory shared by every task on the worker. |
| Egress | A per-task proxy with your organization's allowlist (default: crates.io, the npm registry and github.com). Audit mode, the default, lets unlisted destinations through and records each in the task log; block mode refuses them. A trusted_* task reaches the proxy through the proxy variables in its environment, so a program that ignores them is not stopped; an untrusted task is always in block mode and has no other route out. Cloud metadata, loopback, link-local and private addresses are refused by the proxy in every mode. |
| Loopback | A trusted_* task shares the host's loopback; an untrusted task gets its own network namespace, so tests that start their own servers on 127.0.0.1 work in every tier. |
| Secrets | pass_env values come from the worker's own secrets backend, are injected into the task process and masked in log chunks; never in keys, results, events or summaries. |
What a worker can and cannot see
| Can see | Cannot see |
|---|---|
| Its own organization's tasks that match its labels | Any other organization's tasks, keys, blobs or cache entries |
The lease: the command, the hashed env values, presigned URLs for one prefix of its organization for the lease's lifetime | The cell's database; it talks to the API and, through presigned URLs, to the cell's object store, over TLS |
| The repository mirror it fetched with the credential you placed in its own secrets backend | That credential ever leaving the mirror step: it is not exported to a task or logged |
Its worker token (ciw_, rotated every 24 hours), readable by root only | The task reading it |
| Outbound destinations through the built-in proxy | Inbound connections; there are none |
When the control plane is down
A running task keeps running: the worker buffers log chunks locally (up to 256 MiB) and retries with backoff. If the outage outlasts the lease TTL (300 s, renewed every 30 s; see Limits) the worker kills the task and the cell requeues it when it returns, up to three times, after which the job is infra_error. New submissions fail with exit 8 after retries. Workers are disposable; the API is stateless.