authoxi docs

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 since 0.1.2 is 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:

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:

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.