BIJOTEL
Tamper-evident HMAC audit chain for LLM applications.
BIJOTEL turns the spans your OpenTelemetry GenAI instrumentation already
emits into a HMAC-sealed chain on disk, content-addressable storage with
semantic dedup, and a pre-call policy gate that audits before it blocks.
It's a plug-in to whatever tracer you have (OpenLLMetry,
AnthropicInstrumentor, custom wrappers) — it does not replace your
tracer; it extends it.
Status: v2.17.0 (this release; the latest on PyPI at the time of
writing is 2.16.0, 41 releases, counted 2026-09-10). Production: GENA
(9-ecosystem AI mesh, x86_64, Nuremberg) running BIJOTEL since 2026-05-10,
currently 2.15.0 in the 4 of its 9 containers that carry bijotel, and
ARA (AI Research Agency, aarch64, Helsinki) since 2026-05-25,
currently 2.16.0 (both verified in-container 2026-09-10; 2.15.0 was
released 2026-06-08 and 2.16.0 on 2026-06-15, see the changelog).
Chains as of 2026-09-10T13:53Z, from PROOF.md: GENA 74,773
entries over 123 days, ARA 21,784 entries over 108 days, each
seq 1..N with no gaps, keyless link check VALID over the last 1,000 rows
of each; both chain heads are witnessed daily in Sigstore Rekor through
the federation cross-anchor (100 anchors since 2026-06-05, all 100 with a
Rekor log index, as of 2026-09-10T13:53Z). 2 LLM providers in the same
chain (Anthropic + xAI; the OpenAI SDK adapter is shipped). The dated
snapshot is in PROOF.md (regenerated manually for now; see
docs/ops/proof-page.md).
The 14 bijuterii layers report active on GET /layers of a default
bijotel serve engine; which capabilities run in production versus ship
only is listed in the three-state table in
docs/threat-model.md.
Multi-provider chain (v2.0.0)
The HMAC-sealed chain handles any LLM provider that emits OTel GenAI
spans, in the same table, with the same HMAC secret, the same JCS
canonical body format. Anthropic spans (via
opentelemetry-instrumentation-anthropic) and OpenAI / xAI spans (via
bijotel.wrap()) land side-by-side. bijotel verify walks the whole
chain without distinguishing — the HMAC linkage holds regardless of
who emitted each span.
chain rows on GENA (excerpt, 2026-06-03):
seq 10606 anthropic.chat provider=anthropic claude-haiku-4-5
seq 10605 anthropic.chat provider=anthropic claude-haiku-4-5
seq 10604 openai.chat provider=xai grok-3-mini (gen4 verifier)
seq 10603 anthropic.chat provider=anthropic claude-haiku-4-5 (gen4 extractor)
...
$ bijotel verify --db chain.db
Chain VALID (10606 entries).
That's N-version programming in production: one provider extracts, another verifies; the chain records both and verifies cleanly across both.
Install
pip install bijotel
Optional extras:
pip install "bijotel[anthropic]" # Anthropic SDK + instrumentation
pip install "bijotel[openai]" # OpenAI SDK
pip install "bijotel[api]" # FastAPI + uvicorn → `bijotel serve`
pip install "bijotel[fingerprint]" # sentence-transformers (semantic dedup)
pip install "bijotel[ast]" # tree-sitter (Bash AST code safety)
pip install "bijotel[all]" # everything above
Docker
The published image bundles the [api,fingerprint,ast] extras and runs
bijotel serve --dashboard by default — REST API at /api/*, dashboard
at /:
docker run -p 8080:8080 \
-e BIJOTEL_HMAC_SECRET=your-secret-min-16-chars \
-v bijotel-data:/data \
ghcr.io/octavuntila-prog/bijotel:latest
Then open http://localhost:8080/ for the dashboard and
http://localhost:8080/api/health for the REST liveness probe. The
release workflow pushes a :<version> tag on each release to
ghcr.io/octavuntila-prog/bijotel
(:2.16.0 and :latest present on 2026-09-10; :2.17.0 exists only
once this release is tagged).
Quickstart
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
from bijotel.processors import HmacChainSpanProcessor, CasSpanProcessor
provider = TracerProvider()
provider.add_span_processor(
HmacChainSpanProcessor(
secret_key=bytes.fromhex(os.environ["BIJOTEL_HMAC_SECRET"]),
db_path="chain.db",
)
)
provider.add_span_processor(CasSpanProcessor(db_path="chain.db"))
trace.set_tracer_provider(provider)
AnthropicInstrumentor().instrument()
# Now every anthropic.chat call is sealed in the chain with full canonical
# body, prev_hash linkage, HMAC, and CAS-deduped body storage.
Generate a fresh secret:
export BIJOTEL_HMAC_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
Verify integrity later:
bijotel verify --db chain.db
CLI
After install, the bijotel command exposes 8 subcommands:
bijotel verify --db chain.db # full HMAC re-verification
bijotel inspect --db chain.db 4952 # one entry's canonical body
bijotel stats --db chain.db # chain + CAS + policy stats
bijotel list --db chain.db --since 2026-05-20 # filterable browsing
bijotel export --db chain.db --output out.json # signed portable JSON
bijotel verify-export out.json # auditor-side verification
bijotel regression --db chain.db --window 100 # z-score + IQR drift
bijotel serve --port 8080 --db chain.db # REST API only (Swagger at /docs)
bijotel serve --port 8080 --db chain.db --dashboard # API at /api/* + React dashboard at /
--since uses UTC calendar dates (YYYY-MM-DD, lower bound inclusive
at 00:00:00Z), consistent across all subcommands.
REST API (bijotel serve)
bijotel serve exposes 18 endpoints. Full OpenAPI 3.1 spec at
/openapi.json, interactive Swagger UI at /docs.
| Method | Path | Description |
|---|---|---|
| GET | /health |
Liveness + version + db_exists |
| GET | /version |
Package version metadata |
| GET | /docs |
Swagger UI |
| GET | /redoc |
ReDoc UI |
| GET | /openapi.json |
OpenAPI 3.1 spec |
| GET | /chain |
Paginated chain rows, since/until filters |
| GET | /chain/stats |
Aggregate counters (entries/CAS/dedup/age) |
| GET | /chain/{seq} |
One entry with full canonical body |
| POST | /chain/verify |
Smoke (default) or full=true canonical |
| GET | /policy/rules |
Active rules + closure introspection |
| POST | /policy/evaluate |
Dry-run a request through the engine |
| GET | /layers |
14-layer bijuterii manifest |
| GET | /regression/latest |
Most recent persisted run |
| GET | /regression/history |
Paginated timeline |
| POST | /regression/run |
Execute fresh run (optionally persist) |
| POST | /export |
Download a signed JSON snapshot |
| POST | /export/verify |
Upload + return validity + reason |
Optional Bearer auth
Set BIJOTEL_API_KEY on the serve process and all endpoints (except
/health, /version, /docs, /redoc, /openapi.json) require
Authorization: Bearer <key>. Constant-time comparison
(hmac.compare_digest). Empty / unset env = no auth (dev mode).
Dashboard
A React/Vite dashboard ships in src/bijotel/dashboard/:
| Page | URL | What it does |
|---|---|---|
| Chain Explorer | /chain |
4 stats cards + paginated table + click-row → side panel with full canonical body; Verify + Export buttons |
| Policy Decisions | /policy |
Active-rules grid + live Evaluate form (dry-run a prompt) + layers grid |
| Regression Monitor | /regression |
Status cards + recharts timeline (24h/7d/30d/all) + per-dimension breakdown + Run-Now panel |
| System Status | /system |
Full 14-layer manifest table |
Bundle stays under 100 KB gzip on initial load thanks to per-route code
splitting; the heavy recharts chunk lazy-loads only when
/regression is visited.
Dev:
cd src/bijotel/dashboard
npm install
npm run dev # http://localhost:5173 with /api proxied to :8080
Production build → dashboard_dist/ at project root. Day 12 polish
wires bijotel serve --dashboard to mount it as static.
14 AI safety bijuterii — all active in v2.0.0
Each layer maps to a catalog pattern. status reflects the live
GET /layers response on a healthy production install. There are
three states the endpoint can report:
- active — layer is wired in this process right now and has runtime evidence (chain rows, rule in PolicyEngine, tracker on app state, sibling JSON on disk, etc.).
- available — code ships and is importable, but nothing on this server currently uses it; the host can opt in via config.
- planned — no code yet. In v2.0.0 there are zero
plannedlayers — the catalog is whole.
On a fresh pip install with the v2.0.0 default engine
(prompt_pattern_deny + pii_detection + output_length_limit + ast_safety_check + routing_recommendation), the layers below
report active immediately once their evidence trigger is met
(see column "active when…"). The empty-chain edge case is the only
one where forensic_chain/regression start as
available — they flip to active after the first sealed span.
These states describe a bijotel serve process; for which capabilities
are exercised on the production hosts versus shipped only (and for the
boundary with substrate-guard's eBPF / OPA / Z3 / ZK-SNM / TPM claims,
none of which are in this table), see the three-state table in
docs/threat-model.md.
| # | Bijuterie | Layer | Active when… | v2.0.0 |
|---|---|---|---|---|
| 11 | Forensic-First | HMAC-SHA256 chain (HmacChainSpanProcessor) |
chain table has ≥1 row | ✅ active |
| 2 | Content-Addressable Storage | CAS unique-body table | cas table has ≥1 row | ✅ active |
| 2 | Merkle DAG | dag_nodes + dag_refs reference graph |
dag_nodes has ≥1 row | ✅ active (since v1.5.3) |
| 10 | Compliance-as-Code | PolicyEngine + 11 rule factories | engine on app state | ✅ active |
| 5 | AST-First Code Safety | tree-sitter bash + stdlib Python ast | ast_safety_check rule in engine |
✅ active (since v1.9.1) |
| 15 | Inference Routing | Pareto cost/quality/latency + per-agent budget | routing_recommendation rule in engine |
✅ active (since v1.6.0) |
| D | Containment (Combo D) | Permitted + Safe + Sealed orchestrator | ContainmentGuard on app state |
✅ active (since v1.7.0) |
| 9 | Consensus Voting | Multi-model agreement, N-version pattern | consensus_provider on app state |
✅ active (since v1.8.0) |
| 3 | Energy Accounting | per-call Wh + grams CO2 + region grid intensity | EnergyTracker attached or energy_log rows |
✅ active (since v1.9.0) |
| 16 | Regression Detection | z-score + IQR over input_tokens/output_tokens/cost | chain has ≥5 rows | ✅ active |
| 7 | Deterministic + Semantic Fingerprinting | SHA-256 + sentence-transformers | bijotel_fingerprints.db has ≥1 row |
✅ active (since v1.6.0) |
| 18 | Misalignment Probes | 29 probes across 8 attack categories | misalignment_probes_*.json sibling exists |
✅ active (since v1.9.1) |
| 19 | OTel GenAI Semconv | Compatible with OpenLLMetry, Anthropic/OpenAI instrumentors | always (semantic conventions used throughout) | ✅ active |
| 7 | Provider Protocol | AnthropicAdapter, OpenAIAdapter (xAI via OpenAI-compatible) |
always | ✅ active |
Why no more planned
Up through v1.8.0 the table carried planned for Energy
Accounting and planned for Consensus Voting — both
shipped in v1.8.0 / v1.9.0 with full tests and production proof:
- v1.8.0 — Consensus: real Haiku vs Sonnet votes recorded (factual agreement 1.00, creative agreement 0.15).
- v1.9.0 — Energy: 14-day GENA backfill produced 19.95 Wh / 7.58 g CO2; Haiku-migration savings ≈ 8× captured ex post facto.
v2.0.0 is the tag for the moment the column emptied.
What makes BIJOTEL different
- HMAC-SHA256 tamper-evident chain. Each span carries
prev_hash || canonical_hashre-hashed with a server secret. Any mutation — even reordering — breaks verification. Thebijotel-chain-v1export schema lets external auditors verify with the secret alone, no SQLite access. - Content-addressable storage with semantic dedup. Identical
request bodies share storage; the dedup factor surfaces as a metric
(
/chain/statsfield). The Merkle DAG layer (#2) enables reference-graph queries. - Pre-call policy gate with audit trail. Eight rule factories
(
prompt_pattern_deny,pii_detection,output_length_limit,model_allowlist,model_version_pin,cost_per_call_max,daily_token_budget,rate_limit_calls_per_minute) compose into aPolicyEngine. Decisions:allow/warn/deny. Warnings attach to the span viabijotel.policy.warning. Denies emit a synthetic chain entry withbijotel.blocked=true. - Statistical regression detection on the chain itself. No
separate metrics pipeline.
RegressionDetectorreads fromchain.db, computes baseline + flags drift on input_tokens / output_tokens / cost using z-score AND IQR (defaultBOTHmode minimizes false positives). - Composable with upstream OTel instrumentors. BIJOTEL adds
SpanProcessors on top of your existingopentelemetry-instrumentation-anthropic/opentelemetry-instrumentation-openaichain. It never wraps the SDK call itself, so there's no provider-specific glue to maintain.
Production validation (history)
GENA's production agent ecosystem (Aisophical) has been the rolling integration test since v0.5.0. Each snapshot below is dated; its figures were measured on that date and are not updated in place.
Snapshot 2026-06-07 (v2.14.1):
- 28+ days continuous operation (2026-05-10 → 2026-06-07), 30+ wheel deploys on GENA spanning v0.5.0 → v2.14.1. GENA + ARA both run v2.14.1; the EnergySpanProcessor chain-sealing fix (v2.14.1) is live on both. A cross-org federation service runs on ARA with GENA + ARA as the first two operators (4 Rekor cross-anchors, daily cron).
- 12,900+ chain entries on GENA + 1,400+ on ARA,
bijotel verifyreturnsChain VALIDon both — cross-version and cross-provider HMAC continuity. Both chain heads anchored daily in Sigstore Rekor. - 0 chain breaks across the 14-deploy window; the chain
processor's
BEGIN IMMEDIATEcritical section + WAL mode survived every concurrent-writer scenario including the gen4 add-on (today) writing into the same DB from a separate process. - 2 LLM providers in the chain as of 2026-05-24:
Anthropic (claude-haiku-4-5 + claude-sonnet-4) emitted via
AnthropicInstrumentor, and xAI (grok-3-mini) emitted viabijotel.wrap()on an OpenAI-SDK client pointing athttps://api.x.ai/v1. Both providers verify under the same HMAC. - All 14 layers
activein/api/layerson the defaultbijotel serveengine. - 14-day energy footprint (chain backfill, 2026-05-24): 19.95 Wh / 7.58 g CO2 across 5,459 LLM calls. Haiku migration on 2026-05-21 cut daily CO2 ≈ 8× — captured retroactively, not designed in.
- Cross-provider consensus sample (2026-05-24): Haiku vs Sonnet on a factual prompt scored 1.00 agreement (same answer); on a creative prompt scored 0.15 (genuine disagreement → flag).
Snapshot 2026-09-10 (figures from PROOF.md, measured 2026-09-10T13:53Z; per-source measurement times below):
- GENA: 74,773 chain entries (seq 1..74773, no gaps), 123 days since 2026-05-10; head age 2.1 min at 2026-09-10T13:53:42Z; keyless link check VALID over the last 1,000 rows (seq 73774..74773); bijotel 2.15.0 in-container.
- ARA: 21,784 chain entries (seq 1..21784, no gaps), 108 days since 2026-05-25; head age 185.4 min at 2026-09-10T13:53:48Z; keyless link check VALID over the last 1,000 rows (seq 20785..21784); bijotel 2.16.0 in-container.
- Federation: 100 cross-anchors since 2026-06-05, all 100 with a
Sigstore Rekor log index (measured 2026-09-10T13:53:53Z); 2
operators with 100 submissions each; last anchor
anchor_20260910T034501Z_9224cdat 2026-09-10T03:45:01Z, Rekor log index 2777280145. Each operator submits the last 10 entries of its chain once a day, so the entries between two daily submissions are not individually witnessed. - Daily export verification: each host exports its chain and runs
the keyed
bijotel verify-exportdaily; the 2026-09-10 05:30Z runs reported VALID on both GENA and ARA (host logs read 2026-09-10 12:40–13:03Z). The submitted chain heads were checked by hand against the live chains the same day (GENA seq 74377, ARA seq 21696: both match the chain'shmac_hash). - Entries per UTC day, 2026-09-04 → 2026-09-10: GENA 475–683, ARA 88–512 per day (2026-09-10 is a partial day).
- Not verified by this page: the content correctness of any entry
(whether a span records what the model actually did); the keyed HMAC
over the full chain in this snapshot (the keyless link check covers
the last 1,000 rows;
bijotel verifywith the operator key covers all rows); and the federation'scontinuity_verifiedflag, which is not evidence of a continuity check — operators compare heads by hand. This is a measurement, not a certification and not a compliance claim.
For deep production validation across Rounds 1–3 (46 tests, 0
partial/fail), see release notes in
CHANGELOG.md and the live demo with a
verify-yourself chain at https://bijotel.whiteandpoint.com.
Verifiable production proof
PROOF.md is a dated, read-only snapshot of both production
chains and of the federation cross-anchors. It is produced by
python -m bijotel.tools.proof_stats collect --chain <chain.db> [--federation <federation.db>] --label <NAME>
(one JSON per source; SQLite opened with ?mode=ro; keyless link check
over the last N rows; no network) followed by
python -m bijotel.tools.proof_stats render <a.json> <b.json> -o PROOF.md.
Every number carries its measurement time, and the page states what it
proves and what it does not. Regeneration procedure and the intended
cron: docs/ops/proof-page.md.
Known issues
- Vite dev server binds IPv6-only on some Windows installs.
curl 127.0.0.1:5173returns nothing; usecurl localhost:5173(DNS resolves to ::1) orcurl '[::1]:5173'. Browsers are fine. Only affects the dashboard dev workflow; the built bundle shipped in the wheel is unaffected. - F11 pattern coverage improved to 100% on the R1 production
probe corpus in v2.0.5 (23/23 attack vectors caught with 0 false
positives on the benign control set). Coverage on broader, unseen
attack corpora is necessarily lower; pattern expansion is iterative,
not "done". See the v2.0.5 entry in
CHANGELOG.mdfor the methodology. bijotel verifysignals failure via stderr + exit code 3, not stdout. A surfaced R3 finding; if you script the CLI, branch on exit code, not stdout substring match.
License
MIT
Metadata
Release files for bijotel 2.17.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bijotel-2.17.0.tar.gz | 551.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bijotel-2.17.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 844.9 kB
Release files / bijotel-2.17.0.tar.gz
| Download URL | bijotel-2.17.0.tar.gz |
|---|---|
| Size | 551.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
980a5dfa4cdc8f3733f727c8b0211ffd7923bfcf1892b9a05244c007fccfe1bf
|
|
BLAKE2b-256 checksum How to use checksums |
f3fe3d73a4e5fa7c39f6a36c3d41a7d7b388874ed68a338c84bb7445985c27fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / bijotel-2.17.0-py3-none-any.whl
| Download URL | bijotel-2.17.0-py3-none-any.whl |
|---|---|
| Size | 293.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b8dddeb76b70b317a018c49fd4366107c1add032fc6840aa872678a6f6e48e03
|
|
BLAKE2b-256 checksum How to use checksums |
02053308fb29bc416ddf21b7a74f00c6b0a0f85c665b32f52b9dcdaced77acdd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|