API reference
Every name re-exported from interbolt.
API reference
Everything re-exported from interbolt (the package __init__.py). This is
the public surface; anything not listed here is internal and may change
without notice.
taint, endorse, guard, check, enforce_decision, enforce_decision_sync, configure, default_policy, agent, get_runtime, AgentHandle, RunContext,
track_model_call, Runtime, Policy,
Decision, Event, Finding, Endorsement, Action, Mode, Label, RunIngressEntry, TrustLevel, Capability,
Reporter, ApprovalResolver,
NullReporter, InMemoryReporter, LoggingReporter, JsonlReporter, CompositeReporter, OTelReporter,
describe_decision, describe_event, describe_finding, describe_endorsement,
RECORD_TYPE_EVENT, RECORD_TYPE_FINDING, RECORD_TYPE_ENDORSEMENT,
InterboltError, PolicyViolation, PolicyEvaluationError, ApprovalDenied,
InterboltConfigError, InterboltUsageError,
Tainted, LabeledValue, TaintedBytes,
__version__taint
def taint(value: Any, *, source: str, derived_from: Iterable[Any] | None = None) -> Any: ...Marks value with a Label recording source. For str returns a
Tainted; for bytes returns a TaintedBytes; for a builtin container
(list, tuple, set, frozenset, a Mapping's keys and values) recurses
and labels string leaves to the bounded recursion depth; for any other
scalar returns a LabeledValue. The label only records the source name;
trust is resolved later, at the sink. Needs no configured runtime. See
Taint propagation.
If derived_from is given a non-empty iterable, it marks value as
derived from those values instead of as a fresh ingress point. source
becomes the name of the derivation hop (for example "model"), and the
returned label's lineage is the union of every label found among
derived_from, so trust resolves at the sink exactly as if those original
inputs had reached it directly. If no label is found among derived_from at
all, value is returned completely unmarked, and no run-level ingress event
is recorded for source in this case. See
Taint propagation: model calls and derived values.
endorse
def endorse(value: Any, *, kind: str, note: str | None = None) -> Any: ...Adds kind to the label's endorsements after an explicit validation
step, without touching lineage or how t.trust resolves. Accepts the same
shapes as taint(), and a value with no label anywhere in it passes through
unchanged. note is free text carried only on the emitted Endorsement
record, never on the label. Every call that endorses something emits that
record through the configured reporter, or logs at INFO if no runtime is
configured yet; endorse() itself needs no runtime. Call it only from
deterministic code after a real check, never conditioned on model output.
See Auditing: endorsement and
Policies: endorsement-aware rules.
track_model_call
@track_model_call # source defaults to "model"
def summarize(prompt: str) -> str: ...
@track_model_call(source="gpt-4") # explicit derivation-hop name
async def summarize(prompt: str) -> str: ...Wraps a function so its return value is tainted via taint(result, source=source, derived_from=<the function's bound call arguments>).
Auto-detects sync vs async the same way guard does. Tracks provenance
only; does not evaluate policy, so stack @guard/@handle.guard alongside
it if the call into the model should also be gated. Needs no configured
runtime, the same as taint() itself. See
Taint propagation: model calls and derived values.
guard
@guard # tool name defaults to the function name
def send_email(...): ...
@guard(tool="fs.write") # explicit qualified or bare tool name
def write_file(...): ...Decorates a function or coroutine function so every call is checked against
the current policy before it runs, auto-detecting sync vs async. Internally this is check()
followed by enforce_decision(): on block raises PolicyViolation, on require_approval invokes
the configured ApprovalResolver, on allow calls through. agent_id and run_id come
from the active agent_context, and session_id is never set through this
path. See Identity.
check
def check(
*,
tool: str,
args: Mapping[str, Any],
agent_id: str,
run_id: str | None = None,
session_id: str | None = None,
) -> Decision: ...The framework-agnostic decision core; guard is sugar over check() followed by enforce_decision().
check() only decides and emits; it never raises on a block or require_approval outcome
(it raises only PolicyEvaluationError, fail-closed under enforce). Pair it with enforce_decision()
(below) to act on the result. Collects
labels from args (recursing into containers), evaluates the policy,
returns a Decision, and emits the corresponding Event through the
configured reporter.
agent_id is explicit here rather than read from the
context variable. run_id resolves from the active agent_context when you
pass None, so a dispatch loop inside a run shares that run's id, and its
run.tainted gating, automatically; a fresh id is minted only when no run is
active. Pass run_id= explicitly only to override the ambient value, or to
set a specific id for a call made outside any context.
Requires configure() to have run; raises InterboltUsageError otherwise.
Use this for custom dispatch loops or existing tool registries; see
Identity: custom dispatch loops.
agent_id must match ^[A-Za-z0-9_.-]+$, may not be a
Tainted/TaintedBytes/LabeledValue, and may not be the literal
"default" (reserved for the implicit no-context fallback), or it raises
InterboltConfigError. See
Identity as a policy input.
Policy testing is just check() invoked with synthetic args and taint,
asserted against the returned Decision; there's no separate simulate
function. See Testing.
enforce_decision, enforce_decision_sync
async def enforce_decision(decision: Decision) -> None: ...
def enforce_decision_sync(decision: Decision) -> None: ...The enforcement half of guard, exposed for dispatch loops that call
check() directly. Turns a Decision into control flow purely by its
action: allow returns, block raises PolicyViolation,
require_approval invokes the current runtime's ApprovalResolver and
raises ApprovalDenied if it returns falsy. It reads decision.action,
which check() has already resolved through the mode, so mode handling
lives in one place rather than here: under dry_run the decision arrives as
allow and enforces nothing, and under monitor a real block still
enforces exactly as it does under enforce.
Only the require_approval path touches the runtime (to reach the
resolver); allow and block need no configured runtime. The sync form
raises InterboltUsageError if the resolver returns an awaitable, closing
that coroutine first so it does not leak; the async form awaits it.
Both re-raise PolicyViolation/ApprovalDenied with
the offending Decision on .decision.
A custom dispatch loop is then three lines:
# inside an agent_context, run_id resolves from the active run automatically;
# pass it explicitly only outside a context or to override
decision = check(tool="fs.write", args=args, agent_id=agent_id)
await enforce_decision(decision) # no-op on allow; raises on block / denied approval
result = run_tool(...)See Identity: custom dispatch loops.
configure
def configure(
*,
policy: Policy | None = None,
reporter: Reporter | None = None,
approval_resolver: ApprovalResolver = auto_deny,
mode: Mode | str = Mode.ENFORCE,
audit: bool = False,
) -> Runtime: ...Builds a Runtime, installs it as the process-current runtime, and returns
it. Has no import-time side effects: a module decorated with @guard can
be imported before configure() has run. policy defaults to None,
which loads the built-in default policy (default_policy(), below):
no sources, no sinks, every guarded call falls through to
require_approval. reporter defaults to a fresh NullReporter().
approval_resolver defaults to auto_deny (denies every approval
request). INTERBOLT_MODE and INTERBOLT_AUDIT override mode and
audit; an invalid mode raises InterboltConfigError. See
Policies: modes for the full
precedence chain.
Logging on each call, independent of any configured Reporter: one
INFO-level summary line (effective mode, policy source, source/sink counts,
caller file:line), plus a WARNING if no policy was passed, a WARNING if the
policy's fail_mode changed the effective mode away from the mode=
argument, and a WARNING if INTERBOLT_MODE changed the effective mode. See
Policies.
default_policy
def default_policy() -> Policy: ...Returns the built-in default policy for programmatic use and testing: the
same posture configure(policy=None) uses, exposed directly rather than
only implicitly.
agent
def agent(agent_id: str) -> AgentHandle: ...Returns a durable per-agent handle whose .guard decorates with this
agent_id, resolving the runtime lazily at call time. Captures agent_id
eagerly, so it is safe at import time and needs no Runtime instance, and
it carries identity across a thread-pool boundary where agent_context
cannot. runtime.agent(agent_id) is equivalent. agent_id is validated
eagerly at construction (see check, above, for the exact rule); this
means agent(...) itself can raise InterboltConfigError, not only the
first guarded call through the returned handle. See
Identity: when to use an AgentHandle.
get_runtime
def get_runtime() -> Runtime: ...Returns the process-current runtime, for code that did not keep
configure()'s return value (a different module, or reaching for
Runtime.add_reporter later). This is the same pattern as OpenTelemetry's
get_tracer_provider(). Raises InterboltUsageError if configure() has
not run yet.
AgentHandle
The type returned by agent(...)/runtime.agent(...). Exposes .guard,
usable bare (@handle.guard) or parameterized (@handle.guard(tool=...)),
behaving identically to the module-level guard except that agent_id
comes from the handle instead of the active agent_context. Also exposes
.track_model_call, equivalent to the module-level track_model_call
(above), provided for per-handle symmetry: @support.guard and
@support.track_model_call are both reachable from one handle. The
handle's agent identity plays no role in .track_model_call, since taint
derivation is identity-free.
RunContext
class RunContext: # frozen, slots
run_id: str
agent_id: strThe value agent_context/agent_context_sync yield. A frozen, data-only
snapshot of the run_id and agent_id bound for the block. It stays
readable after the block exits and remains a correct record of what that
run was, but the run's registries are cleared at exit and re-binding from
it is not supported. run_id is the join key on every Decision, Event,
Finding, and Endorsement emitted inside the block. See
Identity.
Runtime
The composition root returned by configure(). One per process.
Attributes:
runtime.policy -> Policy: the compiled policy this runtime enforces.runtime.mode -> Mode: the effective mode, after the precedence chain inconfigure().runtime.approval_resolver -> ApprovalResolver: the resolver invoked on arequire_approvaldecision.runtime.reporter -> Reporter: theCompositeReporterevery record is emitted through (read-only; everyRuntimeholds one internally, even for a singlereporter=passed toconfigure()). See Reporters.
Methods:
runtime.agent(agent_id: str) -> AgentHandle: equivalent to the module-levelagent(...), above.runtime.agent_context(agent_id: str): an async context manager that bindsagent_idand mints arun_idfor the duration of the block, yielding both as aRunContext(theas runclause is optional). Exiting clears both, along with that run's ingress registry and audit findings.agent_idis validated before anything binds (same rule ascheck, above); a rejected value raisesInterboltConfigErrorwith no partial bind and nothing to clean up. See Identity.runtime.agent_context_sync(agent_id: str): the synchronous counterpart; identical binding, validation, cleanup, andRunContextyield, for a call site that cannot useasync with.runtime.check(*, tool, args, agent_id, run_id=None, session_id=None) -> Decision: the same decision core as the module-levelcheck, against this runtime explicitly. Validatesagent_id's charset and rejects a taint carrier unconditionally, but (unlike the module-levelcheck) does not reject the literal"default", since this method is also the path bareguard's no-context fallback resolves through.runtime.add_reporter(reporter: Reporter) -> None: attaches an additional reporter to this live runtime without reconfiguring, modeled on OpenTelemetry'sadd_span_processor. The same non-blocking contract as any other reporter applies; there is no removal, only reconfiguring. See Reporters.runtime.audit_findings() -> list[Finding]: the findings recorded so far by the laundering audit, the observer that catches an untrusted value reaching a sink argument with no label (see Auditing), bounded with oldest evicted first once the cap is reached, or[]ifauditwas not enabled.
Policy
Policy.from_file(path: str) -> Policy
Policy.validate(path: str) -> list[str]from_file loads, validates, and compiles a policy YAML file in one call;
raises PolicyEvaluationError if the file is missing, malformed, fails
schema validation, or a rule's CEL expression fails to parse, and
InterboltConfigError if a rule's CEL expression uses a disallowed
construct, such as .any( in place of exists. validate performs schema
and CEL checks only, without executing an agent, and returns a list of
human-readable problem descriptions, empty if the policy is valid, capturing
every error there
instead of raising. policy.document exposes the validated
PolicyDocument, policy.sources_table the declared source-to-trust
mapping, policy.source the path it was loaded from (None for a
programmatically constructed policy), and policy.fingerprint a stable
"sha256:..." hash of the normalized document, computed once at
construction and stamped onto every Event/Finding/Endorsement this
policy produces. See Policies,
Policy evaluation internals: the fingerprint,
and CI.
Decision
class Decision(BaseModel, frozen=True):
action: Action # ALLOW | BLOCK | REQUIRE_APPROVAL
matched_rule: str | None # name of the first matching rule
matched_condition: str | None # the matched rule's CEL `when` text
tool: str # qualified name
contributing_labels: tuple[Label, ...]
trifecta: frozenset[str] # this call's legs: from_untrusted + declared capabilities
untrusted_sources: frozenset[str] # which contributing source names resolved untrusted
run_tainted: bool # run-level gating
run_ingress: tuple[RunIngressEntry, ...] # the structured record behind run_tainted
run_trifecta: frozenset[str] # every leg satisfied at run scope; empty outside agent_context
mode: Mode
decision_id: str
agent_id: str
run_id: str
session_id: str | NoneReturned by check/guard, attached to PolicyViolation/ApprovalDenied
on .decision, and embedded in the emitted Event. matched_condition is
None for the sink's catch-all rule, when nothing matched, and after an
evaluation error, which also clears matched_rule. Otherwise it is
human-readable CEL: for a when: rule, the text as written in the policy
YAML, and for a require_endorsement: rule, the equivalent expression that
shorthand compiles to.
Event, Finding, Endorsement
Every emitted record shares four fields:
schema_version: int
trace_id: str | None # active OpenTelemetry trace id (W3C hex), or None
span_id: str | None # active OpenTelemetry span id, or None
timestamp: datetimetrace_id/span_id are populated when an OpenTelemetry span is active at
construction, and are None otherwise, including when OpenTelemetry is not
installed. Finding and Endorsement additionally carry the identity
triple (agent_id, run_id, session_id) directly. Event does not,
since its embedded Decision already carries it.
class Event(RecordBase, frozen=True):
decision: Decision
sources: frozenset[str] # every source contributing to the call, trusted or not
outcome: Outcome # the real, pre-dry_run-downgrade result
class Finding(IdentifiedRecordBase, frozen=True):
source: str # the untrusted source whose content reached the sink
tool: str
argument: str # the argument name the content was found in
class Endorsement(IdentifiedRecordBase, frozen=True):
kind: str
note: str | None
lineage: tuple[str, ...] # the endorsed label's lineage
value_id: str # the fresh label id minted for this endorsement hopEvent deliberately does not duplicate the decision's fields. Reach
matched_rule, trifecta, untrusted_sources, run_tainted,
run_ingress, mode, and the identity triple through event.decision,
the single source of truth for what was decided.
outcome is an Outcome enum with four values: allow, block,
require_approval, and evaluation_error. It records what check()
actually computed before any mode-based downgrade, which is what makes a
dry_run rollout informative. Outcome lives in interbolt.models.core
and is not currently re-exported at the top level.
Finding is the laundering-audit record, emitted when untrusted content
reaches a sink argument without a label; see
Auditing. Endorsement is the audited record of one
endorse() call; see
Auditing: endorsement. All three travel
the same Reporter seam, and EVENT_SCHEMA_VERSION (in
interbolt.constants) versions all three together. See
Events for the wire shape and the version history.
Action, Mode, TrustLevel
All enum.StrEnum, so the same value round-trips a policy YAML string, an
environment variable string, and serialized output with no conversion code.
Action:ALLOW,BLOCK,REQUIRE_APPROVAL.Mode:ENFORCE,MONITOR,DRY_RUN. See Policies.TrustLevel:TRUSTED,UNTRUSTED. The result of resolving a source name against the policy at the sink; never stored on aLabel.Capability:READS_PRIVATE("reads_private"),REACHES_EXTERNAL("reaches_external"). The closed set of two tool capabilities, declared with thecapabilities:key on a sink entry and read back fromPolicy.tool_capabilities: the declared tool-to-capabilities mapping, built from thecapabilities:key on each sink entry. A tool whose entry omits the key is absent from the mapping and resolves to the empty set rather than raising. See Policies: declaring what a tool does.
Label
class Label(BaseModel, frozen=True):
source: str
value_id: str
lineage: tuple[str, ...]
ingested_by: tuple[str, ...] = ()
endorsements: tuple[str, ...] = ()See
Taint propagation
and Auditing: endorsement for
endorsements.
RunIngressEntry
class RunIngressEntry(BaseModel, frozen=True):
source: str
trust: TrustLevel
ingested_by: tuple[str, ...]The per-source entry in Decision.run_ingress: the source name, its trust
resolved at decision time against the policy's sources table, and the
agent ids that called taint() with it. See
Events: run_ingress and
Policies: the CEL evaluation context.
Tainted, TaintedBytes, LabeledValue
Tainted (a str subclass) and TaintedBytes (a bytes subclass) carry a
.label: Label and propagate it through the operation subset described in
Taint propagation. LabeledValue wraps a
non-string scalar, exposing .value and .label. All three preserve the
label under copy.copy/copy.deepcopy and drop it under pickling; see
Serialization for the one channel that does
carry it across.
pack, unpack, pack_into, unpack_from
def pack(
value: Any,
*,
key: bytes | str | None = None,
key_id: str | None = None,
include_run: bool = True,
) -> dict[str, Any]: ...
def unpack(
envelope: Mapping[str, Any],
*,
key: bytes | str | None = None,
) -> Any: ...
def pack_into(
mapping: Mapping[str, Any],
*,
key: bytes | str | None = None,
key_id: str | None = None,
include_run: bool = True,
) -> dict[str, Any]: ...
def unpack_from(
mapping: Mapping[str, Any],
*,
key: bytes | str | None = None,
) -> dict[str, Any]: ...pack strips every Tainted/TaintedBytes/LabeledValue carrier in
value down to a plain JSON-representable payload and records what it
stripped in a path-keyed sidecar, returning one plain envelope dict.
unpack reverses it: validates the envelope, verifies mac when key is
given, rebuilds every carrier, and replays the run's ingested source names
into the currently active run. pack_into/unpack_from are sugar for the
dominant case, a top-level state mapping that must keep its shape, adding
or removing one reserved key, constants.WIRE_ENVELOPE_KEY
("__interbolt__"). Both raise InterboltConfigError on every rejection.
See Serialization for the wire format, the
security model, and integration recipes.
The envelope's run block records each source together with the agent ids that ingested it, and the wire schema version is 2. A version 1 envelope is rejected, since the block changed shape and no compatibility path is kept.
OTelReporter
class OTelReporter:
def __init__(self) -> None: ...Maps Event/Finding/Endorsement onto OpenTelemetry spans at the edge;
requires the interbolt[otel] extra. Exported lazily via a module
__getattr__ in interbolt/__init__.py (PEP 562): import interbolt never
requires opentelemetry to be installed, and from interbolt import OTelReporter raises InterboltConfigError with an install hint if the
extra is missing. See Reporters: OTelReporter
and the OTel guide.
Reporter, ApprovalResolver
Protocols in interbolt.models.protocols. Reporter.export takes an
Event | Finding | Endorsement. See Reporters for
Reporter and its six implementations (NullReporter, InMemoryReporter,
LoggingReporter, JsonlReporter, CompositeReporter, OTelReporter).
ApprovalResolver is Callable[[Decision], bool | Awaitable[bool]]:
invoked synchronously at a sync call site, awaited at an async call site.
A sync call site needs a resolver that returns a plain bool; one that
returns an awaitable raises InterboltUsageError. The default, auto_deny,
denies every request.
describe_decision, describe_event, describe_finding, describe_endorsement
def describe_decision(decision: Decision) -> str: ...
def describe_event(event: Event) -> str: ...
def describe_finding(finding: Finding) -> str: ...
def describe_endorsement(endorsement: Endorsement) -> str: ...Each turns its record into a one-line, rich-markup-tagged human summary,
meant for a rich.console.Console, not a bare print() (the raw
[tag]...[/tag] markup otherwise prints literally). describe_decision
is the one to reach for right where a Decision is already in hand
(a caught PolicyViolation/ApprovalDenied, or check()'s direct return
value): it shows the tool, action, matched rule, matched condition (if any),
mode, and untrusted_sources without a trip through the reporter stream.
describe_event/describe_finding/describe_endorsement cover the same
ground for the emitted, versioned records, and are what interbolt inspect
uses internally. See Reporters.
RECORD_TYPE_EVENT, RECORD_TYPE_FINDING, RECORD_TYPE_ENDORSEMENT
The "record_type" string values JsonlReporter tags each line with, and
interbolt inspect reads back to recover which model (Event, Finding,
or Endorsement) a line deserializes to. In interbolt.constants,
re-exported at the top level for a consumer parsing a JsonlReporter log
directly.
Errors
InterboltError (base)
├── decision outcomes
│ ├── PolicyViolation # a real block; carries .decision
│ ├── PolicyEvaluationError # evaluation failed; fail-closed under enforce
│ └── ApprovalDenied # resolver returned False
└── misuse (also subclasses the matching builtin)
├── InterboltConfigError(InterboltError, ValueError) # bad config value
└── InterboltUsageError(InterboltError, RuntimeError) # API used out of sequenceexcept InterboltError catches every exception the library raises. Because
the misuse classes multiply-inherit the matching builtin, except ValueError and except RuntimeError also catch them by their builtin
semantics. taint() needs no configured runtime and works before
configure() has run, so it never raises a usage error.
Command line
The interbolt console script has four subcommands.
interbolt init [path] # write the starter policy (default: policy.example.yaml)
interbolt validate policy.yaml # schema and CEL checks; see Policies: static validation
interbolt inspect log.jsonl # render a JsonlReporter log as a console tree
[--run-id ID] # limit the render to one run
interbolt explain policy.yaml --agent ID # or --group NAME / --tool ns.tool
[--show-eliminated] # also print dead rules, dimmedvalidate, init, and inspect exit 0 on success and 1 on failure.
explain is a reporting command, not a gate: it always exits 0 regardless
of what it finds, since its job is to answer "what can this agent do," not to
pass or fail a build. See Explain.
validate separates warnings from errors and exits non-zero only on
errors, which is what makes it safe to run in a pipeline. See
CI.
__version__
The single source of truth for the package version; pyproject.toml reads
it dynamically from src/interbolt/__init__.py.