Skip to main content

Adaptive trust governance for the agent control plane

Project description

fulcrum-trust

Adaptive trust governance for the agent control plane. Bayesian Beta(α,β) trust evaluation, circuit breaking, and trust-aware routing for AI agent harnesses.

CI PyPI Python 3.9+ Apache 2.0

What is this?

fulcrum-trust implements a Beta distribution trust model for agent-to-agent relationships. When an agent pair's trust score drops below a configurable threshold, the circuit breaks — terminating the interaction before runaway loops occur.

The math: every interaction updates a Beta(α, β) distribution. Trust score = α / (α + β). Starting at (1.0, 1.0) gives an uninformative prior of 0.5. Scores decay exponentially toward 0.5 over time (stale relationships revert to uncertainty).

Bounded detection latency (alpha_max)

By default α grows without bound, so a pair with a long clean history buys a proportionally long runway of tolerated failures before the circuit opens. Setting TrustConfig(alpha_max=K) clamps α at K after every update, converting that runway into a constant: with threshold θ = p/q, the circuit opens within ceil(α_max·(q−p)/p) consecutive failures regardless of prior history (raw model; θ = 0.3 ⇒ ceil(α_max·7/3), e.g. α_max=20 ⇒ ≤ 47).

from fulcrum_trust import TrustManager, TrustConfig

tm = TrustManager(config=TrustConfig(alpha_max=20.0))  # detection ≤ 47 failures

The knob is a tradeoff surface: smaller α_max → tighter detection bound but coarser trust resolution. Pick per deployment — there is no hardcoded value, and alpha_max=None (the default) preserves the original unbounded behavior exactly. Validation requires alpha_max >= alpha_prior > 0; alpha_max == alpha_prior is a legal boundary that freezes success accrual entirely — degenerate in practice.

Recovery under the cap. Bounded detection has a flip side: once the circuit opens with β well past the cap (β > α_max·(q−p)/p), successes alone can never re-cross θ — the score is pinned at α_max/(α_max+β) because α cannot grow. Recovery flows through time decay (both parameters contract toward the uninformative prior, restoring recoverability within a fraction of a half-life), an explicit reset(), or operator action. This asymmetry is intrinsic to capping success evidence; pair the cap with recovery_cooldown_seconds for a governed re-entry probe once decay lifts the score back above threshold.

Claims scope. The engine knob is Implemented (tested in tests/test_evaluator.py::TestAlphaMaxCap). The worst-case bound itself is Proved only for the discrete capped model in Lean — D4 Theorem 3.9 (capped_prior_strict_responsiveness), published in "A Bounded, Machine-Checkable Governance Kernel for Trust-Gated Agent Execution" (DOI 10.5281/zenodo.19900714), which proves threshold crossing within q·(α_max+1) for the Laplace (α+1)/(α+β+2) estimator over Nat. The deployed Python estimator is raw α/(α+β) over float; the two models agree at the prior and diverge with counts, so their constants differ.

CORRESPONDENCE (carried verbatim from the sprint spec — mandatory). The Lean witness q·(α+1) is sufficient, not minimal. The deployed Python raw estimator has a tighter minimal bound (β > α(q−p)/p). Document both constants and which model each belongs to. For θ=0.3 and α_max=20: Lean sufficient bound q(α_max+1)=210; raw-model minimal ≈47. Do not present the Lean constant as the operational detection latency without this note.

Circuit state machine & recovery

Every agent pair carries a circuit in one of four states. evaluate() drives transitions from the trust score; terminate() is the out-of-band kill switch.

State Meaning IPC (CircuitState)
CLOSED trust ≥ threshold — traffic flows TRUSTED (0)
OPEN trust < threshold — pair is isolated ISOLATED (2)
HALF_OPEN recovery probe admitted — one evaluation decides EVALUATING (1)
TERMINATED administratively killed — sticky, bypasses trust math TERMINATED (3)

Recovery from OPEN has two regimes, selected by TrustConfig.recovery_cooldown_seconds.

Default (recovery_cooldown_seconds=None) — direct edge. The circuit closes the moment an evaluation brings trust back to threshold θ; behavior is unchanged from earlier releases.

CLOSED --(trust < θ)--> OPEN --(trust ≥ θ)--> CLOSED

Cooldown-gated (recovery_cooldown_seconds set) — observable probe. The pair holds OPEN until the cooldown elapses since it opened, then the next evaluation is admitted as a HALF_OPEN probe that resolves in a single step. Admission is time-gated only — there is no direct OPEN → CLOSED edge in this regime.

CLOSED    --(trust < θ)--------> OPEN         # circuit opens (stamps opened_at)
OPEN      --(cooldown elapsed)-> HALF_OPEN    # probe admitted (time-gated)
HALF_OPEN --(trust ≥ θ)--------> CLOSED       # probe succeeds → recovered
HALF_OPEN --(trust < θ)--------> OPEN         # probe fails → cooldown restarts

The OPEN → HALF_OPEN edge publishes EVALUATING over the IPC bridge. Under a capped prior (alpha_max), a pair whose β is deep past the cap cannot re-cross θ on successes alone (see Recovery under the cap above) — the cooldown probe becomes recoverable only once time decay lifts the score back over θ. TERMINATED is reachable from any state via terminate() and never recovers without an explicit reset().

Claims scope. The state machine is Implemented and Tested (tests/test_manager_recovery_cooldown.py, tests/test_manager_circuit_persistence.py). Its four-state transition relation corresponds to the ValidTransition predicate formalized in Fulcrum-Proofs at the v0.2.0 tag (regime-conditioned docstring) — a correspondence with the Lean artifact, not a new proof.

Quick Start

from fulcrum_trust import TrustManager, TrustOutcome

# Default: in-memory store, threshold=0.3, 24-hour decay half-life
tm = TrustManager()

# Record interaction outcomes
tm.evaluate("orchestrator", "code-agent", TrustOutcome.SUCCESS)
tm.evaluate("orchestrator", "code-agent", TrustOutcome.SUCCESS)
tm.evaluate("orchestrator", "code-agent", TrustOutcome.FAILURE)

# Check trust
print(tm.get_trust_score("orchestrator", "code-agent"))  # 0.6

# Circuit break check — use this in your agent loop
if tm.should_terminate("orchestrator", "code-agent"):
    raise RuntimeError("Circuit open — trust degraded below threshold")

Persist across sessions with FileStore:

from fulcrum_trust import TrustManager, TrustOutcome, TrustConfig
from fulcrum_trust.stores import FileStore

tm = TrustManager(
    store=FileStore("trust_state.json"),
    config=TrustConfig(threshold=0.3, half_life_seconds=3600),  # 1-hour decay
)

Send trust events to Fulcrum backend with FulcrumStore:

from fulcrum_trust import FulcrumStore, TrustManager, TrustOutcome

tm = TrustManager(
    store=FulcrumStore(
        api_key="your-fulcrum-api-key",
        base_url="https://api.fulcrumlayer.io",  # best-effort REST event target
    )
)

tm.evaluate("orchestrator", "code-agent", TrustOutcome.SUCCESS)

Note: The /api/trust/events REST endpoint is currently DEFERRED — fulcrum-io does not yet expose it. FulcrumStore writes locally first and best-effort ships events with a warning log on failure, so the agent keeps running and local trust state remains correct. For production cross-process integration today, use RedisIPCBridge (fulcrum_trust.ipc.redis_bridge), which writes circuit state to Redis for O(1) reads by the Go Execution Envelope.

Install

pip install fulcrum-trust

# Optional numpy fast path for decay math:
pip install "fulcrum-trust[numpy]"

Documentation

  • API Reference — all public classes and methods
  • Formal Validation — Lean 4 proof backing for the formal termination guarantee
  • Blog post — why agents need circuit breakers
  • RLM Python Prototype — Phase 5 prototype benchmark and architecture (public prototype — unstable API, not production-stable)

Support

Development

git clone https://github.com/Fulcrum-Governance/fulcrum-trust
cd fulcrum-trust
pip install -e ".[dev]"
pytest              # Run tests (requires >=95% coverage)
mypy fulcrum_trust/ # Type check (strict mode)
ruff check .        # Lint
ruff format .       # Format

Part of the Fulcrum Architecture

fulcrum-trust is one of four repositories that make up the Fulcrum governance kernel — a portable, typed, pre-execution control plane that sits between intent and action:

Repo Role License
fulcrum-io Runtime control plane: gRPC/REST, MCP proxy, CLI, dashboard, SDKs BSL 1.1
Fulcrum-Boundary Out-of-process enforcement boundary: transport adapters, 4-stage pipeline Apache 2.0
fulcrum-trust (this repo) Trust engine: Beta(α,β) evaluator, circuit breaker, LangGraph adapter Apache 2.0
Fulcrum-Proofs Formal core: Lean 4 proofs, claim ledger, evidence artifacts MIT

Project docs: Contributing · Security · Changelog · Code of Conduct · Citation

FulcrumStore bridges this package to the main Fulcrum backend with local-first persistence and best-effort REST event shipping. For production cross-process integration today, use RedisIPCBridge, which publishes circuit state for O(1) reads by the Go Execution Envelope. The Go backend has parity tests ensuring its trust implementation matches this Python package's behavior exactly.

See ADR-003 for the original repo-architecture rationale; the out-of-process enforcement boundary was added in April 2026 as GIL and now lives in the Fulcrum-Boundary repo.

Architecture

fulcrum_trust/
├── types.py        — TrustOutcome enum, TrustState, TrustConfig, TrustCircuitOpen
├── evaluator.py    — TrustEvaluator: Beta(α,β) scoring, pair_id generation
├── decay.py        — Exponential decay toward uninformative prior
├── manager.py      — TrustManager: orchestrates evaluator + store + decay
├── context.py      — ContextVar isolation for concurrent evaluations
├── flusher.py      — Background telemetry batching (non-blocking store writes)
├── rlm/             — Phase 5 prototype: public but unstable API, not production-stable
│   ├── context.py  — 128k-bounded long-context externalization into symbolic handles
│   ├── runtime.py  — Restricted `peek` + `llm_batch` navigation runtime
│   ├── prototype.py — Gratitude-loop analysis + lost-in-the-middle benchmark
│   └── fixtures.py — Deterministic 100K+ token synthetic session generator
└── stores/
    ├── base.py     — TrustStore Protocol (structural subtyping)
    ├── memory.py   — MemoryStore (default, in-process)
    ├── file.py     — FileStore (JSON-backed, cross-session)
    └── fulcrum.py  — FulcrumStore (local-first + backend event shipping)

License

Apache 2.0. See LICENSE.

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

fulcrum_trust-0.3.0.tar.gz (228.5 kB view details)

Uploaded Source

Built Distribution

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

fulcrum_trust-0.3.0-py3-none-any.whl (42.8 kB view details)

Uploaded Python 3

File details

Details for the file fulcrum_trust-0.3.0.tar.gz.

File metadata

  • Download URL: fulcrum_trust-0.3.0.tar.gz
  • Upload date:
  • Size: 228.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for fulcrum_trust-0.3.0.tar.gz
Algorithm Hash digest
SHA256 07d8a95a621aaaa2a2a7357d7905932e2b964d0f8d272b83ded32cd40e310800
MD5 fe223dcf7662df21f91dbd385c598047
BLAKE2b-256 9cb4ba833cb01d519d1ba7c786cdee536ed8ee430d915047b4c93443c13aab9c

See more details on using hashes here.

File details

Details for the file fulcrum_trust-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: fulcrum_trust-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 42.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for fulcrum_trust-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 733fed3538b10ba1aaf6674da98c6348deadff5e92b09c849a818aa427c7cf43
MD5 cbe2406232d1de8c9693abf4d9633517
BLAKE2b-256 73f8fd21914cefd7d7b392dc0ae0b1de203d7f255bb3e0cdb70410ab9e14ddb2

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