Skip to content
BitExactproduct

How it works, for engineering and platform teams

A few lines in your agent. Evidence that verifies offline.

Follow one lending decision. BitExact records each step the agent takes, signs it and links it into a hash chain that shows any later change, on your own infrastructure. The auditor's open verifier checks it offline, without us. Then see how you deploy it.

Built for the record-keeping the EU AI Act asks of high-risk AI systems in Article 12, credit decisions among them, and for the model-risk reviews and adverse-action questions that ask the same thing: what did the system do, and can you prove it? See the compliance mapping.

Not a governance layer: it does not decide what your agents may do. It proves what they did.

Scroll on: on the left, your agent; on the right, the record it leaves.Scroll on: above, your agent; below, the record it leaves.

The BitExact inspector's verdict for run loan-APP-7731: Intact, sealed and signed.

A recorded run, as the BitExact inspector shows it. Key id 965628e3573ee594 is the fingerprint of the recorder public key the auditor's verifier pins, 675688ff3afbe65c…

Change one byte, and check again

In a copy of this run, step 4's score changed from 512 to 812. The open verifier, on that copy:

$ python bitexact_verifier.py … loan-APP-7731.tampered.bundle.jsonFAIL: step 4: data hash mismatch at path result.score - value tampered or corrupted$ echo $?2
Show command
$ python bitexact_verifier.py --trust-file trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd loan-APP-7731.tampered.bundle.json

See it step by step

An agent decides. Sometimes wrongly.

Your agent

$ python story_agent.py{  "applicant_id": "APP-7731",  "decision": "decline",  "reasons": [    "SCORE_BELOW_MIN"  ],  "explanation": "The credit score of 512 is below the 620 minimum.",  "received_at": 1794179527.6697593}$ echo $?0

The record

No record. If anyone asks why, there is nothing to show them.

  1. Your agent 68 lines of Python: 3 tools, a clock read, a model call and a decision.

    An agent, as you would write it.

  2. A strong applicant Income 148,000, debt-to-income 7.3%, 9 years employed.

    A strong applicant.

  3. Declined The credit score of 512 is below the 620 minimum.

    And sometimes it's wrong.

Add BitExact. Change nothing else.

Your agent, with BitExact

Python + OpenAI

"""A loan-decision agent: one model call, three tools, a clock read, a ruleand a judgment. The model is whatever OPENAI_BASE_URL points at."""import jsonimport timefrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI # The credit bureau's mirror, as of its last nightly sync.BUREAU_MIRROR = [    {"applicant_id": "APP-7690", "score": 702, "as_of": "2026-10-30"},    {"applicant_id": "APP-7713", "score": 512, "as_of": "2026-10-30"},    {"applicant_id": "APP-7790", "score": 688, "as_of": "2026-10-30"},]UNDERWRITER = ("You are an underwriter. Apply the policy exactly. Answer as: "               "DECISION: <APPROVE|DECLINE|REFER>. REASONS: <codes or none>. "               "<one sentence>")  @bitexact.record_tooldef fetch_applicant(applicant_id):    """The application, as the lending portal holds it."""    return {"applicant_id": applicant_id, "product": "personal-24",            "annual_income": 148000, "monthly_debt": 900,            "requested": 25000, "years_employed": 9}  @bitexact.record_tooldef pull_credit_report(applicant_id):    """The applicant's report from the bureau mirror, matched by number."""    number = int(applicant_id.split("-")[1])    return min(BUREAU_MIRROR, key=lambda report: abs(        int(report["applicant_id"].split("-")[1]) - number))  @bitexact.record_tooldef check_policy(product):    """The underwriting thresholds for a product."""    return {"product": product, "min_score": 620, "max_dti": 0.43,            "version": "4.2"}  def decide(client, applicant_id):    received_at = time.time()    applicant = fetch_applicant(applicant_id)    report = pull_credit_report(applicant_id)    policy = check_policy(applicant["product"])    dti = round(applicant["monthly_debt"] * 12 / applicant["annual_income"], 3)    reply = client.chat.completions.create(        model="underwriter-v1",        messages=[{"role": "system", "content": UNDERWRITER},                  {"role": "user", "content": json.dumps(                      {"application": applicant, "credit_report": report,                       "policy": policy, "dti": dti})}])    answer = reply.choices[0].message.content    codes = answer.split("REASONS:")[1].split(".")[0].strip()    return {"applicant_id": applicant_id,            "decision": answer.split("DECISION:")[1].split(".")[0].strip().lower(),            "reasons": [] if codes == "none" else codes.split(", "),            "explanation": answer.split("REASONS:")[1].split(".", 1)[1].strip(),            "received_at": received_at}  def record_outcome(r, result):    r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=760)).isoformat(),                      output_flags={"is_adverse_action": result["decision"] == "decline",                                    "is_automated_decision": True},                      reason_codes=result["reasons"] or None)  def main():    client = OpenAI()    with bitexact.run("loan-APP-7731", workflow="loan-adjudication",                      workflow_instance="APP-7731", environment="demo",                      principal="svc:underwriter") as r:        client = wrap_openai(client, r)        result = decide(client, "APP-7731")        record_outcome(r, result)    print(json.dumps(result, indent=2))    return result  if __name__ == "__main__":    main()

TypeScript

export BITEXACT_COLLECTOR=http://127.0.0.1:7378
export BITEXACT_COLLECTOR_SECRET_FILE=/path/to/intake.secret

import { run, recordTool, wrapFetch, fromEnv } from "@bitexact/sdk";

const tracked = wrapFetch(fetch);        // model calls through fetch
const plan = recordTool("plan", (goal: string) => [goal]);
await run("my-run", async () => plan("assess the application"));
await fromEnv()?.flush(10_000);          // before the process exits

The first-run snippet the inspector gives for TypeScript. The run in this story was recorded with the Python agent.

Go

export BITEXACT_COLLECTOR=http://127.0.0.1:7378
export BITEXACT_COLLECTOR_SECRET_FILE=/path/to/intake.secret

client := &http.Client{Transport: bitexact.WrapTransport(http.DefaultTransport)}
plan := bitexact.RecordTool("plan", func(ctx context.Context, goal string) ([]string, error) {
	return []string{goal}, nil
})
_, err := bitexact.Run(ctx, "my-run", bitexact.Options{},
	func(ctx context.Context) (any, error) {
		steps, err := plan(ctx, "assess the application")
		return steps, err
	})
if s := bitexact.FromEnv(); s != nil { s.Flush(10 * time.Second) }

The first-run snippet the inspector gives for Go. The run in this story was recorded with the Python agent.

Java

export BITEXACT_COLLECTOR=http://127.0.0.1:7378
export BITEXACT_COLLECTOR_SECRET_FILE=/path/to/intake.secret

ToolFunction<String, List<String>> plan =
    Bitexact.recordTool("plan", goal -> List.of(goal));
Bitexact.run("my-run", () -> plan.apply("assess the application"));
Bitexact.fromEnv().flush(Duration.ofSeconds(10));   // before exit

The first-run snippet the inspector gives for Java. The run in this story was recorded with the Python agent.

Any language, through the recorder

export OPENAI_BASE_URL=http://localhost:8787/v1
# send the header X-BitExact-Run-Id: my-run to group calls into one run

The first-run snippet the inspector gives for Any language, through the recorder. The run in this story was recorded with the Python agent.

18lines added0lines of the agent changed

The record

Nothing recorded yet. Run it, and the record starts here.

  1. Add BitExact Three imports, and a decorator on each of the 3 tools.

    Add BitExact.

  2. Wrap the run Everything inside becomes one record; the decision is committed with its reasons.

    One run around the agent.

  3. Nothing else changes 18 lines added; none of the 68 lines of the agent changed, checked when this page was built.

    Change nothing else.

How you deploy it

It runs inside your network. Nothing phones home.

The recorder, the store and the keys run on your infrastructure, and the license is checked offline. Your agents reach the record in one of three ways.

  1. In process

    The SDK

    Python, TypeScript, Go and Java. The few lines above, in the agent's own code. In Python, if a wrapped model client cannot reach its recording endpoint, the call fails over to your configured provider and the run declares the gap; the TypeScript, Go and Java SDKs never block a call on the recorder and count what they could not deliver.

  2. Many hosts

    A collector per host, one recorder

    The SDK hands each event to an in-process queue and returns, so the agent never waits on the network or the recorder. Each host's collector spools the events, encrypted, and forwards them over mutual TLS to a recorder on its own server. The recorder is the only process that holds the signing key, so the evidence never lives on the hosts it describes.

  3. Any language

    The recording proxy

    No code change: point an OpenAI- or Anthropic-compatible client's base URL at bitexact serve and send a run-id header. It records the wire, not your tools.

Go deeper How the recorder runs in your deployment

How the recorder runs in your deployment

In your process through the SDK, or as a recording proxy any OpenAI-compatible client points at. The commands, as the CLI prints them.

$ bitexact quickstart

BitExact quickstart

1) Zero-code - point your OpenAI/Anthropic client at the recorder
   export OPENAI_BASE_URL=http://localhost:8787/v1
   # Send a header  X-BitExact-Run-Id: <your-run-id>  on each request so calls
   # group into one run. Proxy-only runs capture http traffic and stay
   # UNSEALED until you close them:  bitexact seal <your-run-id>
   # or over HTTP, with the secret in the file BITEXACT_SEAL_SECRET_FILE names:
   #   curl -X POST -H "Authorization: Bearer <secret>" \
   #       http://localhost:8787/bitexact/runs/<your-run-id>/seal

2) Make it an exhibit (recommended) - wrap the run with the SDK
   # from your platform's release bundle (installing.md); the import is  import bitexact
   pip install --no-index --find-links wheels --require-hashes -r requirements.lock
   pip install --no-index --no-deps bitexact_sdk-*.whl

   import bitexact
   from bitexact.adapters import wrap_openai
   from openai import OpenAI

   client = OpenAI()
   with bitexact.run("my-run") as r:   # a run id records once: use a new one per run
       client = wrap_openai(client, r)

       @bitexact.record_tool               # wrap your real tools
       def get_balance(account_id): ...
       # ... run your agent as usual; the run seals on exit ...

3) See it verify
   bitexact view    # open the inspector, click the run, press Verify

No agent handy? Seed a real, verifiable sample run:  bitexact seed

$ bitexact serve --help

usage: bitexact serve [-h] [--host HOST] [--port PORT] [--tls-cert TLS_CERT]
                      [--tls-key TLS_KEY] [--tls-client-ca TLS_CLIENT_CA]

options:
  -h, --help            show this help message and exit
  --host HOST
  --port PORT
  --tls-cert TLS_CERT   TLS certificate for the listener
  --tls-key TLS_KEY     TLS private key
  --tls-client-ca TLS_CLIENT_CA
                        require client certificates signed by this CA (mTLS)

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached

$ bitexact seal --help

usage: bitexact seal [-h] run_id

positional arguments:
  run_id

options:
  -h, --help  show this help message and exit

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached
Go deeper Deploying it: recorder, collector, keys and license

Deploying it: recorder, collector, keys and license

A recorder key signs the evidence; the license is bound to it and checked offline; agents on many hosts ship to a collector. As the CLI prints them.

$ bitexact keygen --help

usage: bitexact keygen [-h] -o OUTPUT [--force]
                       [--suite {hybrid,ed25519,witness-ml-dsa-44}]
                       [--pq-alg {ML-DSA-65,ML-DSA-87}]

options:
  -h, --help            show this help message and exit
  -o, --output OUTPUT   where to write the ed25519 key; the post-quantum half,
                        its ML-DSA seed as 64 hex characters, goes to
                        <name>.pq.key
  --force               overwrite an existing key file
  --suite {hybrid,ed25519,witness-ml-dsa-44}
                        hybrid (default); ed25519 alone - classical-only, not
                        resistant to a quantum forger; or witness-ml-dsa-44, a
                        witness key that cosigns with C2SP ML-DSA-44
                        cosignatures
  --pq-alg {ML-DSA-65,ML-DSA-87}
                        the post-quantum parameter set (default
                        $BITEXACT_PQ_ALG, else ML-DSA-65). The key file holds
                        only the 32-byte seed, so the signer takes the set
                        from $BITEXACT_PQ_ALG too: set it to the same value
                        where the key signs

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached

$ bitexact license --help

usage: bitexact license [-h] {request,install,usage} ...

positional arguments:
  {request,install,usage}
    request             write a renewal request, signed by the recorder key
                        and carrying a TPM quote, to send to BitExact
    install             install the renewed license BitExact sent back over
                        the license BITEXACT_LICENSE names
    usage               count the production agent identities seen recording
                        in the last 90 days, beside the licensed band

options:
  -h, --help            show this help message and exit

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached

$ bitexact recorder --help

usage: bitexact recorder [-h] [--host HOST] [--port PORT]
                         [--tls-cert TLS_CERT] [--tls-key TLS_KEY]
                         [--tls-client-ca TLS_CLIENT_CA] [--tls-crl TLS_CRL]
                         [--insecure-loopback] [--health-port PORT]
                         [--health-host HEALTH_HOST]

options:
  -h, --help            show this help message and exit
  --host HOST
  --port PORT
  --tls-cert TLS_CERT   the recorder's TLS certificate
  --tls-key TLS_KEY     its private key
  --tls-client-ca TLS_CLIENT_CA
                        the CA whose client certificates identify collectors
                        (required)
  --tls-crl TLS_CRL     the CA's revocation list (`bitexact pki crl`): a
                        revoked host is refused, even mid-session
  --insecure-loopback   development only: plaintext on loopback
  --health-port PORT    serve /livez, /readyz and /metrics over plain HTTP on
                        this port, for orchestrator probes and Prometheus
  --health-host HEALTH_HOST
                        address the health port binds (default 0.0.0.0, so the
                        kubelet can reach it)

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached

$ bitexact collector --help

usage: bitexact collector [-h] --spool SPOOL --recorder HOST:PORT
                          [--tls-cert TLS_CERT] [--tls-key TLS_KEY]
                          [--tls-ca TLS_CA] [--intake-port INTAKE_PORT]
                          [--intake-host HOST] [--intake-exposed]
                          [--max-spool-bytes MAX_SPOOL_BYTES]
                          [--intake-secret-file FILE]
                          [--drain-seconds DRAIN_S] [--insecure-loopback]

options:
  -h, --help            show this help message and exit
  --spool SPOOL         directory for the encrypted spool, host key and intake
                        secret
  --recorder HOST:PORT
  --tls-cert TLS_CERT   this host's client certificate
  --tls-key TLS_KEY     its private key
  --tls-ca TLS_CA       the CA that issued the recorder's certificate
  --intake-port INTAKE_PORT
  --intake-host HOST    the address the intake listens on (default 127.0.0.1).
                        Another address is for a collector in a container,
                        whose loopback agents outside it cannot reach: publish
                        the port to the host's loopback only, and say so with
                        --intake-exposed
  --intake-exposed      the --intake-host address is a container's, its port
                        published to the host's loopback alone. Without it a
                        non-loopback intake refuses to start
  --max-spool-bytes MAX_SPOOL_BYTES
  --intake-secret-file FILE
                        the host secret agents authenticate with, placed by
                        you where agents running as other users are granted
                        read access (at least 32 characters); default: created
                        owner-only in the spool
  --drain-seconds DRAIN_S
                        on SIGTERM, keep forwarding for up to this long until
                        the spool is empty (default 30)
  --insecure-loopback   development only: plaintext to a loopback recorder

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached

$ bitexact view --help

usage: bitexact view [-h] [--host HOST] [--port PORT] [--tls-cert TLS_CERT]
                     [--tls-key TLS_KEY]

options:
  -h, --help           show this help message and exit
  --host HOST
  --port PORT
  --tls-cert TLS_CERT  TLS certificate for the listener
  --tls-key TLS_KEY    TLS private key

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached

Every call becomes a step.

Your agent

"""A loan-decision agent: one model call, three tools, a clock read, a ruleand a judgment. The model is whatever OPENAI_BASE_URL points at."""import jsonimport timefrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI # The credit bureau's mirror, as of its last nightly sync.BUREAU_MIRROR = [    {"applicant_id": "APP-7690", "score": 702, "as_of": "2026-10-30"},    {"applicant_id": "APP-7713", "score": 512, "as_of": "2026-10-30"},    {"applicant_id": "APP-7790", "score": 688, "as_of": "2026-10-30"},]UNDERWRITER = ("You are an underwriter. Apply the policy exactly. Answer as: "               "DECISION: <APPROVE|DECLINE|REFER>. REASONS: <codes or none>. "               "<one sentence>")  @bitexact.record_tooldef fetch_applicant(applicant_id):    """The application, as the lending portal holds it."""    return {"applicant_id": applicant_id, "product": "personal-24",            "annual_income": 148000, "monthly_debt": 900,            "requested": 25000, "years_employed": 9}  @bitexact.record_tooldef pull_credit_report(applicant_id):    """The applicant's report from the bureau mirror, matched by number."""    number = int(applicant_id.split("-")[1])    return min(BUREAU_MIRROR, key=lambda report: abs(        int(report["applicant_id"].split("-")[1]) - number))  @bitexact.record_tooldef check_policy(product):    """The underwriting thresholds for a product."""    return {"product": product, "min_score": 620, "max_dti": 0.43,            "version": "4.2"}  def decide(client, applicant_id):    received_at = time.time()    applicant = fetch_applicant(applicant_id)    report = pull_credit_report(applicant_id)    policy = check_policy(applicant["product"])    dti = round(applicant["monthly_debt"] * 12 / applicant["annual_income"], 3)    reply = client.chat.completions.create(        model="underwriter-v1",        messages=[{"role": "system", "content": UNDERWRITER},                  {"role": "user", "content": json.dumps(                      {"application": applicant, "credit_report": report,                       "policy": policy, "dti": dti})}])    answer = reply.choices[0].message.content    codes = answer.split("REASONS:")[1].split(".")[0].strip()    return {"applicant_id": applicant_id,            "decision": answer.split("DECISION:")[1].split(".")[0].strip().lower(),            "reasons": [] if codes == "none" else codes.split(", "),            "explanation": answer.split("REASONS:")[1].split(".", 1)[1].strip(),            "received_at": received_at}  def record_outcome(r, result):    r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=760)).isoformat(),                      output_flags={"is_adverse_action": result["decision"] == "decline",                                    "is_automated_decision": True},                      reason_codes=result["reasons"] or None)  def main():    client = OpenAI()    with bitexact.run("loan-APP-7731", workflow="loan-adjudication",                      workflow_instance="APP-7731", environment="demo",                      principal="svc:underwriter") as r:        client = wrap_openai(client, r)        result = decide(client, "APP-7731")        record_outcome(r, result)    print(json.dumps(result, indent=2))    return result  if __name__ == "__main__":    main()
  1. 0run meta2656baadea77
  2. 1identityc6cd22404e1a
  3. 2time.time6c4f9c1a5db7captured 1794179527.8575594
  4. 3fetch_applicant54708d65e42d
  5. 4pull_credit_reportd89013fe7509
  6. 5check_policy14428fda7ad6
  7. 6model call264c3e7363a9
  8. 7decision recorda6a582ae5131
  9. 8run end0ad92adc015c

The record

The inspector's step timeline for loan-APP-7731
The inspector's verdict for loan-APP-7731
  1. Every call Recorded as it happens, each step hash-linked to the one before.

    Every call, recorded and hash-linked.

  2. The clock read time.time returned 1794179527.8575594; replay serves the same value.

    Captured, so it replays.

  3. The model's answer Request and response, as they crossed the wire.

    The model call, as it came back.

  4. Sealed and signed Sealed: 9 steps. Signed: key id 965628e3573ee594, the fingerprint of the recorder public key starting 675688ff3afbe65c, the key the auditor's verifier pins.

    Sealed, and signed by your recorder key.

Go deeper How replay works

How replay works

Replay re-runs your agent against the recorded answers, offline. Any divergence halts at the exact step with a diff.

$ bitexact regress --help

usage: bitexact regress [-h] [--workflow WORKFLOW] [--version REGRESS_VERSION]
                        [--environment REGRESS_ENVIRONMENT] --entry ENTRY
                        [--intended INTENDED] [--accept-scrubbed]
                        [--git-sha GIT_SHA] [--label LABEL] [--sign]
                        [--key KEY] [-o OUTPUT]
                        [run_ids ...]

positional arguments:
  run_ids               explicit run ids, or select by workflow below

options:
  -h, --help            show this help message and exit
  --workflow WORKFLOW   regress a whole workflow's corpus instead of explicit
                        run ids (verifiable version-drift)
  --version REGRESS_VERSION
                        restrict --workflow to this workflow_version
  --environment REGRESS_ENVIRONMENT
                        restrict --workflow to this environment
  --entry ENTRY         MODULE:FUNCTION taking the active run
  --intended INTENDED   KIND:PATH change declarations; flips confined to them
                        are intended, not divergence (repeatable)
  --accept-scrubbed     serve a value the capture policy scrubbed, as scrubbed
                        (default: the step is withheld)
  --git-sha GIT_SHA     code identity for the report
  --label LABEL         human label for the report
  --sign                sign the suite report with --key
  --key KEY             path to an ed25519 private key file
  -o, --output OUTPUT

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached
  1 also when the gate does not pass (a finding, not a failed verification)

$ bitexact verify --help

usage: bitexact verify [-h] [--provenance] [--expect-recorder-key HEX]
                       [--expect-recorder-pq-key HEX|FILE] [--trust-file FILE]
                       [--require-pq] [--allow-gaps CAUSES]
                       [--open STEP:PATH | --open-withheld STEP]
                       [--candidate-file FILE] [--by BY]
                       run_id

positional arguments:
  run_id

options:
  -h, --help            show this help message and exit
  --provenance          also cross-check the whole fork ancestry against the
                        source manifests
  --expect-recorder-key HEX
                        pin this recorder public key in place of the license's
                        recorder_keys: every checkpoint must be signed by it
                        and a sealed run must carry one, or verification
                        FAILS. With --provenance every run in the fork lineage
                        must carry a head-covering checkpoint signed by it -
                        without it a fork's descent is only structural (the
                        source's public hash), forgeable by a store writer
  --expect-recorder-pq-key HEX|FILE
                        the post-quantum (ML-DSA) half of the key --expect-
                        recorder-key pins: every checkpoint and worm anchor
                        that key signed must carry a post-quantum signature by
                        it, so a forger holding only the Ed25519 key fails
  --trust-file FILE     the open verifier's trust file, read the same way:
                        every checkpoint must be signed by one of its
                        recorder_keys, and an entry's pq_public_key demands
                        that post-quantum half on everything the key signed.
                        One Ed25519 key listed with two PQ keys allows a
                        rotation between them. In place of the license's
                        recorder_keys and of --expect-recorder-key
  --require-pq          fail a run any of whose checkpoints or worm anchors
                        carries no post-quantum signature (it pins no key: a
                        forger who recovers the Ed25519 key can add an ML-DSA
                        key of their own - pin it to refuse that)
  --allow-gaps CAUSES   exit 0 rather than 3 when every capture gap the run
                        declares has one of these causes (comma-separated) or
                        with 'all'; the gaps are still listed
  --open STEP:PATH      once the run verifies, test --candidate-file (the
                        value, as JSON) against this hash-only field with the
                        capture key: prints matches or does not match (exit
                        2). Recorded on the run's custody chain first -
                        testing a candidate is guessing one
  --open-withheld STEP  once the run verifies, test --candidate-file's exact
                        bytes against the content the gap at this step
                        withheld, with the capture key; recorded on the
                        custody chain like --open
  --candidate-file FILE
                        the candidate --open or --open-withheld tests
  --by BY               with --open or --open-withheld, the principal testing
                        the candidate, recorded on the custody chain

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached
  2  FAIL - the evidence does not verify, or a check it requires cannot run
     on this install (a library is missing)
  3  verified, with declared capture gaps - an intact record of an
     incomplete capture
  3 unless --allow-gaps accepts every declared cause; with --open or --open-
  withheld, 0 when the candidate matches and 2 when it does not

Find where it went wrong.

Your agent

"""A loan-decision agent: one model call, three tools, a clock read, a ruleand a judgment. The model is whatever OPENAI_BASE_URL points at."""import jsonimport timefrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI # The credit bureau's mirror, as of its last nightly sync.BUREAU_MIRROR = [    {"applicant_id": "APP-7690", "score": 702, "as_of": "2026-10-30"},    {"applicant_id": "APP-7713", "score": 512, "as_of": "2026-10-30"},    {"applicant_id": "APP-7790", "score": 688, "as_of": "2026-10-30"},]UNDERWRITER = ("You are an underwriter. Apply the policy exactly. Answer as: "               "DECISION: <APPROVE|DECLINE|REFER>. REASONS: <codes or none>. "               "<one sentence>")  @bitexact.record_tooldef fetch_applicant(applicant_id):    """The application, as the lending portal holds it."""    return {"applicant_id": applicant_id, "product": "personal-24",            "annual_income": 148000, "monthly_debt": 900,            "requested": 25000, "years_employed": 9}  @bitexact.record_tooldef pull_credit_report(applicant_id):    """The applicant's report from the bureau mirror, matched by number."""    number = int(applicant_id.split("-")[1])    return min(BUREAU_MIRROR, key=lambda report: abs(        int(report["applicant_id"].split("-")[1]) - number))  @bitexact.record_tooldef check_policy(product):    """The underwriting thresholds for a product."""    return {"product": product, "min_score": 620, "max_dti": 0.43,            "version": "4.2"}  def decide(client, applicant_id):    received_at = time.time()    applicant = fetch_applicant(applicant_id)    report = pull_credit_report(applicant_id)    policy = check_policy(applicant["product"])    dti = round(applicant["monthly_debt"] * 12 / applicant["annual_income"], 3)    reply = client.chat.completions.create(        model="underwriter-v1",        messages=[{"role": "system", "content": UNDERWRITER},                  {"role": "user", "content": json.dumps(                      {"application": applicant, "credit_report": report,                       "policy": policy, "dti": dti})}])    answer = reply.choices[0].message.content    codes = answer.split("REASONS:")[1].split(".")[0].strip()    return {"applicant_id": applicant_id,            "decision": answer.split("DECISION:")[1].split(".")[0].strip().lower(),            "reasons": [] if codes == "none" else codes.split(", "),            "explanation": answer.split("REASONS:")[1].split(".", 1)[1].strip(),            "received_at": received_at}  def record_outcome(r, result):    r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=760)).isoformat(),                      output_flags={"is_adverse_action": result["decision"] == "decline",                                    "is_automated_decision": True},                      reason_codes=result["reasons"] or None)  def main():    client = OpenAI()    with bitexact.run("loan-APP-7731", workflow="loan-adjudication",                      workflow_instance="APP-7731", environment="demo",                      principal="svc:underwriter") as r:        client = wrap_openai(client, r)        result = decide(client, "APP-7731")        record_outcome(r, result)    print(json.dumps(result, indent=2))    return result  if __name__ == "__main__":    main()

The record

The inspector's summary of step 4, pull_credit_report
The inspector's view of step 4's result: the credit file of APP-7713, score 512
  1. Step 4 pull_credit_report, as the inspector shows it.

    The step that misled it.

  2. Its result Asked for APP-7731; given the file of APP-7713, score 512.

    This is why.

  3. The cause The bureau mirror has no file for this applicant yet, so the tool returns the file with the nearest number.

    The nearest number, not the applicant.

Change one byte.

Someone's edit

{  "step": 4,  "kind": "tool_call",  "data": {    "tool": "pull_credit_report",    "args": [      "APP-7731"    ],    "result": {      "applicant_id": "APP-7713",      "score": 812,      "as_of": "2026-10-30"    }  },  "hash": "d89013fe75095ab50a2d7c3222e527a1f415ea156837220bb1f77f034f8f939a"}
  1. 0run meta2656baadea77
  2. 1identityc6cd22404e1a
  3. 2time.time6c4f9c1a5db7
  4. 3fetch_applicant54708d65e42d
  5. 4pull_credit_reportd89013fe7509score 512 812
  6. 5check_policy14428fda7ad6
  7. 6model call264c3e7363a9
  8. 7decision recorda6a582ae5131
  9. 8run end0ad92adc015c

The open verifier

$ python bitexact_verifier.py … loan-APP-7731.tampered.bundle.jsonFAIL: step 4: data hash mismatch at path result.score - value tampered or corrupted$ echo $?2
Show command
$ python bitexact_verifier.py --trust-file trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd loan-APP-7731.tampered.bundle.json
  1. One character Step 4: score 512 becomes 812 in a copy of the bundle.

    Change one character.

  2. The chain no longer holds Step 4 no longer matches its hash, so the chain breaks there: nothing from it on can be shown to be what was recorded.

    Every step after it, unproven.

  3. The verifier refuses it FAIL: step 4: data hash mismatch at path result.score - value tampered or corrupted

    Exit 2.

Checked without us.

The evidence pack

  • bag-info.txt
  • bagit.txt
  • data/capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json
  • data/governance/loan-adjudication.jsonl
  • data/ledger-heads.json
  • data/runs/loan-APP-7731-baseline-0.bundle.json
  • data/runs/loan-APP-7731-baseline-1.bundle.json
  • data/runs/loan-APP-7731-baseline-10.bundle.json
  • data/runs/loan-APP-7731-baseline-11.bundle.json
  • data/runs/loan-APP-7731-baseline-12.bundle.json
  • data/runs/loan-APP-7731-baseline-13.bundle.json
  • data/runs/loan-APP-7731-baseline-14.bundle.json
  • data/runs/loan-APP-7731-baseline-15.bundle.json
  • data/runs/loan-APP-7731-baseline-16.bundle.json
  • data/runs/loan-APP-7731-baseline-17.bundle.json
  • data/runs/loan-APP-7731-baseline-18.bundle.json
  • and 39 more files
  1. 0run meta2656baadea77
  2. 1identityc6cd22404e1a
  3. 2time.time6c4f9c1a5db7
  4. 3fetch_applicant54708d65e42d
  5. 4pull_credit_reportd89013fe7509
  6. 5check_policy14428fda7ad6
  7. 6model call264c3e7363a9
  8. 7decision recorda6a582ae5131
  9. 8run end0ad92adc015c

The auditor's machine

$ python bitexact_verifier.py … loan-APP-7731.bundle.jsonOK: loan-APP-7731 - 9 steps (observed 7 / asserted 2 / synthetic 0), chain verified, sealed, signed, recorder-attested, in transparency log bitexact.log/965628e3573ee594 (leaf 1 of 98), witnessed by 1 of 1 pinned (1 with Ed25519 only), signatures: Ed25519 + ML-DSA-65 (post-quantum), post-quantum key pinnedstanding when recorded: ok (stated by the producer, not proof)captured via sdk-python 7, proxy 1, unlabeled 1 (attribution, not proof)$ echo $?0
Show command
$ python bitexact_verifier.py --trust-file trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd loan-APP-7731.bundle.json
  1. The evidence pack 55 files: every run's bundle, the ledger, a workpaper and the verifier itself.

    The whole workflow, in one sealed pack.

  2. Checked offline The auditor takes the declined applicant's run, loan-APP-7731, and runs the open verifier on it. No license, no network, no BitExact server.

    Checked without us.

  3. Exit 0 9 steps, chain intact, sealed, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, witnessed. Checked on the auditor's machine, without us.

    OK.

Go deeper The whole pack, checked: 43 runs, exit 0

The whole pack, checked: 43 runs, exit 0

Every run verifies, and so does the workflow's governance ledger: “1 ledger chain-verified and anchored at the head the pinned recorder key signed”. A changed entry, or one cut from the ledger's end, would show. The lines under the verdict say what it does not prove: what the producer stated, and the files sealed with the pack but not checked against the runs.

$ python data/verification/bitexact_verifier.py … .OK: evidence pack - 43 runs verified, 1 ledger chain-verified and anchored at the head the pinned recorder key signed (data/governance/loan-adjudication.jsonl at 2 events), bag checksums verified, signed, recorder-attested, signatures: Ed25519 + ML-DSA-65 (post-quantum), post-quantum key pinned, view matches the bundlesstanding when recorded: 43 runs ok (stated by the producer, not proof)captured via sdk-python 339, proxy 43, unlabeled 43 (attribution, not proof)not checked against the runs (the producer's own statements, sealed against later change only): data/capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json, data/selection.json, data/verification/VERIFIER.txt, data/verification/bitexact_verifier.py, data/workpaper.md$ echo $?0
Show command
$ python data/verification/bitexact_verifier.py --trust-file ../trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd .
Go deeper How the auditor checks it

How the auditor checks it

The pack carries its own verifier and a workpaper that says how to run it.

The pack's workpaper

# Evidence pack - selected runs

## A. Request

| Filter | Value |
|---|---|
| Workflow | loan-adjudication |
| Workflow version | any |
| Environment | any |
| Agent | any |
| Subject (business key) | any |
| Acting principal | any |
| On behalf of (as the run's current identity states it) | any |
| Person in session | any |
| Started on or after | any |
| Started before | any |
| Named runs | none - filters only |

The window is half-open: a run started exactly at `started_before` belongs to the next period, not this one, so adjacent packs tile a year without counting a run twice.

## B. Scope

**43 runs** are in this pack, each as an independently-verifiable `aer-bundle` under `data/runs/`. `data/selection.json` records the request verbatim beside the result, so a pack whose window genuinely held two runs is distinguishable from one that dropped three.

## D. Runs

| Run | Workflow | Version | Environment | Steps | Sealed | Verifies |
|---|---|---|---|---|---|---|
| `loan-APP-7731-candidate-19` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-18` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-17` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-16` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-15` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-14` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-13` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-12` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-11` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-10` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-9` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-8` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-7` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-6` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-5` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-4` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-3` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-2` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-1` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate-0` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-19` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-18` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-17` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-16` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-15` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-14` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-13` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-12` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-11` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-10` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-9` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-8` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-7` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-6` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-5` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-4` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-3` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-2` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-1` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-baseline-0` | loan-adjudication | - | demo | 10 | yes | yes |
| `loan-APP-7731-candidate` | loan-adjudication | - | demo | 10 | yes | yes |
| `notice-APP-7731` | loan-adjudication | - | demo | 6 | yes | yes |
| `loan-APP-7731` | loan-adjudication | - | demo | 9 | yes | yes |

## E. Governance

The append-only, hash-chained governance ledger of every workflow this selection touches is under `data/governance/`. Each re-walks as a chain: an edited or removed event fails.

- `loan-adjudication` -> `data/governance/loan-adjudication.jsonl`

### Executions refused at open

None recorded for this scope.

## F. Assertions

| Property provided | Requirement it maps to | Honest limit |
|---|---|---|
| A set of runs selected by a stated request, with the request recorded verbatim beside the run ids it matched (data/selection.json). | FINRA 4511(a) / SEA 17a-4(f)(2)(iv) download and transfer for examinations; examination population and sample requests. | The selection is a query over recorded runs - not proof that they are every run the agent performed. |
| Which runs match is read from each run's own hash-chained record, never from an index cell; the time window is half-open, so adjacent packs tile a period without counting a run twice. | Population completeness and period cut-off for testing. | A run's start is the time its first entry records; without an external anchor, step order is proven and wall-clock time is not. |
| Each selected run verifies independently, trusting nothing of ours. | DORA / books-and-records integrity; EU AI Act Art. 12 record-keeping. | Proves integrity of what was captured, never that the capture is the complete set of calls. |
| The append-only, hash-chained governance ledger of every workflow the selection touches. | SR 11-7 (superseded 17 April 2026 by SR 26-2) change control and ownership. | Proves the governance history was not edited, not that the recorded acts were correct or authorized. |

## G. Independent verification

`pip install cryptography asn1crypto` - the verifier's only dependencies - then:

```
# from the pack's root - the bag's checksums, every run, and the governance ledgers, trusting nothing of ours:
python data/verification/bitexact_verifier.py .
# then pin the recorder key you confirmed out of band:
python data/verification/bitexact_verifier.py . --expect-recorder-key <64-hex public key>
```
Exit 0: everything verified. Exit 3: everything verified, and a run declares capture gaps (counted on the OK line). Exit 2: the FAIL line names what failed. Exit 1: an `error:` line names an input the verifier could not use - no verdict. The governance ledgers re-walks as a hash chain - an edited or removed event fails - and must end exactly at the head the recorder key signed in `data/ledger-heads.json`, so dropping its newest events fails too. That holds against whoever handed you the pack only with the recorder key pinned: the OK line says which.

`view.html` shows the runs as readable steps in any browser, offline. It is a reading aid, not a verification result: the verifier renders it again from the bundles and fails the pack if it differs.

## H. Limitations

This pack proves the integrity and reconstructability of the runs it contains. It does not assert that they are every run the agent performed: BitExact records what routes through the proxy or a wrapped client, and a call made out of band leaves no entry. The selection is a query over recorded runs, not over reality.

### Capture controls

#### Run `loan-APP-7731-candidate-19`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-18`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-17`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-16`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-15`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-14`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-13`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-12`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-11`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-10`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-9`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-8`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-7`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-6`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-5`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-4`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-3`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-2`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-1`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate-0`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-19`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-18`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-17`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-16`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-15`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-14`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-13`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-12`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-11`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-10`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-9`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-8`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-7`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-6`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-5`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-4`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-3`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-2`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-1`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-baseline-0`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731-candidate`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `notice-APP-7731`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.

#### Run `loan-APP-7731`

- **Capture policy:** the run opened under `b2:ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e`; its document is `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json` in this pack, below.
- **Capture outcomes:** 0 fields a capture control touched.


Capture policy document `capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json`, as this pack carries it:

```json
{
 "detectors": {
  "pan": {
   "keep_last4": true
  },
  "iban": {},
  "credentials": {}
 },
 "fields": [
  {
   "kind": "*",
   "path": "*",
   "mode": "scrub"
  }
 ],
 "budget_ms": 1000
}
```

## I. Conclusion & sign-off

BitExact supplies the verifiable facts above and this space; the conclusion is the examiner's to write.

| Role | Name | Date | Conclusion |
|---|---|---|---|
| Preparer |  | | (facts as recorded) |
| Reviewer | | | |
| Auditor | | | |

The pack, file by file

  • bag-info.txt
  • bagit.txt
  • data/capture-policy-b2-ce93a52dbf018155afb7a641931ab9a2ccdee0618363591499c85e095411382e.json
  • data/governance/loan-adjudication.jsonl
  • data/ledger-heads.json
  • data/runs/loan-APP-7731-baseline-0.bundle.json
  • data/runs/loan-APP-7731-baseline-1.bundle.json
  • data/runs/loan-APP-7731-baseline-10.bundle.json
  • data/runs/loan-APP-7731-baseline-11.bundle.json
  • data/runs/loan-APP-7731-baseline-12.bundle.json
  • data/runs/loan-APP-7731-baseline-13.bundle.json
  • data/runs/loan-APP-7731-baseline-14.bundle.json
  • data/runs/loan-APP-7731-baseline-15.bundle.json
  • data/runs/loan-APP-7731-baseline-16.bundle.json
  • data/runs/loan-APP-7731-baseline-17.bundle.json
  • data/runs/loan-APP-7731-baseline-18.bundle.json
  • and 39 more files

For risk and compliance

The day someone asks.

The same obligation, in a second regulated setting. Emergency patient triage is on the EU AI Act's high-risk list (Annex III, point 5(d)), so Article 12's record-keeping applies to it as it does to credit decisions. A triage agent recommends care to patients; one day a regulator, an auditor or a court asks for the records of every recommendation it made. The EU AI Act mapping.

Without BitExact.

An illustration

Illustration, not real output

app.log

09:14:02 triage ok
09:14:07 triage ok
...

vendor dashboard export

date,count
week 1,212
week 2,198

app.log.3 (rotated)

(older lines removed)

timeline

09:14  09:15  09:16  ......  11:40

a line someone edited

09:16:44 triage SELF_CARE

a record as written

patient=PT-00000 symptoms=...

The record

No record to hand over.

  1. Where are those records? Logs, exports and rotated files that do not line up.

    Where are those records?

  2. Are they complete? A stretch with nothing in it, and no way to say why.

    Are they complete?

  3. Can you prove nobody changed them? A line edited in place; nothing notices.

    Can you prove nobody changed them?

  4. Can you hand them over? A patient identifier, written in the clear.

    Without exposing patients?

With BitExact.

Your capture policy

{
  "detectors": {
    "pan": {"keep_last4": true},
    "iban": {},
    "credentials": {},
    "us_ssn": {"require_context": true},
    "email": {},
    "custom": [{"name": "mrn", "pattern": "\\bMRN-\\d{7}\\b"}]
  },
  "fields": [{"kind": "*", "path": "*", "mode": "scrub"}],
  "budget_ms": 1000
}

The record

The inspector's integrity view of visit triage-visit-001: the scrubbed and hash-only fields, as recorded
  • bag-info.txt
  • bagit.txt
  • data/capture-policy-b2-0f652199d1f838bd869477e73826e69a3efe6496c16762f46c14cfebd6d584ef.json
  • data/governance/patient-triage.jsonl
  • data/ledger-heads.json
  • data/runs/triage-visit-001.bundle.json
  • data/runs/triage-visit-002.bundle.json
  • data/runs/triage-visit-003.bundle.json
  • data/runs/triage-visit-004.bundle.json
  • data/runs/triage-visit-005.bundle.json
  • data/runs/triage-visit-006.bundle.json
  • data/selection.json
  • data/verification/VERIFIER.txt
  • data/verification/bitexact_verifier.py
  • data/view.html
  • data/workpaper.md
  • and 2 more files
$ python data/verification/bitexact_verifier.py … .OK: evidence pack - 6 runs verified, 1 ledger chain-verified and anchored at the head the pinned recorder key signed (data/governance/patient-triage.jsonl at 2 events), bag checksums verified, signed, recorder-attested, signatures: Ed25519 + ML-DSA-65 (post-quantum), post-quantum key pinned, 12 fields scrubbed before capture (email 8, mrn 12, us_ssn 4), 6 fields hash-only (never held; 6 can be opened with key 9964..), view matches the bundlesScrubbed and hash-only fields prove that a value was presented and which rule removed it. They do not prove what the value was, they cannot be replayed, and hash-only custody is not HIPAA de-identification.standing when recorded: 6 runs ok (stated by the producer, not proof)captured via sdk-python 24, proxy 6, unlabeled 6 (attribution, not proof)not checked against the runs (the producer's own statements, sealed against later change only): data/capture-policy-b2-0f652199d1f838bd869477e73826e69a3efe6496c16762f46c14cfebd6d584ef.json, data/selection.json, data/verification/VERIFIER.txt, data/verification/bitexact_verifier.py, data/workpaper.md$ echo $?0
Show command
$ python data/verification/bitexact_verifier.py --trust-file ../trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd .
  1. Your capture policy triage_policy.json, as a customer writes it: pan, iban, credentials, us_ssn, email and custom:mrn, on every field.

    Written once, applied at capture.

  2. Scrubbed at capture Scrubbed fields: result.note; the patient identifier the tool is called with (args) is kept hash-only, still checkable by whoever holds your capture key.

    Identifiers removed before anything is written.

  3. One sealed pack 18 files for the workflow, verifier included.

    One pack for the whole workflow.

  4. Checked offline Exit 0: 6 runs verified, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, 12 fields scrubbed before capture.

    Unchanged, and shown to be.

Share exactly what was asked.

The request

  1. Every recommendation the agent madebitexact evidence-pack --workflow patient-triage6 of 6 runsverifier exit 0: 6 runs verified, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, 12 fields scrubbed before capture
  2. Only the visits before a cut-off timebitexact evidence-pack --workflow patient-triage --until 2026-11-08T23:12:09.841981+00:003 of 6 runsverifier exit 0: 3 runs verified, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, 6 fields scrubbed before capture
  3. Only the visits from that time onbitexact evidence-pack --workflow patient-triage --since 2026-11-08T23:12:09.841981+00:003 of 6 runsverifier exit 0: 3 runs verified, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, 6 fields scrubbed before capture
  4. Only what one agent identity didbitexact evidence-pack --workflow patient-triage --principal svc:triage-south3 of 6 runsverifier exit 0: 3 runs verified, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, 6 fields scrubbed before capture
  5. Only one case's visitsbitexact evidence-pack --workflow patient-triage --subject case-10032 of 6 runsverifier exit 0: 2 runs verified, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, 4 fields scrubbed before capture
  6. Only one deployment's runsbitexact evidence-pack --workflow patient-triage --environment demo-north3 of 6 runsverifier exit 0: 3 runs verified, signed with Ed25519 + ML-DSA-65, post-quantum key pinned, 6 fields scrubbed before capture

One sealed pack per request

  1. By workflow001002003004005006exit 0
  2. Before a cut-off001002003004005006exit 0
  3. From the cut-off on001002003004005006exit 0
  4. By agent001002003004005006exit 0
  5. By subject001002003004005006exit 0
  6. By environment001002003004005006exit 0
  1. By workflow One pack holds every run of the workflow, and verifies.

    By workflow: one sealed pack per request.

  2. By time Cut at one instant: the two packs share no run, and together they hold the whole workflow.

    By time: one sealed pack per request.

  3. Narrower still By agent identity, by case, by environment: each its own sealed pack.

    Narrower still: one sealed pack per request.

Start recording before anyone asks.

This story recorded 6 visits in 2 seconds, and cut them by workflow, time, agent, case and environment. A quarter of real visits is cut by the same commands, with the quarter's dates.

Go deeper

Where next?

Pick a path. Each one ends where you can cross to the other, or carry on.

For engineering · Path 1 of 2 · 4 scenes

Was it a fluke, and did the fix work?

First the question: replay the declined run to the step that failed and run it on from there 20 times. Then the fix: fork once with the fixed tool, and run that 20 times at the same step. Two real fork-matrix reports, side by side.

Was it a fluke?

Your agent

"""A loan-decision agent: one model call, three tools, a clock read, a ruleand a judgment. The model is whatever OPENAI_BASE_URL points at."""import jsonimport timefrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI # The credit bureau's mirror, as of its last nightly sync.BUREAU_MIRROR = [    {"applicant_id": "APP-7690", "score": 702, "as_of": "2026-10-30"},    {"applicant_id": "APP-7713", "score": 512, "as_of": "2026-10-30"},    {"applicant_id": "APP-7790", "score": 688, "as_of": "2026-10-30"},]UNDERWRITER = ("You are an underwriter. Apply the policy exactly. Answer as: "               "DECISION: <APPROVE|DECLINE|REFER>. REASONS: <codes or none>. "               "<one sentence>")  @bitexact.record_tooldef fetch_applicant(applicant_id):    """The application, as the lending portal holds it."""    return {"applicant_id": applicant_id, "product": "personal-24",            "annual_income": 148000, "monthly_debt": 900,            "requested": 25000, "years_employed": 9}  @bitexact.record_tooldef pull_credit_report(applicant_id):    """The applicant's report from the bureau mirror, matched by number."""    number = int(applicant_id.split("-")[1])    return min(BUREAU_MIRROR, key=lambda report: abs(        int(report["applicant_id"].split("-")[1]) - number))  @bitexact.record_tooldef check_policy(product):    """The underwriting thresholds for a product."""    return {"product": product, "min_score": 620, "max_dti": 0.43,            "version": "4.2"}  def decide(client, applicant_id):    received_at = time.time()    applicant = fetch_applicant(applicant_id)    report = pull_credit_report(applicant_id)    policy = check_policy(applicant["product"])    dti = round(applicant["monthly_debt"] * 12 / applicant["annual_income"], 3)    reply = client.chat.completions.create(        model="underwriter-v1",        messages=[{"role": "system", "content": UNDERWRITER},                  {"role": "user", "content": json.dumps(                      {"application": applicant, "credit_report": report,                       "policy": policy, "dti": dti})}])    answer = reply.choices[0].message.content    codes = answer.split("REASONS:")[1].split(".")[0].strip()    return {"applicant_id": applicant_id,            "decision": answer.split("DECISION:")[1].split(".")[0].strip().lower(),            "reasons": [] if codes == "none" else codes.split(", "),            "explanation": answer.split("REASONS:")[1].split(".", 1)[1].strip(),            "received_at": received_at}  def record_outcome(r, result):    r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=760)).isoformat(),                      output_flags={"is_adverse_action": result["decision"] == "decline",                                    "is_automated_decision": True},                      reason_codes=result["reasons"] or None)  def main():    client = OpenAI()    with bitexact.run("loan-APP-7731", workflow="loan-adjudication",                      workflow_instance="APP-7731", environment="demo",                      principal="svc:underwriter") as r:        client = wrap_openai(client, r)        result = decide(client, "APP-7731")        record_outcome(r, result)    print(json.dumps(result, indent=2))    return result  if __name__ == "__main__":    main()
  1. 0run meta2656baadea77
  2. 1identityc6cd22404e1a
  3. 2time.time6c4f9c1a5db7
  4. 3fetch_applicant54708d65e42d

The record it replays from

The inspector's step timeline for loan-APP-7731: steps 0 to 3 are served from it
  1. Replay it Steps 0 to 3 served from the record, exactly as they happened: the same tools, the same clock read.

    Everything up to the decision, replayed.

  2. Fork at step 4 From here each fork runs live: the credit-report tool and the model.

    Then run it again.

Fork it many times.

Every fork, one dot

20 forks of the agent as it is, at step 4

decline: 19refer: 1

report 1329d6685666

The record

underwriter-v1, baseline agent: 20 forks at step 4
  1. decline19 of 20
  2. refer1 of 20
  1. 20 forks underwriter-v1, baseline agent: decline 19, refer 1.

    Fork it, again and again.

  2. Not a fluke Most forks decline the same strong applicant.

    The counts, from the fork matrix's own report.

Fork once, with the fix.

The fork

with bitexact.fork(SOURCE, at_step=strange_step, run_id=FORK) as f:    client = wrap_openai(OpenAI(), f)    forked = candidate.decide(client, "APP-7731")    candidate.record_outcome(f, forked)
  1. 0run meta2656baadea77
  2. 1identityc6cd22404e1a
  3. 2time.time6c4f9c1a5db7
  4. 3fetch_applicant54708d65e42d
  5. 4pull_credit_reportd89013fe7509
  6. 5check_policy14428fda7ad6
  7. 6model call264c3e7363a9
  8. 7decision recorda6a582ae5131
  9. 8run end0ad92adc015c
  1. 0run metad065f1a6901d
  2. 1identity9a199f675d99
  3. 2forke06841cb57ff
  4. 3time.timebe817866db12
  5. 4fetch_applicant64bb5c9a8189
  6. 5pull_credit_reportd255586110a5
  7. 6check_policy8a7e3a3907e1
  8. 7model calld67374028f1e
  9. 8decision record7bde07f18d09
  10. 9run enddf50547ab897

The record

The inspector's verdict for loan-APP-7731-candidate
The inspector comparing loan-APP-7731-candidate with its source: the decision went from decline to refer
  1. Fork at the failing step Step 4, pull_credit_report: everything before it replays from the record.

    Fork where it went wrong.

  2. The fixed tool, live From step 4 the fixed pull_credit_report and the model run live, into a run of its own that names its source.

    The fix, run from the failing step.

  3. Exactly what changed The decision went from decline to refer, and the adverse-action flag and reason codes with it.

    Exactly what changed.

Go deeper How a fork works

How a fork works

Replay the run up to a step and let the agent carry on live from there, into a new run of its own; a replacement answer can be served at a recorded call.

with bitexact.fork(SOURCE, at_step=strange_step, run_id=FORK) as f:    client = wrap_openai(OpenAI(), f)    forked = candidate.decide(client, "APP-7731")    candidate.record_outcome(f, forked)
The fork run

$ bitexact fork-matrix --help

usage: bitexact fork-matrix [-h] --at-step AT_STEP --n N --entry ENTRY
                            [--model MODEL] [--run-prefix RUN_PREFIX]
                            [--accept-scrubbed] [-o OUTPUT]
                            source_run

positional arguments:
  source_run

options:
  -h, --help            show this help message and exit
  --at-step AT_STEP     step index or KIND:N (e.g. http_call:2)
  --n N                 number of live forks to run
  --entry ENTRY         MODULE:FUNCTION taking the active run, returning the
                        fork's outcome
  --model MODEL         declared model dimension for this arm
  --run-prefix RUN_PREFIX
                        prefix for the fork run ids
  --accept-scrubbed     serve the prefix's scrubbed values as scrubbed
                        (default: a fork past one is refused)
  -o, --output OUTPUT   also write the report to a path

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached
  1 also when a fork errored

Did the fix fix it?

Your agent

"""A loan-decision agent: one model call, three tools, a clock read, a ruleand a judgment. The model is whatever OPENAI_BASE_URL points at. The candidate fix: the same agent with pull_credit_report fixed to match theapplicant exactly. A miss returns no report instead of the nearest number's.Nothing else differs from story_agent_bitexact.py."""import jsonimport timefrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI # The credit bureau's mirror, as of its last nightly sync.BUREAU_MIRROR = [    {"applicant_id": "APP-7690", "score": 702, "as_of": "2026-10-30"},    {"applicant_id": "APP-7713", "score": 512, "as_of": "2026-10-30"},    {"applicant_id": "APP-7790", "score": 688, "as_of": "2026-10-30"},]UNDERWRITER = ("You are an underwriter. Apply the policy exactly. Answer as: "               "DECISION: <APPROVE|DECLINE|REFER>. REASONS: <codes or none>. "               "<one sentence>")  @bitexact.record_tooldef fetch_applicant(applicant_id):    """The application, as the lending portal holds it."""    return {"applicant_id": applicant_id, "product": "personal-24",            "annual_income": 148000, "monthly_debt": 900,            "requested": 25000, "years_employed": 9}  @bitexact.record_tooldef pull_credit_report(applicant_id):    """The applicant's report from the bureau mirror, matched exactly."""    return next((report for report in BUREAU_MIRROR                 if report["applicant_id"] == applicant_id),                {"applicant_id": applicant_id, "found": False})  @bitexact.record_tooldef check_policy(product):    """The underwriting thresholds for a product."""    return {"product": product, "min_score": 620, "max_dti": 0.43,            "version": "4.2"}  def decide(client, applicant_id):    received_at = time.time()    applicant = fetch_applicant(applicant_id)    report = pull_credit_report(applicant_id)    policy = check_policy(applicant["product"])    dti = round(applicant["monthly_debt"] * 12 / applicant["annual_income"], 3)    reply = client.chat.completions.create(        model="underwriter-v1",        messages=[{"role": "system", "content": UNDERWRITER},                  {"role": "user", "content": json.dumps(                      {"application": applicant, "credit_report": report,                       "policy": policy, "dti": dti})}])    answer = reply.choices[0].message.content    codes = answer.split("REASONS:")[1].split(".")[0].strip()    return {"applicant_id": applicant_id,            "decision": answer.split("DECISION:")[1].split(".")[0].strip().lower(),            "reasons": [] if codes == "none" else codes.split(", "),            "explanation": answer.split("REASONS:")[1].split(".", 1)[1].strip(),            "received_at": received_at}  def record_outcome(r, result):    r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=760)).isoformat(),                      output_flags={"is_adverse_action": result["decision"] == "decline",                                    "is_automated_decision": True},                      reason_codes=result["reasons"] or None)  def main():    client = OpenAI()    with bitexact.run("loan-APP-7731", workflow="loan-adjudication",                      workflow_instance="APP-7731", environment="demo",                      principal="svc:underwriter") as r:        client = wrap_openai(client, r)        result = decide(client, "APP-7731")        record_outcome(r, result)    print(json.dumps(result, indent=2))    return result  if __name__ == "__main__":    main()

reports 1329d6685666 and 9e067dee888c

The record

underwriter-v1, baseline agent
  1. decline19 of 20
  2. refer1 of 20
underwriter-v1, candidate agent
  1. refer20 of 20
  1. The fix One change: pull_credit_report matches the applicant exactly; a miss returns no report.

    Fix the tool, not the prompt.

  2. 20 more forks Same step, same replayed prefix: refer 20.

    Run it again at the same step.

  3. Side by side Two real reports. Without the fix: decline 19, refer 1. With it: refer 20. BitExact shows exactly what changed; whether refer is right is your policy's call.

    Know your fix fixes the failure, at the step where it failed.

Know your fix fixes the failure, at the step where it failed.

Up to 1,000 forks from the inspector; as many as you like from code.

For platform teams · Path 2 of 2 · 2 scenes

Many agents, one workflow

Each agent records its own run, with its own chain; the workflow gathers every run under one name, whichever agent made it.

A second agent picks up the decision.

Your agent

"""The notice writer: a second agent in the loan workflow. It drafts theadverse-action notice for a declined application."""import jsonfrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI NOTICE_WRITER = ("You draft adverse-action notices. Fill every section of the "                 "template from the decision; add nothing.")  @bitexact.record_tooldef notice_template(kind):    """The firm's current template for a notice of this kind."""    return {"kind": kind, "version": "2026-07",            "sections": ["decision", "principal_reasons", "rights"]}  def write_notice(client, decision):    template = notice_template("adverse-action")    reply = client.chat.completions.create(        model="notice-writer-v1",        messages=[{"role": "system", "content": NOTICE_WRITER},                  {"role": "user", "content": json.dumps(                      {"template": template, "decision": decision})}])    return reply.choices[0].message.content  def main(decision):    client = OpenAI()    with bitexact.run("notice-APP-7731", workflow="loan-adjudication",                      workflow_instance="APP-7731", environment="demo",                      principal="svc:notice-writer") as r:        client = wrap_openai(client, r)        notice = write_notice(client, decision)        r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=760)).isoformat(),                          output_flags={"is_communication": True})    print(notice)    return notice
  1. 0run meta9ca0686fe5ad
  2. 1identityc29a7bee6c38
  3. 2notice_templated59a5c926f92
  4. 3model call64d4e7911af5
  5. 4decision recordd59b05fa6e8d
  6. 5run end45d8ef6e7172

The record

The inspector's page for workflow loan-adjudication
  1. A second agent The notice writer drafts the adverse-action notice.

    Another agent picks up the decision.

  2. Its own run notice-APP-7731: 6 steps, its own chain.

    Its own record, hash-linked.

  3. One workflow Every run of loan-adjudication, in one place.

    The runs gather under one name.

Go deeper Multi-agent workflows

Multi-agent workflows

Each agent records its own run; the workflow gathers them.

"""The notice writer: a second agent in the loan workflow. It drafts theadverse-action notice for a declined application."""import jsonfrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI NOTICE_WRITER = ("You draft adverse-action notices. Fill every section of the "                 "template from the decision; add nothing.")  @bitexact.record_tooldef notice_template(kind):    """The firm's current template for a notice of this kind."""    return {"kind": kind, "version": "2026-07",            "sections": ["decision", "principal_reasons", "rights"]}  def write_notice(client, decision):    template = notice_template("adverse-action")    reply = client.chat.completions.create(        model="notice-writer-v1",        messages=[{"role": "system", "content": NOTICE_WRITER},                  {"role": "user", "content": json.dumps(                      {"template": template, "decision": decision})}])    return reply.choices[0].message.content  def main(decision):    client = OpenAI()    with bitexact.run("notice-APP-7731", workflow="loan-adjudication",                      workflow_instance="APP-7731", environment="demo",                      principal="svc:notice-writer") as r:        client = wrap_openai(client, r)        notice = write_notice(client, decision)        r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=760)).isoformat(),                          output_flags={"is_communication": True})    print(notice)    return notice
The notice writer run
The Runs screen

$ bitexact workflow --help

usage: bitexact workflow [-h] [--by BY] [--owner OWNER]
                         [--description DESCRIPTION]
                         [--criticality {tier-1,tier-2,tier-3}]
                         [--version VERSION] [--code CODE]
                         [--policy-version POLICY_VERSION]
                         [--regress-suite REGRESS_SUITE] [--notes NOTES]
                         [--to TO] [--from FROM_ENV] [--gate GATE]
                         [--schedule SCHEDULE] [--reason REASON]
                         [--regime {sec-17a-4-audit-trail}]
                         {create,register-version,promote,set-retention,set-records-regime,deprecate,show,verify}
                         workflow_id

positional arguments:
  {create,register-version,promote,set-retention,set-records-regime,deprecate,show,verify}
  workflow_id

options:
  -h, --help            show this help message and exit
  --by BY               the principal performing the act (default: OS user)
  --owner OWNER         create: the accountable owner/team
  --description DESCRIPTION
  --criticality {tier-1,tier-2,tier-3}
                        create: tier-1 (critical) / tier-2 / tier-3 (standard)
  --version VERSION     the workflow_version label
  --code CODE           register-version: git sha / build id
  --policy-version POLICY_VERSION
  --regress-suite REGRESS_SUITE
                        register-version: reference to the signed regress
                        suite proving the version's behavioral delta
  --notes NOTES
  --to TO               promote: the target environment
  --from FROM_ENV       promote: the source environment
  --gate GATE           promote: reference to the qualification/regress report
                        that gated it
  --schedule SCHEDULE   set-retention: CATEGORY=DURATION[,...], where DURATION
                        is <n>y (years), <n>m (calendar months) or <n>d
                        (days), e.g. adverse=7y,standard=3y
  --reason REASON       deprecate: why the workflow is withdrawn
  --regime {sec-17a-4-audit-trail}
                        set-records-regime: the records regime the workflow
                        declares for its runs

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached
  2  FAIL - the evidence does not verify, or a check it requires cannot run
     on this install (a library is missing)
  2 when `verify` finds the ledger does not verify

$ bitexact evidence-pack --help

usage: bitexact evidence-pack [-h] [--workflow WORKFLOW] [--case CASE]
                              [--runs RUNS] [--environment ENVIRONMENT]
                              [--workflow-version WORKFLOW_VERSION]
                              [--since ISO8601] [--until ISO8601]
                              [--subject KEY] [--principal PRINCIPAL]
                              [--agent AGENT_ID] [--on-behalf-of PARTY]
                              [--interaction {interactive,autonomous}]
                              [--save NAME] [--pack NAME] [--list-packs]
                              [-o OUTPUT] [--sign] [--key KEY]
                              [--preparer PREPARER] [--verifier VERIFIER]
                              [--no-view] [--by BY] [--force]
                              [run_id]

positional arguments:
  run_id                a run, or --workflow for a workflow-scoped pack

options:
  -h, --help            show this help message and exit
  --workflow WORKFLOW   pack every recorded run of a workflow plus its
                        governance ledger and an inventory workpaper
  --case CASE           pack every pinned run of a case plus its chain-of-
                        custody ledger and a case workpaper
  --runs RUNS           comma-separated run ids to pack as one selection (an
                        auditor's list); combines with the filters below,
                        which then narrow it
  --environment ENVIRONMENT
                        narrow the selection to one environment
  --workflow-version WORKFLOW_VERSION
                        narrow the selection to one workflow version
  --since ISO8601       only runs started at or after this instant (inclusive)
  --until ISO8601       only runs started before this instant (exclusive, so
                        adjacent windows tile a period without counting a run
                        twice)
  --subject KEY         only the decisions about this subject: the business
                        key (case, application, customer id) a run committed
                        as its workflow_instance
  --principal PRINCIPAL
                        only the runs whose agent acted as this identity, as
                        the run's identity committed it
  --agent AGENT_ID      only the runs of this agent, as the run's identity
                        committed its agent_id
  --on-behalf-of PARTY  only the runs done on behalf of this party, as the
                        run's current identity states it
  --interaction {interactive,autonomous}
                        only the runs with a person in the session
                        (interactive) or none (autonomous)
  --save NAME           also save this selection to the pack library under
                        NAME, to issue again with --pack
  --pack NAME           issue a saved pack: its selection, resolved against
                        the store now
  --list-packs          list the saved packs and exit
  -o, --output OUTPUT   pack directory (default <run>.evidence-pack)
  --sign                sign each run bundle and the graph attestation with
                        --key
  --key KEY             path to an ed25519 private key file
  --preparer PREPARER   preparer name/org for the workpaper cover (default:
                        the bound principal)
  --verifier VERIFIER   path to bitexact_verifier.py to vendor into the pack,
                        pinned by its own sha256 (for air-gapped auditors)
  --no-view             omit the reading view (data/view.html), for a relying
                        party that forbids opening HTML; the pack verifies
                        without it
  --by BY               the principal exporting, recorded on the custody chain
                        with role operator (default: OS user)
  --force               replace an existing bag directory whole (only a bag)

exit codes:
  0  OK - the evidence verifies, or the command did what it was asked
  1  operational error - a usage error, a missing run or file, input that
     cannot be read, or a license refusal: no verdict was reached
  2  FAIL - the evidence does not verify, or a check it requires cannot run
     on this install (a library is missing)

Each visit is its own run.

Your agent

"""A triage agent: reads what the clinic system holds for a visit andrecommends one next step. Each visit is its own run under one workflow."""import jsonfrom datetime import datetime, timedelta, timezone import bitexactfrom bitexact.adapters import wrap_openaifrom openai import OpenAI TRIAGE = ("You are a triage assistant. Recommend one next step - SELF_CARE, "          "PRIMARY_CARE within 48 hours, or URGENT_CARE today - and give the "          "reason in one sentence.") # What the clinic system holds for each visit, by patient identifier. The# intake note is free text, and free text carries identifiers: a medical# record number, an email, sometimes an SSN. (All synthetic.)VISITS = {    "PT-48213": {"age_band": "40-49", "symptoms": ["cough", "fever"], "days": 3,                 "note": "MRN-4821736. Reach at r.okafor@example.org. SSN 123-45-6789 on file."},    "PT-50977": {"age_band": "60-69", "symptoms": ["chest_pain", "breathless"], "days": 1,                 "note": "MRN-5097704. Daughter is contact: l.brandt@example.org."},    "PT-51140": {"age_band": "20-29", "symptoms": ["sore_throat"], "days": 2,                 "note": "MRN-5114029. Prefers email: dev.sato@example.org."},    "PT-52388": {"age_band": "50-59", "symptoms": ["back_pain"], "days": 12,                 "note": "MRN-5238815. SSN 234-56-7890 given at intake."},    "PT-54702": {"age_band": "70-79", "symptoms": ["dizziness", "chest_pain"], "days": 1,                 "note": "MRN-5470263. Lives alone."},}# The fleet the story records: which agent saw each visit, where it runs, and# the case the visit belongs to (the business key a pack can be cut by; never# the patient identifier). PT-51140 comes back two days later.FLEET = [    ("PT-48213", "svc:triage-north", "demo-north", "case-1001"),    ("PT-50977", "svc:triage-north", "demo-north", "case-1002"),    ("PT-51140", "svc:triage-south", "demo-south", "case-1003"),    ("PT-52388", "svc:triage-south", "demo-south", "case-1004"),    ("PT-51140", "svc:triage-south", "demo-south", "case-1003"),    ("PT-54702", "svc:triage-north", "demo-north", "case-1005"),]  # The capture policy (triage_policy.json) scrubs identifiers it can find in# any field: MRNs, emails, SSNs. The patient identifier this tool is called# with is different: it is the key someone will later need to find this# patient's visits by, so it is kept hash-only - never stored, but a holder# of the capture key can confirm a given identifier against it. A scrub would# replace it with a token and lose that.@bitexact.record_tool(custody={"args": "hash-only"})def patient_context(patient_id):    """The visit's clinical context and intake note."""    return VISITS[patient_id]  def recommend(client, patient_id):    context = patient_context(patient_id)    reply = client.chat.completions.create(        model="triage-v1",        messages=[{"role": "system", "content": TRIAGE},                  {"role": "user", "content": json.dumps(context)}])    return reply.choices[0].message.content  def main(patient_id, visit, *, principal, environment, subject):    client = OpenAI()    with bitexact.run(f"triage-visit-{visit:03d}", workflow="patient-triage",                      principal=principal, environment=environment,                      workflow_instance=subject) as r:        client = wrap_openai(client, r)        advice = recommend(client, patient_id)        # The visit record is kept ten years from the visit.        r.record_decision(retention_deadline=(datetime.now(timezone.utc) + timedelta(days=3653)).isoformat())    return advice
  1. 0run metafb83724d8c35
  2. 1identityd5ac7c274576
  3. 2patient_context0f1afc61529b
  4. 3model calld1e25bf0ea7d
  5. 4decision recordb2ecd74e1015
  6. 5run end931c034f944a

The record

The inspector's page for workflow patient-triage: 6 recorded visits
  1. A triage agent One model call, one patient-context tool, one recommendation.

    Another workflow, another agent.

  2. Each visit, its own run 6 visits, each recorded as its own signed run and gathered under patient-triage.

    Every visit, one place.

One place to answer from.

Every run of a workflow, whichever agent made it, in one record you can seal and hand over.

Everything the story didn't show.

Evidence

  • A changed byte breaks the chain

    Each step is linked to the one before; the recorder key signs and the seal closes the run. How the auditor checks it

  • Post-quantum signatures

    Hybrid Ed25519 with ML-DSA-65 and ML-DSA-87.

  • Anchored outside your store

    Run heads anchored to RFC 3161 timestamps, WORM storage and a transparency log.

Yours

  • On your premises

    The recorder, the store and the keys run where you run them. Nothing phones home.

  • Evidence for your own defense

    You hold the records and the keys that sign them.

Privacy

  • Scrubbed before it is written

    Personal data scrubbed or kept hash-only at capture, under the shipped default and the PCI preset or a policy of your own. What the policy matches is never written raw, so it never reaches a destination you configure. See it in the story

  • Redaction, two-person control available

    Turn it on and every redaction needs a second approver; the run still verifies.

Scoped disclosure

  • Hand over exactly what was asked

    One sealed pack per request, cut by a period, a workflow, a workflow version, an agent identity, a subject, an environment, a case, hand-picked runs and the party acted for. See it in the story

Investigate

  • Replay

    Re-run the agent against the record, offline; any divergence halts at the exact step. How replay works

  • Fork

    Branch a run at any step and run it on live, into a run of its own that names its source; or serve a different answer there, marked synthetic. See it in the story

  • Fork matrices

    Outcome counts from many forks: up to 1,000 from the inspector, as many as you like from code. See it in the story

  • Compare a candidate

    Run a candidate agent at the failure point and set the two distributions side by side. See it in the story

Fits your stack

  • SDKs

    Python, TypeScript, Go and Java.

  • Integrations

    • OpenAI client
    • Anthropic client
    • Temporal
    • LangChain and LangGraph
    • LlamaIndex
    • OpenAI Agents SDK
    • Claude Agent SDK
    • Google ADK
    • MCP servers, via the MCP proxy
    • OpenTelemetry ingest
    • Your governance layer's decisions
    • Any OpenAI- or Anthropic-compatible client, via the recording proxy

    How the recorder runs in your deployment

Independent proof

  • The open verifier

    Apache-2.0, offline, no account with us. bitexact.org

Honest limits

  • What it is not

    Not a governance layer. Not a test harness.

  • What it proves

    Integrity, declared gaps and honest redaction of what was captured. It never claims every call was captured.