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.
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 $?2Show command
$ python bitexact_verifier.py --trust-file trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd loan-APP-7731.tampered.bundle.jsonAn 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 $?0The record
No record. If anyone asks why, there is nothing to show them.
Your agent 68 lines of Python: 3 tools, a clock read, a model call and a decision.
An agent, as you would write it.
A strong applicant Income 148,000, debt-to-income 7.3%, 9 years employed.
A strong applicant.
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 exitsThe 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 exitThe 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 runThe first-run snippet the inspector gives for Any language, through the recorder. The run in this story was recorded with the Python agent.
The record
Nothing recorded yet. Run it, and the record starts here.
Add BitExact Three imports, and a decorator on each of the 3 tools.
Add BitExact.
Wrap the run Everything inside becomes one record; the decision is committed with its reasons.
One run around the agent.
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.
- 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.
- 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.
- Any language
The recording proxy
No code change: point an OpenAI- or Anthropic-compatible client's base URL at
bitexact serveand send a run-id header. It records the wire, not your tools.
- A first sealed run in under 30 minutes. With your license issued, the quick start goes from nothing to a first sealed, verifiable run. For a pilot on one host, one Docker Compose file runs the recorder and the inspector.
- One prerequisite: a TPM 2.0 on the recorder host. The license is renewed by file every 90 days, and each renewal request carries a TPM 2.0 attestation of the recorder host. It is taken only when you request a renewal, never while recording, so a TPM problem can delay a renewal but never stops a call.
- If the proxy is down, calls through it fail. A failure in recording never fails your pipeline: when the recorder cannot do its job, the call goes through and the run declares the gap, unless you turned on strict mode, which refuses the call instead. But no BitExact code runs on the agent's side of the proxy, so if its process is not running, calls pointed at it fail as calls to any stopped service would. Where a proxy outage must not reach the agent, use the SDK or the collector; where you run the proxy, run it beside the agent, under a restart policy, with a liveness probe on
/healthz.
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 reachedGo 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 reachedEvery 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()- 1identity
c6cd22404e1a - 2time.time
6c4f9c1a5db7captured1794179527.8575594 - 3fetch_applicant
54708d65e42d - 4pull_credit_report
d89013fe7509 - 5check_policy
14428fda7ad6 - 6model call
264c3e7363a9 - 7decision record
a6a582ae5131 - 8run end
0ad92adc015c
The record
Every call Recorded as it happens, each step hash-linked to the one before.
Every call, recorded and hash-linked.
The clock read time.time returned 1794179527.8575594; replay serves the same value.
Captured, so it replays.
The model's answer Request and response, as they crossed the wire.
The model call, as it came back.
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 notFind 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
Step 4 pull_credit_report, as the inspector shows it.
The step that misled it.
Its result Asked for APP-7731; given the file of APP-7713, score 512.
This is why.
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"}- 1identity
c6cd22404e1a - 2time.time
6c4f9c1a5db7 - 3fetch_applicant
54708d65e42d - 4pull_credit_report
d89013fe7509score512812 - 5check_policy
14428fda7ad6 - 6model call
264c3e7363a9 - 7decision record
a6a582ae5131 - 8run end
0ad92adc015c
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 $?2Show command
$ python bitexact_verifier.py --trust-file trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd loan-APP-7731.tampered.bundle.jsonOne character Step 4: score 512 becomes 812 in a copy of the bundle.
Change one character.
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.
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
- 1identity
c6cd22404e1a - 2time.time
6c4f9c1a5db7 - 3fetch_applicant
54708d65e42d - 4pull_credit_report
d89013fe7509 - 5check_policy
14428fda7ad6 - 6model call
264c3e7363a9 - 7decision record
a6a582ae5131 - 8run end
0ad92adc015c
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 $?0Show command
$ python bitexact_verifier.py --trust-file trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd loan-APP-7731.bundle.jsonThe evidence pack 55 files: every run's bundle, the ledger, a workpaper and the verifier itself.
The whole workflow, in one sealed pack.
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.
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 $?0Show 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
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.
Where are those records? Logs, exports and rotated files that do not line up.
Where are those records?
Are they complete? A stretch with nothing in it, and no way to say why.
Are they complete?
Can you prove nobody changed them? A line edited in place; nothing notices.
Can you prove nobody changed them?
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
- 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 $?0Show command
$ python data/verification/bitexact_verifier.py --trust-file ../trust.json --expect-witness witness.auditor.example=87cb23483413772935aad1049be5705874339692425a9c7cba450fb5edd3fffd .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.
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.
One sealed pack 18 files for the workflow, verifier included.
One pack for the whole workflow.
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
- Every recommendation the agent made
bitexact 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 - Only the visits before a cut-off time
bitexact 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 - Only the visits from that time on
bitexact 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 - Only what one agent identity did
bitexact 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 - Only one case's visits
bitexact 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 - Only one deployment's runs
bitexact 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
- By workflow001002003004005006exit 0
- Before a cut-off001002003004005006exit 0
- From the cut-off on001002003004005006exit 0
- By agent001002003004005006exit 0
- By subject001002003004005006exit 0
- By environment001002003004005006exit 0
By workflow One pack holds every run of the workflow, and verifies.
By workflow: one sealed pack per request.
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.
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 · 4 scenes Was it a fluke, and did the fix work?Replay to the step that failed, run on from there 20 times, then do the same with the fixed tool.You are on this path
- Straight on Everything the story didn't showEvery capability, in brief.Straight on
- For platform teams · 2 scenes Many agents, one workflowA second agent picks up the decision; a fleet of runs gathers under one name.You are on this path
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()- 1identity
c6cd22404e1a - 2time.time
6c4f9c1a5db7 - 3fetch_applicant
54708d65e42d
The record it replays from
The inspector's step timeline for loan-APP-7731: steps 0 to 3 are served from itReplay 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.
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
decline: 19refer: 1
report 1329d6685666
The record
20 forks underwriter-v1, baseline agent: decline 19, refer 1.
Fork it, again and again.
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)- 1identity
c6cd22404e1a - 2time.time
6c4f9c1a5db7 - 3fetch_applicant
54708d65e42d - 4pull_credit_report
d89013fe7509 - 5check_policy
14428fda7ad6 - 6model call
264c3e7363a9 - 7decision record
a6a582ae5131 - 8run end
0ad92adc015c
- 1identity
9a199f675d99 - 2fork
e06841cb57ff - 3time.time
be817866db12 - 4fetch_applicant
64bb5c9a8189 - 5pull_credit_report
d255586110a5 - 6check_policy
8a7e3a3907e1 - 7model call
d67374028f1e - 8decision record
7bde07f18d09 - 9run end
df50547ab897
The record
Fork at the failing step Step 4, pull_credit_report: everything before it replays from the record.
Fork where it went wrong.
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.
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)$ 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 erroredDid 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
The fix One change: pull_credit_report matches the applicant exactly; a miss returns no report.
Fix the tool, not the prompt.
20 more forks Same step, same replayed prefix: refer 20.
Run it again at the same step.
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- 1identity
c29a7bee6c38 - 2notice_template
d59a5c926f92 - 3model call
64d4e7911af5 - 4decision record
d59b05fa6e8d - 5run end
45d8ef6e7172
The record
The inspector's page for workflow loan-adjudicationA second agent The notice writer drafts the adverse-action notice.
Another agent picks up the decision.
Its own run notice-APP-7731: 6 steps, its own chain.
Its own record, hash-linked.
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$ 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- 1identity
d5ac7c274576 - 2patient_context
0f1afc61529b - 3model call
d1e25bf0ea7d - 4decision record
b2ecd74e1015 - 5run end
931c034f944a
The record
The inspector's page for workflow patient-triage: 6 recorded visitsA triage agent One model call, one patient-context tool, one recommendation.
Another workflow, another agent.
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
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.