Skip to main content

AgenticAuthClient

app.sdk.client.AgenticAuthClient — inline enforcement for agent code you control. Same endpoints, same decisions, same audit records as the sidecar; the difference is only where enforcement sits. Inline gives richer Python exceptions and needs no extra process to run, but — unlike the sidecar, which cannot be bypassed once egress is pinned to it — an agent's own code could in principle skip calling it.

Constructor​

AgenticAuthClient(
base_url: str,
*,
admin_token: str | None = None,
audit_token: str | None = None,
identity_token: str | None = None,
client: httpx.AsyncClient | None = None,
timeout: float = 5.0,
trace_path: str | None = None,
identity_key: AgentKey | None = None,
agent_name: str | None = None,
)

trace_path (or the AGENTIC_AUTH_TRACE_PATH environment variable) makes every authorize call also record one edge event — the same shape drift record writes — attributed to the agent the decision service verified.

identity_key and agent_name must be given together, and make this client an agent's own: it then signs the RFC 8693 actor_token proof of possession on every exchange, and the Agent-Identity assertion each Grant carries. Without them the client can still take hops, but only while identity_required is off.

The three bearers are separate because the planes below are separate. audit_token and identity_token each fall back to audit_token then admin_token, which every route also accepts — so one credential works in a notebook and three work in production.

Records a human's consent and pins the policy version for the session. Mirrors POST /v1/consent. Raises ControlPlaneUnavailable on any non-201 response.

async authorize(ctx: HopContext, *, target, data_class, taint=None, params=None) -> Grant​

Decides and narrows, in that order, always both: authorize never returns a decision without also exchanging for the narrowed token, and never exchanges without a fresh decision backing it. Raises PolicyDenied on a deny, ControlPlaneUnavailable if either the decision or the exchange service can't be reached or returns something unusable.

params is the call's JSON body. It is required when the decision reports transaction_required — a privileged target — and ignored otherwise: authorize then also mints the transaction token bound to exactly those parameters and returns it on the Grant. Omit it for a privileged target and the call raises TransactionParamsRequired before the decision is spent, so nothing is consumed.

taint is an agent-reported provenance label. It is additive only: it can raise the effective taint the decision is made under, never lower it.

async hop(ctx: HopContext, *, target, data_class, taint=None) -> tuple[Grant, HopContext]​

authorize, plus the HopContext to hand to whoever receives the delegation. This is the method most integrations call — it returns exactly what you need to both make the current call and propagate correctly to the next hop.

async call(grant: Grant, method, url, **kwargs) -> httpx.Response​

Makes the call the grant authorizes, merging grant.headers() over any headers you pass — Authorization, plus Txn-Token and Agent-Identity when the grant carries them.

async enroll(agent_id, *, key, agent_name, claims=None, principal_assertion=None, enroll_token=None) -> dict​

Begins, signs and completes enrollment in one call: posts the identity document, signs the server's challenge with key, and returns the outcome body — {"status": "enrolled", ...} or {"status": "quarantined", "reason": ...}. Enrollment grants nothing; it binds an agent's key and, through principal_assertion (an ID token from your IdP), its human owner. An agent that cannot be bound to a person is quarantined rather than trusted.

async revoke(agent_id, *, reason=None) -> dict​

Admin: revokes an agent's trust anchor and every chain built on it. Idempotent. Checked at the next decision, at the next exchange, and by offline verifiers within their freshness window.

use_manifest(manifest) / wrap_result(value, source_ref, *, trusted_sources=None)​

use_manifest adopts this agent's manifest so wrap_result classifies sources the way the PDP does. wrap_result labels a value the agent obtained on its own and returns a LabeledResult with .value and .provenance; an unmatched source is untrusted_external. A label can only raise the effective taint of the next decision.

async healthz() / async readyz() / async jwks()​

Liveness, readiness (the control plane reached its database and Redis just now), and the issuer public keys. None of them needs a credential — a resource server must be able to fetch issuer keys to verify a hop token offline, with no call back on the hot path. readyz raises ControlPlaneUnavailable when the answer is no; that is the answer, not an error to swallow.

Planes​

The hop path is what an agent runs. These are what runs around it, each with its own least-privileged bearer. Every method raises rather than returning something falsy.

client.admin — registration and compiled policy​

Needs the admin bearer. It is the pipeline's credential; nothing on the hop path uses it.

MethodReturns
register_agent(name, manifest, *, override_reason=None)AgentRegistration(agent_id, manifest_version, status)
submit_manifest(agent, manifest, *, override_reason=None)ManifestSubmission(agent, version, status)
manifest(scope, *, version=None)One stored manifest version, verbatim
agents(*, status=None, limit=None, cursor=None)A page of registered agents
agent_id(name)The id enroll/revoke address, resolved from a name
reconciliation(agent_id)The latest declared-versus-discovered diff
scope_catalog(scope, *, version=None)What consenting to a scope actually grants

register_agent and submit_manifest raise ManifestRejected both when a declaration exceeds what discovery advertises (422) and when discovery could not be reached (409, needs_review). Pass override_reason to proceed on the second case: it is stored as the human attestation for that version, so the gap is recorded rather than hidden. Build a consent screen from scope_catalog, not from the manifest — the manifest is the declaration, the catalog is the compiled DAG.

client.audit — decision history and delegation chains​

Needs an audit-reader bearer. Nothing here writes.

MethodReturns
chain(action_id)The full delegation chain for one recorded action, back to the consenting human
decisions(**filters)Filtered history, newest first, stable under concurrent appends

decisions accepts session_id, agent, outcome, deny_reason, enforced, target_kind, since, until, taint, privileged, limit and cursor. enforced=False is the observe-mode query: every call that would have been blocked. Every PolicyDenied and every Grant carries the action_id to hand to chain, so "why was this refused, and who asked for it" is one call away from the exception.

client.identity — what an offline verifier bootstraps from​

Needs an identity-reader bearer. A resource server needs exactly these routes and cannot list agents.

MethodReturns
of(agent_name)One agent's pinned identity: status, anchor version, public JWKS, revoked, as_of
by_id(agent_id)The same material, by id
revocations(*, since=None, limit=None, cursor=None)Revoked agents, newest first, with the as_of a verifier refuses on

Exceptions​

PolicyDenied​

Raised, never returned — a refusal that's easy to ignore gets ignored, and routing around the SDK's own denial is the exact failure mode this product exists to prevent.

AttributeTypeNotes
reasonDenyReasonSame enum as the API's deny reasons
recoverablebool (property)True only for SCOPE_NOT_CONSENTED in the current phase
target, hop, session_id
action_idstr | NoneThe audit record of the refusal, if one was recorded — pass it to client.audit.chain()
taint, taint_sourcestr | NoneThe effective taint the refusal was decided under, and the target that introduced it
policy_versionint | NoneThe pinned policy the refusal was decided against

ControlPlaneUnavailable​

Always fatal to the hop — there is no code path that proceeds with an un-narrowed token when the control plane can't be reached. It says nothing about your request, so a retry is reasonable.

ControlPlaneRejected​

The control plane understood the request and refused it: yours to fix, and retrying it unchanged will fail the same way. Carries status, code, message, details, and the method/path that was refused. Raised by the planes above — a 401 from the wrong bearer, a 404 for an agent that is not registered — never conflated with an outage.

ManifestRejected​

A ControlPlaneRejected for the one case with something actionable in the body.

AttributeTypeNotes
difflist[dict]One entry per tool reference that exceeds or conflicts with what discovery advertises, each with its own remedy
needs_reviewboolTrue when discovery was unavailable (409) rather than contradicted (422) — resubmit with override_reason=
remedies()list[str]One line per diff entry: the tool ref and what to do about it

TransactionParamsRequired​

The decision says this privileged call needs a transaction token and no params were passed to bind. Raised before any exchange or mint, so nothing was spent.

Value objects​

Grant​

Narrowed authority for exactly one call.

AttributeNotes
access_token, scope, expires_in, hop, targetThe narrowed token and what it is narrowed to
transaction_tokenSet only for a privileged target: bound to the exact params passed to authorize
identity_assertionSet only when the client holds an agent key
taintThe effective taint the decision was made under, read from the token's claim
action_idThe audit row this grant was narrowed from — the same id the resource server reads off the token
.headers()Authorization, plus Txn-Token and Agent-Identity when present

Session​

session_id, root_grant, policy_version, scope, user_sub, hop_budget, expires_at, and .context() -> HopContext to start hop 1.

HopContext​

What travels between hops: session_id and token — nothing else. The delegation chain and hop depth are read from the token's verified claims, never tracked as separate client-side state, so a client cannot drift out of sync with the signed truth.

PropertyReturns
.act_chain[human, first delegate, ..., current actor], derived from the token's act claim
.hoplen(act_chain)
.callerWho holds this token — the token's aud if it's a delegated token, else its sub
.headers(){"Authorization": "Bearer <token>", "X-Agentic-Session": session_id}
.advanced(token)A new HopContext for the next hop: same session, the newly narrowed token

app.sdk.drift — the CI gate in process​

The code behind drift record and drift check, callable directly so a pipeline can branch on the report instead of parsing stdout. Runs from files only: no discovery, no control plane, no credentials.

FunctionReturns
aggregate_trace(event_files, *, out=None, command=None, exit_code=None)The EdgeTrace the recorded events deduplicate into, written to out if given
check_drift(manifest_dirs, trace, *, generated_at=None)The DriftReport: result, summary, undeclared, errors, warnings
exit_code_for(report)0 only for a clean, evidenced pass
trace_recorder(path) / trace_env(path)An edge recorder, and the environment a child process needs to record into one file

Every edge verdict comes from the same function the runtime PDP calls, over an index built by the same compiler, so CI and production cannot disagree about what the manifests declare. Lost events and a run that observed nothing are each an error result, never a pass.