heso is the gating SDK — decorators and a proxy that sit between your agent and the side effects it wants to take. It bundles the Rust core as the heso._core wheel, so every gate runs in-process: no subprocess, and no crypto rewritten in Python. The same core powers the Node and browser surfaces, so a verdict is byte-identical wherever it runs.
This page is a reference. If you are starting from scratch, read the Python quickstart first, then keep this open. For the command-line tools shipped alongside the package, see the CLI reference.
Install
The package is on PyPI and requires Python 3.10 or newer. It bundles the Rust core as the heso._core wheel, so there is no separate binary to install.
pip install hesoInstalling heso also installs the heso console script — documented in the CLI reference. To scaffold a project (mint an operator identity and write a starter heso.toml), run heso init.
Initialize
Call heso.init() once at process start. It resolves and installs the active configuration; every decorator, the proxy, and heso.process read from it. Gating needs no binary, since the Rust core ships as an in-process wheel.
heso.init()function
Resolve and install the active configuration. Calling init again replaces the active config.
heso.init(*, project_root=None, binary=None, workflow=None, account=None, clock_override=None, timeout=None, blocking=None) -> Config
Parameters
- project_rootstr | None= None
- Where to discover
heso.tomland the local data directory. Falls back to theHESO_PROJECT_ROOTenvironment variable, then discovery. - binarystr | None= None
- Path to the Rust engine binary. Not required for gate operations (those use the in-process wheel). Falls back to
HESO_BIN. - workflowstr | None= None
- Default workflow label stamped onto each action. Falls back to
HESO_WORKFLOW. Scope a block of actions to a different label withheso.step. - accountstr | None= None
- Default account label for captured actions. Falls back to
HESO_ACCOUNT. - clock_overridestr | None= None
- Pin the captured-at clock (useful for deterministic tests). Falls back to
HESO_CLOCK. - timeoutfloat | None= None
- Gate timeout. Falls back to
HESO_TIMEOUT. - blockingbool | None= True
- When
True(the default), a blocked or suspended action raises so the decorated body never runs ungated. WhenFalse, HESO observes only: the action is still captured, signed, and audited, but a refusal does not raise. Falls back toHESO_BLOCKING.
ReturnsConfig
Config — the resolved, installed configuration.
Example
import heso # Layering: explicit args > env vars > heso.toml > defaults.cfg = heso.init( workflow="vendor-payouts", account="acct_19", blocking=True, # raise on a blocked/suspended action)
Each value resolves in order: an explicit keyword argument wins, then the matching environment variable (the HESO_* names above), then a value read from heso.toml discovery, then the built-in default.
Decorators
Decorators are the common way to gate. Each wraps a function so the call is captured as an action, checked against policy, and signed into a receipt before the body runs. With blocking on (the default), a blocked or routed action raises instead of running.
@heso.tooldecorator
Gate a tool call. The action verb is tool_call. The redact form applies commit-and-reveal redaction to the named fields before signing, so the cleartext never leaves the process.
@heso.tool # verb = tool_call@heso.tool(redact=["api_key"]) # commit-and-reveal redaction first
Parameters
- redactlist[str]
- Field names to redact before signing, using commit-and-reveal so a commitment is recorded in the receipt while the value stays local. See Redaction.
Example
import heso heso.init() @heso.tooldef transfer(payee: str, amount_usd: int) -> str: # The call is captured as a tool_call, evaluated against policy, # and signed into a receipt before this body runs. return stripe.transfers.create(destination=payee, amount=amount_usd) # Redact named fields (commit-and-reveal) before signing.@heso.tool(redact=["api_key"])def call_vendor(api_key: str, endpoint: str) -> dict: return vendor.request(endpoint, key=api_key)
@heso.destructivedecorator
Gate a delete. The action verb is delete. Deletes are a dangerous lane with a pinned floor, so an unmatched delete routes to a human approver by default.
@heso.destructive # verb = deleteExample
import heso heso.init() @heso.destructivedef delete_member(member_id: str) -> None: # verb = delete. Dangerous lanes carry a pinned floor, so an # unmatched delete routes to a human approver by default. db.members.delete(member_id)
@heso.actiondecorator
The lower-level decorator: you assemble an Action yourself instead of inferring the verb and fields. Reach for it when the higher-level decorators don’t fit.
@heso.action # assemble an Action yourselfExample
import hesofrom heso import Action, Verb heso.init() @heso.actiondef export_report(rows: list[dict]) -> str: # Assemble the Action yourself when a decorator's defaults # don't fit — this is the lower-level building block. action = Action(verb=Verb.data_export, tool_name="reports.export", fields={"row_count": str(len(rows))}) outcome = heso.process(action) return write_csv(rows)
Scoping
Use heso.step to tag a block of actions with a specific workflow label — for example, a single agent run — without threading the label through every call.
heso.step()context manager
A context manager that scopes the actions inside it to a given workflow label. Actions captured within the block carry that workflow in their receipt.
with heso.step(workflow="run-42"): ...
Parameters
- workflowstrrequired
- The workflow label applied to every action captured inside the block.
Example
import heso heso.init() with heso.step(workflow="run-42"): transfer("Globex LLC", 4200) # scoped to workflow "run-42" notify_finance() # same workflow label
Wrapping clients
heso.wrapreturns a transparent proxy around a client. It gates the calls that produce side effects and forwards everything else unchanged, so you can keep using the client’s own API.
heso.wrap()function
Wrap a client in a transparent gating proxy.
heso.wrap(client) -> proxyParameters
- clientobjectrequired
- The client instance to wrap (for example, an OpenAI or Anthropic client, or an HTTP client).
Returnsproxy
A proxy that gates the client’s side-effecting calls and forwards everything else verbatim.
Example
import hesofrom openai import OpenAI heso.init() client = heso.wrap(OpenAI()) # .create at the leaf is gated as an llm_call; the call kwargs# become the action fields. Everything else forwards verbatim.resp = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "summarize Q3"}],)
The proxy maps calls to verbs and recurses into nested namespaces:
createis gated as anllm_call.requestis gated as anhttp_request.- It recurses into nested namespaces, so
client.chat.completions.create(...)is gated at the leaf. - The call keyword arguments become the action fields.
- When the gate refuses, the proxy raises
BlockedErrororSuspendedErrorinstead of sending.
For a full walkthrough of gating an LLM client, see OpenAI & Anthropic.
Imperative API
Beneath the decorators sits heso.process — the escape hatch for building and gating an action by hand. It requires that heso.init() has run first.
heso.process()function
Capture, evaluate, and sign a single action you assembled yourself. Requires init.
heso.process(action: Action) -> OutcomeParameters
- actionActionrequired
- The action to gate. Build it with the
Actiontype and aVerb.
ReturnsOutcome
An Outcome describing the decision (its kind is an OutcomeKind).
Example
import hesofrom heso import Action, Verb, OutcomeKind heso.init() action = Action(verb=Verb.payment, tool_name="stripe.transfers.create", fields={"amount_usd": "4200", "payee": "Globex LLC"}) outcome = heso.process(action)if outcome.kind is OutcomeKind.BLOCKED: raise RuntimeError("policy blocked this payment")
Suspend / resume
For long-running agents, an action routed to a human pauses instead of failing. The suspend/resume layer lets you capture an action, save its action hash, wait for a decision elsewhere, and then resume. These are the values and callables it exposes:
configure— set up the suspend/resume behavior for the active config.gated— wrap a call so it participates in the suspend/resume flow.gateandgate_async— gate an action, synchronously or in an async context.resume— continue a previously suspended action once a decision exists.decisionandappend_decision— read the decision for a suspended action, or record one.current_action_hash— the action hash of the action currently in flight, used to correlate a suspension with its later resume.- The values
Gate,ResumeOutcome,SUSPENDED,DENIED,Paused, andContextLost— the result types and sentinels the layer returns.
A suspended action is one your policy routed to an approver. The approver co-signs with their own device-held key, producing an L1 receipt. See Human approval for the full flow.
Errors
The package raises a small, typed set of exceptions. The first two are how a refusal surfaces when blocking is on; the last two are setup and bridge failures.
| Exception | Raised when |
|---|---|
BlockedError | Policy blocked the action and blocking is on, so the decorated body never runs. |
SuspendedError | The action was routed to a human approver and is suspended pending that decision. |
BridgeError | The call into the in-process Rust core failed. |
HesoConfigError | Configuration could not be resolved — for example, a missing or malformed heso.toml, or a gate used before heso.init(). |
Public types you’ll import alongside these include Config, Action, Outcome, OutcomeKind, Verb, and RedactStrategy.
LangChain
To gate tool calls inside a LangChain agent, pass heso.HesoCallbackHandler (lazily imported) as a callback. The handler intercepts tool invocations and runs them through the same gate as the decorators. The full setup is in the LangChain guide.
