Skip to main content

Witness kernel for agentic compositions: diagnoses convention mismatches between MCP servers, LangGraph graphs, and CrewAI crews

Project description

bulla

Witness kernel for agentic compositions.

When AI agents compose tools into pipelines, implicit semantic assumptions (date formats, unit scales, path conventions) can silently produce wrong results. Schema validation passes, but the pipeline is broken. Bulla computes the coherence fee: the exact number of independent semantic dimensions that bilateral verification cannot detect.

Zero heavy dependencies. Only requires PyYAML. No numpy, no scipy, no LLM calls. Installs in under a second.

Naming: Glyph is the open standard — the composition rule, the recomputable receipt format, and the convention registry (glyphstandard.com). bulla (this repo) is its reference implementation. SEAM is the underlying theory (paper); the wider research program is Res Agentica.

Try it now

pip install bulla

30-second quickstart: compose two compositions

The fastest way to see what Bulla does — a human-readable report of exactly which fields to expose to make a composition safe:

# From a checkout of the bulla repo:
bulla compose examples/two-manifest-quickstart/example_fetch_memory_joint.yaml

Output:

  Witness rank (fee): 2  ⚠ refuse_pending_disclosure

  2 blind-spot dimensions forming 2 independent obstruction classes.

  To make this composition safe, expose 4 fields:

    1. tool `fetch`, field `encoding`
       Action: add `encoding` to fetch.observable_schema
       Bridges blind spot on edge: encoding_match
    ...

  Apply all bridges automatically:
    bulla bridge ... --output ..._bridged.yaml

See examples/two-manifest-quickstart/README.md for the full walkthrough. --format json emits the same structured WitnessReceipt as bulla witness.

Sign it, log it, verify it — without trusting the operator

A diagnosis you must take someone's word for is a score; a diagnosis anyone can re-derive is a deed. Every claim below is recomputable — run the commands, don't take the README's word:

bulla key gen                                  # local ed25519 identity (did:key)
bulla certify examples/two-manifest-quickstart/example_fetch_memory_joint.yaml --sign
bulla registry append <certificate.json>       # append-only log (RFC 6962 Merkle)
bulla registry root                            # the root you anchor + gossip
bulla registry prove <index>                   # inclusion proof for one deed
bulla verify <certificate.json> --registry <url> --trusted-root <root>  # remote: pin the root
bulla registry anchor                          # timestamp the whole log (OpenTimestamps)

The trust rule is strict by design: an inclusion proof only counts against a root you obtained independently of the host — your own log, a pinned root, or an OTS-anchored checkpoint. A remote registry's bare claim about itself is classified host-asserted and refused. What this buys: the record survives the operator (append-only, no deletion), can't be backdated past its anchor, and omission is caught because relying parties refuse the unlogged.

Recourse gate: PROCEED / REFUSE on a deed

Where bulla compose reports the seam, the recourse gate enforces a decision on a counterparty's signed, logged coherence deed — PROCEED, or REFUSE with a curable refusal that names the cure. It gates on type signals only: coherence (fee = 0) + authenticity + inclusion under a root you trust independently of the host. It does not verify delivery — a coherent liar passes; performance bonding is roadmap.

# From a checkout, after `pip install -e .`:
examples/gate-quickstart.sh        # ~5s: PROCEED on a clean deed, REFUSE-with-cure on a seam

Then watch the gate catch a lying host (an equivocated registry root) and prevent a real git breach — where the same convention disclosure that clears the coherence fee is what fixes the execution:

python calibration/recourse_gate_closes_loop_git.py   # LOOP CLOSED

Point bulla gate at your own registry + composition (bulla gate --help); nothing is hardcoded.

Other entry points

# Audit your live MCP setup (Cursor, Claude Desktop, …)
bulla audit

# Explicit config path
bulla audit ~/.cursor/mcp.json

# CI gate: fail if any composition exceeds fee threshold
bulla audit --max-fee 3 --format json

# Deterministic audit from saved MCP manifests
bulla audit --manifests examples/canonical-demo/manifests/

bulla audit auto-detects your MCP configuration when possible, scans servers, and prints a short receipt: boundary fee first (cross-server seams), then within-server blind spots, then copy-paste next steps (--max-fee, --format json). If no config is found, stderr suggests a bulla scan … command you can run with zero setup.

Bulla as MCP proxy — the safety co-pilot agents query

bulla audit and bulla compose analyze YAML before deployment. The live MCP proxy sits between an agent and its MCP backends while it runs:

bulla proxy --inject-prompt          # print the agent system prompt
bulla proxy --config servers.yaml    # spawn backends, listen on stdio

The proxy fronts N backend MCP servers as one logical server, namespaces their tools as server__tool, AND injects five bulla__* meta-tools the agent itself calls:

Meta-tool What it returns
bulla__fee Current witness rank (incremental, no recompute)
bulla__blind_spots Enumerated obstruction dimensions
bulla__bridge Repair advice classified value (apply now) vs schema (manifest edit required)
bulla__should_proceed Ternary verdict for a pending call: safe / advise / refuse
bulla__why Aristotle stamp + Lean theorem name backing the recommendation

The agent is the consumer. The system prompt at agents/system_prompt_v1.md tells it when and how to consult Bulla.

Why this is different: every other MCP-aware tool monitors compositions for humans. The Bulla proxy is the participant the agent itself queries — and bulla__why returns an Aristotle run hash plus the Lean theorem (disclosure_characterization, sheaf_realization_characterization_via_cohomology) that backs the recommendation. No competitor can attach formally-verified provenance to safety claims without an analogous formalization arc.

See examples/live-mcp-proxy/README.md for the runnable demo, telemetry walkthrough, and trust-ladder model (observe → advise → auto). The MVP runs in OBSERVE mode only — it never modifies agent traffic.

The seam problem

Two MCP servers. One uses absolute paths (/tmp/src/main.py), the other uses repository-relative paths (src/main.py). Schema validation passes. The agent silently puts the file in the wrong place. Bulla catches this before execution.

See the canonical demo → — frozen MCP manifests, real fee, walks through the bridge runtime.

Calibration results

Tested across 10 real MCP servers (filesystem, github, notion, playwright, tavily, etc.) in 45 pairwise compositions. Labels are annotation-derived (schema-vs-convention), not execution-derived — so this is calibration on a labelled corpus, and real-traffic failure prediction remains open:

Zone Fee P(mismatch) Compositions
Safe 0 0% 15 compositions, all clean
Uncertain 1–3 0–33% 12 compositions
Unsafe 4+ ~100% 18 compositions, all confirmed

On this corpus, fee=0 had zero annotated convention mismatches and fee≥4 concentrated the confirmed mismatches. The fee is computed from schemas alone — no execution required.

See calibration data for the full report.

Python SDK

from bulla import compose_multi

result = compose_multi({
    "filesystem": fs_tools,
    "github": gh_tools,
})

print(result.diagnostic.coherence_fee)   # 30
print(result.receipt.disposition.value)  # "refuse_pending_disclosure"
print(result.decomposition.boundary_fee) # 1

compose_multi() returns a ComposeResult with the diagnostic, a tamper-evident WitnessReceipt, and a fee decomposition partitioned by server. For single-server diagnosis, use compose().

Architecture

Three layers, cleanly separated:

Layer Concern Module
Measurement Composition → Diagnostic (fee, blind spots, bridges) diagnostic.py
Binding Diagnostic → WitnessReceipt (content-addressable, tamper-evident) witness.py
Judgment Policy → Disposition (proceed / refuse / bridge) witness.py

The measurement layer has zero imports from the witness layer. Measurement does not know it is being witnessed.

Commands

Command Purpose
bulla audit Scan MCP config, diagnose cross-server coherence
bulla gauge Diagnose a single MCP server or manifest
bulla diagnose Full diagnostic from a composition YAML
bulla compose Prescriptive report (natural-language fix instructions)
bulla check CI gate with configurable thresholds
bulla scan Scan live MCP servers (zero config)
bulla witness Diagnose and emit WitnessReceipt as JSON
bulla bridge Auto-bridge and emit patched YAML
bulla translate Apply a typed runtime translator (--dimension X --value V --to T)
bulla serve MCP stdio server
bulla proxy Live MCP proxy — injects bulla__* meta-tools agents query at runtime
bulla replay Replay a session trace with flow-level structural diagnosis (renamed from bulla proxy)
bulla discover LLM-powered convention dimension discovery
bulla import langgraph Parse a LangGraph workflow into a Bulla manifest
bulla import crewai Parse a CrewAI crew/agent/task tree into a Bulla manifest
bulla certify-update Semantic SemVer verdict (delta_r, update kind, bridge lower bound) between old/new compositions

Output formats: --format text (default), --format json, --format sarif.

Runtime translation, Session API, framework adapters (new in 0.37.0)

Three additions in 0.37.0. bulla.translate exposes typed runtime translators that produce a WitnessReceipt for every transformation. bulla.Session builds compositions tool-by-tool with rank-1 incremental updates. bulla.LiveSession extends Session with call tracing for MCP proxies. Native bulla.langgraph and bulla.crewai adapters round it out.

bulla.translate

Typed runtime value translation across conventions.

from bulla import translate

result = translate("currency_code", value="USD", to_convention="numeric")
print(result.value)                         # "840"
print(result.evidence.kind)                 # "translator" | "mapping" | "pack"
print(result.receipt.disposition.value)     # "proceed"

Five canonical translators ship registered: currency_code, country_code, language_code, temporal_format, fhir_resource_type. Restricted-pack values raise TranslationUnavailable rather than leaking through. Register your own via @bulla.bridges.register. CLI: bulla translate --dimension currency_code --value USD --to numeric.

bulla.Session

Long-lived composition built tool by tool.

from bulla import Session

s = Session()
s.add_tool("filesystem.read_file", fields=["path"], conventions={"path_convention": "absolute"})
s.add_tool("github.create_file",  fields=["path"], conventions={"path_convention": "repo_relative"})
s.add_edge("filesystem.read_file", "github.create_file")
print(s.fee)                # 1
receipt = s.diagnose()      # full WitnessReceipt

Every add_tool, translate(...), and checkpoint() extends a chained receipt history. Incremental updates use rank-1 Schur complements; a 10,000-seed property test pins bitwise equality with full-rebuild witness_gram.

bulla.LiveSession

Online MCP composition proxy.

from bulla import LiveSession

live = LiveSession(name="checkout")
live.add_server("filesystem", fs_tools)
live.add_server("github", gh_tools)
print(live.fee)             # equals compose_multi({fs, gh}).coherence_fee
live.record_call("github.create_file", inputs={...})
receipt = live.diagnose()

add_server returns AddServerResult with the per-server delta. LiveSession.from_server_tools(...) constructs from a single dict[str, list[dict]].

Native LangGraph and CrewAI adapters

pip install bulla[langgraph]    # or bulla[crewai], bulla[all]
from bulla.langgraph import bind, BullaCallbackHandler
from bulla.crewai     import bind as crew_bind, BullaCrewCallback

# LangGraph: snapshot a compiled or uncompiled StateGraph
session = bind(graph)
print(session.fee)

# CrewAI: walks crew.agents, crew.tasks, task.context, task.tools
session = crew_bind(crew)

Both bind() calls return a bulla.Session with a deterministic composition_hash. Order-independence is property-tested over 50 seeded random graph constructions. BullaCallbackHandler and BullaCrewCallback record live tool invocations into the session's receipt chain. Static AST adapters (bulla.frameworks.{langgraph,crewai}) are unchanged for source-file scanning. See docs/FRAMEWORKS.md.

Awareness-gap demo

A reproducible bundle at examples/awareness-gap-demo/ walks the full failure → diagnose → translate → fix loop on canned filesystem + github manifests, no network or LLM required. bulla scan defaults to a prose narrative covering 39 dimension explanations, with a pairwise-vs-global block that fires only when every pair has fee=0 and the global has fee>0.

Standards ingestion (new in 0.36.0)

Bulla ships 19 seed packs covering the canonical commercial standards plus 5 restricted-source vocabularies as metadata-only references:

Tier Packs Notes
A — fully open, inline iso-4217, iso-8601, iso-3166, iso-639, iana-media-types, naics-2022 Currencies, dates, countries, languages, MIME, industry codes
B — large open, registry-backed ucum, fix-4.4, fix-5.0, gs1, un-edifact, fhir-r4, fhir-r5, icd-10-cm Units, FIX/SWIFT/FHIR/ICD-10/EDIFACT/GS1; values_registry points to authoritative source
Restricted (metadata-only) who-icd-10, swift-mt-mx, hl7-v2, umls-mappings, iso-20022 Pack ships dimension metadata only — licensed values stay behind the registry pointer; consumer obtains license to fetch
# List + inspect packs
bulla pack status src/bulla/packs/seed/iso-4217.yaml
bulla pack verify src/bulla/packs/seed/ucum.yaml          # static inspection (no network)
bulla pack lint   src/bulla/packs/seed/icd-10-cm.yaml     # advisory style hints
bulla pack validate path/to/your/pack.yaml                # schema check

Architectural extensions (Extensions A–E) behind the standards-ingestion sprint:

  • license at pack level — registry_license: open | research-only | restricted describes the upstream registry, not the pack's own metadata (which is always openly authored).
  • values_registry at dimension level — pointer to an external content-addressed registry. Hash format: real sha256:<64-hex> or sentinel placeholder:awaiting-ingest / placeholder:awaiting-license. Literal sha256:0...0 is rejected by the validator.
  • derives_from on PackRef — per-pack standard-version provenance recorded on every receipt's active_packs.
  • Alias-form known_values — items widen from string to { canonical, aliases, source_codes }. Strictly additive; legacy packs unchanged. A field whose enum lists "840" (ISO-4217 numeric) classifies under the same dimension as "USD".
  • Passive mappings: in regular packs — receipt-side translation tables (e.g. ICD-9 ↔ ICD-10 GEMs, FHIR R4 ↔ R5 resource-type renames). Value-blind: the coboundary uses dimension names, so mappings don't change H¹.

End-to-end demos at calibration/data/demos/:

  • cross_pack_receipt_billing.yaml — clinical_emr → billing_system → payer_gateway crossing ISO 4217 + FHIR R4 + ICD-10-CM seams in a single signed receipt.
  • restricted_pack_metadata_only.yaml — composition referencing a license-gated pack issues a valid receipt without consumer-side credentials; bulla pack verify returns status='placeholder' until a real ingest is performed.

Authoritative-source registry hashes are real SHA-256 from live fetches for all 11 fetchable open packs (UCUM, NAICS 2022, ISO 639, IANA Media Types, FHIR R4, FHIR R5, FIX 4.4, FIX 5.0, GS1, UN-EDIFACT, ICD-10-CM); the 5 restricted packs use placeholder:awaiting-license until a license-holder substitutes their own ingest; the 3 fully-inline packs (ISO 3166/4217/8601) carry no registry pointer. Real hashes also propagate onto derives_from.source_hash so receipts bind to the underlying-standard revision transitively. See docs/STANDARDS-INGEST-SOURCES.md and docs/STANDARDS-PACK-MAINTENANCE.md for the full ownership / drift-handling protocol.

Witness-geometry diagnostics (new in 0.35.0)

Beyond the scalar coherence fee, Bulla can surface the full witness geometry of a composition: per-field leverage scores, concentration index (N_eff), coloops/loops, and the matroid-greedy minimum-cost disclosure basis. All quantities are exact rationals (Fraction), never floats.

# Show leverage, N_eff, coloops, and greedy basis on a composition
bulla diagnose composition.yaml --witness

# On a live MCP server or manifest (gauge is the prescriptive command)
bulla gauge tools.json --leverage

# Ask which hidden fields substitute for a target (effective resistance)
bulla gauge tools.json --substitutes read_file path

# Cost-weighted greedy: YAML maps "<tool>:<field>" → rational cost string
bulla gauge tools.json --costs costs.yaml

JSON output adds a witness_geometry block only when the flag is set; default output remains byte-identical to 0.34.0. The mathematical backing is the Witness Gram rank identity and the Kron-reduction theorem, machine-checked in Lean 4. The broader research-program ledger documents 56 Aristotle-verified theorems across the witness-geometry chain (0 sorry). The PyPI package does not vendor Lean; it implements the measurement and receipt layers in Python.

Per-dimension decomposition & interaction score

The scalar fee can be split across the semantic dimensions that carry it, so you can see which conventions are responsible and whether they interact:

from bulla.diagnostic import decompose_fee_by_dimension

d = decompose_fee_by_dimension(comp)
d.by_dimension      # {'amount_unit': 1, 'date_format': 1} — fee_d per dimension
d.total_fee         # 2  — the composition's coherence fee
d.residual          # 0  — interaction score: Σ fee_d − fee
d.dfd_holds         # True — Disjoint Field Decomposition holds

The interaction score residual = Σ fee_d − fee is a structural diagnostic, not a failure predictor: 0 means the dimensions are modular (each fee_d is independently repairable); a positive value means two dimensions are coupled through a shared hidden field (d.shared_columns localizes which (tool, field) columns). This rests on the Per-Dimension Additivity Theorem: under Disjoint Field Decomposition (distinct dimensions touch disjoint columns), Σ fee_d = fee exactly — proven and verified on all 703 corpus compositions (note). The live MCP proxy surfaces the same breakdown: bulla__blind_spots returns fee_by_dimension, interaction_score, and dimensions_modular alongside the blind-spot list.

Quick start with bulla gauge

# Diagnose a live MCP server
bulla gauge --mcp-server "python -m my_mcp_server"

# Diagnose from a manifest JSON (tools/list response)
bulla gauge tools.json

# Save the inferred composition for hand-editing
bulla gauge tools.json -o composition.yaml

# CI gating: fail if coherence fee exceeds threshold
bulla gauge tools.json --max-fee 0

Python API

from bulla import (
    BullaGuard, WitnessBasis, PolicyProfile,
    diagnose, load_composition, witness,
    verify_receipt_consistency, verify_receipt_integrity,
)

# Load and diagnose
comp = load_composition(path="pipeline.yaml")
diag = diagnose(comp)
print(f"Fee: {diag.coherence_fee}, Blind spots: {len(diag.blind_spots)}")

# Witness with provenance
basis = WitnessBasis(declared=3, inferred=1, unknown=0)
policy = PolicyProfile(name="strict", max_unknown=2)
receipt = witness(diag, comp, witness_basis=basis, policy_profile=policy)
print(f"Disposition: {receipt.disposition.value}")

# Verify
ok, violations = verify_receipt_consistency(receipt, comp, diag)
assert verify_receipt_integrity(receipt.to_dict())

BullaGuard (high-level)

guard = BullaGuard.from_mcp_server("python my_server.py")
guard.check(max_blind_spots=0)  # raises BullaCheckError on failure

guard = BullaGuard.from_tools({
    "parser": {"fields": ["amount", "currency"], "conventions": {"amount_unit": "dollars"}},
    "engine": {"fields": ["amount"], "conventions": {"amount_unit": "cents"}},
}, edges=[("parser", "engine")])

MCP Server

Bulla exposes a JSON-RPC 2.0 stdio server with two tools and one resource:

bulla serve   # starts MCP stdio server
  • bulla.witness — composition YAML → WitnessReceipt (structured output)
  • bulla.bridge — composition YAML → patched YAML + receipt chain
  • bulla://taxonomy — convention pack taxonomy

Convention Packs

Layered vocabulary for convention recognition. Later packs override earlier ones.

bulla diagnose --pack financial.yaml pipeline.yaml
bulla scan --pack custom.yaml "python server.py"

Ships with base (11 dimensions) and financial (4 domain-specific dimensions).

CI Integration

# GitHub Actions with SARIF
- run: pip install bulla
- run: bulla check --format sarif compositions/ > bulla.sarif
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: bulla.sarif

Semantic SemVer CI gate:

- uses: jkomkov/bulla@v0.21.0
  with:
    mode: semver
    old-path: compositions/pipeline_old.yaml
    new-path: compositions/pipeline_new.yaml
    fail-on-major: "true"

How it works

Bulla builds a coboundary operator from tool dimensions to edge dimensions for both the observable and full sheaves. The coherence fee is:

fee = rank(δ_full) − rank(δ_obs)

Each unit of fee is an independent semantic dimension invisible to pairwise checks. Bridging increases rank(δ_obs) until it matches rank(δ_full). Rank computation uses exact arithmetic (fractions.Fraction) — no floating-point, no numpy.

Witness Contract

Every receipt binds three hashes: composition (what was proposed), diagnostic (what was measured), receipt (what was witnessed). Receipts chain via parent_receipt_hashes for auditable repair flows.

See WITNESS-CONTRACT.md for the normative specification.

License

Business Source License 1.1

Use grant: non-competing use, plus commercial use processing fewer than 1,000 compositions per month. Converts to Apache 2.0 on 2030-04-01.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bulla-0.40.0.tar.gz (3.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bulla-0.40.0-py3-none-any.whl (523.0 kB view details)

Uploaded Python 3

File details

Details for the file bulla-0.40.0.tar.gz.

File metadata

  • Download URL: bulla-0.40.0.tar.gz
  • Upload date:
  • Size: 3.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for bulla-0.40.0.tar.gz
Algorithm Hash digest
SHA256 5db2376db4904b9664134ac992c6926226f2139f67e30f4502a2150612664ee0
MD5 77bbbe338767003cab61c1a4c25e3826
BLAKE2b-256 0065da9dfab3dbeb5f5ecf07fa0772afb8c1e874b3ac30f50c5da9bb6f7042f2

See more details on using hashes here.

File details

Details for the file bulla-0.40.0-py3-none-any.whl.

File metadata

  • Download URL: bulla-0.40.0-py3-none-any.whl
  • Upload date:
  • Size: 523.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for bulla-0.40.0-py3-none-any.whl
Algorithm Hash digest
SHA256 46ee5a36bd6275e01025e3e6abbffc658ad678b0017f76c896cdf9c93d658da0
MD5 5d1dc72898b070fe9451303e588f2b3b
BLAKE2b-256 8e6617abde37c44a3303189b6cce8673e8e566a1e92a56be155a10a0e843fccf

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page