---
title: Auth
description: How Extentos auth works. Discovery, guidance, validation, and search run with no account; a free account (linked via the OAuth 2.0 device-code flow) unlocks browser-simulator sessions, the scaffold step, and the account-scoped project tools. No payment.
type: reference
platform: mcp
related:
  - /docs/mcp-server
  - /docs/mcp-server/tools/simulation
  - /docs/resources/pricing
  - /docs/getting-started/with-agent
---

The Extentos auth model is **anonymous-first, with a free account for the backend-backed surfaces**. Discovery, guidance, validation, docs search, on-device `LocalSimTransport` simulation, and real-hardware testing through `RealMetaTransport` work with **no account, no email, no payment** — forever. Three things require sign-in: minting browser-simulator sessions at [extentos.com/s](https://extentos.com/s), the `generateConnectionModule` scaffold step (it mints your account-bound project key), and the account-scoped project tools (assistant config, credentials, connection-page writes, analytics). The first time your agent makes a gated call (e.g. `createSimulatorSession`), the backend returns `auth_required` and the device-code flow fires to link a free email-only account. The agent handles the handoff programmatically via the `completeAuthLink` MCP tool — you sign in once in your browser (Google one-click or email + password), the bearer token persists to `~/.extentos/auth.json`, and the original tool call retries automatically. From then on, sessions are unlimited. **No manual token paste, no payment at any tier.** This page covers the identity states, the device-code mechanics, the CLI subcommands, the state files, and the env-var dials.

## Identity states

Extentos has two operational identity states (plus a placeholder for future paid features):

| State | Identifier | What you can do | Cost |
|---|---|---|---|
| **No account** | `installId` (per-machine, at `~/.extentos/install.json`) | Discovery, guidance, validation, and search tools. `LocalSimTransport`. Real-hardware testing through `RealMetaTransport` (your own Meta credentials). | Free forever |
| **Free account** | `accountId` (linked via device-code flow) | Everything above, plus the `generateConnectionModule` scaffold (it mints your account-bound project key), minting browser-simulator sessions, and using the managed AI gateway. Email + Privacy Policy acceptance only. | Free, no payment |
| Managed AI gateway | `accountId` + attestation | Voice-assistant AI on Extentos's key. Usage metered on a prepaid-credit model (every account starts with $2 free). | Metered ([pricing](/docs/resources/pricing)) |

The `installId` is anonymous and never tied to your identity. The `accountId` is created when you sign up at the verification URL during the device-code flow. The gated surfaces are the ones that touch Extentos's backend — the simulator runs on Extentos infrastructure and the scaffold step mints an account-bound project key; every other surface either runs locally or talks directly to Meta.

See [pricing](/docs/resources/pricing) for the full breakdown.

## The device-code flow

Extentos uses the **OAuth 2.0 Device Authorization Grant** ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) — the standard pattern for headless / CLI scenarios where the application can't perform a browser redirect. AI coding agents are exactly that case: the agent runs in a terminal, the developer's browser is somewhere else.

### Sequence diagram

```text
Developer        Agent              MCP server         Backend          Verification URL (browser)
    │              │                    │                  │                    │
    │ "build app"  │                    │                  │                    │
    │─────────────▶│                    │                  │                    │
    │              │                    │                  │                    │
    │              │ createSimulator-   │                  │                    │
    │              │ Session(...)       │                  │                    │
    │              │───────────────────▶│                  │                    │
    │              │                    │ POST /api/.../   │                    │
    │              │                    │ session          │                    │
    │              │                    │─────────────────▶│                    │
    │              │                    │                  │                    │
    │              │                    │   auth_required, │                    │
    │              │                    │   deviceCode,    │                    │
    │              │                    │   userCode,      │                    │
    │              │                    │   verificationUrl│                    │
    │              │                    │◀─────────────────│                    │
    │              │ { status:          │                  │                    │
    │              │   "auth_required"  │                  │                    │
    │              │   verificationUrl, │                  │                    │
    │              │   userCode,        │                  │                    │
    │              │   deviceCode }     │                  │                    │
    │              │◀───────────────────│                  │                    │
    │              │                    │                  │                    │
    │              │ open browser       │                  │                    │
    │              │ at verificationUrl │                  │                    │
    │              │─────────────────────────────────────────────────────────▶  │
    │              │                                                            │
    │              │ completeAuthLink({ deviceCode })                           │
    │              │───────────────────▶│                  │                    │
    │              │                    │ poll /api/.../   │                    │
    │              │                    │ token            │                    │
    │              │                    │◀────────────────▶│  pending, pending  │
    │              │                                                            │
    │  signs up at verificationUrl ────────────────────────────────────────────▶│
    │                                                      │ confirms signup    │
    │                                                      │◀───────────────────│
    │              │                    │                  │                    │
    │              │                    │ poll →           │                    │
    │              │                    │   authToken,     │                    │
    │              │                    │   accountId,     │                    │
    │              │                    │   tier           │                    │
    │              │                    │◀─────────────────│                    │
    │              │ persist to         │                  │                    │
    │              │ ~/.extentos/       │                  │                    │
    │              │ auth.json          │                  │                    │
    │              │                    │                  │                    │
    │              │ retry              │                  │                    │
    │              │ createSimulator-   │                  │                    │
    │              │ Session            │                  │                    │
    │              │───────────────────▶│                  │                    │
    │              │                    │ uses bearer token │                   │
    │              │  session ready    │                  │                    │
    │              │◀───────────────────│                  │                    │
```

### What the agent should show the user

When `createSimulatorSession` returns `auth_required`, the agent receives both `userCode` (an 8-character XXXX-XXXX string, e.g. ABCD-1234) and `verificationUrl` (`https://extentos.com/activate?code=ABCD-1234` — the URL pre-fills the code). The agent **must surface both** to the user, with explicit guidance to cross-check the code on the verification page matches:

```
Sign in to link this terminal:
  1. Open https://extentos.com/activate?code=ABCD-1234 (browser auto-opening)
  2. Confirm the code on the page reads ABCD-1234 (matches what's above)
  3. Sign in (Google or email) and click Approve

I'll detect the link and continue automatically.
```

This is an [RFC 8628 §3.3](https://datatracker.ietf.org/doc/html/rfc8628#section-3.3) anti-phishing cross-check. The verification page renders the user code as a small inline cross-check line ("Verify this matches your terminal: ABCD-1234") above the signup form — it is **not** an entry field, since the URL already carries the code as a query parameter. The user's job is just to confirm what they see on the page matches what their agent printed before clicking Approve. Without this cross-check, a phishing URL with a different valid code would let an attacker have the user approve linking the attacker's device.

The agent's `fixHint` in the tool response carries this guidance verbatim — agents that surface `fixHint` directly to the user (Claude Code, Cursor, Cline default behavior) get this for free.

### Backend endpoints

Verified from `mcp-server/src/cli/login.ts` and `mcp-server/src/tools/handlers/completeAuthLink.ts`:

| Endpoint | Method | Purpose |
|---|---|---|
| `/api/auth/device/code` | POST | Issue a device code. Body: `{ scope: "account_full" }`. Returns `{ deviceCode, userCode, verificationUrl, expiresInSeconds, pollIntervalSeconds }` |
| `/api/auth/device/token` | POST | Poll for completion. Body: `{ deviceCode }`. Returns `{ pending: true, pollIntervalSeconds }` while waiting; `{ authToken, accountId, tier, linkedAt, expiresAt }` on approval |

### Defaults and limits

| Field | Default | Notes |
|---|---|---|
| Device-code window | 600 seconds (10 min) | Backend returns the actual `expiresInSeconds`; `completeAuthLink` enforces a max of 600 |
| Poll interval | 5 seconds | Backend may bump this via `pollIntervalSeconds` in poll responses; agent honors |
| `completeAuthLink.maxWaitSeconds` | 600 | Caller can lower; capped at 600 by the handler |
| `completeAuthLink.pollIntervalSeconds` | 5 | Range 1-30 (clamped) |

### HTTP status codes

| Status | What it means | Retryable |
|---|---|---|
| `200` (`pending: true`) | User hasn't finished signup yet — keep polling | yes (continue) |
| `200` (with `authToken`) | Approved — token issued | done |
| `403` | User declined the signup prompt at the verification page | yes (after asking the user to retry) |
| `404` | Unknown deviceCode (never existed or already consumed) | no (need a fresh code) |
| `410` | Device-code window elapsed (10-min default) | yes (need a fresh code) |
| Other / network failure | Backend unreachable | yes (after the network issue resolves) |

## The `completeAuthLink` MCP tool

The agent-facing primitive for the device-code flow. After `createSimulatorSession` returns `status: "auth_required"`, the agent calls `completeAuthLink` with the returned `deviceCode`.

### Parameters

| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
| `deviceCode` | string | yes | n/a | From the `auth_required` response |
| `maxWaitSeconds` | integer | no | 600 | 1-600 |
| `pollIntervalSeconds` | integer | no | 5 | 1-30 |

### Response on success

```json
{
  "linked": true,
  "tier": "free_account",
  "accountId": "550e8400-e29b-41d4-...",
  "linkedAt": "2026-05-01T12:34:56Z",
  "expiresAt": "2026-07-30T12:34:56Z",
  "authTokenFile": {
    "path": "/home/dev/.extentos/auth.json",
    "written": true,
    "error": null
  },
  "pollCount": 3,
  "summary": "Account linked (tier: free_account). Re-invoke createSimulatorSession to use the new bearer token."
}
```

The `pollCount` lets the agent know how long it took. The `summary` is the agent-facing reminder.

### Error codes

| Code | Cause | Retryable |
|---|---|---|
| `unknown_device_code` | Backend doesn't recognize the deviceCode | no — call `createSimulatorSession` again for a fresh code |
| `device_code_expired` | 10-min window elapsed | yes — call `createSimulatorSession` again |
| `device_code_denied` | User declined at the verification page | yes — ask the developer to retry and accept |
| `backend_unreachable` | Network failure or backend down | yes |
| `backend_invalid_response` | Malformed poll body | no — version mismatch |
| `auth_timeout` | Polled past `maxWaitSeconds` without approval | yes — the developer hasn't finished signup yet |

Telemetry events fire on every flow outcome: `account.login_initiated`, `account.login_completed` (with outcome `success | code_expired | user_denied | network_error | polling_aborted`), and `account.linked` (only on success). Tagged with `installId`, no PII.

## CLI subcommands

`extentos-mcp` is a developer CLI in addition to the MCP server. The auth-related subcommands:

| Subcommand | What it does | When to use |
|---|---|---|
| `login` | Manually trigger the device-code flow. Initiates the flow against `/api/auth/device/code`, prints the userCode + verificationUrl, auto-opens the browser, polls until completion. | When the developer wants to link before the agent's first `createSimulatorSession` call (rare — the agent does this automatically when needed) |
| `logout` | Delete `~/.extentos/auth.json`. Install returns to the anonymous tier (the `installId` survives). | When switching machines, debugging auth state, or revoking the link |
| `whoami` | Not yet implemented (Phase-0 stub — exits with a not-implemented notice). Will print `installId`, `accountId` (if linked), `tier`, auth expiry | Diagnosing auth issues, once it lands |

Run any of them with:

```bash
npx @extentos/mcp-server@latest login
npx @extentos/mcp-server@latest logout
npx @extentos/mcp-server@latest whoami
```

The `login` command output looks like:

```text
Extentos account linking

  User code:          ABCD-1234
  Verification URL:   https://extentos.com/activate?code=ABCD-1234

On the page, confirm the 8-character code matches "ABCD-1234" before approving.
Opening browser… (if it doesn't open, paste the URL above)
Polling for up to 600s. Ctrl-C to cancel.
```

## State files

Auth state lives in `~/.extentos/` by default (overridable with `EXTENTOS_CONFIG_DIR`):

| File | Contents | Created by |
|---|---|---|
| `install.json` | A per-machine install id (`inst_…`), never expires | First MCP server start |
| `auth.json` | `{ authToken, accountId, linkedAt, expiresAt, scope }` after linking | Successful `completeAuthLink` (or `extentos-mcp login`) |
| `consent` | Telemetry consent state (`accepted` / `declined` / unset) | First privacy notice acceptance or `extentos-mcp accept-privacy` / `decline-privacy` |

`logout` deletes `auth.json` only. The install id in `install.json` persists — wiping the entire `~/.extentos/` directory just creates a fresh anonymous install — there is no usage meter to reset in the account-required model, and the fresh install still has to link an account to mint sessions or scaffold (acceptable at MVP scale because it requires the developer to wipe the agent's MCP context each time).

## Environment variables

| Variable | Default | Purpose |
|---|---|---|
| `EXTENTOS_BACKEND_URL` | Production backend | Override the backend URL the auth endpoints hit. Used for local backend development. |
| `EXTENTOS_CONFIG_DIR` | `~/.extentos` | Override the config / auth / consent directory |
| `EXTENTOS_NO_AUTO_OPEN` | unset | Set to `1` to disable browser auto-open. The verification URL is still printed; the developer pastes it manually. Useful for headless / SSH / cloud-hosted-agent environments. |

## Browser auto-open

When the device-code flow fires, the MCP server attempts to open the verification URL in the developer's default browser via:

| Platform | Command |
|---|---|
| macOS | `open <url>` |
| Windows | `cmd.exe /c start "" <url>` |
| Linux | `xdg-open <url>` |

If auto-open fails (no GUI, no default handler, sandboxed environment), the URL is still printed to the agent's tool response and the CLI's stdout. The developer copies it into their browser. Set `EXTENTOS_NO_AUTO_OPEN=1` to skip the auto-open attempt entirely.

## Telemetry consent (separate from auth)

Telemetry consent is a **separate** mechanism from authentication. The MCP server emits anonymous usage telemetry (tool calls, install events, no source code or PII) tagged with the per-machine `installId`. Consent is consent-default with first-run notice — the same pattern used by Vercel CLI, Astro, and Vite.

The consent state lives at `~/.extentos/consent`. It's set by:

| Action | Effect |
|---|---|
| First MCP tool call after install | Privacy notice injected once into the response (`PRIVACY_NOTICE` constant in `mcp-server/src/index.ts`); telemetry stays enabled by default |
| `extentos-mcp accept-privacy` | Records explicit consent |
| `extentos-mcp decline-privacy` | Disables telemetry upload — events still emit locally for debugging but never leave the machine |
| Setting `EXTENTOS_TELEMETRY=0` | Same as `decline-privacy` for the current shell, but doesn't persist to the consent file |

Telemetry consent has nothing to do with whether you're authenticated. A no-account install can opt out of telemetry; a linked account can opt out too. The two systems are independent.

## Per-project MCP access

What an AI agent connected to your MCP can do is governed by **account-held, per-project grants** — not by the auth token's scope. Once your account is linked, an agent can by default read and change a project's dashboard-managed settings; you tighten or turn off individual capabilities per project in the dashboard under MCP access (`/account/mcp-access`).

- **Always allowed** (no grant needed): the connection page and the browser simulator.
- **Governed capabilities** — set per project to Off / Read / Read + Write (assistant config and credentials default to **Read + Write**; analytics defaults to **Read**):
  - **Assistant config** — the realtime model, voice, memory model, and reading managed-gateway usage & cost. This one is **per-field**: you can, for example, let an agent change the model but only *read* the voice. (`getAssistantConfig`, `setAssistantConfig`, `getGatewayUsage`)
  - **Credentials** — check whether a key is set (a masked hint only) and **add keys without the agent ever seeing the value**: `setCredential` returns a dashboard link where you paste the secret yourself, so it never enters the agent's context. (`getCredentialStatus`, `setCredential`)
  - **Analytics** — read your app's production telemetry (shipped-app sessions, events, errors) via `getProjectAnalytics`.

Enforcement is server-side: a grant change takes effect on the agent's next call, with no re-auth. A blocked call returns a `permission_denied` error naming the capability.

## Frequently asked questions

### Why are the simulator and the scaffold account-required when everything else is anonymous?

The browser simulator runs on Extentos's own backend infrastructure — a WebSocket hub keeps the live session alive, the voice proxy handles STT/TTS, and the event log persists — and the scaffold step mints an account-bound project key on that same backend. Those are the Extentos surfaces that cost server-side state, so they're gated behind a free account. The discovery, guidance, validation, and search surfaces (plus on-device `LocalSimTransport` and real-hardware testing through your own Meta credentials) either run on your machine or talk directly to Meta — no Extentos compute, no account needed. Sign-in is a free email-only account (Google one-click or email + password); once linked, sessions are unlimited.

### Why device-code instead of OAuth redirect?

OAuth redirect requires the application to be a web app that can register a callback URL and receive a redirect. An AI coding agent isn't a web app — it's a process running in the developer's terminal. Device-code (RFC 8628) is the OAuth flow designed for exactly this case: the device shows the user a code and a URL, the user authorizes on a separate browser, the device polls to learn the result. No redirect, no callback URL, no web server.

### Can I use Extentos without ever signing up?

The discovery, guidance, validation, and search tools, on-device `LocalSimTransport` simulation, and real-hardware testing through your own Meta credentials all work with no account, no email, no payment — forever. A free account is needed for three things: minting browser-simulator sessions, the `generateConnectionModule` scaffold step, and the account-scoped project tools. The signup ask fires the first time your agent makes one of those calls.

### Does signing up cost anything?

No. The free account is the only tier that exists. No payment, no card, no upfront commitment. Email + Privacy Policy acceptance only (or Google one-click).

### What if the device-code expires before I finish signup?

Call `createSimulatorSession` again to get a fresh deviceCode. The backend issues a new code with a fresh 10-minute window. Your prior install state is preserved.

### What if I'm on a headless machine (cloud agent, SSH session)?

Set `EXTENTOS_NO_AUTO_OPEN=1`. The verification URL prints to the agent's response; the developer copies it to their local browser. The polling continues in the headless environment regardless of where the URL was opened.

### How do I rotate or revoke my auth token?

`extentos-mcp logout` deletes `~/.extentos/auth.json` locally. Server-side revocation exists (tokens carry a `revoked_at`), but a self-serve device-revocation UI isn't live yet — email hello@extentos.com to revoke a device. The next API call from the revoked install will get a 401 and prompt re-linking.

### Is the auth token a secret?

Yes — treat it like any other API token. It's stored at `~/.extentos/auth.json`, written with owner-only permissions (`0600` on macOS/Linux). Don't commit it to version control. Don't share it across machines.

### Is the `installId` a secret?

No — it's a per-machine anonymous identifier. Other apps on the same machine can read `~/.extentos/install.json`; the same value is sent to `api.extentos.com` on every tool call as the telemetry key. It's not a credential.

### What happens if `~/.extentos/auth.json` is corrupted?

The MCP server treats it as not-linked. The next `createSimulatorSession` call returns `auth_required` and the device-code flow re-links the install. Or run `extentos-mcp login` proactively to re-link.

## Related

- **[Pricing](/docs/resources/pricing)** — what the free tier covers and why the browser simulator needs a free account
- **[`completeAuthLink` tool](/docs/mcp-server/tools/simulation)** — the MCP tool reference (full input/output schema)
- **[MCP server overview](/docs/mcp-server)** — the broader tool catalog
- **[Quickstart with an AI agent](/docs/getting-started/with-agent)** — installing the server in the first place
