Deploying agents
An agent is a long-running process that dials out to the server, claims activities matching its capabilities, runs them, and reports back. It never accepts inbound connections, so it runs behind a firewall or inside a private network as long as it can reach the server over HTTPS.
This guide covers minting a token, running an agent by hand, installing it as a systemd service, self-update, and what happens when an agent dies.
Mint a token
Section titled “Mint a token”An org owner issues an agent token, naming the agent id and the capabilities the token is allowed to advertise:
cryo agents issue build-runner-1 \ --name "build runner (hel1)" \ --project acme/web \ --expires-in 90d \ --cap shell --cap polyglot --cap buildThe command prints the token plaintext once. Store it now; it isn’t
recoverable later. Agent tokens are prefixed csat_.
An agent token has two independent bounds: capability and scope.
The token’s capabilities are a ceiling. An agent can advertise a subset of them at runtime but can’t widen past them, so a leaked token can’t claim work the operator didn’t grant.
Scope decides whose work the agent may claim, and therefore whose secrets it receives:
--project <org/project>binds the token to that one project. A bare--project <name>with no org is rejected as ambiguous.--org <slug>binds it to every project in that org: the runner claims any of the org’s runs and receives each run’s project secrets.- Omitting both mints a deployment-wide token. That agent can claim work and read secrets in every org and project on the instance.
Who may mint follows the scope. Either scoped form needs the owner role
in the org named. Minting unscoped is a deployment operator’s call, since
the credential reaches every tenant on the instance. An operator who needs
a scoped token for an org they hold no membership in passes --admin,
which mints through the instance route and skips the org role check.
Operators are the addresses in CRYOSLEEP_INSTANCE_ADMINS, a
comma-separated list on the server. Set it: while it is empty, that check
falls back to accepting any org’s owner, which on a deployment with more
than one org lets one tenant mint a credential that reads the others’
secrets.
The unscoped form is the shared-fleet choice, worth making on purpose
rather than by typing less. --expires-in <dur> puts a clock on the
credential either way. See
teams and projects for how org
and project scope relate.
The agent id is claimed instance-wide, so a scoped mint naming an id another org already holds comes back as a conflict - pick another rather than reusing one. Re-minting an id your own scope holds is how you rotate its token.
Revoking a token (cryo agents revoke <token-id>) is a deployment
operator’s action today, so ask one to pull a leaked credential.
--expires-in keeps that window short in the meantime.
Run an agent
Section titled “Run an agent”export CRYOSLEEP_AGENT_TOKEN=csat_...cryo agent run https://cryo.example.com build-runner-1 \ --cap shell --cap polyglot --cap buildThe two positional arguments are the server URL and the agent id. The
token comes from $CRYOSLEEP_AGENT_TOKEN (the agent scrubs it from its
own environment right after reading it, so jobs it runs never inherit
the server credential).
Capabilities resolve in this order: repeated --cap flags win;
otherwise $CRYOSLEEP_AGENT_CAPABILITIES (comma-separated); otherwise
the default shell,polyglot. Whatever you pick is still capped by the
token.
The languages a job uses (bash, python, node, go, …) must be present on the agent host’s PATH. The agent binary itself is a single static executable with no runtime dependencies.
The job environment
Section titled “The job environment”Jobs run in a scrubbed environment. The agent clears its own env and
re-adds only a small allowlist (HOME, USER, LANG, TERM, the
TZ/locale vars, and the TLS cert paths), then layers on PATH, the
pipeline’s env: block, and the run’s CRYO_* vars. Everything else
the agent process carries stays out of the job, so an operator var or an
injected credential can’t leak into workflow code. This is deliberately
fail-closed: a var you forgot about is dropped, not forwarded.
Two ways to widen it when a job needs more:
- Per workflow: set it in the pipeline’s
env:block. That flows through to the job as-is. - Per agent: set
CRYO_ENV_PASSTHROUGHon the agent to a comma-separated list of names or globs -MYAPP_*,REGION- and any matching var in the agent’s environment is forwarded to every job it runs. Use this when a runner image or host sets env you want all its jobs to see. The bearer token is scrubbed from the agent’s env at startup, so it is never forwardable, even by*.
Either way the value reaches the job’s process, read as $NAME. It is
not visible to a workflow’s expressions: a job’s if: is evaluated before
the job reaches an agent, and two agents can hold different values for the
same name, so a gate reading one could not decide the same way twice. A
constant a gate needs belongs in the pipeline’s vars: block instead.
The agent also forwards PYTHONPATH and NODE_PATH by default (they’re
search paths, not secrets), so a runner image that vendors the language
SDKs at those paths makes them importable without any extra config.
Install as a systemd service
Section titled “Install as a systemd service”The install script fetches the binary and, with --service, registers
a systemd unit:
curl -fsSL https://cryosleep.io/install.sh | sh -s -- \ --service \ --server https://api.cryosleep.io \ --id build-runner-1 \ --token csat_... \ --caps shell,polyglot,buildWithout --service it installs the cryo binary and prints the manual
cryo agent run command. --service needs systemd (Linux); on other
platforms it prints the manual command instead.
The unit runs as a throwaway DynamicUser by default. A build runner
that installs its own toolchain in-flow (nix, devenv) needs a
persistent, privileged home, so pass --user root (or a real account)
to pin a fixed user.
Self-update
Section titled “Self-update”The service unit fetches the published binary on every start and verifies
it against a .sha256 manifest before launching.
A running agent polls that manifest every 5 minutes only when
CRYOSLEEP_AGENT_UPDATE_URL points at it. When a newer build ships, the
agent drains (finishes the in-flight job) and exits; whatever supervises
it restarts it, the pre-start fetch pulls the new binary, and the agent
comes back on the new build. An unreachable download host or a checksum
mismatch keeps the current binary, so a restart always succeeds.
install.sh --service sets CRYOSLEEP_AGENT_UPDATE_URL for you. An
agent you run by hand, or under your own supervisor (nomad, docker, a
hand-written unit), has to set it as well as be supervised by something
that restarts and re-fetches on exit: exiting is the update, so with
nothing to restart it the drain just stops the agent.
Manifests are signed (minisign) and both consumers check the signature
against a public key compiled into cryo before acting: the poll won’t
drain and the pre-start fetch won’t swap the binary on a missing or
invalid signature, so a compromised download host can’t push code onto
the fleet. The pre-start check runs through the already-installed
binary (cryo agent verify-manifest <manifest> <sig>), which also
rejects a manifest published for a different os/arch.
Draining and agent death
Section titled “Draining and agent death”Ask an agent to drain and exit cleanly:
cryo agents shutdown build-runner-1cryo agents ls shows the registry: each agent’s id, status,
capabilities, last build id, and last-seen time.
When an agent dies without draining, its in-flight activity holds a
30-second lease. Once the lease expires, the activity returns to the
queue and another capable agent claims it; the activity re-runs from the
start. A durable cryo step cache makes that replay fast because
completed steps return their recorded output instead of re-running.
An agent started with cryo agent run also enforces a per-activity
timeout of one hour. A job that wedges past the ceiling is abandoned so
it can’t hold the agent’s claim slot forever. The in-process agents that
back cryo run and cryo dev don’t apply this timeout - they’re
interactive, and Ctrl-C is right there.