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.
async consent(*, scope, user_sub, ttl_seconds=None) -> Session
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.
| Method | Returns |
|---|---|
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.
| Method | Returns |
|---|---|
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.
| Method | Returns |
|---|---|
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.
| Attribute | Type | Notes |
|---|---|---|
reason | DenyReason | Same enum as the API's deny reasons |
recoverable | bool (property) | True only for SCOPE_NOT_CONSENTED in the current phase |
target, hop, session_id | ||
action_id | str | None | The audit record of the refusal, if one was recorded — pass it to client.audit.chain() |
taint, taint_source | str | None | The effective taint the refusal was decided under, and the target that introduced it |
policy_version | int | None | The 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.
| Attribute | Type | Notes |
|---|---|---|
diff | list[dict] | One entry per tool reference that exceeds or conflicts with what discovery advertises, each with its own remedy |
needs_review | bool | True 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.
| Attribute | Notes |
|---|---|
access_token, scope, expires_in, hop, target | The narrowed token and what it is narrowed to |
transaction_token | Set only for a privileged target: bound to the exact params passed to authorize |
identity_assertion | Set only when the client holds an agent key |
taint | The effective taint the decision was made under, read from the token's claim |
action_id | The 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.
| Property | Returns |
|---|---|
.act_chain | [human, first delegate, ..., current actor], derived from the token's act claim |
.hop | len(act_chain) |
.caller | Who 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.
| Function | Returns |
|---|---|
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.
Related
- End-to-end walkthrough — every method on this page, in order, with real output
- SDK Client — the request sequence behind
authorize/hop - Enroll your first agent