The runtime caldera.pyz
The program that sets an agent up on a machine and then runs and reports for it: commands, directory layout, the systemd unit, how it supervises the agent, the status file, settings, logs, health checks, updates and rollback of blueprints, the signed update of the runtime itself, behaviour without Caldera, environment variables, Python support and security properties.
What it is
caldera.pyz is one Python 3 file with no dependencies (standard library
only; it carries its own small YAML reader). It is both the command line tool
(caldera init <code>) and the long-running process (caldera run) that
- starts, watches and stops the agent's process,
- reports to Caldera in the agent protocol and applies the commands (pause, resume, run now, settings, log),
- fetches, health-checks and applies new versions of the blueprint, and rolls back,
- on a wish, replaces itself with a signed new runtime (below),
- or, for an agent that keeps its own processes, owns none and passes everything on to an adapter (adapter mode).
It never decides what the agent does. It launches the agent, reads the status file the agent writes, and passes it on.
The same bytes are served at https://app.calderaapp.io/cli/caldera.pyz with
the checksum at /cli/caldera.pyz.sha256 and the signature at
/cli/caldera.pyz.sig. The archive is reproducible: the same sources give the
same bytes.
Getting it
The install dialog shows the command with the checksum embedded:
curl -fsSL https://app.calderaapp.io/cli/caldera.pyz -o caldera.pyz && echo "<sha256> caldera.pyz" | sha256sum -c -
On macOS use shasum -a 256 -c - instead of sha256sum -c - (the dialog offers
that variant). The checksum in the dialog is computed from the very bytes the
server serves, so it needs no second request to the same origin.
Requirements
- Python 3.10 or newer. No minimum is declared in the program; 3.10 is the lowest version it was tested on. It was also tested on 3.13 and 3.14.
- Linux or macOS. It uses
fcntland/procorps, and a systemd user unit for the service. Windows is not supported. - systemd for the service.
initalso works without systemd (--no-service, or nosystemctl); the agent is then started withcaldera run --dir <dir>. - Outbound HTTPS to Caldera. Nothing listens on the machine.
ssh-keygen(OpenSSH) for the signed update of the runtime itself. It is on practically every machine an agent runs on. Without it the runtime keeps working and reports, but does not update itself.
Commands
All commands are python3 caldera.pyz <command>. Errors print
caldera: <message> on stderr and exit 1.
| Command | Does | Needs Caldera? |
|---|---|---|
init <code> [--dir DIR] [--url URL] [--no-service] |
Sets an agent up from a one-time install code. | Yes, the only command that does. |
run [--dir DIR] [--once] |
The long-running process: supervise and report. --once sends one report and exits (a test). |
Reports go out; nothing waits for them. |
status [--dir DIR] |
Shows the agent's state. | No |
pause [--dir DIR] |
Writes the paused file and stops the agent. In adapter mode: calls adapter pause. |
No |
resume [--dir DIR] |
Removes the file; starts the agent if the runtime is not running. In adapter mode: calls adapter resume. |
No |
rollback [--dir DIR] |
Returns to the previous blueprint version. | No |
rollback-runtime [--dir DIR] |
Puts the previous runtime file back and tells a running runtime to restart from it. | No |
pin-runtime <version|off> [--dir DIR] |
Holds the runtime at one version: it updates to no other. | No |
log [name] [--dir DIR] |
Lists the agent's logs, or prints the tail of one. | No |
--version |
Prints caldera 1 (runtime <release>). 1 is the CLI's constant; the release is baked in at build time (dev from source). |
No |
--dirdefaults to the current directory for all butinit. Pass it, orcdinto the agent's directory.init --dirdefaults to~/caldera-agents/<agent name>;--urldefaults to the environment variableCALDERA_URL, elsehttps://app.calderaapp.io. The URL must behttps;httpis accepted only forlocalhost,127.0.0.1and::1.statusprints: agent name, blueprint and version (with the previous one), process (running,pausedornot running), runtime (runningornot running), the last report (okorfailed, and how many seconds ago), the last update (outcome and message), a hold if there is one, and every setting's value.
What init does
- Preview. Reads what the code would install, without spending it. It names the agent, the directory and the secrets. Everything a person can still abort at (a directory that is taken, a secret they do not have) comes before the code is spent.
- Directory check. If the target directory already holds an agent
(
.caldera/config.jsonexists),initstops. - Secrets. For each declared secret: the environment variable
CALDERA_SECRET_<NAME>if set; otherwise asked for on the terminal (hidden input) when stdin is a terminal; otherwise an error, unless the secret isoptional(then it is skipped). An empty answer to an optional secret skips it. Secrets never go to Caldera. - Redeem. Spends the code. The answer carries the agent's token, one set of binding variables per binding, and the bundle. The runtime refuses an answer that installs a different agent than it previewed, and a value with a line break.
- Unpack. Verifies the bundle's SHA-256, unpacks it into
versions/<version>/, validates the manifest and checks that every file the manifest names exists. A bundle that fails any check leaves nothing behind. - Write.
agent.env,secrets.env(both mode 600),config.json, the settings file, and thecurrentlink. - Service. Writes and starts a systemd user unit (unless
--no-service). Ifsystemctlis not found, it prints how to start the agent by hand:caldera run --dir <dir>.
If something fails after step 4, the code is spent and the agent exists in Caldera without a machine. The runtime prints that, and a person deletes the agent in Caldera and issues a new code.
The directory
<dir>/ e.g. ~/caldera-agents/my-agent
versions/<semver>/ one unpacked bundle per version kept (never edited)
current -> versions/X the version that runs; swapped atomically
state/ what survives an update (the manifest's `state` paths)
.caldera/ the runtime's own files; a bundle can never write here (mode 700)
config.json (600) url, agent name, blueprint, previous, settings, held, window, last update
agent.env (600) CALDERA_URL, CALDERA_TOKEN, binding variables
secrets.env (600) what `init` asked for
settings.json (600) the settings as they are now, for the agent to read
paused exists while the agent is paused
run-now created by `run_now`; the agent removes it
update.json (600) the journal of an update in flight
lock held while the version changes
agent.pid the agent's pid and start time (its identity)
runtime.pid the runtime's pid and start time
runtime.json (600) the runtime's own update state: version, previous, hold, pin, last outcome, trial
runtime.lock held while that state changes
last-report.json when the last report went out and whether it worked
runtime.log the runtime's log
agent.out.log (600) the agent's stdout and stderr
caldera.pyz a copy of the runtime, if `init` ran from a .pyz; the file the unit runs, replaced by a self-update
caldera.pyz.previous the runtime that ran before it (mode 700), for `rollback-runtime`
current is a symlink and the swap is one rename. Replacing a symlink with
os.replace is atomic: at every instant current names a complete version. An
update killed at any point leaves current on the old or the new version, never
half of either.
State is outside the version, linked in. Each path in the manifest's state
list lives in <dir>/state and every version gets a symlink to it. A file the
bundle ships at a state path is a seed: copied into state only if nothing
is there yet, so a new version can bring defaults without overwriting what the
agent has learned.
Only two versions are kept: the running one and the previous one. Older ones are deleted after a successful update.
The service
init writes ~/.config/systemd/user/caldera-<name>.service (or under
$XDG_CONFIG_HOME):
[Unit]
Description=Caldera runtime for the agent <name>
After=network-online.target
[Service]
ExecStart="<python>" "<dir>/.caldera/caldera.pyz" "run" "--dir" "<dir>"
Restart=always
RestartSec=10
KillMode=process
[Install]
WantedBy=default.target
then systemctl --user daemon-reload and systemctl --user enable --now caldera-<name>.service. To keep it running when nobody is logged in:
loginctl enable-linger <user>.
The unit never changes when the runtime does. It names
<dir>/.caldera/caldera.pyz, and a self-update replaces that file with one
rename; the unit, its interpreter and its arguments stay as they are.
KillMode=process is not a detail. The default (control-group) kills every
process the unit started when it stops, which would take the agent down with a
runtime restart. The agent must outlive the runtime.
Logs of the runtime: journalctl --user -u caldera-<name>.service and
<dir>/.caldera/runtime.log. Every line passes through a redactor that replaces
the agent token and the typed secrets with their names.
How it supervises the agent
caldera run has two threads, deliberately apart:
- The supervisor looks at the agent once a second and never touches the network.
- The reporter talks to Caldera every interval, with a timeout on every
request (default 10 s;
CALDERA_REQUEST_TIMEOUT, 1 to 120).
A Caldera that hangs for ten seconds delays the next report and nothing else. The agent is kept running, a pause is applied within a second, and the rollback window keeps counting.
At start the runtime first finishes or undoes an update that was interrupted (see the journal).
Process runtime (runtime.type: process)
- The agent is started from
runtime.command(an argument list, never a shell) inversions/<version>/<workdir>, with the environment below, stdin closed, stdout and stderr appended to.caldera/agent.out.log(truncated at start when it is larger than 5 MB). - It is started in its own session, so a crash, restart or
systemctl stopof the runtime does not take it along. A runtime that starts again finds the agent by its pid file (pid and start time, because a pid alone is reused) and adopts it instead of starting a second one. - Stopping: SIGTERM to the process group, up to 10 seconds of grace, then SIGKILL and up to 5 more seconds.
- Restarts: if the agent is not running and not paused, it is started. After a
failure the next try waits 5 seconds, then doubles, up to 300 seconds. A process
that ran longer than 60 seconds resets the count.
run_nowskips the wait.
Compose runtime (runtime.type: compose)
The same contract through docker compose -f <file>: up -d to start, stop to
stop, ps --status running -q to look, with a 300-second timeout per call. It
has not been verified on a real Docker machine. Treat it as untried.
The environment the agent gets
The agent gets the runtime's own environment plus everything in
.caldera/agent.env and .caldera/secrets.env, except CALDERA_TOKEN: the
agent has no use for the token that speaks for the runtime, and a process that
never held it cannot leak it. Plus these variables:
| Variable | Is |
|---|---|
CALDERA_SETTING_<KEY> |
each setting's value when the process started (true or false for bool, empty if unset); the key is upper-cased |
CALDERA_SETTINGS_FILE |
JSON of all settings, rewritten on every change |
CALDERA_PAUSE_FILE |
exists while the agent is paused (the runtime stops the process, so checking it is optional) |
CALDERA_RUN_NOW_FILE |
created by run_now; the agent removes it when it acts |
CALDERA_STATE_DIR, CALDERA_AGENT_DIR |
the state directory and the agent's directory |
CALDERA_AGENT_STARTED |
Unix time (seconds, three decimals) at which this process was started |
CALDERA_BINDING_<PRODUCT>_URL, _WORKSPACE, _TOKEN |
one set per binding (from agent.env) |
CALDERA_URL is also in the environment (it is in agent.env). The agent needs
none of this to run. It is how it cooperates without Caldera.
The status file
The agent reports by writing the JSON file the manifest names as status
(relative to the version directory, for example data/status.json), in the shape
of a protocol report. The runtime forwards only these keys: state, health,
active, gauges, runs, panels. Any other key, such as a heartbeat the
agent keeps for its own health check, is not forwarded.
- A file larger than 200 KB is not read; the report gets a health line "Status file" at warn.
- A file that is not a JSON object costs one health line, not the report.
- A file that does not exist yet means the agent has not said anything.
- Write it atomically (write a temporary file, then rename). The catalog's
starterblueprint does.
The runtime adds agent (version and host name), settings, controls,
acks and applied, plus health items Process and, per binding, Binding <product>. It replaces state with Paused (warn) or Not running (bad,
with "restarting in N s") when that is the truth, and uses Running (ok) if the
agent reports no state.
Commands from Caldera
Each is checked against this agent's manifest and answered with an ack in the next report.
| Command | What the runtime does |
|---|---|
pause |
If pause is in the manifest's controls: writes .caldera/paused, stops the agent. Ack "Paused." |
resume |
If offered: removes the file; the supervisor starts the agent. Ack "Running again." |
run_now |
If offered: refused with "Paused. Resume first." while paused. Otherwise creates the run-now file and skips a restart wait. Ack "Requested." |
set |
Every key must be a declared setting and every value inside its bounds, or nothing changes (all or nothing). Ack "Applied: ." or "Nothing changed: : ." |
log |
Only a name declared in the manifest's logs, and only a file that resolves inside the agent's directory and outside .caldera/. The last 512 KB are uploaded; anything else is answered with the text "There is no such log." No ack: the upload is the answer. |
| anything else | Refused: "This agent does not know the command ..." |
A control the manifest does not offer is refused the same way. controls
defaults to pause and resume; run_now must be named.
The pause is the same file caldera pause writes. That is what makes every
command reversible without Caldera.
Settings
- The manifest declares the schema, bounds and defaults. Values live in
.caldera/config.json; a setting with no stored value reads its default. setrewritessettings.jsonand does not restart the agent. The environment variablesCALDERA_SETTING_<KEY>only change at the next start, so an agent that must react at once readsCALDERA_SETTINGS_FILEbefore each decision.nullclears a setting only if the manifest gives it no default.- A key that is no longer declared by a newer version stays in
config.jsonbut is ignored.
Health checks
The manifest's health.command (argv, run in the new version's working directory,
with the agent's environment including secrets and CALDERA_AGENT_STARTED)
decides whether a version works. Exit 0 passes. It fails on a non-zero exit, a
timeout (timeout_seconds, default 60, at most 600) or a command that cannot
run. A manifest with no health always passes.
Write the check so that it needs the new version to have worked. Compare a
heartbeat in the status file against CALDERA_AGENT_STARTED: a fresh-looking file
the previous version left behind must not pass for the new one. A good check also
verifies the agent's configuration and that the programs it calls are in the
path.
Updates and rollback of blueprints
An update is five steps, and the order is the safety argument:
- Fetch the bundle and verify its checksum. A network error ends the update
as
failed, with a retry after 10 minutes. - Stage it beside the running version and validate it. A refused bundle ends
as
failedand the version is held. - Swap
current, one rename. - Restart on the new version and run
health. - Keep (
previous= the old version, a 30-minute window, outcomeok) whenhealthpasses, or swap back, restart the old version, outcomerolled_backand hold the new one, when it does not.
Is it wanted? The runtime applies desired when all hold: it names the
agent's own blueprint, version is a semantic version (anything else is never
used as a path), it is not the running version, and it is not on hold.
The hold. A version the runtime rolled back from or refused is not applied
again until desired names a different version. Otherwise a local caldera rollback would be undone by the next report. A successful update to another
version clears the hold. The one exception is a failed fetch (Caldera
unreachable): it is retried after ten minutes.
Outcomes (reported in applied.outcome, with a one-line message):
| Outcome | Meaning |
|---|---|
ok |
The new version runs and passed its health check. The message is "installed" before any update. |
rolled_back |
The new version failed its health check, or got worse inside the window, or a person ran caldera rollback. The agent is back on the old one. |
failed |
Nothing changed: the bundle could not be fetched, was refused, or an update was interrupted and undone. |
The window. After a kept update the runtime remembers the agent's level at
that moment. For 30 minutes (fixed, not configurable) it checks every 15
seconds, and rolls back by itself if the level got worse. The level is bad
when the agent is not running, otherwise the level of the state in the status
file (info counts as ok). Health items do not count; a paused agent is not
checked, and a person's pause is never a reason to roll back.
Rollback (caldera rollback, or Caldera naming the old version) swaps
current to the previous version and restarts. It needs no network, only the
disk. Because only two versions are kept, rollback toggles between the running
version and the previous one. To go further back, Caldera names that version and
the runtime fetches it again.
The journal and recovery
update.json records the step reached: fetched, staged, swapped. A process
killed between any two steps is handled at the next start:
- killed at
fetchedorstaged: the leftovers are discarded; the old version never stopped being the right one; - killed at
swapped(the new version is linked but nobody checked it): back to the old version, outcomefailed, hold on the new one.
A lock keeps rollback and an update from interleaving, and keeps the supervisor
from starting the wrong version during one.
Updating the runtime itself
An update of the runtime is code from Caldera running as the agent's user, which
on a real machine holds keys and product tokens. A checksum from the same server
proves nothing against a compromised server or a manipulated release, so every
runtime file is signed outside the server and the runtime refuses an update it
cannot verify. The runtime never updates itself unasked: an admin presses
"Update runtime" on the agent's page, which makes the next answer carry
runtime_desired.
- Download
caldera.pyzandcaldera.pyz.sig, no token. A network error or a missing signature ends asfailed, retried after 10 minutes. - Verify the checksum, then the signature. No key, no
ssh-keygenor a bad signature ends asfailed; the old file stays. - Ask the new file for its version (
python new.pyz --version). A file that does not start, or names another version, ends asfailed. - Swap and restart. The old file is kept as
.previous, the new one replaces it with one rename, and the runtime restarts in place. - Trial. The first successful report within five minutes makes it good
(
ok). A runtime that crashes, or does not report in time, goes back to.previousby itself (rolled_back) and the version is held.
The signature. The release build signs caldera.pyz with an OpenSSH ed25519
key, namespace caldera-runtime. The runtime checks it with
ssh-keygen -Y verify -f <allowed_signers> -I caldera-release -n caldera-runtime -s <sig> < caldera.pyz
against a public key built into the runtime. No ssh-keygen, no signature
(/cli/caldera.pyz.sig answers 404), a signature by another key or for other
bytes: no update. The outcome is failed with the reason in
runtime.applied.message, and the old file is untouched.
Is it wanted? The wish is acted on when runtime_desired is a version string
([0-9A-Za-z][0-9A-Za-z._-]{0,63}, never a path), it is not the running version,
it is not held, it is not ruled out by a pin, and no earlier update is
still on trial.
What is held. A version the runtime went back from, and a file it refused for
what is in it (a checksum, a signature, a file that does not start, another
version than the wish), is not tried again until runtime_desired names a
different version. What is only the machine's or the network's trouble (no
ssh-keygen, no signature yet, a failed download, not running from the unit's
file) is retried after ten minutes while the wish stands.
Restart in place. After the swap the runtime replaces itself with the new
file in the same process: the same process id, so the systemd unit sees one
unbroken process. The agent's own processes are not touched: in process mode the
agent runs in its own session and the new runtime finds it again by its pid file;
in adapter mode there is nothing to adopt. Only a runtime that runs from
<dir>/.caldera/caldera.pyz (what the unit names) can replace itself; one
started from somewhere else says so.
Atomic and stable. The old file is first written to
.caldera/caldera.pyz.previous, then the new file replaces caldera.pyz with one
rename: both files are whole at every instant. The unit is never rewritten.
The trial. The new runtime counts as good after its first successful report
within five minutes (the window restarts with each start of the new runtime). It
goes back to the previous file by itself when that window passes without a
successful report, or when it was started more than three times without one (a
crash loop). Going back puts the previous file in place, holds the version,
restarts in place and reports rolled_back. With Caldera unreachable during the
trial a good update therefore goes back, and is held: that is the price of a check
that needs no trust in the new file.
Outcomes are the blueprint's (ok, rolled_back, failed) and travel in the
report as runtime.applied with the version that works now. While a runtime is on
trial it claims nothing; ok is reported only after it proved itself.
Offline. caldera rollback-runtime swaps the two files (so a second call goes
forward again), holds the version it left, and signals a running runtime to
restart from the file; without a running runtime the file is used at the next
start. caldera pin-runtime <version> makes the runtime update to no other
version than that one, whatever Caldera wishes; pin-runtime off releases it. The
pin is reported. Neither needs a connection. With Caldera unreachable nothing
happens at all: the runtime keeps the file it has.
A runtime installed before this feature cannot update itself. Replace
.caldera/caldera.pyz once by hand; after that it can. See
Operating agents.
Adapter mode
For an agent that keeps its own processes the runtime owns none. It starts nothing, stops nothing, restarts nothing and reads no file of the agent. It calls one command the manifest names with a verb. The interface is described on Adapter mode and its contract.
The Claude account hint
The runtime can report which Claude account its machine is signed in to. See The Claude account hint.
Behaviour with Caldera unreachable
- The reporter logs
report failed: ...and tries again at the next interval. Nothing else changes: the agent keeps running, restarts still happen, pause and resume work, the rollback window keeps counting. - A fetch that fails during an update is outcome
failedand is retried after ten minutes. status,pause,resumeandrollbacknever open a connection.- Stopping the runtime does not stop the agent.
- Only
initneeds Caldera.
Environment variables
| Variable | Used by | Is |
|---|---|---|
CALDERA_URL |
init |
the default for --url |
CALDERA_REQUEST_TIMEOUT |
run and the local commands |
seconds a request may take; 10 by default, clamped to 1 to 120 |
CALDERA_SECRET_<NAME> |
init |
the value for the declared secret <NAME>, for unattended setups |
XDG_CONFIG_HOME |
init |
where the systemd user unit is written (default ~/.config) |
The variables the agent receives are in the table above. Names starting with
CALDERA_ belong to the runtime; a manifest's secrets may not use them.
Security properties
| Property | How |
|---|---|
| The token is private | agent.env and secrets.env are mode 600, written through a temporary file created with that mode from the start, so a secret is never readable in between. .caldera/ is mode 700. |
| The agent never gets the token | CALDERA_TOKEN is removed from the agent's environment. |
| Secrets stay out of logs | Every log line is passed through a redactor for the token and the typed secrets (values of at least 6 characters). |
| No redirects | A redirect would re-send the Authorization header to wherever it points. The protocol has none, so one is refused. |
| No cleartext off this machine | https only, http only for localhost. |
| Errors name no secret | An error carries the HTTP status and the server's error code, never a header, body, token or URL. |
| A bundle is untrusted | Refused whole, leaving nothing, if any entry: has an absolute path, .., a backslash, a drive letter or a NUL; is a symlink, device or other non-regular file; appears twice; lies under .caldera/; or if the archive has more than 5000 entries, unpacks to more than 100 MB (counted while unpacking, not read from headers) or one file to more than 50 MB, or is larger than 20 MB; or lacks a file the manifest names; or its version is not the one asked for. |
| The real path is checked | After resolving, every target must stay inside the directory; every path out of a manifest (workdir, status, logs) is joined through a check on the real path, so a symlink cannot lead a read out of the agent's directory or into .caldera/. |
| The checksum is verified | At install the answer's sha256 must match the bundle. At update the X-Caldera-Sha256 header must match; a missing header means no check, so the runtime relies on Caldera sending it. |
| The runtime update is signed | A runtime file is replaced only after its signature verified against the key built into the running runtime. |
| No shell | runtime.command and health.command are argument lists. |
| Versions are immutable | A version directory is never edited after it is staged. |
| Dotenv lines are safe | A name or value that would break out of a line (a line break or NUL) is refused. |
What it does not do: it does not encrypt secrets.env (a private file is the
protection), and it does not rotate runtime.log.
Known limits
- Token rotation is manual. Rotating the token in Caldera does not reach the
machine; edit
agent.envand restart (see Operating agents). - No container image of the runtime. The compose runtime calls
docker composefrom the host. - No substitution in
.mcp.json. The runtime ships the file as is and passes the variables in the environment; whatever reads the file resolves${NAME}. - The 30-minute window is fixed.
- A change to a secret means editing
secrets.envby hand and restarting.