The Python SDK gates your agent in the same process — no separate service. You wrap a tool, a delete, or an LLM client. Every call it makes is captured, checked against your policy, signed into an Action Receipt, and added to a BLAKE3 audit chain — a hash-linked log. The signing math runs in the bundled Rust core, so there is no extra process and no Python copy of it.
Install
Install the heso package. It requires Python 3.10 or newer and ships the Rust core as the heso._core wheel, so nothing else needs compiling.
pip install hesoFor the other surfaces — the Node and TypeScript SDKs and the browser verifier — see Installation.
Scaffold a project
heso init creates your operator identity and writes a starter heso.toml. The Rust engine creates the identity and starter policy, heso init adds the local data directory to .gitignore, and you can run it again safely — an existing key and policy are left as-is.
heso initIt leaves you with these files:
heso_bootstrap.py # import heso; heso.init()heso.toml # your starter policy (first-match-wins rules).heso/ # minted operator key + audit log + outbox (gitignored)
heso_bootstrap.py is a one-line entry point you can import once at process start:
import heso heso.init()
The operator key, the audit log, and the outbox queue live in a local data directory that heso init adds to your .gitignore. The signing key stays on your machine.
Gate a tool
Call heso.init() once to load the active config, then decorate a function with @heso.tool. Each call to a gated tool is captured as a tool_call action, checked against policy, signed, and audited.
import heso heso.init() @heso.tooldef search(query: str) -> str: # this body only runs if policy lets the call through return web.search(query) results = search("quarterly revenue, ACME Corp")
If policy blocks the call, the SDK raises BlockedError before the body runs, so the action never happens ungated. That is the default — blocking=True. Set blocking=False for observe-only mode: a refused action is still captured, signed, and audited, but it does not raise.
For destructive operations use @heso.destructive, which gates the call as a delete verb. Deletes ride a pinned floor, so a policy can tighten the rule but can never allow one without approval.
@heso.destructivedef delete_record(record_id: str) -> None: db.delete(record_id)
Redact sensitive fields
Pass redact to keep named fields off our servers. The listed fields are commit-and-reveal redacted before the receipt is signed — HESO stores a BLAKE3 fingerprint of the value, never the value itself.
@heso.tool(redact=["api_key"])def call_partner_api(api_key: str, endpoint: str) -> dict: return requests.get(endpoint, headers={"Authorization": api_key}).json()
Here api_key is committed and stripped, while the rest of the call is captured and signed normally. The original value never reaches HESO. See Redaction for the difference between commit-and-reveal and destructive modes and how the markers verify.
Gate an LLM client
To gate a whole client without decorating each call, wrap it with heso.wrap(). It returns a stand-in for the client that gates its create calls as llm_call and its request calls as http_request, and passes everything else straight through.
import hesofrom openai import OpenAI heso.init() client = heso.wrap(OpenAI()) # gated at the leaf — verb llm_call; the call kwargs become the action fieldsresponse = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "summarize the filing"}],)
The stand-in reaches into nested attributes too, so client.chat.completions.create(...) is gated at the final call, and the call’s arguments become the action fields. If the gate refuses, it raises BlockedError or SuspendedError instead of sending. For wiring OpenAI and Anthropic specifically, see OpenAI & Anthropic.
Run it
Make sure the SDK is initialized before your agent runs. The simplest way is to import the bootstrap module — import heso_bootstrap — at the very top of your entrypoint, so heso.init() runs before your other imports. Calling heso.init() yourself at the top of the program does the same thing.
import heso_bootstrap # init before anything else from agent import runrun()
To tag a group of actions with a workflow label, use heso.step() as a context manager. Every action captured inside the block is scoped to that workflow.
import heso heso.init() with heso.step(workflow="run-42"): search("pricing for the enterprise tier") call_partner_api(api_key, "https://api.partner.example/v1/quote")
What you get
Each gated call produced a complete, standalone piece of evidence:
- A signed Action Receipt recording the verb, the tool, the policy verdict, any redaction markers, and the Ed25519 operator signature.
- A new entry on the BLAKE3 audit chain, where each receipt’s hash links to the one before it — so altering an earlier receipt breaks every downstream link.
- An artifact anyone can verify offline: recompute the hash, check the signature, re-derive the trust level. No HESO infrastructure required.
A receipt proves the operator authorized this action under a known policy, and at L1 that a person approved it with a device-held key. It records what was authorized, not whether the action succeeded downstream.
Now check one. The browser verifier re-runs the same Ed25519 and BLAKE3 math locally, and Action receipts walks the wire shape field by field.
