What leaves your runtime
Your local store is never redacted; replay reads its real arguments and results at every capture level. Capture levels and redaction govern only what leaves the process for Pyrula Cloud.
Capture levels
Section titled “Capture levels”A level ships its class and everything below it:
| Level | Adds | Examples |
|---|---|---|
none | structure | ids, kinds, names, timing, status, error codes, error_type, usage counters, a choice interrupt’s option labels |
lifecycle (default for production keys) | your workflow’s data | a workflow’s params, metadata and result, a step’s input and result, emit / signal / interrupt / update payloads |
io (default for development and staging keys) | model and tool traffic, failure text | an LLM step’s message, a tool or sub-agent call’s input and result, an agent turn’s message and reply, failure messages |
full | streams | token deltas, heartbeats, stream-overflow notices |
What Cloud sees, by level:
none | lifecycle | io | full | |
|---|---|---|---|---|
| Names, ids and timing | yes | yes | yes | yes |
| Counts and usage | yes | yes | yes | yes |
| Error type and code | yes | yes | yes | yes |
| Workflow params, step inputs and results | no | yes | yes | yes |
| An agent turn’s message and reply | no | no | yes | yes |
| Error messages | no | no | yes | yes |
| A terminate’s requester you named | no | no | yes | yes |
| Tool and model inputs and outputs | no | no | yes | yes |
| Token deltas and heartbeats | no | no | no | yes |
- Error messages below
ioship three exceptions: a framework explanation (a budget’s cap and spend, a blocked tool’s name and reason), aControlRefusedack, and an identifier-shaped value under a structural key of a structured detail (reason,error). - An agent turn’s input and result stay on your runtime below
io; declare@agent(capture="io")to see them in Cloud. A@workflowthat returns a model’s output ships it fromlifecycleas its result. - A withheld field keeps its key with the value
[omitted: capture level]. Anullstaysnull. - An unclassified field counts as
full, so a new field can’t leak at a lower level. noneis the only level that keeps run content out of Cloud. A run stays visible and a choice interrupt can still be answered.input_fingerprint,branch_optionsandowneronrun:initnever leave at any level,fullincluded.- A failure’s message (
run:error.detail, a step’serror*fields,agent:call_error.error,run:update_failed.detail) ships atioand above after redaction, cut to 2048 characters. Redaction catches token and key patterns, not a plain password quoted in the text, so keep secrets out of exception text. The local journal keeps the whole message. - Below
ioa failure ships itscodeanderror_type(the class name,Outer.Inner; a name that is not a dotted identifier ships asException). - A terminate’s or parent-caused cancel’s
reasonfollows the failure rule. Who asked ships at every level only as a runtime requester:cloud,parent,apiorcli. A requester you pass toterminate(requested_by=...), or the Cloud user id on a dashboard terminate, ships atioand above. - A Kafka-bound run’s
run:initnames the source record (topic, partition, offset, timestamp), never its key or header values. - A tool call held for approval ships its
reason, tool name and call id at every level; its arguments (tool_inputonrun:interrupted) ship atioand above. - Identifiers you choose ship at every level: a run id you pass, a workflow or agent name, an
agent turn’s
thread_id. Use opaque ids, not an email address or a name.
Who sets it
Section titled “Who sets it”A declaration is a ceiling; later settings can only lower it.
@workflow(capture=...)/@agent(capture=...). Acapture=onRunnerorCloudRunReporteracts as the declaration for workflows that have none.PYRULA_CLOUD_CAPTUREsets the level when nothing is declared, otherwise can only lower it. An invalid value stops the runtime at startup.- Agents only: the dashboard’s
config.update, applied last.
With none of these set, Cloud’s default for the key’s environment applies:
| Environment of the key | Default level |
|---|---|
development, staging | io |
production, every other | lifecycle |
Cloud reports the default at registration. The connector waits at most 3 seconds for it (not at
all when a level is declared or PYRULA_CLOUD_CAPTURE is set). Without an answer the level is
lifecycle, and a late answer applies only to events built afterward. pyrula dev prints the
effective level and where it came from.
Redaction
Section titled “Redaction”Everything that leaves (Cloud events, control-command failure acks, OpenTelemetry content and exception text) passes through these steps in order:
- Capture level. A withheld field never reaches redaction.
- Secret-shaped keys. A dict key at any depth containing one of these names
(case-insensitive) is masked with its whole value:
api_key,access_key,private_key,ssh_key,session_id,authorization,password,passwd,passphrase,pwd,secret,token,bearer,credential,cookie,signature,otp,jwt,csrf,xsrf,sessid,connect.sid. Compound names match with.,-, a space or no separator. Usage counters such asinput_tokensare not masked;access_tokensandrefresh_tokensare. - Secret-shaped values in text. Credentials in URLs (
postgres://user:pw@host/db) and inkey=value,key: value, JSON or repr assignments using those names, includingAuthorization: Bearer <token>. - Known token formats, with no key: Anthropic, OpenAI, GitHub, GitLab, AWS, Slack, Google
and Stripe keys, JWTs, PEM private keys and PuTTY
.ppkkeys. - Your policy (below).
- Size caps. A string over 64 KB is cut to a redacted prefix. A dict or list over that is
replaced by
{_truncated, _original_bytes}. Free-text detail such as an error message is capped at 2,048 characters.
Redaction fails closed on what it can’t parse. A secret with no recognizable shape, such as a
bespoke token or a plaintext business secret under an innocent key, still ships; keep it out of
run arguments or use a lower capture level. At full, a secret split across token deltas can
pass in the live stream, while the assembled step:done message is redacted whole.
Your redaction policy
Section titled “Your redaction policy”Extra key names, matched like the built-in ones:
export PYRULA_REDACT_KEYS="tenant_pin,badge_number"Or set a process-wide policy at startup (replaces PYRULA_REDACT_KEYS):
import re
from pyrula.contracts.redaction import RedactionPolicy, set_redaction_policy
set_redaction_policy( RedactionPolicy( extra_keys=("tenant_pin", "badge_number"), never_redact_keys=("signature",), # exempt a name; secret-shaped values in it are still masked extra_value_patterns=(re.compile(r"ACME-\d{6}"),), ))See the redaction API reference.
Your own mask callback
Section titled “Your own mask callback”mask runs once on the whole payload about to leave, after everything above and before the
size caps:
def mask(data): # `data` has already been through the built-in redaction. if isinstance(data, dict) and "customer_email" in data: data = dict(data) data["customer_email"] = "[masked]" return data
set_redaction_policy(RedactionPolicy(mask=mask))If mask raises, the payload is replaced by [redacted: mask failed]. redaction_hook on
AppOptions or RunRouterConfig is separate: it runs on what an HTTP client receives, never on
events sent to Cloud.
Error text
Section titled “Error text”A declined control command’s ack carries the runtime’s own refusal message (capped at 2,048 characters), or for an exception from your code only its type name.
A Kafka dead-letter record’s pyrula.error.message header is [code] ErrorType below io; at
io and above it is the failure text, redacted and cut to 2,048 bytes. Records from pyrula
loggers have the same shapes masked in the message and traceback. A formatter that renders
exc_info itself rather than through exc_text bypasses that.
Replay and fork in the console
Section titled “Replay and fork in the console”Replays and forks run on your side from the local store, so they work at every level. The level
only limits what the dashboard shows: a step’s input needs lifecycle; a tool or LLM call’s
input and result need io.
The outbox
Section titled “The outbox”Undelivered events wait in a local spool (what it holds). Nothing is re-sent until the runtime matches the store’s binding (One store per project and environment).
OpenTelemetry
Section titled “OpenTelemetry”With pyrula.agents.observability.configure, prompt and completion content on spans is off
unless OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT is set or you pass
capture_content=True. configure_langfuse, configure_langsmith and configure_datadog
turn it on by default. Recorded content, error statuses and exceptions are redacted as above.