Skip to content

Python SDK

Client

App and Agent constructors, lifecycle, and identity helpers.

The SDK exposes two top-level client classes — App for application/operator credentials and Agent for workload-scoped credentials. Both are async and share the request surface defined in request().

from alter_sdk import App, Agent, CallerType
# Application-side
app = App(api_key="alter_rk_…")
# Workload-side
agent = Agent(api_key="alter_ak_…")
# Lifecycle
await app.close()
async with Agent(api_key="alter_ak_…") as a:
...

The application-side client. Use for operator/admin work (provisioning agents, minting Connect sessions, managing grants) and for application-backend workloads acting under a stored grant_id.

App(
api_key: str,
*,
timeout: float = 30.0,
caller: str | None = None,
caller_type: CallerType | str = CallerType.SERVICE,
user_token_getter: Callable[[], str | Awaitable[str]] | None = None,
)
ParameterTypeDefaultDescription
api_keystrApp API key (alter_rk_…). Required.
timeoutfloat30.0HTTP request timeout in seconds.
callerstr | NoneNoneOptional caller identifier for audit attribution.
caller_typeCallerType | strCallerType.SERVICESERVICE (default — backend infrastructure) or AGENT (shows in the Agents tab).
user_token_gettercallableNoneOptional sync/async callable returning a user JWT for identity-mode request().
PropertyTypeDescription
actor_idstr | NoneCached actor UUID once the first authenticated call resolves.
last_retry_infoRetryInfo | NoneRetry metadata from the most recent request() call.
base_urlstrBackend base URL the client is pinned to. Diagnostic.

App exposes:

Return a macaroon-style constrained sub-client. Every request from the returned App carries a permanent attenuation that can only narrow access, never broaden it. Two attenuations are available, and at least one of scopes / rule must be supplied:

  • scopes narrows the underlying key to the supplied scope set. The supplied scopes must already be implied by the key — the backend rejects broadening attempts with HTTP 400 constraint_not_narrowing.
  • rule attaches an optional one-off rule, evaluated server-side, that can only further restrict each request the returned client makes — it can never widen access. It is either a deny rule or a require-approval (HITL) rule. Deny shape: {"rule_type": "json_match", "rule_body": {"when": {<attr>: str | list[str]}, "effect": "deny"}}, where <attr> is one of method, provider_id, app_id, agent_id, api_key_id, environment, client_ip, resource_kind, and effect is always "deny". Require-approval shape: {"rule_type": "require_approval", "rule_body": {"effect": "require_approval", "approval": {"approvers"?: [...], "channels"?: ["email"], ...}, "when"?: {...}}} — a matching request is held for human approval (returned as a pending approval) instead of denied; channels: [] sends no email. A raw-token retrieval carries no request method/URL, so method-keyed conditions are treated as satisfied there (a raw token can issue any method) — scope method restrictions through the proxy surface, which carries the request method. Endpoints cannot be scoped by a rule (the URL is not a matchable attribute). If a request is frozen for human approval (HITL 202), the per-request rule is not re-evaluated on the deferred post-approval execution — it cannot widen access (the rule did not match the frozen request, and the grant plus every stored policy rule still bound the execution), but prefer a stored rule when the constraint must hold at execute time.
App.with_constraints(*, scopes: list[str] | None = None, rule: RequestRule | dict[str, Any] | None = None) -> App
ParameterTypeDefaultDescription
scopeslist[str] | NoneNoneOptional non-empty list of scope strings to attenuate to.
ruleRequestRule | dict | NoneNoneOptional one-off rule (deny, or require-approval/HITL) carried on every request the returned client makes; enforced on credential-using calls (token retrieval, proxied provider requests).

At least one of scopes / rule is required.

Raises: AlterValueError (neither scopes nor rule supplied, malformed scopes / rule, nested constraint call), AlterSDKError (client closed).

narrowed = app.with_constraints(scopes=["grants:read"])
await narrowed.list_grants()
# Attach a one-off deny rule:
restricted = app.with_constraints(
rule={
"rule_type": "json_match",
"rule_body": {"when": {"method": "POST"}, "effect": "deny"},
},
)
App.get_agent(agent_id: str | UUID) -> Agent
ParameterTypeDefaultDescription
agent_idstr | UUIDManaged-agent UUID. Must parse as a UUID.

Returns: an Agent instance impersonating agent_id.

Raises: AlterValueError (empty / non-UUID input, App has been closed).

agent = app.get_agent(agent_id="11111111-2222-3333-4444-555555555555")
me = await agent.me()

The parent App’s user_token_getter is intentionally NOT inherited. To bridge a user JWT through the impersonated agent, construct Agent(api_key=…, user_token_getter=…) directly.

await app.close()
async with App(api_key="alter_rk_…") as app:
...

close() shuts down the underlying HTTP clients and drains background tasks. The async context manager is the recommended pattern.


The workload SDK client. Use for AI agents and any code that should be identity-scoped to a managed agent. The agent can reach two kinds of credentials:

  1. User-delegated OAuth grants — a user completed Connect with agent=<this-agent>, creating a per-agent delegation record that authorizes the agent against the user’s grant.
  2. Agent-owned managed-secret grants — an operator provisioned a managed secret directly to the agent via the Developer Portal or App.create_managed_secret_grant(principal={"type": "agent", …}).

Agent.list_grants() returns both in one merged view.

Agent(
api_key: str,
*,
timeout: float = 30.0,
caller: str | None = None,
user_token_getter: Callable[[], str | Awaitable[str]] | None = None,
)
ParameterTypeDefaultDescription
api_keystrAgent API key (alter_ak_…). Required.
timeoutfloat30.0HTTP request timeout in seconds.
callerstr | NoneNoneOptional caller identifier for audit attribution.
user_token_gettercallableNoneOptional sync/async callable returning a user JWT. Used for Connect-time delegation and per-call delegation disambiguation. The JWT does NOT grant access — the agent’s authority comes from the per-agent delegation record bound at Connect time.

Agent pins caller_type=AGENT internally; it is not constructor-configurable.

PropertyTypeDescription
actor_idstr | NoneCached actor UUID once the first authenticated call resolves.
last_retry_infoRetryInfo | NoneRetry metadata from the most recent request() call.
base_urlstrBackend base URL the client is pinned to.
keysnamespaceScoped-key lifecycle (see Agents & Keys).
scopesnamespaceScope catalog discovery (see Calling APIs).

Agent exposes:

Operator surfaces (agents namespace CRUD, authenticate, verify_user_token, revoke_grant, create_managed_secret_grant) are intentionally absent — calling them with an agent key would raise typed errors deep in the request path.

Identical semantics to App.with_constraints() — returns a constrained sub-Agent whose every request carries the same attenuation (the rule is enforced on credential-using calls). Pass scopes to narrow the scope set, rule to attach a one-off deny rule, or both; at least one is required.

Agent.with_constraints(*, scopes: list[str] | None = None, rule: RequestRule | dict[str, Any] | None = None) -> Agent

Async context manager that scopes audit identity for every nested request() call. See Agent.trace() on the Agents page for the full surface.

await agent.close()
async with Agent(api_key="alter_ak_…") as agent:
...

Validate the syntactic shape of an Alter API key without a network round-trip. Re-derives the CRC checksum from the key body and compares — catches single-character transcription errors.

from alter_sdk import is_valid_key
is_valid_key(plain_key: object) -> bool
ParameterTypeDefaultDescription
plain_keyobjectCandidate key string. Non-strings and empty strings return False.

Returns True only for syntactically-valid keys (legacy alter_key_* prefix-only, or scoped alter_<type>_<random>_<checksum> with a matching CRC). The check is a usability gate — a forged checksum still cannot pass backend verification.

from alter_sdk import App, is_valid_key
raw = input("Paste API key: ").strip()
if not is_valid_key(raw):
raise SystemExit("Invalid key shape")
app = App(api_key=raw)