Skip to content
BitExactdocs
Documentation
On this page

Deploying BitExact#

BitExact runs entirely inside your VPC. Nothing in it can phone home; license validation is offline. This document covers the listener, identity, storage, and lifecycle decisions an operator owns.

Prerequisite: a license bound to this deployment's recorder key#

BitExact is licensed use only. There is no evaluation mode. Between expires and the end of grace_days (14 when the license names none) the deployment keeps recording and logs a license_grace warning at most once an hour; renew then. Past expires plus grace_days the deployment is read-only: the recorder records nothing - checked on each request, bitexact serve forwards every call to the provider unrecorded, marked x-bitexact-recording: off, and an SDK run executes its body unrecorded (with BITEXACT_FAILURE_MODE=strict the proxy refuses with a 402 instead) - replay, fork, regression, anchoring, purge and every other producing command refuse, and the viewer refuses the same acts (/api/whoami lists them as license_refuses before anyone clicks); listing, showing, verifying, exporting and packing the evidence already recorded keep working, and so do the statutory acts - placing and releasing a legal hold, and redaction - which a customer owes to a court or a data subject whatever the state of its renewal. A proxy restarted after a lapse must not cut agents off from their provider.

With no license, or one that fails its check, the viewer serves nothing but its sign-in page, the stylesheet and script its pages are drawn with, and its liveness probe. A browser gets a page saying what is wrong - from the license check itself - and where this deployment's license file goes; an API client gets a 402 with the reason in detail. /healthz answers 200 in every license state, with the state in its body ({"status": "ok", "license": "missing", "standing": "red"}), so an orchestrator never restarts a viewer over a license.

Without a valid license only bitexact capture keygen, bitexact keygen, bitexact license, bitexact pki, bitexact recorder-key, bitexact version and bitexact witness answer. bitexact collector, bitexact mcp-proxy, bitexact recorder and bitexact serve start in any license state and check the license on each call: they pass the agents' traffic through, or declare a gap, rather than record.

Generate the recorder key on the host that will sign, keep the private half there, and send the public half for the license to name:

bitexact keygen -o /secure/recorder.key
bitexact capture keygen -o /secure/capture.key
export BITEXACT_SIGNING_KEY=/secure/recorder.key
export BITEXACT_CAPTURE_KEY=/secure/capture.key
export BITEXACT_LICENSE=/secure/bitexact.license
bitexact license

keygen writes two files: the Ed25519 key and its post-quantum (ML-DSA) half, recorder.pq.key, beside it, where every process that signs reads it (BITEXACT_SIGNING_PQ_KEY names it anywhere else). A signer given the key without its half signs Ed25519 alone, and bitexact health says signing_classical_only. keygen prints both public halves - send both for the license - and the trust-file entry that pins them; give that entry to every party that verifies your evidence, so a verifier pinned by it refuses evidence the post-quantum half did not sign. bitexact recorder-key and bitexact recorder-key --pq print the two halves again.

The capture key keys hash-only fields and the digests of content gaps withheld (capture-controls.md). Every process that records or applies the capture policy - the proxy, the viewer, the recorder and each collector - reads it from BITEXACT_CAPTURE_KEY, and all of them hold the same key.

Both keys are secrets and live with the license, owner-only, never in the data directory or its volume: whoever holds a backup of the evidence must not hold the keys that sign it or open its hash-only fields. Each shipped deployment delivers them so:

DeploymentRecorder key and its post-quantum halfCapture key
Pilot (deploy/pilot)secrets/recorder.key and secrets/recorder.pq.key, made by docker compose run --rm keys and installed by setup into the signing volume, which only the recorder mountssecrets/capture.key, made by the same step and installed by setup into the read-only keys volume
Compose (deploy/compose)secrets/recorder-signing.key and secrets/recorder-signing.pq.key, installed by setup into the signing volume, which only the recorder mounts; setup refuses to start the stack without eithersecrets/capture.key, installed by setup into the read-only keys volume, for the recorder, the viewer and the collector
Kubernetes (deploy/kubernetes)the bitexact-recorder-secrets Secret, both files projected into the recorder alone (the viewer is given only the license from it)the bitexact-capture Secret, projected into the recorder and the viewer and mounted in each collector sidecar
systemd (deploy/systemd)/etc/bitexact/recorder-signing.key and /etc/bitexact/recorder-signing.pq.key, owned by the bitexact user/etc/bitexact/capture.key on the recorder, and a copy on each collector host

The recording proxy (bitexact serve) holds a third secret, the host key: 32 random bytes it seals each streamed response's write-ahead journal under (capture-controls.md, What else the policy reaches). It reads the file BITEXACT_HOST_KEY names or, unset, <data directory>.host.key beside the data directory, and makes it owner-only on first use when it is missing. It is never inside the data directory - a path there is refused - so a backup of the store never carries the key that opens a journal left behind, and only a process holding it opens one (bitexact capture journal FILE). In a container the data directory is a volume such as /data, and its default place (/data.host.key) is on a root the image's user cannot write: name a writable file, or one made ahead, outside the data volume. The pilot's keys step makes secrets/journal-host.key and setup installs it with the other secrets, as BITEXACT_HOST_KEY for the proxy and the cli service. The Compose, Kubernetes and systemd deployments run no proxy and need none; give a proxy added to one the key as the capture key is given.

One deployment per key. A license records only when the deployment signs with a recorder key it names, and turning signing off does not slip the binding; a license that names none records nothing. A second stack needs its own key and its own license.

Copying both the license and the key to a second host cannot be refused by an offline product, but it is not invisible: every sealed run commits a per-deployment fingerprint, so one license_id emitting bundles under two fingerprints is evident in the evidence itself.

Trusted platform#

Every 90 days the license is renewed by file (Licensing), and the renewal request carries a TPM 2.0 attestation of the recorder host. It is taken only when you run bitexact license request, never while recording, so a TPM problem can delay a renewal but never stops a call or a recording. The host needs:

  • A TPM 2.0 the product can reach. On Linux, /dev/tpmrm0, readable by the user that runs the request (the tss group, or root); in a container, pass it through (--device /dev/tpmrm0; on Kubernetes, a TPM device plugin). On Windows, the TPM Base Services, which every Windows host has.
  • The endorsement hierarchy with no password, which is the default. Nothing is stored in the TPM: its endorsement and attestation keys are derived from the standard TCG templates each time.
  • The TPM's endorsement-key certificate, in the TPM's NV storage, where discrete TPMs keep it. Firmware TPMs (Intel PTT, AMD fTPM) often do not: obtain it from the manufacturer's certificate service and pass the file to the request.
  • On Linux, integrity measurement (IMA): kernel 5.8 or later, booted with ima_template=ima-ng ima_hash=sha256, securityfs mounted at /sys/kernel/security, and a policy that measures the product's files when they are loaded (measure func=FILE_MMAP mask=MAY_EXEC for the compiled modules, and measure func=FILE_CHECK for anything read as a file). The request reads the measurement log, so run it as root. A host whose policy measured none of the product's files is refused renewal: the attestation is how BitExact learns the files it renews for are the ones it shipped.
  • On Windows, the TPM measures boot, not files. Whether renewals from a Windows recorder host are accepted is stated in your Order Form; a Linux recorder, with Windows agents sending to it, attests files.

The listener#

bitexact serve binds loopback by default. For anything beyond one host, terminate TLS on the listener itself:

bitexact serve --host 0.0.0.0 --port 8787 \
    --tls-cert server.crt --tls-key server.key

Add mutual TLS — only workloads holding a client certificate signed by your CA can record:

bitexact serve ... --tls-client-ca clients.pem

The viewer takes the same pair, so bearer tokens and evidence never cross the network in the clear:

bitexact view --host 0.0.0.0 --port 8788 \
    --tls-cert viewer.crt --tls-key viewer.key

A network bind needs an auth mode as well; the viewer refuses one without (see The viewer below).

Behind a reverse proxy (nginx, Envoy, an SSO-terminating gateway), keep the recorder on loopback or a private interface and let the proxy own the public listener. The recorder does not implement SSO deliberately: identity termination belongs to the infrastructure your security team already audits. Forward X-BitExact-Run-Id unchanged.

Give the proxy the client-facing deadlines too. The recorder applies no request-read timeout of its own: a client that sends its request a byte at a time holds its connection open for as long as it likes, and enough such clients exhaust connections before they exhaust anything else. Set the request-read and header-read deadlines and a per-client connection cap on whatever fronts it (client_header_timeout, client_body_timeout and limit_conn in nginx; the equivalents elsewhere). On a loopback-only recorder with no public listener there is nothing here to set.

Reserved local routes — never proxied upstream, never recorded:

RoutePurpose
/healthzliveness: 200 whenever the proxy answers; store writability and disk headroom in the body (recording: ok or degraded, with reason)
/versionrunning version
/metricszrequest/error/recording-failure counters, latency, store size, the store-write breaker's state and trips
/health/recordingwhether calls are being recorded now (store, breaker, license, last write, disk headroom); 503 with the reason while degraded (Ops health)

Point orchestrator liveness checks at /healthz: it stays 200 while calls pass through, whatever the state of recording. Alert on /health/recording, and set BITEXACT_DISK_MIN_BYTES so it reports degraded before the disk actually fills.

Ending a proxy run over HTTP#

POST /bitexact/runs/<run-id>/seal appends a run's seal - evidence of where it ended - so it is closed until you open it. Make a secret, owner-only, and name it on the recorder:

(umask 077; head -c 32 /dev/urandom | base64 > /etc/bitexact/seal.secret)
export BITEXACT_SEAL_SECRET_FILE=/etc/bitexact/seal.secret

The clients that end runs send it as Authorization: Bearer <secret>:

curl -X POST -H "Authorization: Bearer $(cat seal.secret)" \
    http://localhost:8787/bitexact/runs/loan-42-decision/seal
AnswerWhen
200sealed (or already_sealed)
202through a host collector: the end is queued behind the run's events
401no secret, or the wrong one
403BITEXACT_SEAL_SECRET_FILE is unset, or the run was not recorded by the proxy
402the license is missing, invalid or expired (license_lapsed), or does not name this recorder key (recorder_key_unlicensed)
503the secret file cannot be used, the recorder key is not configured (signing_key_missing), or the signer failed (recorder_unavailable)
409a live SDK run holds the run
404no run by that id
400the run id is not one the store can use
405any method but POST

The route seals only runs the proxy recorded: one it opened, or - through a host collector - one this process sent events for since it started. An SDK run seals itself; the recorder seals what its sources end; anything else is bitexact seal RUN on the recorder's host. The secret lets a client end any run the proxy recorded, so give it to the clients that end runs, not to everything that can reach the proxy. The file is read on every request, so replacing it rotates the secret at once.

The viewer#

bitexact view never edits or deletes a recorded entry. It does act, and every act is itself evidence: placing or releasing a legal hold and requesting or approving a redaction append audit entries to the run's chain (an approved redaction destroys the named values in place and leaves their commitments); a fork, a fork matrix, a regress and a suite run record new runs or reports; a case pins runs on its own ledger. Every such route requires the X-BitExact-Action header and a role that may perform it, and the full route list is docs/api-reference.md. With no authentication configured every caller is admin, so bitexact view refuses any non-loopback --host until BITEXACT_VIEWER_TOKENS or BITEXACT_TRUST_PROXY_AUTH is set. Set BITEXACT_VIEWER_TOKENS to require a token. An API client sends it as Authorization: Bearer <token>. A person opening the inspector in a browser is sent to its sign-in page, which exchanges the token for an HttpOnly, SameSite=Strict session cookie lasting BITEXACT_VIEWER_SESSION_HOURS (default 8). The cookie is signed with the token, so removing a token from the configuration ends every session it opened. Sign out is in the inspector's main menu. Repeated failed sign-ins from one address are throttled. Every request emits a structured access-log line, and evidence-touching actions are additionally recorded on the run's custody chain. For team access, put it behind the same SSO proxy as everything else, and name that proxy in BITEXACT_TRUSTED_PROXY_IPS (default: loopback) so forwarded identity headers are believed from it alone - see identity-access.md. Every request body - an upload to the in-app verify endpoints included - is capped at BITEXACT_MAX_UPLOAD_BYTES (default 64 MiB, 413 above it), and the cap applies before a route authorizes; evidence larger than that is for the offline verifier. A forwarded identity header sent twice is refused.

The agent entrypoint#

Four things in the viewer execute the customer's agent: forking a decision, sweeping one N times, regressing a corpus against the code running now, and running a saved suite as a gate. All four need the callable that runs that agent, and none of them will take it from a request - that would be arbitrary import over HTTP. It is the operator's, named in an environment variable on the viewer process:

BITEXACT_AGENT_ENTRY=myapp.agent:run_once  bitexact view

The entrypoint may take the run as its argument or take none and reach for bitexact.current(); both conventions work.

Unset, each of those four reports that nothing is configured and does nothing; they are off by default. Each is gated by its RBAC action (fork, regress) and the matching license entitlement.

These jobs re-execute the agent sequentially against one rate limit, so one runs at a time; a second is refused with how far along the first is. A sweep is capped at 1000 forks from the browser - a larger one is a batch job for bitexact fork-matrix, which has no cap.

An agent that uses the explicit-capture hooks (record_decision, record_human_decision, record_context_*, record_marker) needs no changes to be forked, replayed or regressed: those hooks are inert outside a recording run, so the same entrypoint serves every mode. The request carries only data - which step to branch at, and optionally a replacement model response - and the branch is gated by the fork entitlement and the fork RBAC action like any other write.

The entrypoint must re-execute the recorded path up to the fork point. If it does not, the replay diverges and the API returns 409 naming the exact diverging step; nothing is sealed. If the agent itself raises while continuing, that is also a 409, naming the agent's own exception - a counterfactual that never ran is never presented as one. A branch is named after its source (<run>-fork-<id>) and, if it did not complete, stays unsealed.

To try it against the seeded sample, point the variable at the sample's own agent:

bitexact seed
BITEXACT_AGENT_ENTRY=bitexact.sample:sample_agent  bitexact view

Then, in the inspector: open loan-decline-sample, press ⑂ Fork, and set Replace a model response to the run's one http_call. The box fills with what the model actually returned - a DECLINE citing 46% DTI and a 604 score. Change that text to an approval and press Create fork. The branch records is_adverse_action: false with no ECOA reason codes and the reviewer's decision as approve, because the sample's agent derives its decision from the model's answer and that same code re-ran against the new one. The replaced step is marked synthetic.

Replace the response rather than forking before it. A fork point earlier than the model call sends the continuation to the real provider, which without an upstream credential returns 401. The branch is still recorded honestly - the 401 is in the chain - but the agent may not check the status, so it can seal having tested nothing. The dialog reports any such refused call rather than showing a bare success, and POST /api/forks returns them in failed_calls.

Regressing and suites from the viewer#

Replay and fork → Replay diff replays a sealed run against the code running now and reports the first divergence. Regression suites on the same screen saves a corpus and a divergence threshold as a named suite, and runs it as the gate it exists to be; the result is written back and shown there.

Comparing two implementations on the same sealed roots needs two callables at once, so a deployment may name several entrypoints:

BITEXACT_AGENT_ENTRIES=baseline=myapp.v1:main,candidate=myapp.v2:main

The default BITEXACT_AGENT_ENTRY appears in that set as default. A request picks by NAME; a module path in a request body is never imported, so offering more entrypoints does not widen what a request can cause to run - the set stays entirely the operator's. A malformed pair is dropped rather than taken as a name. With fewer than two configured, the comparison panel says so and shows the variable to set.

Agents on many servers#

To record agents on many servers into a recorder on its own server - so recording costs the agents almost nothing and the evidence never lives on the hosts it describes - run a collector on each agent host and the recorder service on its own. See distributed recording.

Never in the way#

A failure in BitExact's recording never becomes a failure in your pipeline. Where a model call passes through a BitExact endpoint, what happens when that endpoint cannot be reached depends on whether BitExact code runs on the agent's side of the call:

  • Wrapped clients (wrap_openai, wrap_anthropic) send through the run's own recording endpoint, a server inside the agent's process. If that endpoint cannot be reached - its thread has stopped, or the client is used after its run ended - the call fails over. It goes where the proxy would have sent it, by the same BITEXACT_UPSTREAM* settings, and never anywhere else: a call whose provider has no host configured (Azure, Bedrock) is not sent, and the agent gets the connection error. Each failed-over call is a declared gap: recorder_unavailable on the run, or run_ended noted beside a run that has ended. On a BITEXACT_COLLECTOR host, which keeps no store, that note goes to the host collector and the recorder writes it beside the run. verify then exits 3. Only a connection that never opened fails over, so no call is sent twice. Each call tries the endpoint first, and no failover lasts past the call it was made for. The endpoint is on loopback, so its connect is held to 100 ms: a stopped endpoint adds at most that to a call, not the operating system's connect retries (about 2 s on Windows). The upstream call keeps the client's own timeout. Replay, regression and fork runs never fail over. Strict mode raises the connection error instead.
  • The recording proxy (bitexact serve), which a client reaches by URL, and bitexact mcp-proxy, over stdio or HTTP. No BitExact code runs on the agent's side of these connections, so nothing of ours can fail the call over. bitexact.mcp.wrap_session runs in the agent, but it wraps a session already connected through the proxy, and an MCP session cannot move to another server partway. If the proxy's process is not running, calls pointed at it fail as calls to any stopped service would. Over stdio the MCP server itself is the proxy's child, so it stops with the proxy. Where a proxy outage must not reach the agent, use the SDK or the collector. Where you run a proxy, run it beside the agent it serves, under a restart policy, with a liveness probe on /healthz (see The listener).
  • The SDK's other capture (record_tool, the framework adapters) and the collector, which takes events from an in-process queue, put no process of ours in the call path.

When the recorder cannot do its job - an expired license, a full disk, an unreachable database, a body over BITEXACT_MAX_BODY_BYTES, an argument it cannot serialize, a run id it refuses - the agent's call goes through exactly as it would without BitExact, and the evidence says what it missed:

  • The response says so. Through the proxy, a call recorded with a gap carries x-bitexact-recording: degraded; a call not recorded at all carries off. Through the SDK, run.recording_state is recording, degraded or off, and run.refusal holds the error that turned the run off.
  • The run says so. The run records a declared capture gap: a signed marker naming the cause (license_lapsed, signing_key_missing, recorder_key_unlicensed, build_modified (this build's files are not the release's), payload_oversize, storage_unavailable, serialization, invalid_argument, recorder_unavailable, run_id_rejected, run_not_owned, journal_failed, run_ended, run_context_lost, spool_overflow, queue_overflow, intake_rejected, source_gap, source_conflict, late_after_gap, causal_inconsistency, checkpoint_failed, unclassified; docs/bundle-spec.md says what each means), where it happened, and the size of any body it withheld with its SHA-256 - keyed with the capture key, or dropped with no key, under a capture policy. A field the capture policy could not scrub in time (scrub_failed), or keeps hash-only with no capture key (capture_key_unavailable), is declared in the entry itself and counted with the gaps (docs/capture-controls.md). A capture policy that cannot be used opens no run at all (capture_policy_invalid, on the refusal ledger): the agent runs on, unrecorded. A gap that cannot join the chain (the run was already sealed) is noted beside the run; one with no run at all, or one neither the chain nor the note beside the run will take, goes to the log as capture_gap_unrecorded, every time. So does run_not_owned: a call through the proxy that names a run another writer opened - an SDK run, a run the recorder writes for its sources - is forwarded, answered x-bitexact-recording: off, and written nowhere in that run: no entry, no gap, no note beside it. It is counted and logged with the run id; strict mode refuses it with a 403.
  • Verification says so. bitexact verify exits 3 on a run that declares gaps, listing each cause, until --allow-gaps accepts them; the viewer marks the run "incomplete capture"; the standalone verifier prints the count beside its OK. An intact chain of an incomplete capture is never presented as a complete record.
  • Operations sees it. The proxy's /metricsz counts degraded_total and unrecorded_calls_total by cause, and each change of state is logged at warning: bitexact_degraded when a cause starts degrading the process, bitexact_recovered when an entry next commits with no gap, and bitexact_degraded again if the cause recurs. The proxy's /health/recording answers 503 while recording is degraded, and /healthz stays 200 for traffic.
  • A dead store costs nothing per call. After 5 consecutive store faults (BITEXACT_BREAKER_FAILURES) the store circuit breaker opens: for 60 seconds (BITEXACT_BREAKER_OPEN_S) the store is not asked anything - no write, and no read either, such as the lookups the proxy makes to see whose run a call names - so no call waits on a store that will not answer, and each call is a declared storage_unavailable gap as any failed write is. A write that fails is a fault; a read is one only when the disk or database failed, not when it found a damaged run. Then one trial goes through: a write that commits closes the breaker, a fault opens it for another full period. It stands in front of every backend. Releasing a run's lock and the /healthz store check are never held back. Each change of state is logged at warning as store_breaker; /metricsz reports store_breaker_state (closed, open, half_open) and store_breaker_trips_total. A value that is not a positive number leaves the default.

BITEXACT_FAILURE_MODE=strict restores fail-closed recording - the caller gets an error and no unrecorded response is delivered - for development and conformance testing. Replay, regression and fork runs always raise: they serve answers from the record, so a missing record is their failure.

Storage and the disk-full policy#

The data directory must be a local filesystem (advisory locks are not reliable on NFS/CIFS). When a write or fsync fails — disk full included — the call is still delivered and the run declares a storage_unavailable gap (Never in the way); nothing already recorded is corrupted. Watch /health/recording, set BITEXACT_DISK_MIN_BYTES, and size the volume for your retention window. BITEXACT_MAX_BODY_BYTES (default 32 MiB) caps any single recorded body, and how much of one the recorder holds in memory: a larger one is delivered as it arrives and declared as a payload_oversize gap carrying its size and its SHA-256, keyed with the capture key, or dropped with no key.

Retention#

On the embedded file store:

bitexact purge --older-than 90 --archive /evidence-archive
bitexact purge --older-than 365 --delete --yes   # destroys evidence

Archived runs keep every file (manifest, sidecars, quarantines, reports) and verify from the archive unchanged. Regress reports accumulate beside manifests and are covered by the same purge.

On the PostgreSQL backend purge refuses, by design: it reclaims the file store by scanning runs/ on disk, and there is no such directory. Use bitexact archive instead, which moves a whole month out of the hot tables and leaves a catalog row behind. See PostgreSQL.

HA stance, stated plainly#

One recorder writes one run. The per-run OS lock makes a second writer fail loudly instead of corrupting a chain — contention is an error you see, not a mode you configure. Run one recorder per agent workload (sidecar or per-host); scale by partitioning runs across recorders, not by clustering writers.

Backup and restore#

PostgreSQL (BITEXACT_DATABASE_URL set): the evidence is in the database, and file tooling over the data directory copies almost nothing. Use continuous archiving for point-in-time recovery - see PostgreSQL.

The embedded store is the whole data directory (BITEXACT_DATA_DIR): runs, their checkpoints and custody, the transparency log, case and workflow ledgers, suites and packs. Back up all of it, together.

  • Best: an atomic snapshot of the volume it lives on (a ZFS, LVM or cloud disk snapshot, or a Kubernetes VolumeSnapshot). It needs no pause.
  • Otherwise copy it while nothing writes - stop the recorder, the proxy and any SDK process recording into it - or accept that a run being written as the copy runs can restore with a torn last line. The store quarantines such a tail (<run>.jsonl.torn) and bitexact health reports it as a seal-failure; the entries before it verify.

The restore drill - run it on a scratch host, on a schedule, not only when you need it:

  1. Restore into an empty data directory, or a fresh database.
  2. bitexact reindex (the embedded store's index is derived; rebuild it).
  3. bitexact health - every run is re-verified; exit 2 on any critical finding, such as a run that fails verification. A quarantined torn tail is a seal-failure warning: status degraded, exit 0, so read the report.
  4. Export one recent run per workflow (bitexact export <run> -o r.json) and verify it with the standalone verifier, pinned to your recorder key: that is the check an auditor will make.
  5. If runs are anchored, bitexact health has compared each head against its anchor: a restore that rolled history back below an anchor fails there as anchor-rollback.

Image supply chain#

The Dockerfile pins its base image by digest and builds nothing: it installs one target's release bundle - the compiled product wheel, the open verifier wheel, that target's hash-locked requirements.lock and every dependency wheel it names - with --no-index --require-hashes, then pip check. It carries OCI labels, runs as a non-root user, and health-checks /healthz. Images exist for x86_64 and ARM64 under one multi-architecture tag.

The vendor-side release pipeline is scripts/release.sh, run on the release workstation (releasing.md): after the full checks pass on the tagged commit it compiles the product for every target in scripts/targets.py, with the vendor keys baked in before compiling; writes and signs each build's file manifest; checks every build in a clean runtime of its own kind; generates a CycloneDX SBOM per target (scripts/gen_sbom.py); pushes the images, signs their digest and attests the SBOMs with cosign (customers pin cosign.pub); and attaches the bundles, SBOMs, image tarballs, SDKs and deployment files to a draft release with a cosign-signed SHA256SUMS. deploy/compose and deploy/kubernetes name the image by tag for readability; pin the digest the image line of SHA256SUMS gives (ghcr.io/bitexact/bitexact@sha256:...) in production. deploy/pilot takes the image you loaded from the release tarball through BITEXACT_IMAGE, which the quick start pins to that image's digest.

Every release build carries the vendor keys, compiled in: the build refuses to start without them, and a license is checked against those keys and no other. There is no build in which a customer can sign their own license.

Environment reference#

Every variable the product reads, generated from the code by tools/gen_reference.py (a test fails when one is missing).

VariableMeaning
BITEXACT_ACTORThis agent's stable name, sent with every event through the collector; replay serves each actor its own calls.
BITEXACT_AGENT_ENTRIESNamed entrypoints (name=module:function,...) a suite can compare.
BITEXACT_AGENT_ENTRYmodule:function the viewer forks, sweeps and regresses with.
BITEXACT_AGENT_PACKAGESPackages to treat as agent code, not infrastructure, when the agent itself is installed under site-packages - their nondeterminism is then captured rather than skipped. Not an import allowlist and not a security control.
BITEXACT_BREAKER_FAILURESConsecutive failed store writes that open the store-write circuit breaker (default 5; Never in the way).
BITEXACT_BREAKER_OPEN_SSeconds the open breaker attempts no store write before one trial (default 60).
BITEXACT_CAPTURE_KEYA file holding the capture key (64 hex characters, from bitexact capture keygen), provisioned beside the recorder key and kept out of the data directory and its backups. Hash-only fields are committed under it and a gap's digest of content it did not store is keyed with it; without one, hash-only fields can never be opened and those digests are dropped. A policy's own hash_only_key takes its place.
BITEXACT_CAPTURE_POLICYThe capture policy: a JSON file saying what the evidence keeps of each field - kept, scrubbed by detectors (card numbers, IBANs, credentials by default) or hash-only under a capture key the evidence never holds (see capture-controls.md). Unset, the shipped default scrubs; off keeps every value as captured and every run records that it was off. A policy that cannot be used refuses the process at start.
BITEXACT_CHECKPOINT_EVERYSteps between recorder checkpoints when a signing key is configured, after the one every run gets at its first step (default 100).
BITEXACT_CLI_PREFIXThe command that reaches the CLI on this deployment, when the default for its BITEXACT_DEPLOYMENT is not it (for example docker exec bitexact-viewer bitexact). The viewer's command hints start with it.
BITEXACT_COLLECTORThis host's collector intake (http://127.0.0.1:7378). When set, the SDK and bitexact serve record through the collector to a remote recorder instead of into a local store: events are queued and never waited on, and the agent host needs no store and no license.
BITEXACT_COLLECTOR_HOST_KEYThe file holding bitexact collector's 32-byte host key, which seals its spool segments. Default: <spool>.host.key beside the spool directory, made owner-only on first use; a path inside the spool is refused, so a copy of the spool never carries the key that opens it. A container whose spool is a volume names a writable file on another volume. Not used when BITEXACT_HOST_KEY_KMS_CIPHERTEXT is set.
BITEXACT_COLLECTOR_SECRETThe host secret for BITEXACT_COLLECTOR.
BITEXACT_COLLECTOR_SECRET_FILEA file holding the host secret for BITEXACT_COLLECTOR - the collector writes it as intake.secret in its spool directory. A collector named with no secret turns recording off, never the agent.
BITEXACT_DATABASE_URLPostgreSQL DSN; when set the database is the store and the index (see postgres.md).
BITEXACT_DATA_DIRWhere manifests, sidecars and the run index live (default ./bitexact-data).
BITEXACT_DB_CONNECT_TIMEOUTSeconds to wait for the PostgreSQL pool to open before reporting the database unreachable (default 10).
BITEXACT_DB_LOCK_TIMEOUT_SSeconds a PostgreSQL statement may wait for a lock (default 5): an append never queues for long behind a maintenance lock such as a month's drop.
BITEXACT_DB_STATEMENT_TIMEOUT_SSeconds any one PostgreSQL statement may run before the database cancels it (default 30). A write an agent's call waits on then fails within the bound - a declared gap, never a hung agent; whole-store maintenance (a rebuild, a month's drop) lifts it for its own transaction.
BITEXACT_DEPLOYMENTHow the viewer is deployed - pilot, compose, kubernetes or container, set by the shipped compose files, manifests and image; unset for a pip install. The viewer reports it in /api/env with the command a person types there to reach the CLI, and its license page says where the license file goes.
BITEXACT_DISK_MIN_BYTESFree space below which the proxy's /health/recording answers 503 degraded, naming the shortfall, before the disk fills; /healthz stays 200 for traffic. Recording goes on; a write that then fails is a declared gap.
BITEXACT_DRAIN_TIMEOUTSeconds the recorder waits for in-flight streams to finalize on shutdown, and a Python SDK run waits for the entries its writer thread still holds: at the run's exit, and when the agent reads an entry's handle (default 30). Past it the agent goes on: each entry still unwritten is declared a storage_unavailable gap beside the run, naming the journal bitexact repair folds it from, and the run is left unsealed.
BITEXACT_ENVThe environment label the viewer shows and the sample records (prod, staging, ...).
BITEXACT_EVENTS_CONFIGThe TOML file naming where events are delivered - Standard Webhooks receivers, Splunk HEC, Datadog Logs - with every secret in a file of its own (see events.md). Unset: nothing is enqueued and nothing is sent. A file that does not load turns delivery off and raises events-config-invalid.
BITEXACT_FAILURE_MODEopen (default): a BitExact failure never reaches the agent - the call is forwarded untouched, the response carries x-bitexact-recording: degraded (or off with no valid license), and the run gets a declared capture gap. strict: BitExact's own failures are raised and refused as errors, for development and conformance testing.
BITEXACT_GROUP_PRECEDENCEThe bound groups in the order that decides a principal's role when it is in more than one (comma-separated, most authoritative first). A principal in two or more bound groups the list does not all rank holds no role. A principal in one bound group needs no entry.
BITEXACT_HASH_ALGblake2b-256 (default) or sha256 for FIPS-constrained deployments.
BITEXACT_HEALTH_PORTPort the container's HEALTHCHECK probes /healthz on (default 8787); set it to the inspector's port when running the image as the viewer.
BITEXACT_HEARTBEAT_SSeconds between liveness heartbeats (default 60): the collector's to its recorder, which tells a quiet agent from a dark host, and the row the served proxy and the in-process SDK upsert for each capture path they record through. A collector refuses to start on a value that is not a positive number of seconds; the proxy and the SDK use 60.
BITEXACT_HLC_MAX_SKEW_MSHow far ahead of bitexact recorder's own clock a source's hybrid logical clock may run (default 300000) before the recorder declares a causal_inconsistency gap beside the event.
BITEXACT_HOST_KEYThe file holding this host's 32-byte key, which seals the proxy's stream journals (bitexact capture journal opens them only on the host that sealed them). Default: <data dir>.host.key beside the data directory, made owner-only on first use; a path inside the data directory is refused, so a copy of the store never carries the key that opens it. A container whose data directory is a volume names a writable file outside it.
BITEXACT_HOST_KEY_KMS_CIPHERTEXTA file holding this host's key encrypted by AWS KMS: the CiphertextBlob of aws kms generate-data-key --key-spec AES_256. Set, the host key that seals the collector's spool segments and the proxy's stream journals is that 32-byte data key, decrypted by KMS once per process into memory; no host-key file is read or made, and BITEXACT_HOST_KEY is not used. A ciphertext KMS will not decrypt, a plaintext that is not 32 bytes, or a process without boto3 (pip install 'bitexact-sdk[aws]') is refused at start. Only a host whose IAM role may decrypt it opens what it sealed.
BITEXACT_HOT_MONTHSMonths kept queryable before bitexact archive moves a month to the archive.
BITEXACT_HSM_KEY_LABELLabel of the ed25519 key object inside the HSM.
BITEXACT_HSM_PINPKCS#11 user PIN.
BITEXACT_HSM_PKCS11_LIBPKCS#11 module path for the recorder key.
BITEXACT_HSM_PQ_LABELLabel of the ML-DSA key object inside the HSM: the recorder key's post-quantum half, signed with CKM_ML_DSA.
BITEXACT_HSM_SLOTPKCS#11 slot holding the recorder key.
BITEXACT_IMAGE_DIGESTThe container image digest committed into every run's watermark for attribution: the operator's statement of what is running, never measured and never verified. The pilot sets it from the image ID step 1 resolves; another deployment sets it to the digest it pulled (a Kubernetes pod cannot read its own image digest, so name it in the manifest). Unset, the watermark names none.
BITEXACT_LICENSEPath to the offline license file. Nothing records or replays without one; the proxy passes calls through unrecorded.
BITEXACT_LOG_EVERY_SHow often the recorder service signs the transparency log's head and asks the witnesses to cosign it, when the log has grown (default 60).
BITEXACT_LOG_ORIGINThe transparency log's name in its checkpoints (default bitexact.log/<recorder key id>). Witnesses and auditors name the log by it.
BITEXACT_LOG_WITNESSESComma-separated witness URLs asked to cosign the transparency log's checkpoints (by bitexact log checkpoint and the recorder service). Unset: checkpoints are signed and unwitnessed.
BITEXACT_MAX_BODY_BYTESLargest request or response body, or MCP message, the recording proxy and bitexact mcp-proxy hold to record (default 33554432, 32 MiB; a value that is not a positive whole number uses the default). A body over it is never held: it still reaches the agent whole, as it arrives, and its record is withheld, declared payload_oversize by its hash and length. Strict mode refuses it instead.
BITEXACT_MAX_UPLOAD_BYTESCap on any request body the viewer accepts, including a bundle or report uploaded to its in-app verify (default 64 MiB).
BITEXACT_MODEDefault run mode for the SDK: record or replay.
BITEXACT_OTEL_CONTENTHow content ingested from OpenTelemetry spans by bitexact.otel_span_processor() and bitexact.record_otel_spans() is kept: keep (default) leaves it to the capture policy; hash records a tool call's args and result and a model call's request and response hash-only. Any other value is taken as hash, so a mistyped setting never stores more than was asked. It applies to spans alone, where BITEXACT_CAPTURE_POLICY matches a field by kind and path whichever door recorded it; a processor's or batch's content= argument takes its place (see integrations.md).
BITEXACT_PQ_ALGThe post-quantum parameter set bitexact keygen makes: ML-DSA-65 (default) or ML-DSA-87 (CNSA 2.0).
BITEXACT_PRICING_FILEThe customer's own pricing table, a JSON file: {"currency": "USD", "per_tokens": 1000000, "models": {"<model as the run records it>": {"input": "2.50", "output": "10.00", "aliases": ["gpt-4o"]}}}, with optional cache_read_input, cache_write_input, cache_write_input_5m, cache_write_input_1h, audio_input, audio_output and tool_use_input rates, every rate a decimal string, and no name given twice. A call is priced by the model its provider reported running, else the one it requested; a count with no rate leaves it unpriced, never dropped. Cost is derived from the token counts each provider reported, shown in the inspector and by bitexact cost, never carried in an evidence pack, and never written to a chain. Unset, BitExact reports tokens only: it never supplies prices. A table that cannot be used is refused by the inspector and bitexact cost, never replaced by a default.
BITEXACT_RECORDERThe recorder (HOST:PORT) a keyless inspector or CLI hands its acts to. When set and no recorder key is configured, the process writes no chain itself: holds, releases, redactions, custody records, forks, ledger events, seals, anchors, purge records and log checkpoints are acts the recorder checks, writes and signs, and the license binding and log audit read the key's public halves from it. Its acts are refused when the recorder cannot be reached.
BITEXACT_RECORDER_POLICYJSON file of run-token issuance rules for bitexact recorder: {"rules": [{"peer": glob, "runs": glob, "kinds": [...], "acts": [...]}]}. A collector gets a token only for the kinds a rule gives its certificate identity in that run; acts are the acts a peer may hand the recorder (docs/distributed.md). A deployment act (ledger, log-checkpoint, log-backfill, ledger-heads, access, events-retry) is granted only by a rule whose runs is exactly *, matched as written, never as a glob. With no policy the recorder issues nothing. Read on every request.
BITEXACT_RECORDER_TLS_CAThe CA file a process that names BITEXACT_RECORDER checks the recorder's certificate against.
BITEXACT_RECORDER_TLS_CERTThe client certificate a process that names BITEXACT_RECORDER presents: the identity the recorder's policy grants acts to.
BITEXACT_RECORDER_TLS_KEYThe private key of BITEXACT_RECORDER_TLS_CERT.
BITEXACT_REQUIRE_DUAL_CONTROLSet to 1 to require a second principal to approve every redaction, and to refuse on the command line, where --by is typed and not authenticated, every redaction request or approval and every removal of a critical capture expectation.
BITEXACT_RUN_GRANTSet by the SDK, never by hand: bitexact.mcp.proxy_env() puts the active run's grant (JSON: its id, its loopback endpoint and its secret) in the environment it starts bitexact mcp-proxy with, so the proxy records into that run. Absent, the proxy records only into runs it may write itself.
BITEXACT_SDK_REQUEST_LOGSet to 0 to stop the SDK's in-process recorder printing a JSON line per model call to stderr (default: every call is logged). Failures are always logged; the standalone recorder is unaffected.
BITEXACT_SEAL_GRACE_SHow long bitexact recorder waits, after a run is asked to end, for every source that wrote to it to finish (default 30). Then it seals the run anyway, with a declared source_gap for each source still open.
BITEXACT_SEAL_SECRET_FILEA file holding the secret (at least 32 characters) a client sends as Authorization: Bearer <secret> to end a run through the proxy's POST /bitexact/runs/<run-id>/seal. Unset, the route refuses every seal; set, it still seals only runs the proxy recorded. Read on every request, so replacing the file rotates the secret.
BITEXACT_SIGNING_KEYPath to the recorder's ed25519 private key; with it every run is checkpointed and signed.
BITEXACT_SIGNING_KMS_CIPHERTEXTPath to the AWS KMS-encrypted recorder key envelope (see recorder-key-custody.md).
BITEXACT_SIGNING_KMS_PQ_CIPHERTEXTPath to the AWS KMS-encrypted post-quantum half of the recorder key.
BITEXACT_SIGNING_PKCS11Set to 1 to sign with a PKCS#11 HSM key instead of a file (see recorder-key-custody.md).
BITEXACT_SIGNING_PQ_KEYPath to the recorder key's post-quantum (ML-DSA) half, when it is not <name>.pq.key beside BITEXACT_SIGNING_KEY (see recorder-key-custody.md).
BITEXACT_SSO_AUDIENCEThe client id the recorder requires in an ID token's aud (with several audiences, also its azp).
BITEXACT_SSO_ID_TOKEN_FILEA file holding the operator's ID token, for a command line that names BITEXACT_RECORDER: every act it sends carries it, so a recorder that verifies who acts (BITEXACT_SSO_ISSUER) accepts its acts when --by is the principal the token proves. A file named and unreadable sends no act.
BITEXACT_SSO_ID_TOKEN_HEADERThe request header an inspector in the SSO proxy mode (BITEXACT_TRUST_PROXY_AUTH) reads the signed-in person's ID token from, to hand the recorder with each act (default Authorization, as oauth2-proxy's --pass-authorization-header sends it; a Bearer scheme is taken off). Read only from a trusted proxy peer; sent twice, it is not read.
BITEXACT_SSO_ISSUERSet on the recorder to have it verify who acts: the OpenID Connect issuer, exactly as its ID tokens' iss. Every act that names a person - a hold, a release, a redaction request, approval or rejection, a custody or access record, a dead-letter retry, a ledger event - must then carry that person's ID token, which the recorder verifies itself (signature against the issuer's keys, issuer, audience, expiry and not-before); an act whose by is not the principal the token proves, or that carries none, is refused. The records it writes carry a verified_identity (issuer, subject, claim, principal), never the token; two-person control compares verified people; a role grant needs no stated posture. Needs BITEXACT_SSO_AUDIENCE and one of BITEXACT_SSO_JWKS_FILE / BITEXACT_SSO_JWKS_URL; the recorder refuses to start with any of them set without the rest. Unset, principals are the inspector's statement, recorded as asserted.
BITEXACT_SSO_JWKS_CACHE_SSeconds the recorder uses keys fetched from BITEXACT_SSO_JWKS_URL before fetching them again (default 900).
BITEXACT_SSO_JWKS_FILEA pinned JWKS file holding the issuer's signing keys, for an offline or air-gapped recorder. Read again when it changes, so rotating keys is replacing the file. Exclusive with BITEXACT_SSO_JWKS_URL.
BITEXACT_SSO_JWKS_REFRESH_SThe least seconds between two fetches of BITEXACT_SSO_JWKS_URL for a key id the set does not hold (default 30), so a rotation is picked up at once and unknown key ids cannot make the recorder hammer the issuer.
BITEXACT_SSO_JWKS_URLThe issuer's JWKS endpoint (https://; plain http:// only on loopback). Fetched keys are used for BITEXACT_SSO_JWKS_CACHE_S; a token naming an unknown key id fetches the set again, at most once per BITEXACT_SSO_JWKS_REFRESH_S, and each fetch replaces the whole set, so a withdrawn key is withdrawn. Keys past the cache period that cannot be fetched again are not used.
BITEXACT_SSO_LEEWAY_SSeconds of clock skew the recorder allows on an ID token's exp, nbf and iat (default 60, at most 300).
BITEXACT_SSO_PRINCIPAL_CLAIMThe ID-token claim the recorder takes as the principal: sub (default) or email, which must then be one the issuer verified (email_verified: true). Two acts are one person when they carry one issuer's subject, whatever this claim says.
BITEXACT_TRUSTED_PROXY_IPSPeers whose forwarded identity headers are believed (default loopback; * for any).
BITEXACT_TRUST_PROXY_AUTHSet to 1 to take identity from the reverse proxy's X-Forwarded-* headers.
BITEXACT_TRUST_PROXY_GROUPSRead X-Forwarded-Groups and resolve the role from a bound group, for a proxy that forwards membership rather than a role. Off by default: the header is one many deployments already emit for other consumers, and reading it without an opt-in would silently change what an existing proxy grants. With it on, a request carrying the header twice is refused; with it off the header is not read at all.
BITEXACT_UPSTREAMOpenAI-compatible upstream the recorder forwards to (default https://api.openai.com).
BITEXACT_UPSTREAM_ANTHROPICAnthropic upstream the recorder forwards to (default https://api.anthropic.com).
BITEXACT_UPSTREAM_AZUREAzure OpenAI resource for openai/deployments/... calls, e.g. https://<resource>.openai.azure.com. No default: unset, such a call is refused (502) rather than sent to another provider with its key.
BITEXACT_UPSTREAM_BEDROCKAmazon Bedrock runtime for model/<id>/... calls, e.g. https://bedrock-runtime.us-east-1.amazonaws.com, authenticated with a Bedrock API key. No default: unset, such a call is refused (502).
BITEXACT_UPSTREAM_GEMINIGoogle Gemini API upstream for v1beta/... and models/<m>:<method> calls (default https://generativelanguage.googleapis.com).
BITEXACT_UPSTREAM_VERTEXVertex AI upstream for v1/projects/... calls (default https://aiplatform.googleapis.com; set a regional host such as https://us-central1-aiplatform.googleapis.com).
BITEXACT_VAPI_HMAC_SECRET_FILEA file holding the HMAC secret (at least 32 characters) of a Vapi HMAC credential in its default form: x-signature is the hex HMAC-SHA256 of <x-timestamp>.<body>. bitexact.vapi.route() accepts a request that passes this or BITEXACT_VAPI_SECRET_FILE. Read on every request.
BITEXACT_VAPI_SECRET_FILEA file holding the shared secret (at least 32 characters) Vapi sends to bitexact.vapi.route() as Authorization: Bearer <secret> or X-Vapi-Secret: <secret>. With neither this nor BITEXACT_VAPI_HMAC_SECRET_FILE set the route answers 503 and records nothing. Read on every request, so replacing the file rotates the secret.
BITEXACT_VIEWER_COOKIE_SECURESet to 1 to mark the session cookie Secure even when the inspector cannot see that the browser used HTTPS. Without it the cookie is Secure when the request arrived over HTTPS directly, or through a trusted proxy that sends X-Forwarded-Proto: https.
BITEXACT_VIEWER_REPORT_INLINE_RUNSLargest number of runs the viewer's whole-store reports (overview, health, coverage, retention, trust, a workflow's matrix) cover per request (default 2000). Past it a report is computed in the background, served with the instant it was computed (X-BitExact-As-Of), and answered 202 until the first computation finishes.
BITEXACT_VIEWER_SESSION_HOURSHow long a browser sign-in with a viewer token lasts (default 8, at most 168). The session cookie is signed with the token, so removing a token from the configuration also ends every session it opened.
BITEXACT_VIEWER_TOKENSComma-separated token:role or token:name:role entries giving each token a role. A named token's principal is token:<name>, so two people with named tokens are two principals and can complete a two-person redaction; an unnamed token's principal is token:<role>. The separator is a colon, a name is letters, digits and ._@+-, and a value this cannot parse, or one set but empty, fails closed, refusing every request.
LANGCHAIN_PROJECTOlder name of LANGSMITH_PROJECT, honored the same way.
LANGSMITH_ENDPOINTLangSmith endpoint recorded as a cross-reference by the LangChain adapter.
LANGSMITH_PROJECTLangSmith project recorded as a cross-reference by the LangChain adapter.