Metadata-Version: 2.4
Name: alter-sdk
Version: 0.24.0
Summary: Alter Vault Python SDK - OAuth token management with policy enforcement
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: 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 == "mcp"
Requires-Dist: fastapi (>=0.100.0,<1.0) ; extra == "fastapi"
Requires-Dist: fastmcp (>=2.14,<4) ; 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: 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.0.10,<2.0) ; extra == "langchain"
Requires-Dist: mcp (>=1.24,<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: starlette (>=1.0.1,<2.0) ; extra == "mcp"
Project-URL: Homepage, https://alterauth.com
Description-Content-Type: text/markdown

# Alter SDK for Python

Official Python SDK for [Alter Vault](https://alterauth.com) — credential and authorization layer for apps and AI agents that call third-party APIs.

Provider credentials are never returned to application code. The SDK injects the credential, refreshes it, and writes the audit row — application code only calls `vault.request()` (or `vault.proxy_request()` when the backend should make the outgoing call instead of the SDK).

## Install

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

Requires Python 3.11+.

## Quick example

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

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

async def main():
    async with App(api_key="<api-key>") as vault:
        response = await vault.request(
            HttpMethod.POST,
            "https://api.example.com/resource",
            grant_id="<grant-id>",
            json={"example": "payload"},
        )
        print(response.status_code, response.json())

asyncio.run(main())
```

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

## Two runtime modes

The SDK exposes two ways to reach a third-party API:

- `vault.request(...)` — **retrieve mode**. The SDK fetches the token from the backend and makes the outgoing call itself. Returns the third-party response.
- `vault.proxy_request(...)` — **proxy mode**. The backend holds the token, makes the outgoing call, and returns the result. Application code and the SDK never observe the token. Required for any grant configured with human-in-the-loop approval; available for any other grant when wire-level audit, strong token isolation, or backend-side policy enforcement matter.

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—behave the same in retrieve and proxy modes.

See [runtime modes](https://docs.alterauth.com/concepts/runtime-modes) for the tradeoffs and when to pick each.

## 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 attested operation ids and/or operation families, optional parameter conditions, and one of three effects: `deny`, `redact`, or `step_up`. Redaction strips named outbound request-body fields; step-up requires `max_session_age_seconds` as an integer from `1` through `86400`.

## 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 NoDelegatedGrantError

try:
    await vault.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.request(grant_id=results[0].grant_id, url=..., method=...)
```

`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`. 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.

`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`.

## 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
        delegable=False,                   # may the recipient delegate onward? (default no)
    )
    print(result.grant_id, result.depth, result.expires_at)
except GrantNotDelegableError:
    # The held grant was not marked delegable when it was created,
    # so it cannot be passed on. Ask the owner for a delegable grant.
    ...
```

The held grant must have been created as **delegable** (chosen at connect time). 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.

`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 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.request(HttpMethod.GET, url, grant_id=grant_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) |
| Runtime modes (retrieve vs proxy) | [Concept](https://docs.alterauth.com/concepts/runtime-modes) |
| 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.

