Metadata-Version: 2.1
Name: alter-sdk
Version: 0.3.1
Summary: Alter Vault Python SDK - OAuth token management with policy enforcement
Home-page: https://alterai.dev
Keywords: oauth,tokens,security,policy,vault
Author: Alter Team
Author-email: founders@alterai.dev
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
Requires-Dist: httpx[http2] (>=0.25.0,<0.26.0)
Requires-Dist: pydantic (>=2.5.0,<3.0.0)
Description-Content-Type: text/markdown

# Alter SDK for Python

Official Python SDK for [Alter Vault](https://alterai.dev) - Credential management for agents 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
- **API Key and Custom Credential Support**: Handles OAuth tokens, API keys, and custom credential formats automatically
- **HMAC Request Signing**: All SDK-to-backend requests are signed with a derived HMAC-SHA256 key for integrity, authenticity, and replay protection
- **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, ActorType, Provider, HttpMethod

async def main():
    vault = AlterVault(
        api_key="alter_key_...",
        actor_type=ActorType.AI_AGENT,
        actor_identifier="my-agent",
    )

    # Make API request — token injected automatically, never exposed
    response = await vault.request(
        "CONNECTION_ID",  # from Alter Connect (see below)
        HttpMethod.GET,
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        query_params={"maxResults": "10"},
    )
    events = response.json()
    print(events)

    await vault.close()

asyncio.run(main())
```

### Where does `connection_id` come from?

**OAuth connections:**
1. User completes OAuth via [Alter Connect](https://docs.alterai.dev/sdks/javascript/quickstart) (frontend widget)
2. The `onSuccess` callback returns a `connection_id` (UUID)
3. You save it in your database, mapped to your user
4. You pass it to `vault.request()` when making API calls

**Managed secrets** (API keys, service tokens):
1. Store credentials in the [Developer Portal](https://portal.alterai.dev) under **Managed Secrets**
2. Copy the `connection_id` returned
3. Use the same `vault.request()` — credentials are injected automatically

```python
# You can also discover connection_ids programmatically:
result = await vault.list_connections(provider_id="google")
for conn in result.connections:
    print(f"{conn.id}: {conn.account_display_name}")
```

## Usage

The `request()` method returns the raw `httpx.Response`. The token is injected automatically and never exposed.

### Simple GET Request

```python
response = await vault.request(
    connection_id,
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
)
```

### POST with JSON Body

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

### URL Path Templating

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

### Query Parameters and Extra Headers

```python
response = await vault.request(
    connection_id,
    HttpMethod.POST,
    "https://api.notion.com/v1/databases/{db_id}/query",
    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_...",
    actor_type=ActorType.BACKEND_SERVICE,
    actor_identifier="my-service",
) as vault:
    response = await vault.request(
        connection_id,
        HttpMethod.GET,
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    )
# Automatically closed
```

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

### Using Managed Secrets

For your own APIs with API keys or service tokens (no OAuth flow needed):

```python
async with AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.BACKEND_SERVICE,
    actor_identifier="my-service",
) as vault:
    response = await vault.request(
        "MANAGED_SECRET_CONNECTION_ID",  # from Developer Portal
        HttpMethod.GET,
        "https://api.internal.com/v1/data",
    )
```

The credential is injected automatically as the configured header type (Bearer, API Key, Basic Auth).

### Connection Management

#### List Connections

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

```python
from alter_sdk import AlterVault, ActorType

async with AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.BACKEND_SERVICE,
    actor_identifier="my-service",
) 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 |
| `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
from alter_sdk import AlterVault, ActorType, Provider, HttpMethod

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

response = await vault.request(
    connection_id,
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    run_id="550e8400-e29b-41d4-a716-446655440000",  # auto-generated UUID if omitted
    thread_id="thread-xyz",
    tool_call_id="call_abc_123",
)
```

> **Note**: `run_id` is auto-generated as a UUID v4 if not provided. All sub-actions within a single `request()` call share the same `run_id` for audit log grouping.

### 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=ActorType.AI_AGENT,
    actor_identifier="email-assistant-v2",
    actor_name="Email Assistant",
)

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

# Audit logs and policies are tracked per agent
await email_agent.request(
    gmail_connection_id,  # from Alter Connect
    HttpMethod.GET,
    "https://gmail.googleapis.com/gmail/v1/users/me/messages",
)
await calendar_agent.request(
    calendar_connection_id,  # from Alter Connect
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
)

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

## Configuration

```python
from alter_sdk import AlterVault, ActorType

vault = AlterVault(
    api_key="alter_key_...",              # Required: Alter Vault API key
    actor_type=ActorType.AI_AGENT,        # Required: ActorType enum
    actor_identifier="my-agent",          # Required: Unique identifier
    timeout=30.0,                         # Optional: HTTP timeout in seconds
    actor_name="My Agent",               # Optional: Human-readable name
    actor_version="1.0.0",               # Optional: Version string
    framework="langgraph",               # Optional: AI framework
    client_type="cursor",                # Optional: 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, actor_identifier, 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(
        connection_id,
        HttpMethod.GET,
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    )
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.SENTRY       # "sentry"

# Provider enums are used for filtering (e.g., list_connections(provider_id=Provider.GOOGLE))
# request() takes a connection_id string as its first argument
await vault.request(connection_id, HttpMethod.GET, url)
```

## Requirements

- Python 3.11+
- httpx[http2]
- pydantic

## License

MIT License

