Agent SDK · Start here

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):

Shell
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

Fieldtypedescription
client_idstrOAuth client alias (e.g. 'matilda-code'). Required.
scopestrSpace-separated OAuth scopes. Defaults to 'openid offline_access'.
identity_provider_idstr | NoneRoute straight to a federated IdP (e.g. Google SSO).
callback_portint | NoneFixed loopback port. Defaults to a random ephemeral port.
timeout_msintHow long to wait for the browser callback. Defaults to 300_000 (5 min).
open_browserCallable[[str], None | Awaitable[None]] | NoneCalled with the authorize URL.
success_redirectstrURL the browser is 302-redirected to on success. Defaults to 'https://matilda.maincode.com/cli/signed-in'.
error_redirectstr | NoneURL for the error case.
token_storeStorageAdapter | NoneCustom token persistence. Defaults to memory_storage().
token_lockTokenLock | NoneCross-process critical-section lock for token refresh.
on_eventCallable[[LoginFlowEvent], None] | NoneSubscribe to login flow state events.
httphttpx.AsyncClient | NoneOverride 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

Fieldtypedescription
client_idstrOAuth client alias. Required.
scopestrSpace-separated OAuth scopes. Defaults to 'openid offline_access'.
token_storeStorageAdapter | NoneCustom token persistence. Defaults to memory_storage().
token_lockTokenLock | NoneCross-process lock for token refresh.
on_eventCallable[[LoginFlowEvent], None] | NoneSubscribe to login flow events. If omitted, prints user code to stderr.
httphttpx.AsyncClient | NoneOverride 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

Fieldtypedescription
client_idstrOAuth client alias.
token_storeStorageAdapterToken persistence adapter.
token_lockTokenLock | NoneCross-process lock.
httphttpx.AsyncClient | NoneOverride HTTP transport.
metadataAuthServerMetadata | NonePre-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