OpenClaw EnterpriseDOCSGitHub

Console sign-in and destination recovery

Overview

The console authenticates a browser session and restores a requested supported page when the user can still access it. Agent recovery resolves an opaque key only after session admission and ends at ordinary Agent detail.

Entry Points

Flow

graph TD
  A["Open console route or recovery link"] --> B["Check session"]
  B -->|Null result| C["Sign in"]
  C --> B
  B -->|Malformed reply| R["Show Session unavailable"]
  R -->|Retry| B
  B -->|Valid account and binding| D["Confirm session"]
  D -->|page intent| E["Authorize page reads"]
  D -->|Agent key| F["Resolve authorized Agent"]
  F -->|allowed| E
  F -->|unavailable| G["Select Agents"]
  G --> E
  E --> H["Render allowed page"]

Execution Trace

1. Discover sign-in and bind the session

apps/controller/src/console/console.mts:loadPage, readSession, showLogin

On a cold visit, the shell's static “Loading OpenClaw Enterprise…” status paints before any script runs; CSS shows it only after 400 ms, so a fast load never flashes it. The independent async observer in apps/controller/src/console/startup-recovery.mts handles the native entry-module load error from #console-entry in apps/controller/src/console/index.html. If the observer arrives after the error, it checks at window load (or immediately if the document is already complete). While the .startup-recovery marker remains, it reports that the Console could not load and offers Reload page. Clicking the button reloads the same URL. If the observer is unavailable, the static text still directs the reader to browser Reload. Recovery never automatically reloads the page or retries API requests.

The healthy console.mts entry removes the marker synchronously before session reads, so a slow or unavailable session follows its own path below. loadPage takes the status over in place while checking the session and announces “Checking session…” only if still pending after 400 ms.

readSession reads GET /api/auth/session. Of successful replies, only null confirms that the browser is signed out. A non-null reply must include authenticated: true, a nonblank sessionKey, and nonblank account ID, email, and name. If the reply is malformed, the console shows Session unavailable and withholds private content and reads. Retry repeats the session check.

showLogin reads GET /api/auth/providers; a true google flag adds its Continue with Google button, and discovery failure keeps password login. A true oidc flag adds a button labelled from oidcSignIn.label only when oidcSignIn.authorizationUrl is https:; its start URL must use that endpoint's origin and path, as Google's must use its fixed endpoint. An explicit password: false makes the form recovery-only and changes the provider-error advice from "use your password" to asking an administrator. The console keeps the password form behind Recovery sign-in while discovery is pending, so early focus or autofill cannot make it the default sign-in method. Selecting recovery opens the form immediately; a later discovery reply preserves that choice, its input, and keyboard focus. Discovery that permits passwords or fails shows the form without requiring the recovery action. If that action has keyboard focus, focus moves to Email when the form replaces it.

While sign-in is pending, the console ignores repeat password submissions and provider activations. Keyboard focus stays on the control that started the request unless the reader moves it. Generations reject late redirects. With sessionBinding, loadPage exchanges the button's stored attemptId once for its key. Tabs then send their pinned x-occ-session-key, so a replaced cookie yields login. A refused callback returns with authError and makes no exchange, so a failed exchange (providerExchangeFailure) reports a rate limit, an outage, the browser-origin check, a provider that the API no longer configures (403), or an attempt that expired or was replaced (401), never a refused identity. Password sign-in reports 429 with the Retry-After wait. The provider budgets send no Retry-After, so provider start and the exchange say to wait a minute. Each reports the origin check with its own text; every other password 400, 401 or 403 keeps one message. On the API side, sendAuthEndpoint answers 429 with the wait and 503 with an outage message; every refusal keeps the endpoint's one message. When login interrupts a recognized console route, navigation.mts records its page, validated Namespace ID, recognized Agent version and tab, and a Secrets page link's validated Secret ID, without unrelated query parameters. Password sign-in carries that intent in the current view; external sign-in stores it in tab sessionStorage with the one-use attempt ID and a ten-minute limit. The console consumes it only after confirming that attempt and its session, and clears it on failure or logout. showExpiredLogin captures the validated current route before clearing private state on an API 401 or foreground session recheck. A null session result in loadPage also retains the requested route, including after a failed check. It says the session expired only when this tab had one: a mark in tab sessionStorage, set when a session resolves and cleared by logout, tells a reload after expiry from a first visit. Sign-in reauthorizes that destination; it never restores drafts or replays a mutation. A 403 remains an access denial and does not start login. The login page accepts no return URL.

A native Agent recovery link can supply a 32-character lowercase hexadecimal key in the console root fragment. loadPage removes the fragment before sign-in and retains it across a failed session check, and resolves it only after session confirmation with GET /native-admin/return/:key. A valid authorized response routes to ordinary Agent detail; failed or malformed responses open Agents, and a failed lookup says so once. The native Agent host redirects a qualifying root or chat navigation here. Opening native chat still requires a separate user action. authError=<provider> shows a generic, one-time error. With Google or OIDC, account-disabled says the account is disabled and to ask an administrator to enable it; provider-unavailable and unavailable say to try again later. The authentication flow owns the server side.

apps/controller/src/auth/index.ts:requireTrustedBrowserOrigin checks Origin before sign-in/out, even for SDK calls bypassing Better Auth middleware; headerless CLI requests remain supported. The browser stores no credentials.

2. Authorize the destination

apps/controller/src/console/console.mts:loadPage confirms the account and session binding before loading readable Namespaces or opening the requested page. Agent and Namespace reads then follow their usual authorization rules. An inaccessible resource remains unavailable. A foreground recheck of an open Agent also confirms the session before refreshing private reads. When the Namespace read fails after the session resolved, the page offers Sign out beside Retry, so an account refused with 403 is not stranded. A session or Namespace read that does not reach the API (the browser is offline, or the read times out) tells nothing about the session. A signed-in tab keeps its shell, says that it could not reach OpenClaw Enterprise, and moves focus to Retry. The page and Namespace data are cleared, as for other failures. See the parent flow.

Debugging and Verification

The focused cases in tests/browser/console.test.ts exercise the console against controller sessions and resource reads, including a late startup observer after a native entry failure and during a pending session read. Their external provider exchange and recovery endpoint are modeled; they do not establish a deployed OAuth or native-gateway integration.

The registered case “a session reply missing its binding blocks private reads until Retry” in tests/browser/console-refocus-focus.test.ts checks that a successful reply without a binding hides private content and sends no private reads. After Retry, Namespace and Agent reads carry the confirmed binding.

Search documentation