Metadata-Version: 2.4
Name: firedrill-run
Version: 2.1.0
Summary: Native Python client for Firedrill
License: # Firedrill client use permission
        
        The new Firedrill client code in this artifact is proprietary, not open-source.
        You may install, run and copy the official client artifact to access Firedrill,
        including as a dependency of your application, a coding-agent integration or a
        CI job. You may include it with those integrations for that purpose, retaining
        this notice and all applicable third-party notices. You do not have to publish
        your application, tests, service source or data to use the client.
        
        This permission applies only to the client code included in the official
        artifact. It does not grant rights to private Firedrill service/engine code,
        access to another account's data, or use of Firedrill trademarks to imply
        endorsement. Access to the service still requires valid authorization. This
        permission sets no price or service availability commitment.
        
        Imported and third-party components retain their own licenses. This notice does
        not restrict rights already granted under those licenses, including licenses of
        earlier distributed Firedrill versions. Required component notices are included
        with the artifact.
        
        Except as required by applicable law or a separate written agreement, the client
        is provided as-is, without warranties or a guarantee of fitness for a particular
        purpose. Rights not expressly granted here remain reserved.
        
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: certifi==2026.7.22
Dynamic: license-file

# Firedrill Python client

For your first use, [authorize and select scope](#authorize-the-operator-and-choose-scope),
then [run your actual callable](#first-tool-free-run). 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. An explicitly selected recipe bound still applies. Inspect
the returned population and `preview` expansion before execution; generated
scenarios do not promise 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:

```python
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.

```sh
python -m pip install firedrill-run==2.1.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

```python
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):

```python
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:

```python
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

```python
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:

```python
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.

```python
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.

```json
{
  "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.

```python
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:

```json
"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:

```python
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:

```python
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.

```python
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.

```python
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:

```python
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.

```python
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:

```python
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.

```python
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.

```python
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.

Use the same CI exit convention as the CLI in your native runner:

```python
from firedrill import simulation_exit_code

result = client.simulate(recipe, agent, submission_origin="ci")
raise SystemExit(simulation_exit_code(result))
```

The helper returns `1` for a blocked/inconclusive gate, `5` for execution errors,
and `6` for interrupted/pending work. A zero exit without a configured gate means
execution completed, not that an answer was judged correct. The generated
workflow additionally reads authoritative results in its final reporting step,
so an SDK script returning zero cannot hide a failed gate. Preserve its
`FIREDRILL_CI_RESULT_DIRECTORY`; the SDK saves only immutable batch/scope
references there immediately after admission, before target claims.

`result.gate_counts` (`gateCounts` on the wire) covers the whole retained
population even when `result.runs` is only a compact first page. Compact receipts
use `gateCounts: None` when complete counts are unknown, never an invented zero.
Job summaries show actual passed/failed/inconclusive/not-gated counts and put
technical execution/assessment details in a collapsible section.

For native fractional semantic scores, use `gate.minimumScores`, for example
`{"quality": 3}`. `minimumLevels` remains for authored ordinal levels; missing
or unassessable required scores yield an inconclusive gate, not a rounded pass.

`report_github(batch_id, oidc_token, idempotency_key="saved-key")` is available
on sync and async clients to publish a retained batch through its verified
GitHub App connection, without invoking the target again. Prefer the generated
`firedrill ci init RECIPE.json --trigger pull_request --github-app` workflow
instead of handling short-lived workflow identity tokens yourself. The service
verifies repository access and workflow identity; a declared repository name
alone grants no publication authority. Tokens never enter command-target
processes, result data or summaries.

Use `list_batches(filters={"origin": "ci", "repository": "OWNER/REPOSITORY",
"workflowRunId": "RUN_ID"})` to find one workflow's retained results without
changing the selected project/environment. Clear the cursor when changing filters.
`github_connections(cursor=None, limit=25)` pages verified repository/workflow
connections in the selected scope. `github_report(batch_id)` reads an existing
publication receipt. Both have native async equivalents; neither read installs
the App, publishes a comment or runs your agent.

- `list_batches(cursor=None, limit=25, filters=None)`, `summary(batch_id)` and
  `compare(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; use `await` with
  `AsyncFiredrill`. Omit cursor first, then follow opaque `nextCursor` (1–512
  characters) unchanged until null. On `FD_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 nullable `lastExecution` in the selected environment. Unused
  revisions stay visible. `scenarios()` is a first-page alias.
- `scenario(scenario_id, revision, project_id=None)` and `event(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)` and `assessment(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:

```python
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.

```python
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.

```python
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:

```python
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:

```python
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.

```python
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:

```python
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:

```python
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.

```python
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)` and
  `history(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`, `resume` and `destroy` take the instance ID and explicit `epoch` and
  `idempotency_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 exact `expiresAt` first; 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:

```python
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

```python
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

```python
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

```python
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.
