Authentication.
AgentAuth on the runner — login, restore, and automatic token refresh.
The agent SDK provides AgentAuth — a managed auth tier that wraps the client SDK's TokenManager. On successful login, a TokenManager is auto-configured on the runner's MatildaCore. 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. It's the only publicly accessible client ID at the moment — additional client IDs will be documented as access opens up. 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):
npx 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:
configureClient({
baseUrl: 'https://matilda.maincode.com/api',
accessToken: process.env.MATILDA_API_KEY,
});runner.auth.loginWithBrowser(opts)
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 core.
const tokens = await runner.auth.loginWithBrowser({
clientId: 'matilda-code',
openBrowser: (url) => console.log(`Open: ${url}`),
});
console.log(tokens.accessToken);BrowserLoginOptions
| Field | type | description |
|---|---|---|
| clientId | string | OAuth client alias (e.g. 'matilda-code'). Required. |
| scope | string | Space-separated OAuth scopes. Defaults to 'openid offline_access'. |
| identityProviderId | string | Route straight to a federated IdP (e.g. Google SSO). |
| callbackPort | number | Fixed loopback port. Defaults to a random ephemeral port. |
| timeoutMs | number | How long to wait for the browser callback. Defaults to 300_000 (5 min). |
| openBrowser | (url: string) => void | Promise<void> | Called with the authorize URL. |
| successRedirect | string | URL the browser is 302-redirected to on success. Defaults to 'https://matilda.maincode.com/cli/signed-in'. |
| errorRedirect | string | URL for the error case. |
| fetchImpl | typeof fetch | Override fetch. Defaults to the global fetch. |
| tokenStore | StorageAdapter | Custom token persistence. Defaults to memoryStorage(). |
| tokenLock | <T>(fn: () => Promise<T>) => Promise<T> | Cross-process critical-section lock for token refresh. |
| onEvent | (e: LoginFlowEvent) => void | Subscribe to login flow state events. |
Returns Promise<TokenSet>.
runner.auth.loginWithDeviceFlow(opts)
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.
const tokens = await runner.auth.loginWithDeviceFlow({
clientId: 'matilda-code',
onEvent: (e) => {
if (e.type === 'user_code') {
console.log(`Visit ${e.verificationUri} and enter code: ${e.userCode}`);
}
},
});If no onEvent handler is provided, the SDK prints the user code and verification URL to stderr automatically.
DeviceLoginOptions
| Field | type | description |
|---|---|---|
| clientId | string | OAuth client alias. Required. |
| scope | string | Space-separated OAuth scopes. Defaults to 'openid offline_access'. |
| timeoutMs | number | Polling timeout. Defaults to 300_000 (5 min). |
| signal | AbortSignal | Abort the polling loop. |
| fetchImpl | typeof fetch | Override fetch. Defaults to the global fetch. |
| tokenStore | StorageAdapter | Custom token persistence. Defaults to memoryStorage(). |
| tokenLock | <T>(fn: () => Promise<T>) => Promise<T> | Cross-process lock for token refresh. |
| onEvent | (e: LoginFlowEvent) => void | Subscribe to login flow events. Defaults to defaultDeviceOnEvent. |
Returns Promise<TokenSet>.
runner.auth.restore(opts)
Adopts tokens that were already persisted (e.g. by createFileTokenStore) without repeating the interactive login. Returns null when the store holds nothing usable, so a caller can fall back to loginWith*.
import { createFileTokenStore } from '@maincode-ai/matilda-agent-sdk';
import { homedir } from 'node:os';
import { join } from 'node:path';
const { store, lock } = createFileTokenStore(join(homedir(), '.matilda', 'tokens.json'));
const tokens = await runner.auth.restore({
clientId: 'matilda-code',
tokenStore: store,
tokenLock: lock,
});
if (!tokens) {
// No persisted tokens — fall back to interactive login
await runner.auth.loginWithDeviceFlow({
clientId: 'matilda-code',
tokenStore: store,
tokenLock: lock,
});
}Parameters
| Field | type | description |
|---|---|---|
| clientId | string | OAuth client alias. |
| tokenStore | StorageAdapter | Token persistence adapter. |
| tokenLock | <T>(fn: () => Promise<T>) => Promise<T> | Cross-process lock. |
| fetchImpl | typeof fetch | Override fetch. |
| metadata | AuthServerMetadata | Pre-fetched server metadata (skips discovery). |
Returns Promise<TokenSet | null>.
runner.auth.getTokens()
Returns the current token set from the managed TokenManager, or null if not authenticated.
const tokens = await runner.auth.getTokens();
if (tokens) {
console.log(`Token expires at: ${new Date(tokens.expiresAt).toISOString()}`);
}Returns Promise<TokenSet | null>.
runner.auth.logout()
Clears the token store, destroys the TokenManager, and restores the core's previous getToken provider (important when the core is the shared default singleton).
await runner.auth.logout();Returns Promise<void>.
Token persistence
By default, tokens are stored in memory (memoryStorage()). For cross-process persistence (e.g. CLI sessions), use createFileTokenStore:
import { Runner, MatildaCore, createFileTokenStore } from '@maincode-ai/matilda-agent-sdk';
import { homedir } from 'node:os';
import { join } from 'node:path';
const tokenPath = join(homedir(), '.matilda', 'tokens.json');
const { store, lock } = createFileTokenStore(tokenPath);
const runner = new Runner({
core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }),
});
await runner.auth.loginWithBrowser({
clientId: 'matilda-code',
tokenStore: store,
tokenLock: lock,
});The file store uses a 0600 JSON file with a lockfile-based single-writer lock to prevent cross-process refresh races.
StorageAdapter interface
interface StorageAdapter {
get(key: string): string | null | Promise<string | null>;
set(key: string, value: string): void | Promise<void>;
remove(key: string): void | Promise<void>;
}TokenManager interface
interface TokenManager {
getAccessToken(opts?: { forceRefresh?: boolean }): Promise<string>;
getTokens(): Promise<TokenSet | null>;
setTokens(tokens: TokenSet): Promise<void>;
clear(): Promise<void>;
}TokenSet interface
interface TokenSet {
accessToken: string;
refreshToken?: string;
idToken?: string;
expiresAt: number; // epoch milliseconds
}AuthError class
class AuthError extends Error {
readonly code: string; // OAuth error code (e.g. 'invalid_grant', 'authorization_pending')
readonly retryable: boolean; // true for transient 5xx/network; false for revoked tokens
}LoginFlowEvent type
type LoginFlowEvent =
| { type: 'state'; status: 'idle' | 'awaiting_user' | 'exchanging' | 'authenticated' | 'error' }
| { type: 'authorize_url'; url: string }
| { type: 'user_code'; userCode: string; verificationUri: string; verificationUriComplete?: string };