Authentication.
Managed login flows, token restore, and automatic token refresh on the runner.
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):
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 flagsThen supply it as a static access token:
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.
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.
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_*.
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.
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).
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:
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:
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
await manager.get_access_token(force_refresh=False) # -> str
await manager.get_tokens() # -> TokenSet | None
await manager.set_tokens(tokens) # -> None
await manager.clear() # -> NoneTokenSet
@dataclass(frozen=True)
class TokenSet:
access_token: str
expires_at: int # epoch milliseconds
refresh_token: str | None
id_token: str | NoneAuthError class
Imported from matilda_client:
class AuthError(MatildaError):
code: str # OAuth error code (e.g. 'invalid_grant', 'authorization_pending')
retryable: bool # True for transient 5xx/network; False for revoked tokensLoginFlowEvent
Imported from matilda_client — three frozen dataclasses with a type discriminator:
# 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