# Authentication

> AgentAuth on the runner — login, restore, and automatic token refresh.

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

---

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

```bash
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:

```ts
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.

```ts
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.

```ts
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*`.

```ts
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.

```ts
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).

```ts
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`:

```ts
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

```ts
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

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

## `TokenSet` interface

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

## `AuthError` class

```ts
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

```ts
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 };
```
