Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Connect to Pyrula Cloud

The scaffold runs offline against a scripted model, so no provider key. This page uses Python 3.11 to 3.15 and a POSIX shell, in an empty directory. Stop pyrula dev with Ctrl+C.

In Cloud, open Setup for your project and environment and copy the key. A key is for one environment; its runs appear only there.

A development or staging key sends prompts, replies and tool traffic (capture level io); a production key keeps them on your runtime (lifecycle). To change that, see What leaves your runtime.

Terminal window
pip install "pyrula[agents,cloud]"
pyrula init
export PYRULA_CLOUD_API_KEY="<key>"
pyrula dev app.py

pyrula init prints wrote app.py. pyrula dev runs the agent once and prints a link to the run:

Target: app.py:support
Using SQLite store .pyrula/dev.db - runs survive a restart; --fresh starts empty
Cloud: connecting to <origin>
Cloud: connected to <origin> as runtime <runtime-id> (project <project-id>, environment <environment>)
support submitted <run-id>
tool lookup_order
completed - Order A-1042 has shipped. Total $42.00.
Cloud: run <run-id> -> <link>

If Cloud is unreachable or refuses the key, pyrula dev keeps running locally and prints one line saying why. Pyrula retries an unreachable Cloud, or one answering 500 or 429, with backoff. Runs that finish meanwhile are held in memory (bounded by PYRULA_CLOUD_BUFFER_MAX) and sent once it connects; a process that stops first drops them and logs cloud_held_discarded_at_stop. To check setup without starting anything:

Terminal window
pyrula dev --check

It never registers a runtime and exits 1 if a check failed; fixes are in Troubleshooting. With no key, pyrula dev prints Cloud: off (no PYRULA_CLOUD_API_KEY) - running locally only.

For an existing runtime, install "pyrula[cloud]" and set the key where your workers start:

Terminal window
pip install "pyrula[cloud]"

Keys look like pyr_<env>_... with <env> of live, test or dev. A malformed key stops start-up with a ConfigurationError naming PYRULA_CLOUD_API_KEY. With PYRULA_CLOUD_CONTROL=off the key is not read.

VariableEffect
PYRULA_CLOUD_URLCloud to connect to. Defaults to https://api.pyrula.io; set it for self-hosting.
PYRULA_CLOUD_CONTROLUnset, derived or full: report and accept cloud commands. telemetry: report only. off: no connector is built. Other values stop start-up.
PYRULA_RUNTIME_NAMELabel for this runtime in the UI. Defaults to a generated name.
PYRULA_RUNTIME_IDStable identity across restarts. Defaults to a fresh id per process.
PYRULA_CLOUD_CAPTURECapture level cap, see What leaves your runtime.
PYRULA_CLOUD_ALLOWED_RUNNERSComma-separated workflow/agent names Cloud may act on; control for any other name is refused. Unset allows all.
PYRULA_CLOUD_OUTBOX_PATHSpool location, or off to disable.
PYRULA_CLOUD_OUTBOX_SYNCfull fsyncs every spool write.
PYRULA_CLOUD_OUTBOX_MAX_AGE_SHow long an unacked spooled event is kept. Default 7 days.
PYRULA_CLOUD_CLAIM_GATE_SECONDSWait for the first store-binding verdict before a polling worker takes work. Default 10; 0 disables. See below.
PYRULA_CLOUD_DRAIN_DEADLINE_SMost time a shutdown spends sending buffered events. Default 5.
PYRULA_REDACT_KEYSExtra field names to redact (redaction policy).

Critical events (run lifecycle, steps, tool and usage results, terminals) go to a local spool, the outbox, before they’re sent, and are deleted once Cloud accepts them. Token deltas and other live-stream events aren’t spooled; when one is dropped, Cloud is told where the gap is.

The spool is on by default at $XDG_STATE_HOME/pyrula/ (~/.local/state/pyrula/), one file per project.

  • Containers: put PYRULA_CLOUD_OUTBOX_PATH on a persistent volume, or the spool dies with the container.
  • Several projects on one host: never share one PYRULA_CLOUD_OUTBOX_PATH. Rows are separated only by environment name, so one project’s production key would send another’s production rows.
  • Power loss: writes reach the OS, not the disk (~29 µs per event). Set PYRULA_CLOUD_OUTBOX_SYNC=full to fsync each event.
  • A damaged spool: an unreadable file never stops your app from starting. Pyrula renames it to <spool>.corrupt-<UTC yyyymmddTHHMMSSZ>, logs one outbox_quarantined warning and starts a fresh spool; the newest three moved files are kept. A spool written by a newer SDK stops the connector; upgrade. A spool held open by another live process is left alone, and one it cannot read or write (another user’s state directory, a read-only mount) logs one outbox_unreadable warning and runs without a spool.
  • Opting out: PYRULA_CLOUD_OUTBOX_PATH=off keeps delivery to the life of the process, with no backpressure.
  • Events already redacted for your capture level, as plaintext SQLite. File 0600, directory 0700.
  • A row stays until Cloud accepts it or PYRULA_CLOUD_OUTBOX_MAX_AGE_S passes; an expired row counts as dropped and is reported.
  • A restarted runtime sends the rows a killed process left for the same project and environment. This relies on flock; on Windows or a filesystem without it, those rows age out instead.
  • A row is only sent while this runtime matches the store binding current when it was written.
  • Deleting the file while stopped discards the backlog.
  • Keys without a project id get a file named for a digest of the key; rotating the key starts a new file.

A run store belongs to one project and environment. The first runtime to register against an unbound store binds it. Bind a production store first, so a laptop or CI job holding a production key can’t do it by accident:

Terminal window
pyrula store binding show
pyrula store binding set --project P --environment E
pyrula store binding clear

clear warns that the store’s history will go to whichever environment binds next. Stop that store’s workers first.

Give each environment’s workers their own store: a separate Valkey, a separate database index on a standalone server, or a key namespace (PYRULA_VALKEY_NAMESPACE or --namespace), the only option on a cluster. Unset means the namespace "default".

  • A Valkey ACL limiting a runtime to ~<namespace>:* keeps it out of other namespaces. The store needs +eval +evalsha +script|load and sends no FUNCTION command (recipe).
  • A worker whose key belongs to another project or environment runs nothing from the store, reports nothing and accepts no control. It logs an ERROR, reports not ready on /readyz, and the Workers page shows a notice. A mismatched process can still submit runs; the store’s matched workers run them.
  • While the store is unbound and Cloud has not answered register, a polling worker waits up to PYRULA_CLOUD_CLAIM_GATE_SECONDS before taking work. A store already bound to the key’s project skips the wait.
  • Run Valkey with maxmemory-policy noeviction: an evicted binding unbinds the store, and the next runtime to check binds it again.
VariableDefaultEffect
PYRULA_CLOUD_OUTBOX_HIGH_WATER_BYTES256 MBAbove this, submit() raises BackpressureRejected. Claimed work keeps running and drains the backlog.
PYRULA_CLOUD_OUTBOX_HARD_CAP_BYTES2x high waterAbove this, the spool refuses new events and claiming stops.

Only an ack or the age limit deletes rows. The heartbeat reports egress_pressure (ok, high_water or saturated). Cloud trails by a fraction of a second under healthy load (median 0.21 s for run_start) and by seconds or minutes after a burst or outage.

Shutdown drains the queue up to PYRULA_CLOUD_DRAIN_DEADLINE_S. With a spool, critical events left behind go out on the next start and only stream detail is lost; without one, undelivered critical events are lost too. The runtime logs the counts at exit:

cloud_drain_timeout stream_discarded=N critical_undelivered=M spool_enabled=S
  • Key set but pyrula-cloud-client not installed: start-up raises a configuration error. Install it or unset the key.
  • No error, nothing in the UI: the key probably isn’t reaching the process (an unexported variable, or a container without the environment). Check where the runtime runs:
Terminal window
python -c "import os; print(bool(os.environ.get('PYRULA_CLOUD_API_KEY')))"

View this page as Markdown