SDK reference /Python — heso

Python — heso

Reference for the heso Python package. Decorators and a client proxy capture each action in-process, run it through the Rust core, and raise before an unauthorized side effect fires.

heso gates your agent from inside your process. It bundles the Rust core as the heso._core wheel, so every gate runs in-process: no subprocess, no crypto rewritten in Python. The same core powers the Node and browser surfaces, so a verdict is byte-identical wherever it runs.

New to HESO? Start with the Python quickstart, then keep this open. For the command-line tools that ship with the package, see the CLI reference.

What this proves

A receipt proves you authorized an action under your policy. It does not prove the action succeeded downstream.

Common tasks

The four things most people come here to do, and where each one lives:

  • Gate a tool — decorate the function with @heso.tool. The call is captured as a tool_call and checked against policy before the body runs.
  • Gate an LLM client — wrap it with heso.wrap(client). Every completion is gated as an llm_call; the rest of the client API is untouched.
  • Redact a field — pass @heso.tool(redact=["field"]). The value is commit-and-reveal redacted before signing, so cleartext stays local.
  • Route to a human — use @heso.gated for durable suspend/resume. A first call parks the action; a later call resumes it once an approver decides.

Install

Install heso from PyPI. It needs Python 3.10 or newer and bundles the Rust core as the heso._core wheel, so there is no separate binary.

bash
pip install heso   # or: uv add heso

Installing the package also installs the heso console script (see 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 config that every decorator, the proxy, and heso.process read from. It fails fast if no heso.toml can be found.

heso.init()function

Resolve and install the active config. Calling init again replaces it.

python
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 find heso.toml and the local data directory. Falls back to HESO_PROJECT_ROOT, then discovery up from the CWD.
binarystr | None= None
Path to the heso-verify-cli binary. Not needed for gating (that uses the in-process wheel); kept for tests that verify receipts offline. Falls back to HESO_BIN.
workflowstr | None= None
Default workflow label stamped on each action. Falls back to HESO_WORKFLOW, then "default". Override a block with heso.step.
accountstr | None= None
Default account label for captured actions. Falls back to HESO_ACCOUNT, then "default".
clock_overridestr | None= None
Pin the captured-at clock to an RFC-3339 instant (for deterministic tests). Falls back to HESO_CLOCK.
timeoutfloat | None= None
Kept for API compatibility; unused on the wheel path. Falls back to HESO_TIMEOUT.
blockingbool | None= True
When True (the default), a blocked or suspended action raises so the body never runs ungated. When False, HESO observes only: the action is still captured, signed, and audited, but a refusal does not raise. Falls back to HESO_BLOCKING.

ReturnsConfig

Config — the resolved, installed configuration.

Example

bootstrap.pypython
import heso

# explicit args > env vars > heso.toml > defaults
cfg = heso.init(
    workflow="vendor-payouts",
    account="acct_19",
    blocking=True,  # raise on a blocked/suspended action (the default)
)
Configuration layering

Each value resolves in order: an explicit keyword argument, then the matching environment variable (the HESO_* names above), then a value 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 (verb tool_call). The call arguments become the action fields. The redact form commit-and-reveal redacts the named fields before signing, so cleartext never leaves the process.

python
@heso.tool                      # verb = tool_call
@heso.tool(redact=["api_key"])  # redact fields before signing

Parameters

redactlist[str]
Field names to redact before signing. A commitment is recorded in the receipt while the value stays local. See Redaction.

Example

tools.pypython
import heso

heso.init()

@heso.tool
def transfer(payee: str, amount_usd: int) -> str:
    # Captured as a tool_call, checked against policy, and signed
    # into a receipt before this body runs.
    return stripe.transfers.create(destination=payee, amount=amount_usd)

# Redact named fields before signing (commit-and-reveal).
@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 (verb delete). delete is a floored lane, so an unmatched delete routes to a human approver by default. Flagged fields are dropped destructively, not recoverable.

python
@heso.destructive   # verb = delete

Example

cleanup.pypython
import heso

heso.init()

@heso.destructive
def delete_member(member_id: str) -> None:
    # verb = delete. delete carries a pinned floor, so an unmatched
    # delete routes to a human approver by default.
    db.members.delete(member_id)

@heso.action()decorator

Declare a fine catalog action on a gated function. It gates with the catalog’s coarse verb and stamps action.domain / action.action on the receipt as a display label. The engine still classifies the call structurally; the declaration can only raise the lane, never lower it.

python
@heso.action("domain.action_id")   # declare a catalog action

Parameters

catalog_idstrrequired
A "domain.action_id" from the shipped catalog, e.g. "payment.authorize_payment". An unknown id raises ValueError at decoration time.

Example

charge.pypython
import heso

heso.init()

# Declare a catalog action ("domain.action_id"). It gates with the
# catalog's coarse verb (here, payment) and stamps action.domain /
# action.action on the receipt. An unknown id raises ValueError at
# decoration time.
@heso.action("payment.authorize_payment")
def charge_card(amount: int, to: str) -> str:
    return stripe.charges.create(amount=amount, destination=to)

Scoping

Use heso.step to tag a block of actions with a workflow or account label — for example a single agent run — without threading the label through every call. Steps nest: an inner override wins, an unset field falls through to the outer step, then to the config default. The scope is task- and thread-local.

heso.step()context manager

A context manager that scopes the actions inside it to a given workflow and/or account label. Captured actions carry those labels in their receipt.

python
with heso.step(workflow="run-42", account="acct_19"):
    ...

Parameters

workflowstr | None= None
Keyword-only. The workflow label applied to actions captured inside the block.
accountstr | None= None
Keyword-only. The account label applied to actions captured inside the block.
redactlist[str] | None= None
Keyword-only. Extra field names to redact for actions inside the block, unioned with each decorator’s own redact list.

Example

run.pypython
import heso

heso.init()

with heso.step(workflow="run-42", account="acct_19"):
    transfer("Globex LLC", 4200)   # scoped to workflow "run-42"
    notify_finance()               # same workflow + account labels

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 keep using the client’s own API.

heso.wrap()function

Wrap a client in a transparent gating proxy.

python
heso.wrap(client) -> proxy

Parameters

clientobjectrequired
The client 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

agent.pypython
import heso
from 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:

  • .create() is gated as an llm_call.
  • .request() is gated as an http_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 BlockedError or SuspendedError instead 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.

python
heso.process(action: Action) -> Outcome

Parameters

actionActionrequired
The action to gate. Build it with the Action type and a Verb.

ReturnsOutcome

An Outcome describing the decision (its kind is an OutcomeKind).

Example

manual.pypython
import heso
from 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 captures an action, parks it under a session, and runs the body once an approver decides. A cleared action re-mints to an L1 receipt. See Human approval for the full flow.

@heso.gated

The primary suspend/resume surface. The decorated body runs only after the action is approved and this caller wins the local exactly-once claim. Thread a session with _heso_session= (auto-generated when omitted); calling the function again with the same session is the resume.

@heso.gateddecorator

Gate a side-effecting function behind durable suspend/resume. Returns the body’s result on a successful fire, heso.SUSPENDED while parked or after it already fired, and heso.DENIED on a terminal refusal. With raise_on_suspend=True a park raises heso.Paused.

python
@heso.gated(*, sla="1d", on_timeout="deny",
            tool_version=None, expires_at=None,
            raise_on_suspend=False)

Parameters

slastr= "1d"
Hard deadline for the approval, as a duration (30m, 12h, 2d). Signed into the suspension.
on_timeoutstr= "deny"
What happens at the SLA deadline: deny, escalate, or auto_approve.
raise_on_suspendbool= False
When True, a park raises heso.Paused instead of returning heso.SUSPENDED.

Example

payout.pypython
import heso

heso.init()

@heso.gated
def wire_funds(payee: str, amount_usd: int) -> str:
    # Runs ONLY after an approver approves. First call parks and
    # returns heso.SUSPENDED; a later call with the same session resumes.
    return bank.wire(payee, amount_usd)

out = wire_funds("Globex LLC", 50_000, _heso_session="payout-7")
if out is heso.SUSPENDED:
    ...  # waiting on a human; call again later with the same session

resume, decision, append_decision

The top-level helpers for advancing a session out of band:

  • heso.resume(session) — re-read the head, apply the decision, fire-once-or-replay, and return a ResumeOutcome. A bare resume drives the engine lifecycle but cannot run the body; re-call the @gated function with the same _heso_session to run it.
  • heso.decision(session) — read the current lifecycle state off the chain head (approved / denied / expired / escalated / completed / suspended), or None if there is no chain yet.
  • heso.append_decision(session, kind, *, reason=None, decided_at=None) — append a decision link (approved / denied / escalated / expired) to a session’s chain.
  • heso.current_action_hash() — the action hash in flight, exposed while a gated body runs, to correlate a suspension with its later resume.
approve.pypython
import heso

heso.init()

# Record an approver decision out of band...
heso.append_decision("payout-7", "approved", reason="cfo ok")

# ...then advance the lifecycle. Re-call the @gated fn with the same
# session to actually run the body; bare resume() only drives the engine.
outcome = heso.resume("payout-7")

finalize_l1, finalize_quorum

The out-of-band finalize path: when an approver co-signs through the cloud relay, the operator stitches the L1 together locally. Both verify the assembled receipt and push it to the outbox, superseding the suspended action.

  • heso.finalize_l1(action_hash, suspended_content, relayed_parts) — assemble and finalize a single-approver L1receipt. Refuses unless the relayed record’s decision is approved. Raises OperatorKeyMismatchError if the operator key rotated since suspend.
  • heso.finalize_quorum(action_hash, suspended_content, relayed_parts) — the k-of-n sibling. Every leg must be approved. A quorum is still L1, carrying a multi_approval block — not a higher level.
Key rotation fails closed

If the operator key rotates between suspend and finalize, the assemble fails closed and re-suspends under the new key — never a silent mint under a stale key. The cloud relays the detached co-signature and holds no signing key. See Trust levels for how L1 is re-derived at verify.

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.

ExceptionRaised when
BlockedErrorPolicy blocked the action and blocking is on, so the decorated body never runs.
SuspendedErrorThe action was routed to a human approver and is suspended pending that decision.
BridgeErrorThe call into the in-process Rust core failed.
HesoConfigErrorConfiguration could not be resolved — a missing or malformed heso.toml, or a gate used before heso.init().

Public types you import alongside these include Config, Action, Outcome, OutcomeKind, Verb, and RedactStrategy.

Framework adapters

Beyond the decorators, heso ships drop-in adapters for the major agent frameworks. Each is exposed lazily, so a bare import heso never imports a framework you are not using. Every adapter runs each call through the same gate as the decorators, so the receipt is identical no matter which framework produced the action.

  • LangChain & LangGraph — pass heso.HesoCallbackHandler() as a callback to gate every tool invocation. See the LangChain guide.
  • CrewAIheso.crewai (for example heso.crewai.heso_before_hook).
  • OpenAI Agents SDKheso.openai_agents.
  • Claude Agent SDKheso.claude_agent.
  • Pydantic AIheso.pydantic_ai.
  • LangGraphheso.langgraph for graph-node gating.
  • MCPheso.mcp (for example heso.mcp.wrap_call_tool) to gate Model Context Protocol tool calls.

Next steps