Metadata-Version: 2.4
Name: alter-sdk
Version: 0.32.0
Summary: Alter Vault Python SDK - privileged access for AI agents: vaulted credentials, default-deny policy, approval policies, and an audit trail for every call made through Alter
License: MIT
License-File: LICENSE
Keywords: oauth,tokens,security,policy,vault
Author: Alter Team
Author-email: founders@alterauth.com
Requires-Python: >=3.11,<4.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Provides-Extra: aws
Provides-Extra: fastapi
Provides-Extra: identity
Provides-Extra: langchain
Provides-Extra: mcp
Provides-Extra: otel
Requires-Dist: boto3 (>=1.28.0,<2.0.0) ; extra == "aws"
Requires-Dist: cryptography (>=50.0.0,<51.0) ; extra == "identity" or extra == "mcp"
Requires-Dist: fastapi (>=0.100.0,<1.0) ; extra == "fastapi"
Requires-Dist: fastmcp (>=2.14,<4,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.0.*,!=3.4.1.*,!=3.4.2.*,!=3.4.3.*,!=3.4.4.*,!=3.4.5.*,!=3.4.6.*,!=3.4.7.*,!=3.4.8.*,!=3.4.9.*) ; extra == "mcp"
Requires-Dist: h2 (>=4.4.1,<5)
Requires-Dist: httpcore (>=1.0.9,<2.0)
Requires-Dist: httpx[http2] (>=0.25.0,<1.0)
Requires-Dist: joserfc (>=1.7.6,<2) ; extra == "mcp"
Requires-Dist: langchain-core (>=0.3.85,<2.0.0,!=1.0.*,!=1.1.*,!=1.2.*,!=1.3.0,!=1.3.1,!=1.3.2) ; extra == "langchain"
Requires-Dist: langchain-mcp-adapters (>=0.2.2,<1.0.0) ; extra == "langchain"
Requires-Dist: langgraph (==1.2.14) ; extra == "langchain"
Requires-Dist: langgraph-sdk (==0.4.7) ; extra == "langchain"
Requires-Dist: mcp (>=1.30.0,<2.0) ; extra == "mcp"
Requires-Dist: opentelemetry-api (>=1.0,<2.0) ; extra == "otel"
Requires-Dist: pydantic (>=2.5.0,<3.0.0)
Requires-Dist: pyjwt (>=2.15.0,<3.0) ; extra == "identity" or extra == "mcp" or extra == "langchain"
Requires-Dist: starlette (>=1.0.1,<2.0) ; extra == "mcp"
Requires-Dist: tzdata (>=2024.1)
Requires-Dist: urllib3 (>=2.8.0,<3) ; extra == "aws" or extra == "langchain"
Project-URL: Homepage, https://alterauth.com
Description-Content-Type: text/markdown

# Alter SDK for Python

Official Python SDK for [Alter Vault](https://alterauth.com) — privileged access management for AI agents: vaulted credentials, default-deny policy down to the parameter, approval policies for risky calls, and an audit trail for every call made through Alter.

Provider calls execute through Alter’s proxy. Credentials stay in Alter, where policy and approval requirements are enforced.

## Install

```bash
pip install alter-sdk
```

Requires Python 3.11+.

## Quick example

Make an authenticated API call — no token ever touches application code.

```python
from alter_sdk import PendingApproval
import asyncio
from alter_sdk import App, HttpMethod

async def main():
    async with App(api_key="<api-key>") as vault:
        response = await vault.proxy_request(
            HttpMethod.POST,
            "https://api.example.com/resource",
            grant_id="<grant-id>",
            json={"example": "payload"},
        )
        if isinstance(response, PendingApproval):
            for gate in response.gates:
                print("Approval required:", gate.approval_url)
            response = await vault.await_approval(response.approval_id)
        print(response.status_code, response.body_json())

asyncio.run(main())
```

For a full walkthrough — sign-up, key minting, OAuth — see the [Quickstart](https://docs.alterauth.com/quickstart).

## Provider calls

Use `app.proxy_request(...)` (Python) / `app.proxyRequest({...})` (TypeScript) for provider calls. The backend holds the credential, executes the request, and returns the provider result.

- `app.proxy_upload_request(...)` — proxy mode for large request bodies: the body is declared by size and SHA-256, every policy runs, and it is then sent once without Alter holding it. `app.deliver_approved_upload(...)` sends one that needed approval.
- `app.proxy_stream_request(...)` — proxy mode for downloads and Server-Sent Events: provider bytes stream through with backpressure. Binary request bodies are up to 5 MiB. Neither can wait for approval.

Proxy mode returns an `ApprovalResult` with `status_code`, response headers/body,
`body_truncated`, and `duration_ms` (provider round-trip milliseconds; `None`
for results created by older workers). Managed-secret secondary header/query
injections and AWS SigV4—including query parameters, raw/JSON bodies, and
temporary session tokens—are injected by the backend.

See [provider calls](https://docs.alterauth.com/concepts/identity#provider-calls-execute-through-alter) for the execution boundary.

## Policy rule helpers

`content_match_rule(...)` builds operation-aware request rules for `with_constraints(rule=...)` with local validation before the first API call. It accepts operation ids (a cataloged provider's attested ids, or the ids a custom integration's developer mapped) and/or operation families, optional parameter conditions, and one of three effects: `deny`, `redact`, or `step_up`. A custom integration's operations project only the request's query-string parameters, compared as text, so any other parameter condition (a body, header or path parameter, or a numeric comparison) denies the matching request; a `redact` rule that names only custom operations is refused, and a redaction that reaches one through a family denies the request. Redaction strips named outbound request-body fields; step-up requires `max_session_age_seconds` as an integer from `1` through `86400`. Parameter conditions compare with `equals`, the glob operators `any_in` / `not_subset_of` (where `*`, `?` and `[...]` are wildcards), their literal twins `in` / `not_all_in` (the same list compared literally, case-insensitively — `in` fires when any member is listed, `not_all_in` when not all members are; use them for literal statements, ids and channels), the presence operator `exists` (a boolean: `True` fires when the request carries the parameter, `False` when it does not), the numeric `gt` / `gte` / `lt` / `lte`, and `outside_windows`.

`operation_allowlist_rule(...)` is the positive counterpart: it builds an `operation_allowlist` request rule under which only the listed operation ids and/or operation families may run. Every other classified operation is denied, and so is any request the catalog cannot classify. Several allowlists on one chain intersect, so a per-request allowlist can only narrow access. At least one of `operations` / `families` is required.

```python
from alter_sdk import operation_allowlist_rule

read_only = app.with_constraints(
    rule=operation_allowlist_rule(operations=["query", "generalledger/query_a_report"]),
)
```

## Recovering from missing-grant errors

When a request fails because the user hasn't authorized the provider yet, the SDK exposes recovery context on the typed error so you can drive a re-consent flow without re-deriving anything from the call site:

```python
from alter_sdk import PendingApproval
from alter_sdk import NoDelegatedGrantError

try:
    await vault.proxy_request(provider="<provider-id>", user_token=jwt, url=..., method=...)
except NoDelegatedGrantError as e:
    session = await vault.create_connect_session_for_error(
        e,
        allowed_origin="https://app.example.com",
    )
    # Surface session.connect_url to the user — popup, redirect,
    # out-of-band message, whatever your framework does.
    results = await vault.poll_connect_session(session.session_token)
    # Missing grants return a new id. A CredentialRevokedError repair keeps the
    # existing one (operation="reauth"), so always read grant_id off the result.
    response = await vault.proxy_request(grant_id=results[0].grant_id, url=..., method=...)
    if isinstance(response, PendingApproval):
        for gate in response.gates:
            print("Approval required:", gate.approval_url)
        response = await vault.await_approval(response.approval_id)
```

`create_connect_session_for_error` and `poll_connect_session` are available on both `App` and `Agent` so the catch block can recover from whichever client raised.
For `CredentialRevokedError`, the helper binds the session to the error's exact
grant. A successful repair reports `operation == "reauth"` and the same
`grant_id` — as does any completion that returns an already-connected grant:
`"reauth"` means an existing grant came back with its id preserved, not that
it was necessarily broken. Naming a target is an optimization, not a requirement: an error carrying no
grant id (an older backend), a grant this caller cannot address (pass
`user_token` to reach an end user's connection), or a grant that is no longer
active all fall back to an ordinary session that reports
`operation == "creation"` with a new `grant_id`. Read the id off the result.

If Alter observes the session pending but receives no callback before the local
deadline or server expiry, polling raises `ConnectTimeoutError`. Inspect
`error.details["reason"]` (`poll_deadline_elapsed` or `session_expired`). A
provider may keep an authorization error on its own page and never redirect, so
the exception lists that alongside browser closure or an abandoned flow rather
than claiming an exact provider error Alter did not receive.

If a fresh provider authorization cannot start, `poll_connect_session` raises
`ConnectConfigError`. This includes the typed
`provider_configuration_unavailable` and
`shared_dev_credential_unavailable` terminals; show the exception's safe
message to the caller and ask the app administrator to correct provider
availability before creating a new session. Existing grants may remain usable.

`ConnectConfigError` also carries a third terminal,
`managed_oauth_scope_approval_required`, raised by `create_connect_session`
(HTTP 409) as well as by `poll_connect_session`: the scopes the application
requests exceed what Alter's managed OAuth client is approved for. The app
administrator cannot widen it — contact Alter to have the scopes approved, or
narrow the application's configured scopes to the approved set, and only then
create a new session. The response is `retryable: false`, so every fresh
session fails identically until the approval changes. On the
`create_connect_session` path the exception's `details` is the full response
body, so `details["details"]["unapproved_scopes"]` names exactly which scopes
need approving.

A fourth terminal, `agent_allowlist_exceeded` (HTTP 422), refuses a Connect
session that delegates to an agent (`agent=...`, or an agent's own key with a
user token) when the agent's capability allowlist cannot use what the user
would grant. `create_connect_session` (and `connect()`, which creates the
session first) raises it before the user is sent to the provider, with the
check at `details["details"]["detail_reason"]` (`provider_not_allowlisted`,
`scopes_not_allowlisted` or `grant_scopes_malformed`) and
every refused provider in `details["details"]["uncovered_providers"]`.
`poll_connect_session` raises it when the allowlist was narrowed before the
user finished; that failure carries only `details["error_code"]`. An
administrator widens the agent's allowlist, or the application narrows the
scopes it requests or passes a covering `scope_constraint`, before a new
session. A provider the session names in neither `allowed_providers` nor
`required_scopes` is dropped instead when the agent cannot use it, and listed
in `ConnectSession.dropped_providers` (`provider_id`, `check`). Onward
`delegate()` raises `BackendError` for the same code.

`NoDelegatedGrantError` and `GrantNotFoundError` carry `provider_id` / `agent_id` / `app_user_id` recovery context when the original lookup was identity-mode; `CredentialRevokedError` carries `provider_id` / `app_user_id`. See the [error reference](https://docs.alterauth.com/reference/errors) for the full surface.

On the **agent** path, a `grant_not_found` for an explicit `grant_id` surfaces as `AgentDelegationMissingError` — a subclass of `GrantNotFoundError`, so an `except GrantNotFoundError` still fires. It means the grant is not delegated to this agent, or a user/base grant id was passed where the agent's own delegation id is required (get it from `agent.list_grants`). Recover by delegating the agent through Connect (`agent.create_connect_session`), or resolve by provider instead of passing a `grant_id`.

## Managed-secret Connect: a user's own key, or a delegation

One Connect session names a managed-secret provider (`provider`, the provider's slug) and the user. The slug is unique per app: several providers can share a template or display name, so the slug, not the template name, selects one. When the user holds no grant on the provider, the hosted page asks for their own API key; when they do, it lets them approve its use. With `agent`, the user's access is also delegated to that agent:

```python
from alter_sdk import App

app = App(api_key="<app-api-key>")

session = await app.create_managed_secret_connect_session(
    provider="<provider-slug>",
    user_token="<user-jwt>",
    agent="<agent-id-or-name>",  # optional: omit for the application's own use
    requested_grant={"label": "<label>"},  # optional: names the key's grant
)
# Open session.connect_url in the user's browser (a popup, the system browser
# or the @alter-ai/connect widget). session.path is "key_entry" when the user
# will enter a key, "fast_path" when they already hold a grant.
```

The user signs in, enters the key (or picks the grant), may set their own usage limits, and confirms. The key is the user's own grant: the application can revoke it but not edit it. Use it like an OAuth grant, through the grant: by `grant_id`, or by `provider` with the user's token, which resolves one grant. Several matching grants raise `AmbiguousGrantError` (HTTP 409 `ambiguous_grant`); narrow the call with `label` or `account`, or pass `grant_id`. Once the user has set a limit, use `proxy_request()` (`request()` raises `RestrictedGrantRequiresProxyError`). `repair_grant_id` lets the user replace their key in place (same `grant_id`).

An agent asks for itself with `Agent.create_managed_secret_connect_session`, which takes no target:

```python
from alter_sdk import Agent

agent = Agent(api_key="<agent-api-key>")

session = await agent.create_managed_secret_connect_session(
    provider="<provider-slug>",
    user_token="<user-jwt>",
    delegable=True,  # optional: ask that this agent may share the access onward
)
# On approval the page delivers the delegation id this agent passes as
# grant_id to agent.proxy_request() (a delegation is proxy-only).
```

The user decides whether the delegation may be shared onward: for an agent's own request the page's checkbox starts off for a new delegation, so the user opts the agent in; for an existing delegation it starts from the stored setting. `session.delegable_requested` echoes the request.

`ConnectConfigError` means an administrator or the application must change something first; `error.details["error"]` names it (`secret_not_found`, `secret_template_mismatch`, `idp_signin_not_configured`, `binding_hosts_required`, `provider_contract_repair_required`, `no_eligible_grants`). Against an Alter backend that predates an option the call used, the SDK raises `ServerFeatureUnsupportedError`. `template_slug` and `delegated_agent_id` are deprecated aliases of `provider` and `agent`: they still work and issue a `DeprecationWarning`.

## Onward delegation (agent to agent)

An agent that holds a grant can hand a scoped-down copy to another agent — without asking the credential owner to consent again. Use `agent.delegate()` to mint a child grant for the second agent:

```python
from alter_sdk import Agent, GrantNotDelegableError

agent = Agent(api_key="<agent-api-key>")

try:
    result = await agent.delegate(
        "<grant-id>",              # a grant this agent already holds
        "<other-agent-id>",        # the agent that should receive access
        scope_constraint=["chat:write"],   # optional: narrow to fewer scopes
        ttl_seconds=3600,                  # optional: shorten the lifetime
    )
    print(result.grant_id, result.depth, result.expires_at)
except GrantNotDelegableError as e:
    # e.detail_reason says why: "holder_not_delegable" (the user did not allow
    # onward sharing), "ceiling_off" (the operator ceiling is off), ...
    print("cannot share onward:", e.detail_reason)
```

The held grant must have been created as **delegable**: the user allowed onward sharing when they approved it (Connect with `delegable=True` asks them; only their choice is stored), and the credential's operator ceiling — a managed-secret provider's delegation policy or the OAuth provider's agent sharing setting — must still allow it on every call. The child can only narrow — fewer scopes, a shorter lifetime — never widen. A grant narrowed with `scope_constraint` is proxy-only: call it with `agent.proxy_request(...)`. Onward delegation is opt-in at every hop: pass `delegable=True` only when the recipient should be allowed to delegate further. Omit `delegable` to keep the existing delegation's setting — a new delegation is then a leaf, and delegating again to the same agent leaves its current setting unchanged; pass `delegable=False` to turn onward delegation off.

`delegate()` also takes `rules`, `tags` and `label`: the rules are created with the child and can only narrow, tags start from the parent's, and the recipient selects the child by `label` on `proxy_request()` and `list_grants()`. The result's `approver_warnings` names any `require_approval` rule whose approvers were not verified against the directory. An application tags an existing grant with `app.update_grant_tags(grant_id, tags)`, which replaces the whole map, and tags a new one with `tags` on `create_connect_session()`, `create_managed_secret_connect_session()` or `mint_grant()`.

`agent.list_grants()` returns each grant with `parent_grant_id` (the grant it was minted under, or `None` for a root) and `depth` (its distance from the root), so the full delegation chain can be reconstructed from the flat list.

## OpenTelemetry trace propagation

When the application runs an OpenTelemetry SDK, Alter requests automatically carry the active span's W3C `traceparent`, so the audit trail — and any spans the organization streams to its own OTLP collector — join the application's traces. No configuration is required and `opentelemetry` is never installed by the SDK itself — it uses whatever OpenTelemetry the application installed; without it (or without an active span) the SDK behaves exactly as before. (The optional `alter-sdk[otel]` extra is available to record the supported `opentelemetry-api` version range in the application's dependency tree.)

```python
from alter_sdk import PendingApproval
from opentelemetry import trace

tracer = trace.get_tracer("the-application")

# Inside an async function; `vault` from the quick start above.
with tracer.start_as_current_span("handle-user-request"):
    # This call's audit events share the surrounding trace's ids.
    response = await vault.proxy_request(HttpMethod.GET, url, grant_id=grant_id)
    if isinstance(response, PendingApproval):
        for gate in response.gates:
            print("Approval required:", gate.approval_url)
        response = await vault.await_approval(response.approval_id)
```

## Documentation

Full docs are at **https://docs.alterauth.com**.

| Topic | Page |
|---|---|
| Getting started end-to-end | [Quickstart](https://docs.alterauth.com/quickstart) |
| The mental model | [How Alter works](https://docs.alterauth.com/how-it-works) |
| Calling APIs on behalf of users (OAuth + JWT) | [Guide](https://docs.alterauth.com/guides/call-apis-on-behalf-of-users) |
| Identity provider setup | [Administration guide](https://docs.alterauth.com/admin/identity-provider) |
| Provisioning backend secrets | [Guide](https://docs.alterauth.com/guides/provision-secrets-for-backend-services) |
| Scoped credentials for AI agents | [Guide](https://docs.alterauth.com/guides/give-an-agent-scoped-access) |
| Human-in-the-loop approvals | [Guide](https://docs.alterauth.com/guides/add-human-in-the-loop-approvals) |
| OpenTelemetry trace propagation | [Calling APIs](https://docs.alterauth.com/reference/python-sdk/calling-apis#opentelemetry-trace-propagation) |
| Provider calls through Alter | [Concept](https://docs.alterauth.com/concepts/identity) |
| Exposing credentials to Claude Code | [Guide](https://docs.alterauth.com/guides/integrate-with-claude-code-mcp) |
| Per-method API reference | [Python SDK reference](https://docs.alterauth.com/reference/python-sdk) |
| Errors | [Error reference](https://docs.alterauth.com/reference/errors) |

## License

MIT. See `LICENSE`.

## Support

Email **founders@alterauth.com** or open an issue at https://github.com/alter-ai/alter-vault.

