Python SDK reference
pip install authoxi
⚠️ This page documents 0.7.0, which is not published yet. PyPI currently has
0.1.2. This page is generated from source, so anything added since0.1.2is documented here but will NOT be in the package you install today — it ships in the next release.
We are telling you this rather than letting you find out from an
ImportError. To check what you actually have:pip show authoxi. You can also verify a signed decision with no install at all: do it in your browser.
Every signature below is generated from the source by
inspect, and the build fails if it drifts — so a parameter documented here exists, and one that exists is documented here.
from authoxi import AgentControl, keygen, verify_event
AgentControl — governing agents
The tenant-side client. One instance per tenant secret key; every call below is an authenticated control-plane operation.
AgentControl
Control-plane operations for a tenant (authenticated with its secret key).
Two ways to construct it:
from_url— point it at a running authoxi API by URL and it builds the HTTP transport for you:cp = AgentControl.from_url("https://api.authoxi.com", secret_key="sk_live_…"). This is the everyday path.AgentControl(http, secret_key=…)— inject any object with.get/.post/.delete(anhttpx.Client, or a FastAPITestClientfor in-process tests/demos).
admin_create_tenant(name: str) -> dict[str, Any]
Create a tenant on the agent control plane. Requires the master key; returns its
secret_key.
Admin-only. Everything else on this class acts as one tenant, with the secret key.
Named admin_create_tenant (not create_tenant) to match
admin_create_tenant, which POSTs the same /admin/tenants route,
and to be unmistakably distinct from create_tenant, which targets a
DIFFERENT plane (/accounts/tenants) and deliberately returns no secret_key.
:param name: a human-readable tenant name.
:returns: the new tenant, including its one-time secret_key (sk_live_…).
create_agent(name: str, *, allowed_scopes: list[str] | None = None, kind: str = 'agent', principal_did: str | None = None) -> 'Agent'
Generate a keypair, register the agent, and hand back a bound handle — the one-call start::
agent = cp.create_agent("procurement-bot")
wallet = agent.issue_mandate(budget="5000.00", escalate_over="1000.00",
actions=["payment"])
wallet.authorize("payment", "250.00")
The handle carries the agent_id and the private key, so you stop threading them through
every call. The key is generated locally and never sent — same guarantee as doing it by
hand. Use register_agent instead when you bring your own key (an HSM, an existing
identity), or when you only have a public key to register.
register_agent(*, public_key: str, name: str, allowed_scopes: list[str] | None = None, kind: str = 'agent', principal_did: str | None = None) -> AgentInfo
Register an agent from a public key you already hold. Returns agent_id and its
did:key. See create_agent for the one-call version that makes the key for you.
Pass only the public key (see keygen). The agent keeps the private key;
authoxi never sees, stores, or transmits it — so we cannot impersonate your agent,
and neither can anyone who breaches us.
allowed_scopes is the ceiling: a passport may request a subset of it and nothing
more (an over-request is refused with scope_not_granted).
kind is what sort of actor this is — agent (unattended), service (acting
for the organisation), human, or agent-on-behalf-of-human. It is signed into
every passport this agent mints and cannot be chosen by the agent itself at runtime,
which is the only reason a relying party can trust it. agent-on-behalf-of-human
requires principal_did naming the person: the claim means someone is accountable,
so naming nobody is refused.
verify_agent(*, agent_id: str, challenge: str, signature: str) -> dict[str, Any]
Check an agent's proof-of-possession WITHOUT minting a passport — a third party
confirming "does this agent hold the key it claims?" (get a challenge from
passport's flow, or issue your own). Returns {agent_id, agent_did, verified, verified_at}. Raises Unauthorized if the signature does not verify.
passport(*, agent_id: str, priv: Ed25519PrivateKey, scopes: list[str] | None = None, audience: str | None = None) -> str
Prove possession of the agent's key and get a short-lived passport (an EdDSA JWT).
The agent signs a single-use challenge with its private key — no shared secret ever crosses the wire. Relying parties verify the passport offline against the published JWKS, so the hot path never calls us.
Pass audience to bind the passport to one relying party — it goes into the token's
aud claim, and a firewall configured with a matching AUTHOXI_PASSPORT_AUDIENCE
will accept the passport ONLY against that deployment. A leaked passport is then useless
anywhere else. Leave it unset (the default) and the passport is unbound.
Raises BudgetExceeded (HTTP 402) if the agent is over its meter. That is the
enforcement lever: over budget → no fresh passport → within one TTL (≤5 min) the agent
can reach neither a model nor a tool. There is no inference proxy, by design.
create_type(*, name: str, policy: dict[str, Any], description: str = '') -> dict[str, Any]
Create an agent type — a role, in the AWS-IAM sense.
The policy is an envelope over the four pillars
({keyring, meter, wallet, identity}) that both caps and defaults what any
agent of this type may be granted. Authority is clamped in three tiers:
grant ⊆ type ⊆ tenant guardrail, so a mistake in one grant cannot exceed the role.
assign_type(*, agent_id: str, type_id: str) -> Any
Bind an agent to a type. Its grants are re-clamped to that type's policy.
grant(*, agent_id: str, system: str, scopes: list[str]) -> dict[str, Any]
Keyring: grant scoped, revocable access to one connected system.
Enforced per action at the firewall, not per session. Everything not granted is
denied — there is no ambient authority. Raises Denied if the grant would
exceed the agent's type policy.
list_types() -> list[dict[str, Any]]
Every agent type (role) in the tenant.
get_type(type_id: str) -> dict[str, Any]
One type, including its full policy envelope. Raises NotFound if unknown.
list_grants(*, agent_id: str | None = None) -> list[dict[str, Any]]
The keyring: every scoped grant, optionally narrowed to one agent — what an agent currently holds, which is exactly what you revoke against.
revoke_grant(grant_id: str) -> dict[str, Any]
Revoke one keyring grant — the agent lives on but loses access to that system.
The counterpart to grant, so access is never a one-way door.
set_budget(*, agent_id: str, limit_units: int, resource: str = 'inference_usd', period: str = 'day') -> dict[str, Any]
Meter: cap an agent's usage of a resource per period (day / month).
limit_units are integers (µUSD for inference_usd) — never floats, which
do not compare or serialize reliably. Enforcement is at credential issuance, not in
the model path: over budget → passport returns 402 → the agent is cut off.
No policy means meter-only (record, don't block). A policy makes it a hard cap.
consume(*, agent_id: str, units: int, resource: str = 'inference_usd') -> MeterResult
Report usage against the meter. Returns {"allowed": bool, "used", "limit"}.
The guard is inside the write (one atomic UPDATE … WHERE used + n <= limit), so
concurrent consumes cannot jointly overshoot the cap. allowed=False is returned
with HTTP 200 — a refusal is an answer, not an error.
get_usage_policy(*, agent_id: str, resource: str = 'inference_usd') -> dict[str, Any]
Read back an agent's configured budget. Raises NotFound when there is no
policy — which means metered-only (never blocked), not an error you must handle.
clear_budget(*, agent_id: str, resource: str = 'inference_usd') -> None
Remove an agent's budget — back to metered-only. "No policy row" is the ledger's
own encoding of "never block", so deleting the row IS the operation (there is no
limit=∞ to set). The counterpart to set_budget; idempotent.
usage_report(*, period: str = 'day') -> dict[str, Any]
Every agent's spend this period (day/month) with its cap (null =
metered-only) — the usage dashboard in one call.
issue_mandate(*, agent_id: str, actions: list[str], budget: str = '0', escalate_over: str | None = None, escalate_over_sensitivity: int | None = None, subject_kind: str = 'agent', subject_did: str | None = None, parent_mandate_id: str | None = None) -> MandateInfo
Issue a signed mandate — what an agent may do, and when a human must sign off.
actions is the only required part, and it is exhaustive: anything not listed is
denied. The two supervision levers are independent, and you set whichever fits the work:
Money — budget is the hard ceiling for the mandate's whole life; escalate_over
is where the agent stops being autonomous. Both are decimal strings ("9000.00");
money is never a float. Note escalate_over="0" means escalate everything (every
amount is >= 0), not "no money here" — for that, just leave it unset::
cp.issue_mandate(agent_id=a, budget="5000.00", escalate_over="1000.00",
actions=["payment"])
Sensitivity — escalate_over_sensitivity (0 routine · 1 elevated · 2 sensitive ·
3 critical) sends any action of at least that KIND to a human, whatever it costs. This is
the lever for work that moves no money, which is most agent work — escalate_over can
only ask "how much?", and the answer for a read is always zero::
cp.issue_mandate(agent_id=a, actions=["data_access", "data_export"],
escalate_over_sensitivity=2) # reads run; exports need a human
Either trigger escalates; an action type the taxonomy doesn't label reads as critical (fail closed). Omit both and the mandate is an allow-list with no supervision.
Pass parent_mandate_id to delegate: carve this mandate out of an existing one.
budget is checked against the parent's remaining capacity (its own spend plus
whatever it has already delegated), actions must be a subset of the parent's, and
escalate_over can only make the child more supervised than its parent, never less.
Revoking the parent revokes every mandate delegated from it, recursively.
authorize(*, agent_id: str, mandate_id: str, action: str, amount: str | None = None, system: str | None = None, scope: str | None = None) -> Decision
The decision. Returns allow / deny / escalate, and leaves a signed
record either way.
Three outcomes, not two — that is the whole design. With only allow/deny, every action
a human should look at becomes a denial and the agent is useless; escalate is what
lets it be autonomous for the 99% and supervised for the 1% that can hurt you. On
escalate the action does not happen and you get a pending_id: take it to
to_sign and resolve.
The budget guard is one atomic UPDATE … WHERE spent + amt <= budget, so the 51st
dollar of a $50 mandate never passes and concurrent calls cannot jointly overshoot.
to_sign(*, pending_id: str, approver_mandate_id: str, reason_class: str, reason_code: str) -> Any
The exact bytes the human must sign to resolve an escalation.
Fetch these, sign them in the approver's own client with the approver's own key,
then call resolve. The signing must not happen on a server that holds the
human's key — a server that can forge her approval puts you back where you started.
The reason code is inside the signed bytes: a free-text "approved" is worthless, a structured reason is a label, and it cannot be rewritten afterwards.
resolve(*, pending_id: str, resolution: str, approver_mandate_id: str, signature: str, reason_class: str, reason_code: str) -> dict[str, Any]
Resolve an escalation with a human's signature (approve or deny).
authoxi verifies that signature against the public key in the approver's own mandate. If it does not verify, the action does not proceed. An approval is a signature or it is a rumour: this is what makes it non-repudiable — she cannot later say she never approved it, because the signature could not exist without her private key.
Emits a signed loss-event/v2 that any stranger can verify offline
(from authoxi import verify_event) — with no account, and no call back to us.
counters(agent_id: str) -> dict[str, Any]
What the gate says it authorized. The number the record is reconciled against.
Hand this to agentlox as gate_authorized and it becomes the difference between "our
records look complete" and "our records ARE complete".
agentlox can already prove its copy has no internal holes: a missing seq in a signed,
gate-numbered sequence is self-evident. What it cannot see alone is a truncated tail —
the most recent K decisions quietly never stored. The record just ends, and an ending
looks like an ending. Only the gate knows how many it issued.
Which is exactly why this call exists here and why agentlox must never make it itself. A witness that phones the gate, receives the count, and reports it back has not reconciled anything; it has produced one statement and copied it twice. The two numbers have to come from two parties, or the comparison is theatre. So the count travels through you.
Returns {agent_id, agent_did, decisions_authorized, gate}. gate is hosted or
self_hosted: a record whose gate the customer also runs is weaker evidence than one
whose gate they do not, and the reader is entitled to know which they are holding.
list_agents() -> list[dict[str, Any]]
Every agent in the tenant, newest first.
get_agent(agent_id: str) -> dict[str, Any]
One agent, including the IAM type_id it assumed (which the registration
response does not carry). Raises NotFound if unknown.
list_mandates(*, subject_id: str | None = None) -> list[dict[str, Any]]
Every mandate in the tenant, newest first; pass subject_id to scope to one
agent (or one human approver). Once the issue response is gone this is the only way
back to a mandate_id.
list_pending(*, status: str = 'pending', limit: int = 200) -> dict[str, Any]
The escalation queue — every call blocked on a human, plus how settled ones ended.
Pass status="resolved"/"expired" to see history. This is what a console
reconciles against, so a dropped notification is a delay, not a lost decision.
get_tenant() -> dict[str, Any]
This tenant's own record — the "who am I" for the secret key in hand.
connect(*, provider: str, secret: str) -> dict[str, Any]
HOSTED: the secret is sealed under the tenant's KMS at the control plane.
declare_connection(*, provider: str) -> dict[str, Any]
SELF-HOSTED: no secret sent — the credential lives in the customer's data plane.
list_connections() -> list[dict[str, Any]]
Every active connection for the tenant (metadata only — never the secret).
get_connection(provider: str) -> dict[str, Any]
One connection's metadata. Raises NotFound if there is no active
connection for provider.
revoke_connection(provider: str) -> dict[str, Any]
Revoke a connection and destroy its sealed secret — the firewall stops injecting
that credential immediately. The counterpart to connect /
declare_connection.
revoke_agent(agent_id: str) -> dict[str, Any]
Kill switch: revoke an agent. Its passport stops being refreshed and every future
authorize() denies with agent_revoked — the enforcement half of "keep an agent
from going rogue". Idempotent.
The endpoints have existed since the control plane shipped; this exposes them on the SDK so a caller does not drop to raw HTTP for the one operation they reach for in an incident.
revoke_mandate(mandate_id: str) -> MandateInfo
Revoke one money mandate without revoking the agent — the agent lives on, but this
spend authority is dead. Future authorizes against it deny with mandate_revoked.
Revoking a parent revokes everything delegated from it, recursively.
approve_escalation(*, pending_id: str, approver_mandate_id: str, approver_private_key: 'Ed25519PrivateKey | str', reason_class: str, reason_code: str, resolution: str = 'approve') -> dict[str, Any]
Resolve an escalation in one call: fetch the exact bytes to sign, sign them with the approver's own key client-side, and submit.
This collapses the raw dance (to_sign → decode → sign → resolve) that
every caller otherwise reimplements — the single most error-prone path in the SDK, because
the reason code is inside the signed bytes and must match on both calls. The private key
is used locally and never sent; authoxi verifies against the public key inside the
approver's own did:key, which is what makes the approval non-repudiable.
approver_private_key is the approver's raw Ed25519 key — an Ed25519PrivateKey or a
base64url-encoded 32-byte seed. resolution is "approve" or "deny".
await_decision(pending_id: str, *, timeout: float = 120.0, hold: float = 20.0) -> dict[str, Any]
Block until an escalated call is resolved by a human, then return the outcome.
No polling loop to write: this long-polls the gate, which holds each request open until the
decision lands (or a bounded server hold elapses) and returns immediately on resolution — so
the agent resumes within milliseconds of approval, not on a poll interval. Transparently
re-waits until timeout total seconds.
Returns {pending_id, status, resolution, approved, reason_code, approver_did} where
status is resolved / expired / pending (only if timeout is hit first)
and
approved is the single boolean to branch on.
authorize_and_wait(*, agent_id: str, mandate_id: str, action: str, amount: str | None = None, system: str | None = None, scope: str | None = None, timeout: float = 120.0) -> dict[str, Any]
Authorize an action and, if it escalates to a human, block until they decide — in one call.
The frictionless path: allow / deny return immediately; escalate transparently
waits for the human and folds their verdict into the same result. The returned dict carries
both the original decision fields and the final status / approved.
mint_token(*, agent_id: str, system: str, scopes: list[str], ttl_seconds: int = 300, constraints: dict[str, Any] | None = None) -> MintedToken
Mint a down-scoped, expiring credential for one connected system.
scopes must be within the agent's keyring grant — an over-request is refused, so you
always know exactly what you got. The default is a signed capability token any enforcement
point verifies offline; if a native provider minter is configured server-side you get a real
provider credential instead. Either way the agent never holds the root secret.
Returns {token, token_type, system, scopes, expires_at, agent_did} (see
MintedToken).
reason_codes() -> ReasonCodeTaxonomy
The authoritative reason-code taxonomy this deployment publishes.
resolve / approve_escalation / to_sign all take a
reason_code + reason_class that are hashed INTO the signed bytes, so the labels
must be valid and stable. Discover them here instead of hardcoding an enum — a console
that forks its own taxonomy quietly makes the cross-product loss dataset non-comparable.
:returns: the taxonomy grouped as deny (system denials), approve (human
approval reasons), and deny_human (the high-signal human-deny codes); see
ReasonCodeTaxonomy. Unauthenticated: the taxonomy is public.
policy_bundle() -> PolicyBundle
The firewall's decision inputs for this tenant, as ONE read-only bundle.
The read that makes self-hosting work: a self-hosted data plane syncs this with its tenant secret key and serves the firewall's reads — server lookup, tool classification, keyring — from a local cache, needing no control-plane database. It is a projection of data the tenant already owns; nothing here is a secret.
:returns: {tenant_id, servers, tool_policies, grants} (see
PolicyBundle).
:raises Unauthorized: if the client holds no valid tenant secret_key.
jwks() -> dict[str, Any]
The public JWK Set for verifying agent passports OFFLINE.
A relying party (e.g. the MCP firewall) fetches this once and verifies every passport against it with no call back to authoxi on the hot path. Public by design — this issues no auth header, so it works before any tenant secret is in hand.
:returns: a standard {"keys": [...]} JWK Set.
register_detector(*, name: str, public_key: str) -> dict[str, Any]
Register a signal source (e.g. agentlox's anomaly detector). Pass only the
public key from keygen — the detector keeps its private key, so authoxi
can never forge a signal in its name.
submit_anomaly_signal(*, detector_id: str, agent_id: str, severity: str, reason_code: str, signature: str) -> dict[str, Any]
Submit a detector's signed claim that an agent looks anomalous.
severity is low / medium / high / critical; only high and
critical trigger an immediate revoke (lower severities are acknowledged and change
nothing — see register_detector and sign_anomaly_signal for how to
produce signature). Returns {agent_id, action_taken, event_id}.
list_detectors() -> list[dict[str, Any]]
Every registered signal source for the tenant, active or revoked.
deactivate_detector(detector_id: str) -> dict[str, Any]
Turn a detector off — its signals are refused from that instant. The off switch
for a rogue or decommissioned detector (a signal can revoke an agent, so a compromised
detector must be stoppable). Raises NotFound if unknown.
register_mcp_server(*, name: str, url: str, provider: str | None = None) -> dict[str, Any]
Register an upstream MCP server the firewall may proxy calls to.
The url is SSRF-checked server-side (https/http only, must resolve to a routable
public address) — pointing it at an internal/metadata address is refused.
Pass provider to opt into credential injection: on an allowed call, the firewall
unseals that connection's secret (see connect) and attaches it to the
forwarded request. Leave it unset and the firewall forwards no credential — the
original, still-default behaviour.
list_mcp_servers() -> list[dict[str, Any]]
Every registered upstream, with the provider each injects (if any).
delete_mcp_server(name: str) -> dict[str, Any]
Remove a registered upstream. Raises NotFound if the name is unknown.
set_tool_policy(*, server: str, tool: str, action_type: str, amount_field: str | None = None, currency_field: str | None = None, required_scope: str | None = None) -> dict[str, Any]
Bind an MCP tool to the action_type a mandate is written against — without this the
firewall cannot know that stripe_create_charge IS a payment and the allow-list
has nothing to bite on. tool="*" sets the server's default. amount_field/
currency_field are dotted paths into the call's arguments where the money is.
list_tool_policies() -> list[dict[str, Any]]
Every tool→action binding for the tenant.
delete_tool_policy(*, server: str, tool: str) -> dict[str, Any]
Remove one tool→action binding (exact match; the server default is tool="*").
Raises NotFound if there is no such policy.
guard(*, agent_ref: str, action: str, amount: str | None = None, currency: str | None = None, system: str | None = None, scope: str | None = None, principal_did: str | None = None, ctx: dict[str, Any] | None = None, trace_ref: str | None = None) -> dict[str, Any]
Authorize by a name you choose, with nothing registered and no mandate issued.
cp.guard(agent_ref="invoice-bot", action="payment", amount="250.00")
The first call for a name provisions a managed agent and an implicit dryrun mandate,
so it succeeds — and starts the shadow record that would_have_blocked reads
and mandate_proposal turns into a real one. Every later call for the same
agent_ref reuses the same agent.
A gate that blocks on day one is a gate that is removed on day two. This is the path that lets an operator see what would happen before anything is enforced.
Note the name: this is the tenant-side front door, called by a backend holding the
secret key. An agent governs its own actions with guard, which is a
context manager and holds no issuing credential.
would_have_blocked(*, agent_id: str | None = None, limit: int = 5000) -> dict[str, Any]
What flipping to deny would have changed over the shadow window.
The payoff of running in dryrun: the calls that went through and would not have. It reports the money at risk and a sample, so "should we enforce this yet?" is answered with the operator's own traffic rather than with a guess.
mandate_proposal(agent_id: str) -> dict[str, Any]
A mandate derived from what this agent has actually done.
Writing an allow-list from memory produces one that is wrong in both directions — too
narrow to work and too broad to help. This proposes one from the observed traffic; you
edit it and issue it with issue_mandate.
team(*, actor: str | None = None) -> list[dict[str, Any]]
Everyone who can administer this tenant, and what each of them may do.
add_member(*, email: str, role: str, actor: str | None = None) -> dict[str, Any]
Invite a person to the control plane. role is one of the tenant's roles —
an unknown one is refused rather than defaulted, because a typo that silently became
viewer would be a demotion nobody noticed, and a typo that became owner would be
worse.
set_member_role(member_id: str, *, role: str, actor: str | None = None) -> dict[str, Any]
Change what someone may do. The server refuses to remove the last owner — a tenant nobody can administer is unrecoverable without us, and that is a support ticket we should never be able to cause.
remove_member(member_id: str, *, actor: str | None = None) -> dict[str, Any]
Revoke someone's access to the control plane.
audit(*, limit: int = 100, action: str | None = None, actor: str | None = None) -> dict[str, Any]
What PEOPLE did to this tenant's control plane.
A different question from what agents did, and deliberately a different record: the signed decision trail is about actions taken under a mandate, this is about who granted the mandate in the first place. An auditor asks both, and conflating them makes neither answerable.
Read-only, with no delete anywhere in the product — a log its own subject can prune is a log that is empty exactly when someone needs it.
compliance_pack(*, agent_id: str | None = None, limit: int = 50) -> dict[str, Any]
Assemble the evidence pack for this tenant, or for one agent.
The thing worth knowing before you read one: it says what it does NOT cover. Every
obligation carries covered / partial / not_covered, and the largest gap is
structural — authoxi decides, it does not retain. A pack proves the authority an
agent acted under and points at the record of what it did; it cannot be that record.
Deterministic and hashed, so an auditor handed a document can later check it is the one that was generated.
registry(*, limit: int = 50, offset: int = 0, kind: str | None = None, status: str | None = None) -> dict[str, Any]
Every agent this tenant owns, with what the outside world can see about each.
The useful column is resolvable: an agent whose did does not resolve is one that
cannot be identified by anyone it calls, so its signatures name nobody. That is a
deployment defect which is invisible from inside — everything works, and every origin
it talks to sees an anonymous request.
registry_entry(agent_id: str) -> dict[str, Any]
One agent's public face. Raises NotFound for an agent of another tenant —
the same answer as for one that does not exist, which is the only answer that does not
confirm the id.
authorize_payment(*, agent_id: str, merchant: str, amount: str, approver_did: str, cart: Any = None, currency: str = 'USD', mandate_id: str | None = None, protocol: str | None = None, purchase_id: str | None = None, ttl_seconds: int = 300) -> dict[str, Any]
Decide a purchase, and get back the exact bytes a human must sign.
decision = cp.authorize_payment(
agent_id=agent.agent_id,
merchant="shop.example",
amount="250.00",
cart={"items": [...], "total": "250.00"},
approver_did=alice_did,
)
if decision["decision"] == "allow":
proof = sign_payment_proof(decision["proof"], alice_private_key)
The signature is the approver's, never ours — see sign_payment_proof. And pass
the cart: same total, different line items, is a different purchase, and "they
approved the amount" is exactly the argument a disputing cardholder makes when the
items were swapped.
Agent — the runtime side
What the agent itself holds and calls. It keeps its private key; the control plane never sees it.
Agent
A runtime agent, bound to its own identity: it holds the keypair, refreshes its own passport, meters its own cost, and issues mandates without you re-stating who it is.
Get one from create_agent (which makes the key for you), or construct
it directly when you already have an agent_id and its private key.
issue_mandate(*, actions: list[str], budget: str = '0', escalate_over: str | None = None, escalate_over_sensitivity: int | None = None, parent: 'Mandate | None' = None) -> 'Mandate'
Issue this agent's mandate and get a bound handle back, so authorizing is one call
with no ids to thread. escalate_over supervises on money; escalate_over_sensitivity
supervises on the KIND of action (see issue_mandate) — set it when the
agent's work isn't spending. Pass parent to delegate out of an existing mandate.
revoke() -> dict[str, Any]
Kill switch: revoke this agent. Every future authorize denies.
refresh_passport(scopes: list[str] | None = None, *, audience: str | None = None) -> str
Obtain a fresh passport, signing the challenge with the key this handle holds — so you
never touch the private key yourself. Raises BudgetExceeded if the agent is over its
meter (over budget → no passport → cut off). Pass audience to bind the passport to one
relying party; see passport.
meter(*, cost_units: int, resource: str = 'inference_usd') -> None
Charge this agent's meter for work it just did. Raises BudgetExceeded when over.
Call it AFTER the model call, with the real cost: tokens x price in µUSD. authoxi
does not count tokens and never will — it has no tokenizer, no pricing table, and no
business acquiring one. It is the ledger and the enforcement point; something that sits
in the call path does the counting (agentlox's SDK already does, on every patched
provider call).
This replaces a chat() method that mocked the LLM — it returned
f"[mocked completion for: {prompt}…]" and was published to PyPI in a customer-facing
package. The metering underneath it was real, which is exactly what made it dangerous:
the thing that looked like the product was a stub, and the thing that was real was buried
inside it. A demo scaffold is not an API. This is the primitive it was pretending to be.
guard(action: str, amount: 'str | Callable[..., str] | None' = None, *, mandate_id: str, system: str | None = None, scope: str | None = None, wait: bool = True, timeout: float = 25.0) -> '_Guard'
The lightest way to enforce a decision: wrap YOUR OWN credentialed call so it only runs when authoxi allows it. No MCP server, no sidecar — authoxi never sees the payload or the credential, only the decision, exactly like the MCP firewall but in your process instead of ours.
Works as a block or a decorator, one object both spellings — the same shape as
guard and guard, so the guard you reach for is the
same construct whichever handle you hold::
with agent.guard("stripe:refund", "5000.00", mandate_id=mnd):
stripe.Refund.create(charge=charge_id, amount=500000) # your key, your call
@agent.guard("payment", amount=lambda inv: inv["amount"], mandate_id=mnd)
def pay(inv):
return psp.charge(inv) # runs only if authorized
On deny this raises Denied before the block runs — the guarded code never
executes. On escalate it blocks for a human by default (wait=True, same
transparent wait as authorize_and_wait); pass wait=False to
raise Escalated immediately instead and handle the retry yourself. Either way,
the block runs if and only if the action was actually authorized. mandate_id is
keyword-only and required — an Agent is not bound to one wallet, so it must be
told which authority to spend against (hold a Mandate to drop it).
Verification — prove it, without us
Check a signed decision offline. No client, no key, no network call — these functions work even if authoxi is down, and that is the point: an audit record that needs the vendor alive to validate is not an audit record.
verify_event(event: dict[str, Any]) -> VerifyResult
Verify every signature present on a loss-event/v2.
What a pass proves: the event was signed by the holder of the key inside its own
did:key and not one byte has changed since; and if an approver signature is present,
a specific human approved this exact action and cannot later deny it.
What it does NOT prove: that the key belongs to who it claims (a signature binds an
action to a key; binding a key to a human is your identity process); that no event was
suppressed (use verify_stream, which checks seq for holes); or that the
action actually executed (this is a record of an authorization, not of an effect).
verify_stream(events: list[dict[str, Any]]) -> tuple[bool, list[Check]]
Verify a run of decisions — signatures and continuity.
A signature proves an event was not altered. It proves nothing about one you never
received, and the easy attack on an audit trail was never forgery, it was deletion.
seq is monotonic per agent and signed by the gate, so suppressing decision 42 leaves
a hole at 42 that cannot be renumbered around without forging every later signature.
A verifier that checks signatures but not continuity is checking the wrong thing.
An empty stream is not a verified stream. Deletion is the attack this function exists to
catch, so the total-deletion case must not be its one blind spot: an adversary who suppresses
every event would otherwise be handed the same green answer as an operator whose record is
genuinely whole. There is no signature to check and no seq to find a hole in, and "I could
not verify anything" is a different claim from "everything checks out". This returns False.
VerifyResult
The verdict.
ok — every signature present on the event holds and nothing was altered.
verified — a human signed it, and that signature is valid. Derived from the
signature, never from the event's own verified field, which is just a claim.
| field | type |
|---|---|
ok |
bool |
verified |
bool |
checks |
list[Check] |
Check
One thing that was checked, and whether it held.
| field | type |
|---|---|
name |
str |
ok |
bool |
detail |
str |
Keys
The agent generates its own keypair. You register only the public half.
keygen() -> tuple[Ed25519PrivateKey, str]
A fresh Ed25519 keypair. Returns (private key object, public key as base64url) — the private key never leaves the agent; only the public key is registered.
sign(priv: Ed25519PrivateKey, message: bytes) -> str
Undocumented.
Errors
Raised by the calls above. Catch these, not Exception.
BudgetExceeded
A metered call was blocked because the agent is over budget.
Denied
An action was denied by policy. reason_code says why.
Escalated
An action needs a human. pending_id is the decision awaiting a signature.
AuthoxiError
Base exception for all authoxi errors.
Carries the server's structured code and HTTP status_code so callers can branch on
the failure without scraping the message. Subclasses below give the common HTTP classes a
name; from_api_error maps a server code to the right one.
Human authentication
The human side — sign-up, sign-in, sessions, OAuth. The anchor that starts the delegation chain to an agent.
AuthoxiClient
Sync authoxi client. Uses a persistent httpx.Client — safe to call repeatedly.
close() -> None
Undocumented.
admin_create_tenant(name: str, *, metadata: dict[str, Any] | None = None) -> Tenant
Undocumented.
admin_get_tenant(tenant_id: str) -> Tenant
Undocumented.
admin_rotate_secret(tenant_id: str) -> str
Undocumented.
signup(email: str, password: str, *, name: str | None = None, metadata: dict[str, Any] | None = None) -> tuple[User, Tokens]
Undocumented.
signin(email: str, password: str) -> Tokens
Undocumented.
request_otp(email: str | None = None, *, phone: str | None = None, purpose: str = 'signin', name: str | None = None, first_name: str | None = None, last_name: str | None = None, metadata: dict[str, Any] | None = None) -> OTPState
Undocumented.
verify_otp(otp_id: str, code: str) -> tuple[User, Tokens]
Undocumented.
me(access_token: str) -> User
Undocumented.
verify_token(access_token: str) -> User
Validate a bearer token, return its User. Raises InvalidToken on 401.
signout(refresh_token: str, *, access_token: str) -> None
Undocumented.
refresh(refresh_token: str) -> Tokens
Undocumented.
forgot_password(email: str, *, redirect_url: str | None = None) -> None
Undocumented.
reset_password(token: str, new_password: str) -> None
Undocumented.
update_password(access_token: str, new_password: str) -> None
Set/update the current user's password (Bearer-authenticated).
AsyncAuthoxiClient
Async authoxi client.
Two credential modes:
publishable_key— required for/auth/*flows. Sent asX-Authoxi-Key.master_key— required foradmin_*operations. Sent asAuthorization: Bearer <master_key>.
aclose() -> None
Undocumented.
admin_create_tenant(name: str, *, metadata: dict[str, Any] | None = None) -> Tenant
Undocumented.
admin_get_tenant(tenant_id: str) -> Tenant
Undocumented.
admin_rotate_secret(tenant_id: str) -> str
Undocumented.
signup(email: str, password: str, *, name: str | None = None, metadata: dict[str, Any] | None = None) -> tuple[User, Tokens]
Undocumented.
signin(email: str, password: str) -> Tokens
Undocumented.
request_otp(email: str | None = None, *, phone: str | None = None, purpose: str = 'signin', name: str | None = None, first_name: str | None = None, last_name: str | None = None, metadata: dict[str, Any] | None = None) -> OTPState
Undocumented.
verify_otp(otp_id: str, code: str) -> tuple[User, Tokens]
Undocumented.
me(access_token: str) -> User
Undocumented.
verify_token(access_token: str) -> User
Validate a bearer token, return its User. Raises InvalidToken on 401.
signout(refresh_token: str, *, access_token: str) -> None
Undocumented.
refresh(refresh_token: str) -> Tokens
Undocumented.
forgot_password(email: str, *, redirect_url: str | None = None) -> None
Undocumented.
reset_password(token: str, new_password: str) -> None
Undocumented.
update_password(access_token: str, new_password: str) -> None
Set/update the current user's password (Bearer-authenticated).
Also exported
Response models and lower-level types: ACTOR_KINDS, ASSURANCE_LEVELS, Accounts, AccountsSync, ActorKind, AgentInfo, AgentRuntime, AgentSigner, AnyVerifyResult, AssuranceLevel, ChallengeRequired, Conflict, Decision, EmailTaken, Forbidden, GLOSSARY, InvalidCredentials, InvalidRequest, InvalidToken, KeyCreated, KeyRecord, KeyValidation, Mandate, MandateInfo, MeterResult, MintedToken, NotFound, OTPExpired, OTPInvalidCode, OTPLocked, OTPState, PAYMENT_SCHEMA, PaymentProofResult, PolicyBundle, RateLimited, ReasonCodeTaxonomy, START_HERE, Sense, ServiceKeyCreated, ServiceKeyRecord, SignatureVerification, Tenant, TenantRecord, Term, Tokens, Unauthorized, User, acts_for_human, assurance_at_least, build_payment_proof, cart_hash, connect, did_key, did_key_from_public, explain, generate_keypair, guard, jwk_from_public, jwk_thumbprint, jwks, key_from_seed, key_to_seed, keyid_for_public, normalize_kind, payment_signing_input, public_from_did_key, public_from_jwks, receipt_link, runtime, sign_anomaly_signal, sign_payment_proof, sign_record, sign_request, verify_any, verify_payment_proof, verify_record, verify_request.