Firedrill Python client
For your first use, authorize and select scope, then run your actual callable. Native Python does not need Node, the CLI, synthetic Tools or a private source checkout. The operating reference below is optional depth, not an onboarding checklist.
Run/batch inspection honors the client's selected project/environment, including reports, events, exact values, assessment history, sequences and run-owned Tools. Comparisons carry the same selected pair. Wider credential rights do not override that selection. A service must confirm the read context before the client returns content; an older service that ignores it fails explicitly. Follow/recovery keeps the original scope and never substitutes cached data after a context rejection.
Discover capabilities and authoring schemas
client.discover() reads accepted recipe versions, supported capabilities, actual
engineering bounds, current permission names and schema IDs without allocation.
root = client.schema("recipe") reads its canonical structural input JSON Schema;
client.schema("recipe", pointer="/properties/target", sha256=root["sha256"])
selects one exact node. Local $refs refer to that same root schema. Both methods
exist on AsyncFiredrill with await, with no Node/CLI dependency.
Use preview for semantic/cross-field/service validation: schema metadata is not
an authorization grant, resource reservation or proof of execution. On
FD_DISCOVERY_INCOMPATIBLE, update explicitly rather than silently downgrade a
recipe or create work to probe compatibility. context() still reads the current
authority/scope separately.
Population preparation and service inspection
client.materialize(recipe_or_path, axes=[{"path":"/priority", "values":["normal","urgent"]}], selection={"method":"matrix","seed":"release"})
returns an explicit recipe plus deterministic population provenance. Paths are
relative to each scenario's input. sample requires an explicit count; it
selects without replacement. No target, Tool instance or evaluator runs during
preparation. Review the returned recipe, then pass population["recipe"] to the
usual simulate call. The recipe's run limit still applies; up to 1,000 generated
scenarios are allowed, not a promise of exhaustive behavior coverage.
client.services.observe(instance_id, epoch=epoch, cursor=0, evidence_cursor=0)
returns service-attested state, pending work and activity. Follow the returned
cursors with the same epoch; pending reads scheduled work only. These reads
never invoke the agent. services.conform(build_id, suite, idempotency_key=key)
runs explicit operation fixtures in disposable isolated instances; read the
retained receipt with services.conformance(receipt_id). The suite uses
schemaVersion: 1, cases, per-case id, actorOperations, optional records,
and steps containing operationId, arguments, and expect path/value checks.
Passing covers supplied cases only; it does not change the build's status.
For publication-time checks, pass the same suite as optional conformance=...
to services.publish(...) or services.publish_directory(...). This gates
publication on your explicit fixtures, not universal service fidelity. Inspect
the retained publication result before creating instances.
services.create_observation_connection(instance_id, epoch=epoch, idempotency_key=key) returns a one-time protected read-only UI grant for an active
run owned by your invocation actor. Explicitly call reveal_launch_url() only in
a private delivery path; ordinary metadata omits it and retries never repeat it.
All these operations also exist on AsyncFiredrill with await.
For compatible provider-shaped routes, an active target's
context.tools[alias].export_connection("http") includes nativeHttpBaseUrl
and scopeHeaders when supplied by the service. Pass those scope headers plus
the derived bearer token to your HTTP client. Only authored routes exist, not
a complete vendor API or unrestricted proxy. Do not substitute a Firedrill
operator token or log the derived connection.
Recorded observations and target usage
Inside your native callback, context.observe("agent.decision", {...}) records
bounded runner-reported evidence. For measurements you actually have:
context.record_metrics(
latency_ms=response.elapsed_ms,
model_usage={"provider": response.provider, "model": response.model,
"inputTokens": response.input_tokens, "outputTokens": response.output_tokens},
)
Include costUsd only when known. No model tariff or missing zero is invented.
Each update is this invocation's cumulative value, not an additive increment.
Native start events, observations and measurements are attributed to the runner,
journaled before return and delivered in the background during execution,
including sequence steps. Keep context.observe(...) synchronous in sync or
async callbacks; no manual polling is needed. Sync callback threads marshal
private journal writes to the coordinator loop. Delivery needs that loop to run;
it does not keep callbacks alive after process exit. Interrupted delivery retries
the same event IDs, never the target. These events do not prove external side
effects or a passing assessment.
An explicit recipe target can carry optional metadata with
schemaVersion: "2026-10-15-target-metadata.1", source and resetCoverage.
For example, add "source": {"revision": actual_revision, "dirty": "unknown"}
and "resetCoverage": {"applicationDatabase": "customer_managed", "cache": "not_reset"}.
Optional source.contentDigest is sha256: plus 64 lowercase hex characters;
dirty accepts clean, dirty or unknown. Coverage keys are
applicationDatabase, cache, filesystem, conversationMemory,
outstandingJobs, browserProfile, externalState; each accepts
customer_managed, not_reset, not_used or unknown. Omitted values stay
unknown. Native Python preserves this JSON metadata from a recipe file, inline
dict or explicit target dict. For a callback, put metadata in its explicit
{"kind": "native", "name": "...", "metadata": {...}} recipe target.
Declarations neither implement reset hooks nor prove reset/source fidelity.
Large simulate()/result() responses automatically use the compact result path.
result.assessment_counts, result.batch["counts"] and result.gate describe all
runs; result.runs can be the first page. If result.pagination["complete"] is
false, continue with client.list_runs(result.batch_id, cursor=result.pagination["nextCursor"]) (await with AsyncFiredrill). Keep outputs
paged instead of loading the whole campaign into memory.
Native Python 3.11+ sync and async clients. No Node, CLI wrapper or world runtime. The one runtime dependency is certifi's certificate bundle. Install in your own agent project; no Firedrill source checkout is required.
python -m pip install firedrill-run==2.0.0
For an offline release wheel, verify its separately supplied checksum and install from its absolute path, preserving its real filename and version.
No API address is needed: Firedrill() and FiredrillLogin() use Firedrill
production automatically. Development, staging, local and custom control API
addresses are not accepted. Remove old URL overrides rather than changing them
to another environment; existing recovery files remain bound to their original
service and are never migrated implicitly. Redirects and environment HTTP
proxies are not used. Customer application and synthetic Tool connection URLs
remain separate from this production control-service policy.
This client requires the versioned production API contract. An older service is not a compatible fallback.
Request more execution capacity
request = client.capacity.request(
resource="runs", requested_capacity=100000,
message="Larger simulation campaign", idempotency_key="saved-key",
)
requests = client.capacity.list()
Both methods are awaitable on AsyncFiredrill. Requests use the client's selected
project/environment. Resources are runs, services, storage or other;
reply_to is optional. Do not include credentials or simulation data. Reuse the
same key and input after an uncertain reply. Request receipt does not grant more
capacity or start work. Notification sent means provider acceptance, not email
delivery. Shared execution and payload controls are not per-account allowances.
HTTPS trust
Control/API calls, native WorkOS sign-in and HTTP targets use verified TLS with
hostname checks. Normal Python/OS trust is preserved. When Python's default
context has no loaded CA certificates and neither SSL_CERT_FILE nor
SSL_CERT_DIR is explicitly set, the client adds the installed certifi roots to
that request's context. It does not change global Python settings or download a
trust store during a request.
Enterprise/custom trust remains explicit: set SSL_CERT_FILE to your approved
CA bundle or SSL_CERT_DIR to your OpenSSL-compatible hashed certificate
directory before starting Python. Those overrides suppress the certifi fallback,
including when an override is empty or invalid; the client never replaces an
explicit choice with public roots. A valid custom trust setup can still contain
both enterprise and public CAs if your administrator deliberately supplies them.
Certificate or hostname failures return tls_certificate_untrusted; invalid
trust setup returns tls_trust_configuration. Neither disables verification nor
automatically retries a request. For a saved mutation, keep its existing
operation/key and follow recovery instructions; a TLS error does not authorize
starting the target again. For offline installation, provide the reviewed
certifi==2026.7.22 wheel alongside the client wheel and install through an
explicit --no-index --find-links directory. Do not omit the dependency.
Authorize the operator and choose scope
Firedrill login authorizes the coding-agent operator on the human's behalf—not
its model provider or the application being simulated. Ordinary SDK calls never
open a browser or create an account. CI supplies an authorized scoped
FIREDRILL_TOKEN from its secret store and never runs interactive login.
If you already used the separately installed CLI's firedrill login and
firedrill init, use Firedrill.from_config() to reuse its public
scope and protected login. The CLI is optional; native Python sign-in needs no
Node process. For a fresh Python-only setup, run this deliberately in a human's
interactive terminal (not CI or captured model output):
import os
from firedrill import FiredrillLogin
if "FIREDRILL_TOKEN" in os.environ:
raise SystemExit("An explicit CI token is selected; do not replace it with human login.")
def verify(value):
# These are the human approval URL/code, never an access or refresh token.
print("Open:", value["verificationUrl"])
print("Approval code:", value["userCode"])
def choose_organization(value):
active = [item for item in value["organizations"] if item["status"] == "active"]
if active:
for item in active:
print(item["organizationId"], item["name"])
selected = input("Choose an organization ID shown above: ").strip()
if selected in {item["organizationId"] for item in active}:
return {"organizationId": selected}
raise ValueError("No available organization selected")
if value["canCreate"]:
name = input("Name your first Firedrill organization: ").strip()
if name:
return {"name": name}
raise ValueError("Finish email verification/invitation or restore organization access, then retry")
login = FiredrillLogin()
status = login.login(on_verification=verify, on_organization=choose_organization)
print("Firedrill authorized:", status["signedIn"])
The human completes the displayed WorkOS approval; no token is pasted into chat
or source. Organization choice is requested only when needed. A pending
invitation or uncertain setup is not permission to create a replacement
organization. Preserve the protected login state and follow the error's repair
direction. login.status() reads current authorization; login.logout() ends
this interactive session, not separately issued CI credentials.
AsyncFiredrillLogin provides the same explicit flow with
await login.login(...); its callbacks may be sync or async.
Sync and async SDK clients stay bound to their first resolved login credential
and organization. Automatic renewal continues for that identity. Replacing the
shared login stops an existing client with FD_SDK_IDENTITY_CHANGED before it
sends a request under the replacement. Verify the intended account and scope,
then construct a new client deliberately. Recover old work with its original
identity, not a replacement target run.
After authorization, discover authorized projects and their environment pair:
from firedrill import Firedrill
account = Firedrill()
for project in account.projects()["items"]:
print(project["id"], project["name"], project["environment"]["id"])
If you deliberately need a new project, save one key before calling
project = account.create_project("My application", idempotency_key="my-application-first-project").
It returns project["id"] and project["environment"]["id"] (the new Default
environment). Reuse the exact key/name after uncertain delivery, not a new key.
Use that actual pair as FIREDRILL_PROJECT_ID and FIREDRILL_ENVIRONMENT_ID, or
pass project_id and environment_id to the constructor. Never invent IDs or
put credentials in config/recipes. An explicit token takes precedence over saved
login and never silently falls back to another identity.
First tool-free run
from firedrill import Firedrill
client = Firedrill() # explicit environment-selected scope; constructor args also work
result = client.simulate({
"name": "Greeting variation",
"scenarios": [{"id": "greeting", "name": "Greeting", "input": "Hello"}],
}, target=lambda value, context: {"reply": value.upper()})
print(result.report_url)
This example executes your callable, not a model substitute. Use your existing application in the callback to exercise its actual behavior. Zero checks is valid; no universal score or passed flag is inferred. The local service retains the recipe, run, output, attributed evidence and any requested assessment.
For an awaitable application, use the native async client in your existing loop:
from firedrill import AsyncFiredrill
client = AsyncFiredrill()
async def target(value, context):
context.raise_if_cancelled()
return await existing_application(value) # your application's existing callable
result = await client.simulate("firedrill/scenario.json", target)
Recipe limits.maxRuns optionally caps the expanded run count for this batch.
It accepts a positive integer or JSON null (Python None); null is the default
and means no caller-selected run-count cap. A numeric cap is an execution choice,
not a cumulative account usage quota. Per-request payload,
scenario/repetition/concurrency, output and execution safeguards still apply.
simulate(recipe, target=None, *, operation_id=None, allow_parallel=False) accepts
an inline recipe or JSON file. Callback targets need no serialized source; a
missing wire target becomes a named native target. schemaVersion defaults to
the canonical 2026-10-14 contract; an explicit other version fails locally,
before a request or journal is created. Remaining recipe validation and canonical
defaults stay with the service. Both one and many runs return the same BatchResult with batch, runs,
report_url and canonical server gate; to_dict() preserves wire spellings.
reportUrl and url are aliases for the same retained report_url, not additional reports.
Results from simulate, run and resume also carry operation_id, serialized as
operationId by to_dict(). Preserve it even for a partial result: it selects the
same protected journal on resume, without creating replacement work. The
client's last_operation_id remains available. result(batch_id) reads the API
record only and does not invent a local operation handle.
With an explicitly configured service portal origin, the report URL opens the
retained batch with its exact organization/project/environment context. Normal
portal sign-in and authorization still apply. A local service without that
configuration returns an authenticated API resource instead. Neither link is a
public share link or an access grant.
Individual report(run_id) dictionaries also preserve optional reportLink
(REPORT_LINK_VERSION, ReportLink): kind == "portal" names the exact retained
run's scoped overview; kind == "api" names its authenticated API fallback.
inspect_report preserves it inside the operator document's value. Older
services may omit it. Never infer an origin, credential or access from a link.
Command and HTTP recipes need no callback. Commands execute literal argv without
a shell and receive the versioned target envelope on
stdin. FIREDRILL_INPUT_JSON contains only JSON input; run/attempt IDs have their
own environment variables. The client removes FIREDRILL_TOKEN and
FIREDRILL_CREDENTIAL from child environments while retaining customer target
model configuration. HTTP targets receive the same envelope from your runner,
without the control token, redirects or automatic retries.
The envelope's schemaVersion is 2026-10-17-target-invocation.1. It contains
separate input, scenario: {"id": ...}, configuration, runId, attemptId,
target-only tools and optional current step: {"id": ..., "ordinal": ...}.
Read client.schema("target-invocation") for its structural transport schema.
The runner keeps checks, rubrics, metric selectors, dataset references and future
sequence steps; they are not target input. Explicit task input should contain
only the knowledge the application actually needs.
Callback context includes schema_version (TARGET_INVOCATION_VERSION), run_id,
attempt_id, ID-only scenario (TargetScenario), configuration, a
Unix-seconds deadline, remaining_seconds, cancelled, raise_if_cancelled()
and observe(kind, json_payload). Observations are bounded runner-reported
evidence, not service-attested side effects. Never put secrets in output/evidence.
Target exceptions retain their type, not arbitrary potentially secret-bearing
exception text.
Callback input/configuration are detached JSON values, not references to retained
coordinator data. This is least-disclosure, not a hostile-code sandbox: native
callbacks share the customer's process and commands still have its filesystem.
Optional task measurements
Use scenario metrics to retain up to 32 named finite numeric values from frozen
run evidence. They are observations, not assertions or evaluator scores. This
native arithmetic application demonstrates the actual recipe shape; replace the
callback with your agent before drawing conclusions about that agent. It needs
your configured origin, authentication and selected project/environment.
from firedrill import Firedrill, TASK_METRIC_VERSION, TaskMetricDefinition
net_revenue: TaskMetricDefinition = {
"schemaVersion": TASK_METRIC_VERSION,
"id": "net-revenue", "label": "Net revenue", "path": "/output/netRevenueUsd",
"unit": "USD", "direction": "neutral",
}
recipe = {
"schemaVersion": "2026-10-14", "name": "Refund outcome exploration",
"target": {"kind": "native", "name": "revenue-summary"},
"scenarios": [{"id": "refund-exceeds-sales", "name": "Refunds exceed sales",
"input": {"grossUsd": 100, "refundsUsd": 107}, "metrics": [net_revenue]}],
"repetitions": 3, "concurrency": 1,
"limits": {"timeoutMs": 10000, "maxRuns": 3, "maxOutputBytes": 4096},
}
client = Firedrill()
result = client.simulate(recipe, target=lambda value, context: {
"netRevenueUsd": value["grossUsd"] - value["refundsUsd"],
})
print([group.get("taskMetrics") for group in client.summary(result.batch["id"])["groups"]])
Zero and negative task values are valid. Missing evidence, missing selected values
and non-numeric values stay unavailable, never coerced to zero. TaskMetricResult
and TaskMetricSummary preserve canonical wire names, definition digests, coverage,
provenance and measured/unavailable states. Summaries keep median/range and bounded
run samples separate per exact definition and scenario/configuration population;
absent historical fields remain absent. direction is only a declared preference,
not a score, CI rule or automatic winner. Units are not converted, and this USD
task value is not an inference-cost estimate. AsyncFiredrill uses the same recipe
and retained shapes without a Node bridge or client-side grading implementation.
Without source, JSON Pointers select from {input, output, metrics}. /input/...
is scenario-declared data; /output/... and /metrics/... are runner-reported.
An explicit canonical service source selects frozen attested Tool state/activity/
pending work, not a later live instance. Read the task-metric authoring schema
for exact fields. record_metrics still accepts only its documented telemetry
fields: put custom task measurements in your actual JSON output.
These docs describe the source contract, not a deployment or verification receipt.
Optional rubric and gate
Numeric exact constraints can include
"numeric": {"schemaVersion": "2026-10-16-numeric-check.1", "unit": "USD", "absoluteTolerance": 0.01}.
Use only numeric expected values with equals, lte or gte. Equality compares
absolute difference; lower/upper bounds expand by the declared tolerance. The
tolerance is finite and nonnegative, defaulting to zero; missing/non-numeric
evidence stays unknown. This client exports NUMERIC_CHECK_VERSION. Units are
labels, not inferred or converted. Supply the same JSON recipe as other clients;
the service validates/evaluates the canonical check.
Observation-only runs do not need scoring. When the user wants quality assessment,
this self-contained example requests a separate platform evaluator budget: three
sequential trials, not unlimited evaluation. Save it as firedrill/scored.json.
The native renderer below uppercases supplied notice text; it is a real teaching
application, not an AI agent or a promised passing score. Replace it with your
actual application before drawing conclusions about that application.
{
"schemaVersion": "2026-10-14",
"name": "Delivery notice quality",
"target": {"kind": "native", "name": "delivery-template"},
"scenarios": [{
"id": "delay", "name": "Explain a delivery delay",
"input": {
"task": "Give a brief delivery update using only these facts.",
"facts": {"status": "delayed", "deliveryDay": "Friday", "customerAction": "none"},
"label": "Your shipment is delayed until Friday. No action is needed."
},
"checks": [{"id": "label-present", "description": "Return the label field",
"path": "/output/label", "comparator": "exists", "required": true}],
"rubric": {
"id": "delivery-facts", "version": "1",
"policyVersion": "2026-10-15-assessment.1",
"dimensions": [{
"id": "facts",
"applicability": {"state": "applicable"},
"missingEvidence": "not_assessable",
"description": "Judge output.label against input.facts. Accept equivalent wording; do not require the input label's exact phrasing.",
"anchors": [
"Contradicts the delivery facts or invents a harmful instruction.",
"Mostly omits the facts or adds unsupported promises.",
"Preserves some facts but misses an important fact or is materially ambiguous.",
"Preserves all three facts without unsupported claims, with minor clarity issues.",
"Clearly and concisely preserves the delay, Friday delivery and no action needed, without unsupported claims."
]
}]
}
}],
"repetitions": 3,
"concurrency": 1,
"limits": {"timeoutMs": 10000, "maxRuns": 3, "maxOutputBytes": 4096},
"gate": {"requireCompleted": true, "requireExactChecks": true, "minimumLevels": {"facts": 3}}
}
The five anchors correspond to levels0–4, not probabilities. Task facts live in
input and the dimension description; do not rely on a scenario display label
as evaluator context. The exact check protects output shape while the rubric
permits valid wording variation.
from firedrill import Firedrill
client = Firedrill() # approved origin and selected project/environment from above
result = client.simulate(
"firedrill/scored.json",
target=lambda value, context: {"label": value["label"].upper()},
)
print(client.summary(result.batch["id"]))
print(client.report(result.runs[0]["id"]))
Inspect execution, exact checks, dimension level/state/explanation/evidence and
gate separately. A dimension can be not_assessable with level: null: that
quality evidence is unknown, not level0 or proof of application failure. The gate
is inconclusive unless another known requirement already blocks it; exact checks
and completion still apply independently. Pending assessment stays pending. Retain all three
trials and failed/interrupted/unassessed counts, not just the best result.
Applicability is authored case policy, not a grader's judgment. Explicit
applicability or missingEvidence requires rubric
"policyVersion": "2026-10-15-assessment.1" (also exported as
firedrill.ASSESSMENT_POLICY_VERSION). Omitted applicability means applicable.
A dimension that does not apply to the case can instead declare
"applicability": {"state": "not_applicable", "reason": "This case is information-only; no action is authorized."}.
It retains state not_applicable and level: None in native Python, separate
from missing or unsupported evidence (not_assessable/None). The JSON wire
value is null. The evaluator cannot invent this exemption.
"missingEvidence": "not_assessable" is the only supported missing-data policy:
missing evidence never becomes a score, a pass or an automatic exclusion. An
excluded dimension cannot satisfy its required minimumLevels entry; that gate
remains inconclusive unless another known requirement blocks it. Other dimensions
are not reweighted. Changing this authored policy changes the pinned rubric;
past assessments are not rewritten.
For an optional descriptive composite, add this composite field inside the
scenario's rubric before submitting the recipe above:
"composite": {
"schemaVersion": "2026-10-16-composite.1",
"method": "weighted_mean_ordinal",
"weights": [{"dimensionId": "facts", "weight": 1}]
}
Select only dimension IDs declared in that rubric, once each, with positive integer weights from 1–1000. This explicitly authored index uses the 0–4 ordinal coding; it is not a probability, a universal quality score or a gate. The service derives it from retained assessment evidence. Python does not calculate a grade or invoke Node. Inspect the optional service result as an ordinary dictionary:
from firedrill import CompositeResult
report = client.report(result.runs[0]["id"])
composite: CompositeResult | None = report["assessment"].get("composite")
print(composite) # None when no policy was requested
With AsyncFiredrill, await client.report(run_id) and inspect the same dictionary.
COMPOSITE_VERSION, CompositePolicy, CompositeComponent and CompositeResult
are exported native types/constants; wire keys remain camelCase. Read state,
reason, components and requiredExactChecks, not only value.
Missing/unassessable or selected not-applicable dimensions do not become zero or
silently reweight the remaining dimensions. A required exact violation yields
constraint_violation; unknown required checks yield not_assessable. Both
retain value: None (null on the wire). The optional CI gate is unchanged, and
these report reads invoke neither the target nor an evaluator.
This evaluator reads text observations. For a dimension that requires pixels,
audio or video, declare "evidenceKinds": ["image"], ["audio"] or ["video"]
(combine kinds when all are required). Omitted kinds mean text. Unsupported
required kinds remain not_assessable/None; captured-file metadata cannot
produce a visual/audio grade. Text-compatible dimensions can still be assessed.
A recipe whose dimensions are all unsupported makes no evaluator request.
Declare these requirements explicitly; they are not inferred from criterion prose.
After an intentional application change, run the same recipe again and compare
the two actual batch IDs. Keep scenario/rubric, configurations, repetitions and
any selected service seeds consistent; record the application revision and its
own randomness/reset policy. This tool-free recipe declares no service seed.
Comparisons describe retained populations, not a causal effect or automatic winner.
summary() keeps notApplicable counts separate from notAssessable and missing
counts. A paired dimension reports notApplicablePairs separately from
missingOrNotAssessablePairs; neither contributes a numeric observation.
Quality pooling also requires the same known evaluator configurationDigest,
not only the same provider/model/protocol. Differences are reported as
evaluator_configuration or evaluator_configuration_unavailable. Historical
missing configuration remains unknown and excludes paired quality pooling;
individual retained reports stay readable. These wire keys remain camelCase in
the native result dictionaries.
A reassessment explicitly evaluates frozen evidence again; it does not run the
application. Do not request one implicitly to repair an unknown result. Target
provider credentials/costs remain separate from operator login and evaluation.
Use client.compare(baseline_batch_id, candidate_batch_id) for those retained
batches. Deliberate reassessment is
client.reassess(run_id, idempotency_key=saved_reassessment_key), which reuses
the retained rubric. Supplying rubric=selected_rubric changes assessment criteria,
not the application's past output. Async clients use the same data with awaited
methods. No Node process or CLI is needed for this native path.
Inspect each assessment's evaluationAttempts for the frozen nonsecret
configuration/configurationDigest, state and separate provider usage.
Configuration includes request model, protocol/instructions digest, input/output
bounds and transport limits; it never contains a provider key. The evaluator
configuration digest is distinct from the target configuration digest. Known
provider usage remains retained when structured scores are rejected; missing or
unparseable provider usage remains unknown, not zero. Native sync and async reads
preserve these dictionaries without Node. Reading history does not create an
assessment or replay a target.
Optional run-bound Tools
Tool-free simulations remain valid. To allocate fresh Tool state for each run, select immutable builds, explicit datasets/seeds and operation grants in the recipe:
recipe = {
"name": "Application with a controlled inbox",
"services": [{
"alias": "inbox", "buildId": selected_build_id,
"datasetId": selected_dataset_id, "seed": "42",
"operations": ["list"],
}],
"scenarios": [{"id": "one", "name": "One", "input": {}}],
}
def target(value, context):
response = context.tools["inbox"].invoke(
"list", {}, idempotency_key=f"{context.run_id}:list-once",
)
return existing_application(value, response) # your own application
result = client.simulate(recipe, target)
Async callbacks use await context.tools[alias].invoke(...). The returned value
is the unchanged semantic operation response, including result.outcome; an
operation error is not an agent assessment verdict. Every invocation requires an
explicit stable key. Unknown acknowledgement is not retried automatically: while
the run still has authority, reconcile the same key and unchanged arguments.
scenario.serviceDatasets can override a binding's dataset by alias, for example
{"inbox": another_dataset_id}. Each expanded scenario/configuration/repetition
receives its own instance. An omitted dataset means no seed records, never starter
data. Tool instances are run-managed: preparation, closure and retained evidence
belong to Firedrill, not target lifecycle/reset calls. Finish, cancellation, expiry
or revoked authority retires that run's grants and preserves service evidence.
Binding a request to a run/attempt does not prove it was authored by an AI agent.
tool.instance_id, tool.epoch, tool.operations, tool.to_dict() and normal
repr(context)/repr(tool) omit grant secrets. context.to_dict() is also
metadata-only. For an existing protocol integration,
tool.export_connection("http") or tool.export_connection("mcp") is an explicit
secret export returning the exact URL, token and scoped grant metadata. The HTTP
invoke body is {"operationId": ..., "arguments": ...} and the stable key belongs
in Idempotency-Key. Do not substitute the operator token or copy a grant to
another run, instance, project or environment.
Command stdin and HTTP target JSON include an explicit tools binding map, with
HTTP/MCP connections carrying only these derived target credentials. They contain
no operator token or lease token. FIREDRILL_INPUT_JSON remains input alone; no
grant is added to child environment variables. This intentional secret-bearing
envelope is never journaled by the helper. Do not log it or include it in captures.
Known exact derived tokens echoed in target output/events/stdout/stderr are
redacted before journaling and evidence submission, including before log chunking.
This is not a general detector for transformed secrets or arbitrary credentials.
Preparation heartbeats run before target entry. A claim remains claimed in its
private journal until exact returned aliases/builds/datasets/seeds/grants/routes
are checked. Unconfirmed preparation remains replayable on resume without
inventing target execution. Confirmed setup rejection records
runner.preparation_failed and interrupted execution, with no runner.started
or target duration. Authority/transport uncertainty preserves the claim for
reconciliation. The target deadline starts after preparation. A journal entry
becomes running immediately before invocation; recovery never invokes that
target again if its outcome is uncertain. Already terminal claims are reconciled
without preparing replacement instances.
Optional bounded sequences
Use scenario.sequence with schemaVersion: "2026-10-14-sequence.1" for ordered
target calls and explicit service operations. The service owns conditions,
bounded polling, virtual-time advancement and service snapshot/restore; Python
does not reimplement their semantics. The same simulate, run and resume
entry points work for sync/async callbacks, literal commands and HTTP targets.
from firedrill import SEQUENCE_VERSION
result = client.simulate({
"name": "Two explicit application calls",
"scenarios": [{
"id": "follow-up", "name": "Follow-up", "input": "default input",
"sequence": {"schemaVersion": SEQUENCE_VERSION, "steps": [
{"id": "first", "kind": "target", "input": None},
{"id": "second", "kind": "target", "input": "Follow up"},
]},
}],
}, target=existing_application)
retained_steps = client.sequence(result.runs[0]["id"])
Inside each target callback context.step is {"id": step_id, "ordinal": index};
the command/HTTP envelope has the same step field. Explicit input: None is JSON
null, not a request to reuse the scenario input. An omitted step input uses the
scenario input. Each target step receives freshly checked run-bound Tool grants,
including new epochs after an explicit service-only restore. Restoring service
state does not reset your process, model memory or application database.
The service supplies one absolute sequence deadline. The helper recomputes the
remaining time after grant refresh and starts no target after it expires. Each
target output is at most 128 KiB (or the lower recipe bound); a complete step
observation is at most 256 KiB. Larger captures belong in context.attach.
Native shared-memory and cooperative-cancellation limitations still apply.
Recovery journals persist sequence phase, then a step's running marker just
before invocation. Output/events are saved as complete before acknowledgement;
lost acknowledgements replay those exact records without rerunning the target.
A saved running step with unknown output raises RecoveryRequired until its
retained run is terminal. resume(id) without a native callable can reconcile a
saved completion but does not invoke the next native target. Journal reads require
the operation lock. Derived grants and target envelopes are never journaled.
sequence(run_id) returns retained service records in both sync and async clients;
reading it does not dispatch any step or create an assessment.
Captured artifacts
context.attach({"path": local_path, "artifact": metadata}) declares and uploads
an exact capture under this run/attempt. Call it inside the callback before
returning. Sync callbacks receive an available reference directly; async callbacks
use await context.attach(...). The operator credential and lease are not exposed
in the context or returned reference.
import hashlib
from datetime import datetime, timezone
from pathlib import Path
# An actual capture produced by your application; retain it for interrupted uploads.
capture = Path("captures/trace.zip") # owned by you, mode 0600, regular file, no links
content = capture.read_bytes()
reference = context.attach({
"path": capture,
"artifact": {
"id": "trace-1", "name": "trace.zip", "kind": "trace",
"mediaType": "application/zip", "bytes": len(content),
"sha256": hashlib.sha256(content).hexdigest(),
"capturedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"metadata": {},
},
})
The helper validates the original private file and records its absolute path, metadata, digest and exact declaration key before network writes. Files are bounded to 16 MiB; the service enforces its per-run count/byte bounds. Supported formats are PNG, JPEG, WebP, ZIP, JSON, text and WebM—not inline HTML/SVG. An acknowledged attachment is retained evidence of those bytes, not proof that its claims are true or that an external side effect happened.
A lost declaration/upload acknowledgement preserves the pending intent. Resume the same operation, with the original capture unchanged. The helper reconciles exact intent before finishing the run and does not reinvoke the target. If the failed attachment call prevented your callback from returning, recovery cannot invent that return value: the retained target error remains an error. Pending new uploads need the original active lease; already accepted exact content can be reconciled without new target execution. Local capture files are never automatically deleted.
Retrieve retained captures
The sync and async clients expose artifacts(run_id),
artifact_content(run_id, artifact_id) and
download_artifact(run_id, artifact_id, output). These are scoped evidence reads,
not target execution or evaluation:
files = client.artifacts(run_id)
available = [item for item in files["items"]
if not item.get("metadataOnly") and item.get("state") == "available"
and item.get("mediaType") == "image/png"]
if available:
# Choose an extension matching the selected artifact's mediaType.
saved = client.download_artifact(run_id, available[0]["id"], "captures/review.png")
print(saved["path"], saved["artifact"]["mediaType"], saved["verified"])
Await these methods on AsyncFiredrill. The download verifies retained size,
SHA-256 and media type and confirms the selected project/environment. It rejects
redirects and publishes a mode 0600 file at an explicit new path in an existing
directory, never overwriting a file or symlink. No automatic open or extraction
occurs. artifact_content instead returns verified bytes and their metadata.
Metadata-only expired/deleted entries remain visible as history, with no names
or payload; pending uploads are not available evidence. Retry an interrupted
download, not the original simulation. Cancelling an async download's await can
leave its verified file saved because the bounded download runs in a worker
thread. Check the chosen filename before retrying; no existing file is replaced.
Documents use the same retained evidence path: kind: "file" with plain text,
JSON, text/markdown, application/pdf or application/octet-stream. Opaque
files are download-only attachments, never interpreted or executed. Attach the
original private file with its exact byte count and SHA-256 while the invocation
is active; the browser helper does not manufacture documents. The authenticated
API owns storage and retention. An available manifest plus verified download
establishes returned bytes, not a client claim about the deployed storage
provider, replicas or backups. No direct bucket URL or cross-origin redirect is
accepted by these download methods.
Recipes have no fixed client scenario/configuration/service item quota.
Repetitions and desired concurrency must be positive safe integers. Finite JSON,
8 MiB recipe input and canonical expanded-payload bounds remain, as do timeouts,
actual service capacity and explicit caller budgets (limits.maxRuns: None
means no caller run cap). Sync and async runners allocate workers using the
lesser of desired concurrency, admitted population and reported
operations.resources.runSlotLimit. Older services without that disclosure
remain serially recoverable. Recovery reads the admitted batch instead of
requiring a new admission preview or silently submitting replacement work.
The runner_capacity progress notification/default stderr discloses requested
concurrency, local worker ceiling and reported service slots. This is not a
reservation or proof of execution. Python native execution supports command,
HTTP and Python callbacks; submission can queue an isolated Node recipe, but
executing it still requires the explicitly prepared TypeScript isolated adapter.
Coordination and recovery
Private compilation failures preserve FiredrillError.build_id, operation_id
and failure; to_dict() uses wire keys buildId, operationId and failure.
services.build(build_id) returns an optional failure with versioned category
diagnostics, repair directions and selected relative file/one-based line/column
locations. The public ServiceBuild, ServiceBuildFailure and
ServiceBuildDiagnostic types describe this response. Compiler stderr, source
lines and host paths are not returned. A null failure means no structured
diagnostic was recorded, not success. Fix source and publish with a new operation
key; the old failed key recovers its retained failure without rebuilding.
Partial Tool setup receipts retain these diagnostics and existing instances.
Canonical server definition rejections keep FiredrillError.code (normally
FD_INVALID_INPUT), HTTP status, repair and request_id. The bounded
error.validation dictionary gives exact field paths with array indices, issue
codes/messages, totalIssues and omittedIssues; at most 20 issues and 20 path
parts per issue are returned. str(error) includes the server's short human
summary. error.to_dict() uses wire spellings inside {"error": ...}, including
the current operationId/batchId/runId when available. Missing, malformed or
legacy raw diagnostics are not exposed as validation. Python does not duplicate
the service's definition validator: preview/save_scenario may send invalid
definitions and receive these errors without creating a simulation journal;
simulate can already have a protected create_intent journal when the service
rejects the definition, with an operation ID but no batch or target invocation.
An unchanged retry/resume repeats that rejection; fix the definition and use a
new deliberate operation. Do not edit or discard an uncertain execution journal.
The helper owns scope checks, durable intent, runner claims, 15-second heartbeats,
bounded stdout/stderr/output, retained evidence and finish reconciliation. It does
not bundle or reimplement simulation state or assessment logic. Journals default
to ~/.local/state/firedrill/python. Set FIREDRILL_JOURNAL_DIRECTORY for a
selected CI recovery location; an explicit state_dir takes precedence. Python
and TypeScript execution journals have different formats, so retain separate
locations for their executions; their protected sign-in store is shared.
Directories must
be owner-private (0700), files 0600. Journals contain sensitive case/evidence and
lease state, but never the operator token. Keep them outside source control. A
process lock prevents two local owners of the same operation.
from firedrill import TransportError, RecoveryRequired
try:
result = client.simulate(recipe, target, operation_id="my-deliberate-operation")
except (TransportError, RecoveryRequired) as error:
print(error.code, error.operation_id, error.batch_id, error.repair)
# After connectivity/authority is restored:
result = client.resume(error.operation_id, target=target)
Only resume the original operation after a lost response. A new simulate call
with a new ID creates a new batch. last_operation_id also exposes the local
handle if the caller cancels its foreground wait. resume(id) without a callback
can reconcile completed output/finish requests; it cannot execute more native
work. A saved running invocation without a local result is never re-invoked:
the client reads retained state and raises RecoveryRequired while execution is
uncertain. Lease expiry becomes interrupted in the service, not a target retry.
Exact event IDs/content and finish idempotency keys survive lost responses.
Actual native callback entry and successful command process creation emit
runner.started. An HTTP request emits only runner.http_dispatch, which is not
proof of remote application invocation. The helper retains measured monotonic
metrics.durationMs for the target adapter, including awaited callback/upload work;
this is runner-reported elapsed time, not service-attested or inference-only latency.
Cancellation before invocation produces no invented start or target duration.
run(batch_id, target=None, ...) attaches your customer-owned runner to an existing
batch, validates its selected scope, and uses the same journal/claims without
creating a second batch. Useful when an operator prepared work through the API or
MCP. The control service cannot run a laptop callback on its own.
Native callbacks share process memory. Parallel native, command and HTTP targets
require an explicit retained targetExecution declaration when recipe concurrency
exceeds one. Fresh simulate(..., allow_parallel=True) can record it after review;
existing run/resume cannot change an admitted recipe to add it. This acknowledges
shared-state risk, not isolation. Sequential execution is unchanged.
No adapter silently resets your database, application memory or borrowed service.
Timeouts/cancellation cannot guarantee that arbitrary threads, cancellation-
suppressing async code or remote HTTP effects stop. Such outcomes are interrupted
with an explicit stop-unconfirmed reason and never automatically retried. POSIX
command groups receive bounded TERM/KILL followed by an exact owned-group absence
observation, not just a leader-exit check. The retained runner.command_stop
receipt distinguishes group_absent from unconfirmed; escaped descendants are
not observed and this is not a container sandbox. Windows can stop only the
owned child here and always reports process-tree stop as unconfirmed. Captured
output prefixes remain evidence. An adapter-observed unconfirmed stop is retained
in the private operation/sequence journal, stops new coordinated claims and
sequence steps, and remains fenced on resume even if the service lease is already
terminal. Resume can retain existing completion/evidence, not establish process
cleanup or authorize more execution; inspect the customer runner separately.
request_timeout (default 10 seconds) bounds an individual control exchange.
Recipe limits.timeoutMs bounds one target invocation, or the whole declared
sequence rather than each step separately; follow_timeout (default 300
seconds) bounds execution coordination and queue following, not an invented
target duration. assessment_timeout (default 300 seconds, or 0 to return
without waiting) is a separate wait after execution reaches a terminal state.
Set these options on Firedrill(...) or AsyncFiredrill(...). Waiting for an
assessment never claims more work or invokes the target again. Active invocations
retain their separate target bound. A bound/cancellation can return the retained
partial result with pending assessment and its operation/batch handles; assessment
is never fabricated as complete. resume(operation_id) without a native callback
can inspect retained work, including a compact large-batch result, without
restarting that callback.
Cancellation prevents new work and is observed by active runners on heartbeats.
Small control surface
Both clients expose context(), projects(), create_project(name, idempotency_key=...), batch(id), result(id), list_runs(batch_id, cursor=None, limit=100), report(run_id, cursor=None, limit=100), scenarios(project_id=None)
and cancel(batch_id, idempotency_key=...). Async methods are awaited. Use explicit
idempotency keys for retryable project/cancellation mutations.
For separate preparation/execution/observation, use submit(recipe, idempotency_key=...), resume_submission(key), run(batch_id, target=...) and
follow(batch_id, timeout=...). The ordinary simulate remains the one-call path.
Submission keys reconcile creation only; runner operation IDs reconcile execution.
Follow requests no target, cancellation or assessment, and settled does not mean
passed. Both native classes expose these methods; await them on AsyncFiredrill.
For an explicitly selected repository config, both classes expose a synchronous factory; creating the client does not send a request:
client = Firedrill.from_config("firedrill.config.json") # FIREDRILL_TOKEN supplies authority
async_client = AsyncFiredrill.from_config("firedrill.config.json") # no await for construction
The public config needs only projectId and environmentId, each a nonempty
string of at most 2,048 characters. Select both or neither. Credentials and
unknown settings are rejected, not ignored. from_config() rejects conflicting
project/environment selections across config, explicit options and environment.
Legacy apiUrl, api_url, base_url and FIREDRILL_API_URL declarations are
accepted only when they identify https://api.firedrill.run. Remove those
unnecessary settings from new integrations. Every supplied address is checked,
including an environment value shadowed by an explicit option. Another origin,
credentials, query, fragment or path fails with FD_API_ENDPOINT_LOCKED before
sign-in, credential storage or network access. Hostname case, an explicit HTTPS
default port and a trailing slash normalize to the same production origin;
there is no switch to enable development endpoints. Null/empty scope options
are not valid selections. Direct constructors retain explicit-argument
precedence for project/environment only, not for control API addresses.
Config files are bounded to 64 KiB and recipe files to 8 MiB, read as strict UTF-8
JSON through a regular-file descriptor. Final-component symlinks, FIFOs, devices,
directories and malformed inputs fail with a typed FiredrillError without
echoing source contents. This does not prohibit symlinked parent directories or
claim a filesystem sandbox. There is no ancestor search, executable config,
automatic credential-file discovery or implicit browser authorization.
Tool-free command/HTTP/native execution remains valid. Service authoring and explicit lifecycle controls are available below; run-bound Tools are selected explicitly as described above. Arbitrary application-state reset and browser execution are not inferred or performed by the helper.
Scoped machine credentials
Both clients expose client.credentials for an identity explicitly authorized to
manage its own machine credentials. The service—not the client—checks that the
exact project/environment and requested capabilities narrow the issuer's authority.
Machine credentials cannot create projects or delegate credentials.
issued = client.credentials.create(
label="CI inspection", capabilities=["read"],
expires_in_seconds=3600, idempotency_key=saved_issuance_key,
)
print(issued) # metadata plus [redacted], never the token
metadata = issued.to_dict()
secret = issued.export_secret() # intentional export to your protected credential store
# Do not print secret or put it in a recipe, target envelope, model prompt or source.
page = client.credentials.list(limit=25)
revoked = client.credentials.revoke(issued.credential["id"])
AsyncFiredrill uses the same namespace/signatures with await. Methods use native
Python transport; no CLI or Node process is launched. create uses the client's
selected scope unless both project_id and environment_id are explicitly supplied.
Capabilities must be a distinct nonempty subset of read, runs:write,
runs:execute, assessments:create, services:publish, services:create,
services:invoke, and services:control; none is silently added. Lifetime is
60–7,776,000 seconds, default 2,592,000. WorkOS-member-owned keys survive normal
interactive sign-out, but remain limited by expiry, revocation and the owner's
current membership and permissions. Development bootstrap-derived keys remain
parent-bound. Retained expiresAt is authoritative, not proof of current access.
Creation requires a caller-saved visible-ASCII operation key (1–200 characters,
no spaces). After an uncertain response, use the exact key and unchanged input;
the service can recover the same secret without creating a second credential.
No automatic retry occurs. Replay after revocation/expiry does not resurrect it.
IssuedCredential redacts normal repr, str and to_dict; export_secret() is
the explicit secret path. Do not serialize private internals. Keep the returned
metadata ID for revocation rather than using the secret as an ID.
list(cursor=None, limit=50) returns metadata only; follow nextCursor unchanged
until null. revoke(credential_id) is an exact-ID idempotent operation; repeat that
same revocation after uncertain acknowledgement, never issue a replacement as an
implicit recovery. These methods keep no secret-bearing credential journal.
Retained inspection and explicit reassessment
Both sync and native async clients expose:
list_batches(filters={...}) and list_runs(batch_id, filters={...}) use the
same wire-named filters: q (literal case-insensitive name/ID prefix),
createdAfter / createdBefore (inclusive/exclusive UTC submission bounds,
year 0001–9999 and at most three fractional digits), execution, origin,
scenarioId, configurationId, and issue. Issue values are incomplete,
exact_violation, assessment_unavailable, and below_level. The latter
requires dimensionId and numeric minimumLevel 0–4. Unknown/N/A/missing
assessments are not zero. Conditions match the same run, not unrelated rows.
Clear the cursor when changing filters; otherwise reuse it unchanged.
page = client.list_batches(filters={"origin": "ci", "issue": "incomplete"})
low = client.list_runs(batch_id, filters={"issue": "below_level", "dimensionId": "answer", "minimumLevel": 3})
New simulations default to immutable client-declared sdk origin. In ordinary
CI, use simulate(recipe, target, submission_origin="ci") (also native async),
or set FIREDRILL_SUBMISSION_ORIGIN=ci. Only with that resolved origin and
GITHUB_ACTIONS=true does the SDK automatically record GitHub server, repository,
run ID, attempt, commit, ref, workflow, job and event metadata. Optional
FIREDRILL_CI_PR_NUMBER records the PR number; an empty value is omitted. It reads
no git commands, event files or environment dumps. Malformed advertised metadata
is rejected before new admission.
Alternatively pass ci_context matching exported CiContext and
CI_CONTEXT_VERSION; this requires the ci origin. The first local create
journal freezes the declaration. run(batch_id, target, ci_context=...) and
resume(operation_id, target, ci_context=...) preserve the original batch's
submission; an explicit different context is rejected. Omit the context to
recover without deriving a replacement from the current job. Historical records
without metadata remain unknown. Context is caller-declared provenance, not
execution-host attestation, and does not grant access.
Set FIREDRILL_CI_REPORT=github_summary for an optional compact job summary when
simulate, run or resume returns. Native sync and async paths support this
without Node. The summary contains retained execution/assessment counts, the
separate gate and authenticated report/original workflow links—not inputs,
outputs, names or credentials. The current reporting job is validated
independently; it can report a batch submitted by an earlier workflow. Only the
existing regular GITHUB_STEP_SUMMARY file is used, with a 16 KiB append and
1 MiB total limit; no fallback file is created. result.ci_report
(CiReportReceipt) says written or unavailable with a failure code. An
uncertain write is not retried and never changes simulation or gate state.
result.to_dict() and batch_receipt(result) retain it as ciReport. Ordinary
result/report/summary inspection never writes a job summary.
list_batches(cursor=None, limit=25, filters=None),summary(batch_id)andcompare(baseline_batch_id, candidate_batch_id).compare_pairs(baseline_batch_id, candidate_batch_id, cursor=None, limit=25)pages all paired cases with exact run references/differences; useawaitwithAsyncFiredrill. Omit cursor first, then follow opaquenextCursor(1–512 characters) unchanged until null. OnFD_COMPARISON_PAGE_CHANGED, restart the first page. Summary samples are not the complete paired-case list.list_scenarios(project_id=None, environment_id=None, q=None, cursor=None, limit=100)for project-owned definitions, with nullablelastExecutionin the selected environment. Unused revisions stay visible.scenarios()is a first-page alias.scenario(scenario_id, revision, project_id=None)andevent(run_id, event_id)for exact retained resources. These additive methods are source/compile-checked here; endpoint qualification belongs to the coordinator receipt.assessments(run_id)andassessment(run_id, assessment_id)for retained history.reassess(run_id, idempotency_key=saved_key, rubric=None)as a separate explicit evaluator action. None reuses the retained rubric; it never invents one.
Read methods return the service's wire dictionaries and never run targets, request evaluation or recalculate gates. Reassessment uses frozen evidence, may consume platform evaluator budget and never reruns the customer's target. Customer target-model costs remain separate. A lost reassessment response requires the same saved key and unchanged rubric, not a new action. This slice qualified missing-rubric rejection and read-only history without spending evaluator budget.
Follow nextCursor until null, even if a page contains fewer than the requested
limit. SDK result reads automatically fall back to the compact result when the
full aggregate is too large; continue with its pagination cursor and never create
another execution to repair a large report. Large exact-check operands may be omitted: null with
omittedValues is not an observed null. The metadata retains byte count/digest
and run-output path or scenario check reference. Ordinal distributions and paired
observations are not calibrated probabilities, causal claims or automatic winners.
Read reports without filling your coding agent's context
Opt in to a compact execution receipt rather than printing every output and the whole recipe:
import json
from firedrill import batch_receipt
result = client.simulate(recipe, target)
print(json.dumps(batch_receipt(result)))
After asynchronous execution, use the same pure batch_receipt(result) helper
without await. It preserves batch identity, actual execution counts, assessment
counts when known, gate, report URL and the real recovery operationId when
present. At most ten small run references are included; sample.complete says
whether they cover the population. This is not a run page or success verdict,
and no cursor is invented. Use its reads paths or the inspection methods below
for the full retained population and evidence. Unknown whole-population assessment
counts remain None unless the existing result covers every run. No request,
evaluation or execution occurs, and the original SDK result remains unchanged.
Use inspect_batch(batch_id) and inspect_report(run_id, limit=10) for operator
views. Large values become references with their source, JSON Pointer, byte count
and digest. Existing batch() and report() keep their original response shapes.
Operator report event pages accept limit 1–20, cursor and the separate
check_cursor; follow each returned cursor unchanged.
Both sync and async report / inspect_report accept flat optional
event_kind (exact, case-sensitive, 1–120 characters) and event_producer
(literal, case-sensitive prefix, 1–200 characters). Choose values from retained
events; no trimming, case folding or search-expression parsing occurs. These
filters select activity only, not checks, scores or gates. Invalid controls or
Unicode are rejected before a request.
page = client.report(run_id, event_kind="runner.started", limit=25)
if page["nextCursor"] is not None:
next_page = client.report(run_id, event_kind="runner.started", limit=25,
cursor=page["nextCursor"])
print(next_page["events"])
Use await for the same calls on AsyncFiredrill. Raw pages retain their 1–100
limit (default 100); compact operator pages use 1–20 (default 10). Omit cursor
for the first page. Filtered continuation cursors are opaque, at most 1024
characters and bound by the service to the run, scope and exact filters. Keep the
same filter arguments on continuation; clear the cursor when changing them.
Unfiltered decimal cursors remain supported. Never invent or decode a filtered
cursor. Additive activity metadata retains wire names eventKind and
eventProducer in filters, plus filtered and the exclusive afterSequence
anchor; exported ActivityPageMetadata / ActivityFilters describe that wire
shape. Operator views retain it in document["value"]["activity"]. Sequence
gaps from filtering are not missing evidence. check_cursor still pages checks
independently.
document = client.inspect_report(run_id)
print(document["value"])
reference = next((item for item in document["references"] if item["source"] == "output"), None)
if reference is not None:
part = client.read_value(
run_id, source="output", pointer=reference["pointer"],
sha256=reference["sha256"], limit=4096,
)
print(part["text"]) # A bounded JSON fragment, not necessarily complete JSON.
# Fetch another chunk only if the investigation needs it.
if part["nextOffset"] is not None:
next_part = client.read_value(
run_id, source="output", pointer=reference["pointer"],
offset=part["nextOffset"], sha256=part["sha256"], limit=4096,
)
print(next_part["text"])
AsyncFiredrill accepts the same arguments; await each method. read_value
supports output, input, scenario, configuration, target, assessment
and event, plus check_actual/check_expected with the exact check_id;
event requires its exact event_id, including literal service/sequence
separators. For example:
client.read_value(run_id, source="check_actual", check_id=check_id).
For an omitted check operand, omit sha256 on the first read: its canonical
assessment digest is a different representation. Continue with the returned
serialization digest. These reads select pinned data or frozen closure evidence,
not live Tool state. Missing observations return unavailable rather than null.
Read a recipe subtree
with read_batch_value(batch_id, pointer="/scenarios/0"). Pointers use JSON
Pointer escaping (~0 for ~, ~1 for /); empty selects the whole source.
Each call returns only one chunk: 8192 bytes by default, with a permitted limit
of 256–32768. Continue only with the returned nextOffset and sha256, keeping
the source, pointer and event/check ID unchanged. Offsets count UTF-8 bytes, not Python
characters. Parse JSON only after retrieving its complete serialization. Changed
or unavailable content produces an error, never a successful empty value. These
reads do not rerun your target, request evaluation or automatically fetch the
remainder. RetainedValueChunk, RetainedValueReference and OperatorDocument
are exported typed dictionaries; their response keys preserve wire camelCase.
Content availability and removal
Both clients expose client.retention. policy() reads the active new-resource
policy; metadata(kind=..., id=..., cursor=..., limit=...) returns
{"records": [...], "nextCursor": ...} with fixed noncontent facts only. Omit
filters you do not need and follow the opaque cursor unchanged. Existing history
is not assigned retroactive expiry. A list row with metadataOnly: True has no
scenario/name/recipe payload. Full expired, deleted or deletion-pending reads
raise a typed 410 instead of returning empty successful content.
Removal requires the freshly authorized human owner; CI/machine credentials do not gain deletion authority:
preview = client.retention.preview(targets=[{"kind": "batch", "id": batch_id}])
# Review resources, requiredCascade, survivingDefinitions and exact scope.
# Save this exact input and key before dispatch; this choice consents to the cascade.
removal = client.retention.remove(
targets=preview["targets"], graph_digest=preview["graphDigest"],
confirmation_digest=preview["confirmationDigest"],
cascade=preview["requiredCascade"], idempotency_key="remove-selected-batch-1",
)
print(client.retention.get(removal["id"]))
The async equivalent awaits the same methods and keyword arguments. Cascade is
mandatory; [] is an explicit empty choice. If dependencies changed, review a
new preview before confirming. No method retries deletion automatically. On an
uncertain response, inspect the known removal ID or explicitly resend only the
original saved commit/key. reconcile(removal_id) deliberately continues that
same intent with human-owner authority; it creates no replacement operation.
Pending/blocked is not success. livePayloadDeleted is not instant backup
erasure: backup retention is separate and restoration reapplies tombstones.
Optional service bindings may declare
"control": {"schemaVersion": SERVICE_CONTROL_VERSION, "operations": [...]}
for authored scenario-controller interventions. Those IDs must be distinct and
disjoint from target operations; malformed or overlapping choices fail before
admission. Declared service sequence steps use control authority separately.
The target's context.tools, exported grants and callable operations retain only
its target operation set. This is authored synthetic behavior, not a claim that
every external dependency or fault can be controlled.
For a privately published build that actually declares both operations, a binding and ordered service-step fragment can look like:
binding = {
"alias": "dependency", "buildId": build["id"], "seed": "31", "operations": ["lookup"],
"control": {"schemaVersion": SERVICE_CONTROL_VERSION, "operations": ["set_availability"]},
}
intervention = {
"id": "outage", "kind": "service", "service": "dependency",
"operationId": "set_availability", "arguments": {"available": False},
}
Place the binding in recipe["services"] and the intervention between real
target steps in the scenario sequence. End it with an explicit later control
step or supported service-only restore. Controller and target observations are
separate evidence; an outage is not automatically an agent failure. This does
not intercept arbitrary networks or extend target connection authority.
Advance a managed Tool clock during a live simulation
For a new run, explicitly declare the binding's control as
{"schemaVersion": SERVICE_CONTROL_CLOCK_VERSION, "operations": [], "clock": {"advance": True}}.
Import SERVICE_CONTROL_CLOCK_VERSION from firedrill; its value is
2026-10-15-service-control.2. Operation-only version 1 remains supported and
does not authorize live clock changes. The target receives only its separate
ordinary operation grants.
controls = client.interventions.controls(run_id)
tool = next(item for item in controls["bindings"] if item["alias"] == "dependency")
if (not tool["interventionAvailable"] or not tool.get("clockAdvance")
or tool["instanceId"] is None or tool["epoch"] is None
or tool.get("virtualTimeUs") is None):
raise RuntimeError("Clock control is unavailable")
input = {
"action": "clock", "service": tool["alias"], "instanceId": tool["instanceId"],
"epoch": tool["epoch"], "toUs": tool["virtualTimeUs"] + 60_000_000,
"maxEvents": 100, "reason": "Observe one minute of managed Tool time",
}
# Save this exact input and key before sending; do not recompute after uncertainty.
receipt = client.interventions.apply(run_id, input, operation_key="dependency-clock-1")
print(receipt["intervention"]["result"])
The async client exposes the same methods with await. toUs is an absolute
microsecond timestamp; maxEvents is required, 1–1000. Inspect the result's
reachedUs, scheduledEventsProcessed, failures and stoppedEarly rather than
assuming that recorded completion means the requested time was reached.
An exact-key replay returns the original receipt; any further bounded advancement
is a new deliberate action. Backwards advancement is rejected. This never
advances your agent's clock, extends wall-clock leases, reruns the target, or
rewinds its memory. The run and Tool history record operator attribution.
Manual Tool clocks and run-owned interventions both reject a backward destination
with FD_SERVICE_CLOCK_REWIND_UNSUPPORTED before clock movement or event dispatch.
That definite rejection does not fence the Tool as uncertain. Read its current
clock before submitting a corrected destination with a new operation key.
Optional built-in Tools
For optional manual use, start one or many named independent instances with the authenticated, scoped client. This path does not start your agent:
release = client.services.catalog_release("gmail")
setup = client.services.start({
"tools": [{
"alias": "mailbox",
"source": {"kind": "catalog", "catalogId": release["id"],
"releaseDigest": release["releaseDigest"]},
"data": {"kind": "starter"}, # or explicit empty state
"operations": [operation["id"] for operation in release["operations"]],
"seed": "0", "ttlSeconds": 1800,
}],
})
print(setup["operationKey"], setup["status"], setup["items"])
# After partial completion, recover the same journaled intent:
recovered = client.services.resume_start(setup["operationKey"])
Review the declared operations; add distinct aliases for additional Tools or
copies. Saved data uses source:{"kind":"build","buildId":...} with
data:{"kind":"saved","datasetId":...}. Discover exact selections with
catalog(), builds(), build(id), datasets(build_id=...) and
dataset(id, include_records=True). All first-start selections are checked
before allocation, including exact dataset compatibility. Empty means empty;
starter records are an explicit catalog choice.
The client generates a durable private operation key unless you supply
operation_key. wait_timeout_ms bounds acquisition following; it does not
change instance lifetime. Partial success keeps its instances; recovery uses
the retained build, not a newer catalog release, and never replaces an ended
instance. on_progress is advisory; notification errors cannot interrupt setup.
Ready receipts contain token-free endpoints, not automatically issued grants.
Create connection credentials explicitly and deliver them privately.
item["instanceObservation"] uses
schemaVersion:"2026-10-16-tool-start-observation.1", state and
lastConfirmedAt. Only confirmed follows a fresh authorized instance GET;
cached create responses are not current availability. A failed read leaves
any retained instance explicitly unconfirmed/last-observed, with null time for
historical unknown confirmation. Access hints are absent for unconfirmed or
inactive instances. Authorization rejection stops the compound operation rather
than continuing allocations or returning cached instance data. Preserve its
original key/private journal; prior confirmed progress remains historical, not
proof of continuing access or availability. Sync and async share this behavior.
Use instances(lifecycle="active") or instances(lifecycle="history") for
newest-first pages and follow their opaque cursor. history(instance_id)
continues to expose retained lifecycle events after termination. Existing
snapshot/reset/rewind/clock/extend/pause/resume/destroy methods retain explicit
epoch/key controls. Async clients use the same APIs with await; no Node process
or event-loop wrapping is required. Tool-free simulations remain valid, and
recipe-bound Tools allocate independently for each case.
Manual start plans have no fixed instance-count quota. The normalized plan has a 4 MiB serialized boundary; creation remains sequential under actual service capacity. Response/private-journal byte bounds remain. Preserve the original key and retained service receipts after a partial or uncertain setup. The CLI keeps its smaller 256 KiB selected-file/interactive-plan input bound.
services.rewind(instance_id, snapshot_id, epoch=current_epoch, idempotency_key=saved_key) aliases the manual Tool reset operation. It
restores the snapshot's modeled state, virtual clock, pending work and random
generator state, increments epoch, fences old grants and preserves history.
Use the same exact input/key after an uncertain response; changing alias is not
a new restore. Neither operation resets customer agent memory, application
databases, wall clocks or external providers. Run-owned Tools retain their own
run/sequence lifecycle.
For simulation definitions without manual allocation, acquire a build/dataset:
Inspect the qualified catalog before selecting a controlled dependency:
release = client.services.catalog_release("gmail")
acquired = client.services.acquire(
release["id"], release_digest=release["releaseDigest"],
initial_data="starter", idempotency_key="acquire-mailbox-1",
)
if acquired["phase"] != "ready":
print(acquired["id"], acquired["phase"], acquired["error"])
print(client.services.acquisition_by_key("acquire-mailbox-1"))
else:
print(acquired["buildId"], acquired["datasetId"])
Save that intended key/input before sending. Choose initial_data="empty" when
you want no starter records. catalog() lists metadata; acquisition(id) and
acquisition_by_key(key) inspect the original scoped receipt. Async clients offer
the same methods with await, without Node or a subprocess.
Acquisition creates project-owned immutable build/dataset IDs, not an instance,
credential or target execution. Once ready, pin the IDs in your existing
recipe's services binding with your chosen alias, seed and explicit operations;
normal simulation preparation allocates independent instances. Manual exploration
can use services.create instead. Starter datasets retain their declared modeled
time. A pending, failed or uncertain acquisition is not a ready Tool; inspect the
same key rather than silently selecting another release or repeating under a
replacement key.
Catalog releases expose a bounded semantic API/MCP subset and read-only record view, not complete vendor REST APIs or vendor application clones. Private service authoring and tool-free simulations remain equally valid.
Optional service authoring and lifecycle
Both clients expose client.services. Sync calls return the retained wire
dictionary; async calls are awaited, with network and selected-file I/O outside
the event loop. All compilation, data/state, permissions and history remain in
Firedrill. There is no Node dependency or local service runtime in the package.
services = Firedrill().services
build = services.publish_directory(
"./my-tool", manifest_path="manifest.json", entry_point="service.ts",
files=["service.ts", "formatting.ts"], view_path="view.json",
idempotency_key=saved_build_key,
)
dataset = services.seed(build["id"], records, idempotency_key=saved_dataset_key)
instance = services.create(
build["id"], dataset_id=dataset["id"], seed="42", actor_operations=["read"],
idempotency_key=saved_create_key,
)
response = services.invoke(
instance["id"], "read", {}, epoch=instance["epoch"],
idempotency_key=saved_call_key,
)
view_path is optional, explicit data-only JSON, not customer JavaScript. Only
selected source files are read: 1–128 visible relative .ts/.js/.mjs files,
at most 1 MiB each and 4 MiB total, including the entrypoint. The manifest is at
most 1 MiB, the view declaration 64 KiB. Traversal, hidden path segments, symbolic
links, hard links, nonregular files and invalid UTF-8 are rejected. Whole-directory
upload is never implicit. publish(manifest=..., entry_point=..., files=[{"path": ..., "content": ...}], view=..., idempotency_key=...) accepts explicit source text
instead. The service validates the canonical manifest/view schema and compiles it.
publish(..., dataset_policy=policy) and
publish_directory(..., dataset_policy=policy) also accept explicit finite JSON
containing schemaVersion: "2026-10-15-dataset-policy.1" and declared
unique/reference/count constraints. The service validates the canonical
contract; use client.schema("dataset-policy") to discover it. No Node process
or local engine is needed. Policy is pinned to the published build, separate
from its namespace JSON Schemas; relationships are never inferred from IDs.
seed(build_id, records, metadata=metadata, idempotency_key=saved_key) accepts
optional schemaVersion: "2026-10-15-dataset-metadata.1" metadata. Its optional
provenance declares kind (authored, generated, imported) and sensitive;
imported data requires authorizedImport: True, a caller assertion rather than
verified consent. Optional mappings pin each target dataset's exact ID/version
and namespace, using literal JSON Pointers. Read
client.schema("dataset-metadata") for the full shape. Keep input/key unchanged
after uncertain delivery. Both sync and async clients preserve these fields.
Retained build details include datasetPolicy and datasetPolicyDigest; lists
contain only its digest/constraint count. Dataset dictionaries retain metadata,
validation and validationStatus, distinguishing schema/domain/mapping checks
from caller-declared provenance. Missing or null older fields mean not recorded,
not clean or synthetic data. A successful validation receipt explicitly does
not establish real-world fidelity; no provenance is invented when omitted.
Additional methods (also native async):
get(instance_id),list(cursor=None, limit=100)andhistory(instance_id, cursor=None, limit=100)inspect retained resources/events.snapshot(instance_id, epoch=..., idempotency_key=...)returns the snapshot ID.reset(instance_id, snapshot_id, epoch=..., idempotency_key=...)restores this instance's snapshot and returns its advanced epoch.clock(instance_id, epoch=..., to_us=..., max_events=None, idempotency_key=...)explicitly advances virtual time with an optional bounded event count.pause,resumeanddestroytake the instance ID and explicitepochandidempotency_key. Destroy stops use while retaining state and history.extend(instance_id, epoch=..., expected_expires_at=..., expires_at=..., idempotency_key=...)changes a manual Tool's real-world expiry only. Read the current epoch and exactexpiresAtfirst; both timestamps must be canonical UTC ISO strings including milliseconds. Persist the observed/new timestamps and key before sending and reuse them unchanged after an uncertain response. The new deadline must be later and within the service's configured maximum. This does not advance simulated time, resume paused compute or renew connection credentials; expired and run-managed Tools cannot be extended this way.
seed(build_id, [], ...) is intentionally empty. Omitting dataset_id creates
no seed records. The explicit decimal seed and actor_operations are required;
no sample data or broad permissions are inferred. Use selected authored operations
to read or mutate domain state; get(id)["runtime"] is an observed runtime summary,
not an unrestricted state database. history is the service lifecycle/evidence
page, not a fabricated client log. Follow returned nextCursor until null.
Mutations need a caller-saved stable key and exact input. The client never retries
automatically or advances an epoch after failure. After uncertain delivery, inspect
get/history and replay only that same key/input. A new key means a new action.
These explicit controls do not maintain an automatic service-action journal;
simulation journals remain separate. Engineering bounds protect each operation.
Native source-file guards are qualified on POSIX, not Windows.
Scoped HTTP, MCP and UI instance connections
client.services exposes create_connection(instance_id, *, epoch, audience, purpose, allowed_operations, ttl_seconds, idempotency_key),
connections(instance_id, cursor=None, limit=100) and
revoke_connection(instance_id, connection_id, idempotency_key=...).
Use audience="http", "mcp" or "ui" and an explicit purpose="manual" or "target"; purpose
records intent, not execution attestation. Grants can only narrow the existing
actor permissions and belong to the selected project/environment/instance/epoch.
Async service methods are awaited directly; no CLI/Node subprocess is used.
ConnectionGrant has safe repr() and metadata-only to_dict().
grant.reveal_token() explicitly accesses the one-time credential for protected
storage or authorized target configuration. grant.reveal_launch_url() explicitly
returns a UI launch URL whose fragment is also secret; normal representations omit
both. A UI grant requires an actual available declared view and its permitted
operation subset. Do not serialize internal attributes,
include credentials in evidence or pass the operator token to the target.
Identical-key replay returns metadata with token_delivery="not_repeated" and
no secret. Lost delivery requires inspecting/revoking that exact connection and
creating a new grant with a new saved key. An unrevoked grant can still be expired,
epoch-stale or parent-revoked. UI launch uses a one-time exchange for an isolated
browser session, not the operator credential. HTTP compatibility is limited to
the selected service's declared service.httpRoutes; it is not an unrestricted
provider proxy. The same seeded instance and operation permissions apply.
Provenance
The orchestration and adapters here are newly authored for the root-owned v0.1
wire contract. No legacy source was copied or adapted, and no legacy checkout is
part of this package's dependency graph. Runtime dependencies: Python standard
library plus certifi==2026.7.22 (Mozilla CA bundle, MPL-2.0). The dependency's
own licence/notices remain with its separately installed package; its certificate
data is not copied into this client wheel. Packaging build dependencies:
setuptools and wheel.
Artifact wire fields and recovery ordering follow the root-owned artifact contract
and sibling TypeScript attachments.ts; file I/O, transport, sync/async bridging and
journaling are native Python implementations, without a TypeScript/Node runtime.
Service wire fields, request bounds and selected-file policy follow the sibling
TypeScript services.ts; transport and descriptor-relative reads are new native
Python code. The qualification reuses the successor CLI's explicit room fixture;
that fixture is replaceable and is not bundled as product defaults.
Run-bound bindings and grant URLs follow ServiceBindingSchema and the control
coordinator's /v1/runs/:id/services response. Native context bridging, exact
response validation, in-memory-only secret delivery and recovery are newly authored
Python code. No private service-host/runtime implementation is embedded.
Sequence wire fields/recovery ordering follow the root-owned SequenceReply and
SequenceCompletion contracts and sibling sequences.ts. The native Python
step coordinator is newly authored; service-clock and restore semantics are not
copied into the client.
Credential wire/version and recovery semantics follow the canonical credential
contract and sibling credentials.ts. The native implementation does not embed
the service's credential derivation or authorization logic.
Prepare without execution
Both native clients expose preview and save_scenario; use await with
AsyncFiredrill. No Node/CLI bridge is involved. With client already
authenticated and scoped to the selected project/environment:
scenario = {"id": "route", "name": "Choose a supplied route", "input": {"destination": "library"}}
estimate = client.preview({
"name": "Route preview", "scenarios": [scenario],
"target": {"kind": "native", "name": "choose_route"}, "repetitions": 3,
})
print(estimate["admission"], estimate["expansion"], estimate["limitations"])
saved = client.save_scenario(scenario, expected_revision=None, idempotency_key="save-route-1")
retained = client.scenario(saved["id"], saved["revision"])
Preview accepts an inline recipe or selected JSON path, requires a target declaration but never a callable, and invokes no target/provider/Tool. It creates no batch or saved definition and reserves no capacity. Current admissibility is an estimate, not guaranteed admission; runner readiness, reset/capture setup, customer inference/runtime costs and remaining assessment budget are not proved.
Save requires runs:write, but no target binding. None explicitly creates a new
scenario ID; an intentional append supplies the current revision and a new key.
Stale bases conflict without overwriting history. Reconcile an uncertain response
with the same key and unchanged input/base revision. Reuse retained["scenario"]
inside a later recipe. Environment-restricted definition reads require an
authorized environment context, supplied from the client or environment_id.
The definition remains project-owned; saving does not admit a simulation.
Queue now, attach a runner later
queued = client.submit(recipe, idempotency_key="release-2026-10-16-1")
retained = client.resume_submission("release-2026-10-16-1")
# Later, when execution is intended, in the prepared customer process:
result = client.run(retained["batch"]["id"], target=my_application_adapter)
# AsyncFiredrill exposes the same methods with await.
Submit accepts an explicit JSON recipe or path, including a native target
descriptor without a callable. Any parallel non-isolated recipe must already
declare targetExecution before submission; queue-only admission cannot defer
that acknowledgement to a later runner. Command/HTTP targets attach without a callback. Python may
queue an isolated_node descriptor, but executing it requires the existing
TypeScript isolated adapter; Python does not silently select another boundary.
The service canonically validates before admission; no Node/CLI subprocess is
needed to submit. The client requires read and runs:write in its selected scope.
Save a unique printable ASCII key (1–200 characters, no whitespace) before
dispatch. Exact recipe, API/scope/identity and original origin/CI context are
durably saved under state_dir/submissions before admission. Repeating the key
checks the frozen intent. resume_submission(key, submission_origin=None, ci_context=None) reads that original intent; it never relabels it with today's
workflow environment. Explicit changed declarations conflict. Keep the private
state directory; absent journals do not cause replacement work. A key reused
under a different local scope conflicts rather than replacing its original intent.
SubmissionReceipt is a dictionary with schemaVersion, compact current batch
snapshot, submissionKey and scoped runner/inspection continuation. It omits
the frozen recipe; use scoped retained reads if those values are needed.
It confirms creation/reconciliation,
not that the batch remains queued or passed; every replay reads authorized cloud
state. SubmissionError.to_dict() adds the saved submission key and selection to
the typed error, without credentials. Submit never invokes/claims targets,
cancels, follows execution, requests assessment or writes CI summaries.
Cancelling an async submission wait may leave its bounded admission request in
flight: reconcile the same key, do not assume nothing was admitted. Submission
journals are rejected by execution recovery. After a runner begins, use its real
execution operation ID and journal to reconcile uncertain execution.
Observe the retained batch
observation = client.follow(batch_id, timeout=30, poll_interval=1)
print(observation.observation["state"], observation.result.gate)
# AsyncFiredrill: observation = await client.follow(batch_id, timeout=30)
Follow needs the original selected project/environment and reads context and compact results only. It does not claim work, invoke a callable, cancel targets, request assessment, change journals or write a CI summary. Another local/CI runner may own execution. Read endpoints may reconcile expired leases server-side.
timeout is seconds, default 300, maximum 3600; poll_interval is .25–30 seconds,
default 1. timeout=0 attempts one snapshot with a shared 30-second observation
ceiling across context and result reads, subject to each transport timeout.
Optional stop=threading.Event() or async task cancellation stops observation,
not execution. One bounded authenticated GET may finish in its daemon read thread
after observation stops; it cannot dispatch another request or update the saved
snapshot. There is no nested event loop or Node dependency.
FollowResult.to_dict() uses 2026-10-16-follow.1 with result and observation.
States are settled, timeout, interrupted, and read_error. Settled requires
terminal execution and whole-population assessment coverage with no pending
entries, not a passing result. Missing coverage stays unsettled. Other states
identify the last-confirmed snapshot, client-read completion timestamp and exact
batch/scope continuation. No confirmed snapshot raises FOLLOW_UNCONFIRMED;
access, identity, scope and retention rejections raise instead of serving cached
content. Retained result/gate are never replaced by observation status. Read
remaining runs with list_runs and their evidence with inspect_report.
Declare the population's purpose
recipe["populationPurpose"] = {
"schemaVersion": "2026-10-16-population-purpose.1",
"kind": "exploration",
"rationale": "Explore behavior across explicit input variations",
}
exploration, fixed_suite, held_out, and failure_selected are caller-declared
intent, not independent attestations. Native JSON recipe loading retains the
optional field. Preview/summary/comparison expose populationDisclosure, distinct
from matrix/sample materialization metadata. Missing metadata is not_recorded,
not an inferred purpose. Held-out does not prove unseen cases; failure-selected
scores do not estimate production incidence. Exported PopulationPurpose,
PopulationDisclosure and PopulationProvenance types only describe wire fields.
Preparation, cleanup and diagnostics
Both native clients accept on_progress on simulate, run and resume.
Fresh simulate reads the canonical non-reserving preview before admission and
notifies {"phase": "preexecution", "preexecution": preview, "operationId": id}.
The preview includes expansion, target boundary, configured resource ceilings and
separate customer costs. Notifications are advisory: async callbacks are scheduled
but not awaited, errors do not change execution, and callbacks are not an approval
barrier. Use preview separately for an explicit application review step.
Without on_progress, a bounded JSON progress.preexecutionSummary is written
to stderr before admission, leaving stdout untouched. It discloses selected
scope, counts, numeric bounds, isolation and separate
unknown customer costs, not inputs, Tool credentials, target URLs or command
arguments. A supplied callback replaces that presentation. Neither mode prompts
or waits for approval in CI.
Numeric assessmentPolicy limits come from the service; explicit null is
preserved, while an older service omitting that policy is reported as "unknown".
Recovery uses the original key; possibly dispatched admissions never depend on a
new preview. Replayed saved estimates use preexecution_retained.
Tool start progress and results include the existing policy read; its optional
resourceEnvelope describes enforced ceilings, not measured use or reservations.
Older-service absence is unknown, not unlimited capacity.
BatchResult.lifecycle, to_dict() and batch_receipt preserve the service's
optional lifecycle object, including additive fields. Evidence completeness is
limited to declared captures and run-owned Tool closure records. It does not
prove every intended observation exists; missing lifecycle is unknown. Native
command execution cleans up the owned process group even on normal completion.
Leader exit starts cleanup independently of inherited output pipes; draining
those pipes has its own short bound rather than consuming the target deadline.
Recovery of a saved running native, command or HTTP invocation fences further automatic
dispatch even after its service lease becomes terminal, because a terminal lease
does not establish that the customer-local or remote invocation stopped.
Escaped descendants remain unobserved. Windows child-only cleanup cannot confirm
group absence, so it reports unconfirmed cleanup instead of success.
Native wrapper exceptions can explicitly carry stop_unconfirmed = True
(stopUnconfirmed = True is also recognized) when observing the wrapper's
completion does not establish that its inner work stopped. Both sync/async
adapters preserve that flag through bounded exception cause/context/group
chains, record runner.target_stop, and persist the coordinator stop fence.
Ordinary exceptions and returned metrics do not imply this flag. Preserve the
original exception chain; no SDK can infer arbitrary detached work from a
wrapper that silently returns a normal value.
Both clients expose result_lifecycle(batch_id) for authorized metadata-only
BatchLifecycleRead access without reading expired recipe/output payloads.
result and follow preserve the original content-unavailable error and attach
this separately read receipt as error.retained_lifecycle when available;
to_dict() uses error.retainedLifecycle. Every read keeps the original selected
project/environment. Metadata expiry or a secondary read failure does not replace
the original error, fabricate a BatchResult, or return a stale follow snapshot.
Follow's observation deadline bounds this extra read too. When child metadata
has been removed, lifecycle.evidence.metadataComplete: false identifies that
unknown coverage; it is not a claim that evidence or cleanup was unnecessary.
The optional lifecycle.cleanup.targetStopsUnconfirmed counts retained native
or HTTP stop observations separately from command process-group cleanup.
FiredrillError.to_dict() uses 2026-10-17-client-diagnostics.1 with category,
workStarted, uncertain, retry and retained IDs. workStarted includes
operation side effects; possibly sent admissions remain "unknown", even without
a batch response. same_request means read-only retry safety; reconcile requires
the retained intent/key; never forbids automatic unchanged retries. These fields
do not perform a retry or authorize a new target invocation.
Private journal directory, lock, read, decode and atomic-save failures are typed
FiredrillError diagnostics (JOURNAL_IO_ERROR, JOURNAL_INVALID, or the precise
unsafe/ownership code). They retain the original execution operation_id, known
batch and single active-run handles. Positively unsent work reports
workStarted: false; a sent or unreadable intent remains "unknown", not a
claim that nothing happened. Preserve the original private journal and any
temporary file, repair local storage, and reconcile the same operation ID.
Submission and Tool-setup errors retain their original submission/setup keys,
not the journal's internal hashed filename. Catch FiredrillError to handle
these storage failures as well as transport/recovery errors. No automatic
replacement execution or deletion of partial recovery files is performed.
Metadata
Release files for firedrill-run 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| firedrill_run-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / firedrill_run-2.0.0-py3-none-any.whl
| Download URL | firedrill_run-2.0.0-py3-none-any.whl |
|---|---|
| Size | 158.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7a74e7b0d153c6e7689c1b4378a915f9c6eb1181a0c288cb49f9a7d7c4068b98
|
|
BLAKE2b-256 checksum How to use checksums |
89ebdb42cdb5981e144f0d7ddb9584424ca4a7fa92b6b4b6e1e0cf6ef186d236
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.6
|