# Authentication

> Managed login flows, token restore, and automatic token refresh on the runner.

Source: https://maincode.com/docs/python-agent-sdk-authentication
Section: Agent SDK · Matilda documentation

---

The agent SDK provides `AgentAuth` — a managed auth tier layered on the client SDK's `TokenManager`. On successful login, a `TokenManager` is auto-configured as the runner client's token provider. Every subsequent request automatically carries a managed access token with single-flight, skew-aware auto-refresh.

## OAuth client ID

The agent SDK uses the `matilda-code` OAuth client ID, which supports both PKCE browser login and RFC 8628 device flow. All examples in this documentation use `matilda-code`.

## API keys

Agents can also authenticate with an `mc_live_` API key instead of OAuth — useful for CI and server-side deployments where there's no browser. Keys are minted with the client SDK's `matilda-key` CLI (social-login friendly — no code required):

```bash
matilda-key create-api-key --name "my-agent"
# or without installing: uvx --from matilda-client matilda-key create-api-key --name "my-agent"
# mints an mc_live_… key after device-flow sign-in; see the client SDK docs for flags
```

Then supply it as a static access token:

```python
import os

from matilda_agent_sdk import MatildaClient, configure_default_client

configure_default_client(
    MatildaClient(
        base_url="https://matilda.maincode.com/api",
        token=os.environ["MATILDA_API_KEY"],
    )
)
```

## `runner.auth.login_with_browser(...)`

Managed PKCE browser login (RFC 8252 loopback). Starts a temporary local server, opens the browser, receives the auth code, exchanges it for tokens, and auto-wires a `TokenManager` on the runner's client.

```python
tokens = await runner.auth.login_with_browser(
    client_id="matilda-code",
    open_browser=lambda url: print(f"Open: {url}"),
)
print(tokens.access_token)
```

### Parameters

| Field | Type | Description |
| - | - | - |
| `client_id` | `str` | OAuth client alias (e.g. 'matilda-code'). Required. |
| `scope` | `str` | Space-separated OAuth scopes. Defaults to 'openid offline\_access'. |
| `identity_provider_id` | `str \| None` | Route straight to a federated IdP (e.g. Google SSO). |
| `callback_port` | `int \| None` | Fixed loopback port. Defaults to a random ephemeral port. |
| `timeout_ms` | `int` | How long to wait for the browser callback. Defaults to 300\_000 (5 min). |
| `open_browser` | `Callable[[str], None \| Awaitable[None]] \| None` | Called with the authorize URL. |
| `success_redirect` | `str` | URL the browser is 302-redirected to on success. Defaults to 'https\://matilda.maincode.com/cli/signed-in'. |
| `error_redirect` | `str \| None` | URL for the error case. |
| `token_store` | `StorageAdapter \| None` | Custom token persistence. Defaults to memory\_storage(). |
| `token_lock` | `TokenLock \| None` | Cross-process critical-section lock for token refresh. |
| `on_event` | `Callable[[LoginFlowEvent], None] \| None` | Subscribe to login flow state events. |
| `http` | `httpx.AsyncClient \| None` | Override HTTP transport. |

Returns `TokenSet`.

## `runner.auth.login_with_device_flow(...)`

Managed RFC 8628 device flow. Requests a device code, prints the user code and verification URL to stderr (by default), and polls until the user authorises.

```python
def show_login(event):
    if event.type == "user_code":
        print(f"Visit {event.verification_uri} and enter code: {event.user_code}")


tokens = await runner.auth.login_with_device_flow(
    client_id="matilda-code",
    on_event=show_login,
)
```

If no `on_event` handler is provided, the SDK prints the user code and verification URL to stderr automatically (`default_device_on_event`).

### Parameters

| Field | Type | Description |
| - | - | - |
| `client_id` | `str` | OAuth client alias. Required. |
| `scope` | `str` | Space-separated OAuth scopes. Defaults to 'openid offline\_access'. |
| `token_store` | `StorageAdapter \| None` | Custom token persistence. Defaults to memory\_storage(). |
| `token_lock` | `TokenLock \| None` | Cross-process lock for token refresh. |
| `on_event` | `Callable[[LoginFlowEvent], None] \| None` | Subscribe to login flow events. If omitted, prints user code to stderr. |
| `http` | `httpx.AsyncClient \| None` | Override HTTP transport. |

Returns `TokenSet`.

## `runner.auth.restore(...)`

Adopts tokens that were already persisted (e.g. by `create_file_token_store`) without repeating the interactive login. Returns `None` when the store holds nothing usable, so a caller can fall back to `login_with_*`.

```python
from pathlib import Path

from matilda_client import create_file_token_store

token_store = create_file_token_store(Path.home() / ".matilda" / "tokens.json")

tokens = await runner.auth.restore(
    client_id="matilda-code",
    token_store=token_store.store,
    token_lock=token_store.lock,
)

if not tokens:
    # No persisted tokens — fall back to interactive login
    await runner.auth.login_with_device_flow(
        client_id="matilda-code",
        token_store=token_store.store,
        token_lock=token_store.lock,
    )
```

### Parameters

| Field | Type | Description |
| - | - | - |
| `client_id` | `str` | OAuth client alias. |
| `token_store` | `StorageAdapter` | Token persistence adapter. |
| `token_lock` | `TokenLock \| None` | Cross-process lock. |
| `http` | `httpx.AsyncClient \| None` | Override HTTP transport. |
| `metadata` | `AuthServerMetadata \| None` | Pre-fetched server metadata (skips discovery). |

Returns `TokenSet | None`.

## `runner.auth.get_tokens()`

Returns the current token set from the managed `TokenManager`, or `None` if not authenticated.

```python
tokens = await runner.auth.get_tokens()
if tokens:
    print(f"Token expires at epoch ms: {tokens.expires_at}")
```

Returns `TokenSet | None`.

## `runner.auth.logout()`

Clears the token store, destroys the `TokenManager`, and restores the client's previous token provider (important when the client is the shared default).

```python
await runner.auth.logout()
```

Returns `None`.

## Token persistence

By default, tokens are stored in memory (`memory_storage()`). For cross-process persistence (e.g. CLI sessions), use `create_file_token_store` — imported from `matilda_client`:

```python
from pathlib import Path

from matilda_agent_sdk import MatildaClient, Runner
from matilda_client import create_file_token_store


async def main():
    token_path = Path.home() / ".matilda" / "tokens.json"
    token_store = create_file_token_store(token_path)

    runner = Runner(MatildaClient(base_url="https://matilda.maincode.com/api"))

    await runner.auth.login_with_browser(
        client_id="matilda-code",
        token_store=token_store.store,
        token_lock=token_store.lock,
    )
```

The file store uses a `0600` JSON file with a lockfile-based single-writer lock to prevent cross-process refresh races.

## `StorageAdapter` protocol

Imported from `matilda_client`:

```python
class StorageAdapter(Protocol):
    def get(self, key: str) -> str | None | Awaitable[str | None]: ...
    def set(self, key: str, value: str) -> None | Awaitable[None]: ...
    def remove(self, key: str) -> None | Awaitable[None]: ...
```

## `TokenManager`

```python
await manager.get_access_token(force_refresh=False)  # -> str
await manager.get_tokens()                           # -> TokenSet | None
await manager.set_tokens(tokens)                     # -> None
await manager.clear()                                # -> None
```

## `TokenSet`

```python
@dataclass(frozen=True)
class TokenSet:
    access_token: str
    expires_at: int              # epoch milliseconds
    refresh_token: str | None
    id_token: str | None
```

## `AuthError` class

Imported from `matilda_client`:

```python
class AuthError(MatildaError):
    code: str         # OAuth error code (e.g. 'invalid_grant', 'authorization_pending')
    retryable: bool   # True for transient 5xx/network; False for revoked tokens
```

## `LoginFlowEvent`

Imported from `matilda_client` — three frozen dataclasses with a `type` discriminator:

```python
# StateEvent        (type="state")         — status: 'idle' | 'awaiting_user' | 'exchanging' | 'authenticated' | 'error'
# AuthorizeUrlEvent (type="authorize_url") — url: str
# UserCodeEvent     (type="user_code")     — user_code, verification_uri, verification_uri_complete
```
