alter-sdk 0.2.2
Alter Vault Python SDK - OAuth token management with policy enforcement
Requires Python: <4.0,>=3.11
Keywords: oauth, tokens, security, policy, vault

# Alter SDK for Python

Official Python SDK for [Alter Vault](https://alterai.dev) - OAuth token management with policy enforcement.

## Features

- **Zero Token Exposure**: Tokens are never exposed to developers - injected automatically
- **Single Entry Point**: One method (`vault.request()`) for all provider APIs
- **Type-Safe Enums**: `Provider` and `HttpMethod` enums with autocomplete
- **URL Templating**: Path parameter substitution with automatic URL encoding
- **Automatic Audit Logging**: All API calls logged with request metadata (HTTP method and URL) for full audit trail
- **Real-time Policy Enforcement**: Every token request checked against current policies
- **Automatic Token Refresh**: Tokens refreshed transparently by the backend
- **Actor Tracking**: First-class support for AI agent and MCP server observability

## Installation

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

## Quick Start

```python
import asyncio
from alter_sdk import AlterVault, Provider, HttpMethod

async def main():
    vault = AlterVault(api_key="alter_key_...")

    # Make API request - token injected automatically, never exposed
    response = await vault.request(
        Provider.GOOGLE,
        HttpMethod.GET,
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        user={"user_id": "alice"},
        query_params={"maxResults": "10"},
    )
    events = response.json()
    print(events)

    await vault.close()

asyncio.run(main())
```

## Usage

### Simple GET Request

```python
response = await vault.request(
    Provider.GOOGLE,
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    user={"user_id": "alice"},
)
```

### POST with JSON Body

```python
response = await vault.request(
    Provider.SALESFORCE,
    HttpMethod.POST,
    "https://api.example.com/v1/items",
    user={"user_id": "alice"},
    json={"name": "New Item", "price": 99.99},
    reason="Creating new item",
)
```

### URL Path Templating

```python
response = await vault.request(
    Provider.SALESFORCE,
    HttpMethod.PUT,
    "https://api.example.com/v1/items/{item_id}",
    user={"user_id": "alice"},
    path_params={"item_id": "123"},
    json={"price": 89.99},
)
```

### Query Parameters and Extra Headers

```python
response = await vault.request(
    "notion",
    HttpMethod.POST,
    "https://api.notion.com/v1/databases/{db_id}/query",
    user={"user_id": "alice"},
    path_params={"db_id": "abc123"},
    extra_headers={"Notion-Version": "2022-06-28"},
    json={"page_size": 10},
)
```

### Context Manager

```python
async with AlterVault(api_key="alter_key_...") as vault:
    response = await vault.request(
        Provider.GOOGLE,
        HttpMethod.GET,
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        user={"user_id": "alice"},
    )
# Automatically closed
```

> **Note:** After `close()` is called, subsequent `request()` calls raise `AlterSDKError`. `close()` is idempotent — calling it multiple times is safe.

### Connection Management

#### List Connections

Retrieve OAuth connections for your app, optionally filtered by provider:

```python
from alter_sdk import AlterVault

async with AlterVault(api_key="alter_key_...") as vault:
    # List all connections
    result = await vault.list_connections()
    for conn in result.connections:
        print(f"{conn.provider_id}: {conn.account_display_name} ({conn.status})")

    # Filter by provider with pagination
    result = await vault.list_connections(provider_id="google", limit=10, offset=0)
    print(f"Total: {result.total}, Has more: {result.has_more}")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `provider_id` | `str \| None` | `None` | Filter by provider (e.g., `"google"`) |
| `limit` | `int` | `100` | Max connections to return |
| `offset` | `int` | `0` | Pagination offset |

Returns `ConnectionListResult` with: `connections` (`list[ConnectionInfo]`), `total`, `limit`, `offset`, `has_more`.

#### Create Connect Session

Generate a session URL for end-users to authenticate with OAuth providers:

```python
session = await vault.create_connect_session(
    end_user={"id": "alice"},
    allowed_providers=["google", "github"],
    return_url="https://myapp.com/callback",
)
print(f"Connect URL: {session.connect_url}")
print(f"Expires in: {session.expires_in}s")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `end_user` | `dict` | *required* | Must contain `"id"` key |
| `attributes` | `dict \| None` | `None` | Connection attributes |
| `allowed_providers` | `list[str] \| None` | `None` | Restrict to specific providers |
| `return_url` | `str \| None` | `None` | Redirect URL after OAuth flow |

Returns `ConnectSession` with: `session_token`, `connect_url`, `expires_in`, `expires_at`.

### AI Agent Actor Tracking

```python
vault = AlterVault(
    api_key="alter_key_...",
    actor_type="ai_agent",
    actor_identifier="email-assistant-v2",
    actor_name="Email Assistant",
    actor_version="2.0.0",
    framework="langgraph",
)

response = await vault.request(
    Provider.GOOGLE,
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    user={"user_id": "alice"},
    run_id="550e8400-e29b-41d4-a716-446655440000",
    thread_id="thread-xyz",
    tool_call_id="call_abc_123",
)
```

### Multi-Agent Deployments

Each agent must create its own `AlterVault` instance with a unique actor identity. Do not share a single instance across agents.

```python
# Each agent gets its own vault instance
email_agent = AlterVault(
    api_key="alter_key_...",
    actor_type="ai_agent",
    actor_identifier="email-assistant-v2",
    actor_name="Email Assistant",
)

calendar_agent = AlterVault(
    api_key="alter_key_...",
    actor_type="ai_agent",
    actor_identifier="calendar-agent-v1",
    actor_name="Calendar Agent",
)

# Audit logs and policies are tracked per agent
user = {"user_id": "alice"}
await email_agent.request(
    Provider.GOOGLE, HttpMethod.GET,
    "https://gmail.googleapis.com/gmail/v1/users/me/messages",
    user=user,
)
await calendar_agent.request(
    Provider.GOOGLE, HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    user=user,
)

# Clean up each instance
await email_agent.close()
await calendar_agent.close()
```

## Configuration

```python
vault = AlterVault(
    api_key="alter_key_...",              # Required: Alter Vault API key
    base_url="https://api.alter.com",     # Optional: Custom API URL
    timeout=30.0,                         # Optional: HTTP timeout in seconds
    # Actor tracking (optional)
    actor_type="ai_agent",               # "ai_agent" or "mcp_server"
    actor_identifier="my-agent",         # Unique identifier
    actor_name="My Agent",               # Human-readable name
    actor_version="1.0.0",               # Version string
    framework="langgraph",               # AI framework
    client_type="cursor",                # MCP client type
)
```

## Error Handling

```python
from alter_sdk import AlterVault, Provider, HttpMethod
from alter_sdk.exceptions import (
    AlterSDKError,              # Base exception for all SDK errors (including validation: api_key, actor_type, URL scheme, path_params)
    PolicyViolationError,       # Policy denied access (403)
    ConnectionNotFoundError,    # No OAuth connection found (404)
    TokenExpiredError,          # Token refresh failed (400/502)
    TokenRetrievalError,        # Other backend errors
    NetworkError,               # Backend or provider unreachable
    TimeoutError,               # Request timed out (subclass of NetworkError)
    ProviderAPIError,           # Provider API returned error (4xx/5xx)
)

try:
    response = await vault.request(
        Provider.GOOGLE,
        HttpMethod.GET,
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        user={"user_id": "alice"},
    )
except PolicyViolationError as e:
    print(f"Policy denied: {e.message}")
    print(f"Policy error: {e.policy_error}")  # Detailed policy failure reason
except ConnectionNotFoundError:
    print("No OAuth connection - user needs to authenticate")
except TokenExpiredError as e:
    print(f"Token expired for connection: {e.connection_id}")
except TimeoutError as e:
    print(f"Request timed out — safe to retry: {e.message}")
except NetworkError as e:
    print(f"Network issue: {e.message}")
except ProviderAPIError as e:
    print(f"Provider error {e.status_code}: {e.response_body}")
```

## Supported Providers

```python
from alter_sdk import Provider

Provider.GOOGLE       # "google"
Provider.GITHUB       # "github"
Provider.SLACK        # "slack"
Provider.MICROSOFT    # "microsoft"
Provider.SALESFORCE   # "salesforce"
Provider.SENTRY       # "sentry"

# Strings also work for any provider
await vault.request("notion", HttpMethod.GET, url, user=user)
```

## Requirements

- Python 3.11+
- httpx[http2]
- pydantic

## License

MIT License