Caldera docs
calderaapp.io

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 fcntl and /proc or ps, and a systemd user unit for the service. Windows is not supported.
  • systemd for the service. init also works without systemd (--no-service, or no systemctl); the agent is then started with caldera 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
  • --dir defaults to the current directory for all but init. Pass it, or cd into the agent's directory.
  • init --dir defaults to ~/caldera-agents/<agent name>; --url defaults to the environment variable CALDERA_URL, else https://app.calderaapp.io. The URL must be https; http is accepted only for localhost, 127.0.0.1 and ::1.
  • status prints: agent name, blueprint and version (with the previous one), process (running, paused or not running), runtime (running or not running), the last report (ok or failed, 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

  1. 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.
  2. Directory check. If the target directory already holds an agent (.caldera/config.json exists), init stops.
  3. 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 is optional (then it is skipped). An empty answer to an optional secret skips it. Secrets never go to Caldera.
  4. 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.
  5. 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.
  6. Write. agent.env, secrets.env (both mode 600), config.json, the settings file, and the current link.
  7. Service. Writes and starts a systemd user unit (unless --no-service). If systemctl is 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) in versions/<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 stop of 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_now skips 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 starter blueprint 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.
  • set rewrites settings.json and does not restart the agent. The environment variables CALDERA_SETTING_<KEY> only change at the next start, so an agent that must react at once reads CALDERA_SETTINGS_FILE before each decision.
  • null clears a setting only if the manifest gives it no default.
  • A key that is no longer declared by a newer version stays in config.json but 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:

  1. Fetch the bundle and verify its checksum. A network error ends the update as failed, with a retry after 10 minutes.
  2. Stage it beside the running version and validate it. A refused bundle ends as failed and the version is held.
  3. Swap current, one rename.
  4. Restart on the new version and run health.
  5. Keep (previous = the old version, a 30-minute window, outcome ok) when health passes, or swap back, restart the old version, outcome rolled_back and 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 fetched or staged: 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, outcome failed, 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.

  1. Download caldera.pyz and caldera.pyz.sig, no token. A network error or a missing signature ends as failed, retried after 10 minutes.
  2. Verify the checksum, then the signature. No key, no ssh-keygen or a bad signature ends as failed; the old file stays.
  3. Ask the new file for its version (python new.pyz --version). A file that does not start, or names another version, ends as failed.
  4. Swap and restart. The old file is kept as .previous, the new one replaces it with one rename, and the runtime restarts in place.
  5. 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 .previous by 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 failed and is retried after ten minutes.
  • status, pause, resume and rollback never open a connection.
  • Stopping the runtime does not stop the agent.
  • Only init needs 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.env and restart (see Operating agents).
  • No container image of the runtime. The compose runtime calls docker compose from 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.env by hand and restarting.