Agent SDK · Start here

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

Shell
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 flags

Then supply it as a static access token:

TypeScript
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.

TypeScript
const tokens = await runner.auth.loginWithBrowser({
  clientId: 'matilda-code',
  openBrowser: (url) => console.log(`Open: ${url}`),
});
console.log(tokens.accessToken);

BrowserLoginOptions

Fieldtypedescription
clientIdstringOAuth client alias (e.g. 'matilda-code'). Required.
scopestringSpace-separated OAuth scopes. Defaults to 'openid offline_access'.
identityProviderIdstringRoute straight to a federated IdP (e.g. Google SSO).
callbackPortnumberFixed loopback port. Defaults to a random ephemeral port.
timeoutMsnumberHow long to wait for the browser callback. Defaults to 300_000 (5 min).
openBrowser(url: string) => void | Promise<void>Called with the authorize URL.
successRedirectstringURL the browser is 302-redirected to on success. Defaults to 'https://matilda.maincode.com/cli/signed-in'.
errorRedirectstringURL for the error case.
fetchImpltypeof fetchOverride fetch. Defaults to the global fetch.
tokenStoreStorageAdapterCustom token persistence. Defaults to memoryStorage().
tokenLock<T>(fn: () => Promise<T>) => Promise<T>Cross-process critical-section lock for token refresh.
onEvent(e: LoginFlowEvent) => voidSubscribe 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.

TypeScript
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

Fieldtypedescription
clientIdstringOAuth client alias. Required.
scopestringSpace-separated OAuth scopes. Defaults to 'openid offline_access'.
timeoutMsnumberPolling timeout. Defaults to 300_000 (5 min).
signalAbortSignalAbort the polling loop.
fetchImpltypeof fetchOverride fetch. Defaults to the global fetch.
tokenStoreStorageAdapterCustom token persistence. Defaults to memoryStorage().
tokenLock<T>(fn: () => Promise<T>) => Promise<T>Cross-process lock for token refresh.
onEvent(e: LoginFlowEvent) => voidSubscribe 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*.

TypeScript
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

Fieldtypedescription
clientIdstringOAuth client alias.
tokenStoreStorageAdapterToken persistence adapter.
tokenLock<T>(fn: () => Promise<T>) => Promise<T>Cross-process lock.
fetchImpltypeof fetchOverride fetch.
metadataAuthServerMetadataPre-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.

TypeScript
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).

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

TypeScript
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

TypeScript
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

TypeScript
interface TokenManager {
  getAccessToken(opts?: { forceRefresh?: boolean }): Promise<string>;
  getTokens(): Promise<TokenSet | null>;
  setTokens(tokens: TokenSet): Promise<void>;
  clear(): Promise<void>;
}

TokenSet interface

TypeScript
interface TokenSet {
  accessToken: string;
  refreshToken?: string;
  idToken?: string;
  expiresAt: number;  // epoch milliseconds
}

AuthError class

TypeScript
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

TypeScript
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 };