OpenClaw EnterpriseDOCSGitHub

Agent Native Admin UI Flow

Overview

OCC authorizes exact-Agent browser access through the OCE session or a host-only handle, then proxies HTTP and WebSockets under the selected access mode.

Entry Points

Flow

graph TD
  C0["Create Agent enables browser chat"] --> C1["Authorize provisioning and reserve Agent identity"]
  C0 --> O1["Ordinary create: authorize Agent creation<br/>and Configuration update"]
  O1 --> O2["Check generation and no Agent references<br/>then prepare origin and policy"]
  O2 -->|refused| C3
  O2 -->|valid| O3["Save Configuration and Agent together<br/>then deploy through normal workflow"]
  O3 --> A
  C1 --> C2["Prepare exact origin and trusted-proxy policy"]
  C2 -->|conflicting policy or feature disabled| C3["Refuse before queueing work"]
  C2 -->|valid| C4["Save plan and first version"]
  C4 --> A
  L["Local first-Agent selects Control UI"] --> L2["Create Agent, derive origin,<br/>save Configuration"]
  L2 --> A
  A["Console opens Agent detail tab"] --> P{"operate and administer<br/>answers both false?"}
  P -->|yes| PH["Hide card, no request"]
  P -->|"no or unknown"| B["GET exact Agent native-admin status"]
  B --> C["Resolve OCC session and exact Agent browser principal"]
  C --> D{"Exact Agent exists?"}
  D -->|no| E["Return protected-route error"]
  D -->|yes| F{"Installation enabled?"}
  F -->|no| G["Return disabled"]
  F -->|yes| H["Call resolveNativeAdminAvailability"]
  H --> I{"Selected version available?"}
  I -->|no| J{"Stopped without a version?"}
  J -->|yes| X["Return stopped without an origin"]
  J -->|no| Y["Return unavailable"]
  I -->|yes| K{"Agent desired running?"}
  K -->|no| ST["Return stopped with derived origin"]
  K -->|yes| R2{"Newer exclusive replacement?"}
  R2 -->|yes| Y
  R2 -->|no| M{"Native config supported?"}
  M -->|no| N["Return unsupported with derived origin"]
  M -->|yes| CO{"Compute owner matches?"}
  CO -->|no| CF["Return 503 dependency error"]
  CO -->|yes| EP{"Private gateway endpoint supported?"}
  EP -->|no| N
  EP -->|yes| O["Return available Agent URL"]
  O --> OPEN["Open Agent host directly or exchange<br/>a challenge for a host-only handle"]
  OPEN -->|HTTP| Q["Resolve host to Agent"]
  OPEN -->|WebSocket| V["OCC guards client socket during admission"]
  V --> Q
  Q --> R{"Parent session and selected Agent access mode<br/>still valid?"}
  R -->|no| S["Reject request; audit exact-Agent authorization denial"]
  R -->|yes| T["Resolve current version, native config and Compute owner"]
  T -->|owner mismatch| RF["Refuse before key lookup or gateway connection"]
  T -->|matching owner| U["Strip browser credentials;<br/>proxy to private Gateway"]
  T -->|WebSocket client reset| VB["Stop before gateway connection"]
  T -->|WebSocket client connected with matching owner and exact Origin| VC["Proxy 101 upgrade with version lease"]
  U -->|shared-cookie mode and sandbox routing enabled| W["Load separate preview origin"]
  W --> Z["Route to sandbox listener"]
  Z --> AA["Deliver HTML in isolated iframe"]

Execution Trace

0. Console opts in before the first deployment

apps/controller/src/console/agents/create.mts:renderCreateAgent apps/controller/src/http/agents.ts:createAgentHandlers apps/controller/src/gateway/native-admin.ts:prepareNativeAdminConfiguration packages/occ/src/index.ts:createAgent packages/occ/src/index.ts:provisionAgent

Enable browser chat sends nativeAdmin: true. The adapter derives the origin from Installation settings, refuses conflicting policy, and limits the normalized Agent suffix to 214 characters.

createAgent authorizes Agent creation and Configuration update, checks expectedConfigurationGeneration and absence of Agent references, then prepares policy. Configuration and Agent save atomically; the caller deploys normally.

Guided provisioning authorizes and prepares policy before queueing. Retries reuse its fingerprinted plan and identity. The worker creates and deploys.

0a. Local first-Agent opts into native admin

scripts/first-agent.ts:loadLocalInstallation scripts/first-agent.ts:nativeConfiguration scripts/first-agent.ts:ensureManagedConfiguration

Using Local Setup browser state, the helper saves disabled Configuration generation 1, creates the Agent, and derives its origin. Generation 2 enables controlUi, pins origins, requires x-openclaw-scopes, and grants occ-workspace-files trusted-proxy operator.admin with device auto-approval before deployment. Identity grants supply admin independently of device scopes. Existing helper-owned names keep this selection.

1. Console renders native admin availability

apps/controller/src/console/agents/native-admin.mts:renderNativeAdminAccess

Agent detail reads ${path}/native-admin when operate or administer is allowed or unknown. Disabled/denied access stays hidden; other refusals explain why. Only available enables Open chat or Open Agent UI. Shared-cookie mode uses noopener noreferrer. Host-only exchange opens a window and completes step 5a, accepting challenges only from that window and origin. Redemption clears window.opener; errors close the pending window. An audited 403 in tab sessionStorage suppresses reads until logout, sign-in, or a new tab. activeRevisionId or desiredRuntimeState changes refresh access; concurrent requests coalesce into one follow-up. Browsing a historical version does not change the selected destination. Stopped/unsupported states offer Open draft. Unavailable states offer View deployment for readable versions, otherwise Refresh access.

OCE owns durable configuration. Runtime notices come from apps/controller/src/drivers/compute/runtime-startup.ts:gatewayManagedModeEnvironment; see Gateway managed mode.

2. OCC protects the availability route

apps/controller/src/http/native-admin.ts:nativeAdminStatusOperation

createNativeAdminAccess owns host interception, upgrades and socket shutdown. A getter supplies the controller after bootstrap. Shared route admission remains in apps/controller/src/index.ts.

GET /namespaces/:namespaceId/agents/:agentId/native-admin requires a human session and exact-Agent operate or administer, not read. Administration takes precedence over chat. Authorization and existence checks precede even disabled.

3. Shared availability resolver checks feature and selected version state

apps/controller/src/http/native-admin.ts:getNativeAdminStatus apps/controller/src/http/native-admin.ts:resolveNativeAdminAvailability packages/occ/src/index.ts:getBrowserAccessibleActiveAgentRevision

getAgentBrowserAccess handles disabled access; getBrowserAccessibleActiveAgentRevision handles enabled access, requiring origin, domain and selected version. Both use authorizeAgentAccess and one Agent read. IAM failures propagate, never selecting chat.

ResourceConflictError becomes only status: "stopped" for an authorized stopped Agent without a selected version, or for an operator whose Agent is stopping. NoActiveAgentRevisionError for a running Agent awaiting activation returns unavailable. Other dependencies, including IAM/State outages, remain 503 before gateway routing.

With a selected version, OCC derives its native target. An administrator viewing a non-running Agent with a selected version receives stopped with host/origin; a newer exclusive replacement returns unavailable. Unsupported trusted-proxy auth, machine admin grants/device auto-approval, controlUi.enabled, exact allowedOrigins, or host-header/device-auth settings return unsupported with the target and reason.

Compute id and implementation must match revision.compute; changing selection does not migrate versions. A mismatch raises 503 DEPENDENCY_UNAVAILABLE before endpoint/key/transport. Chat selects getGatewayChatEndpoint, administration selects getGatewayEndpoint. Missing or unclean private wss: endpoints return unsupported; chat never falls back to administration. Only available carries gatewayBase, omitted from browser responses by nativeAdminAvailabilityData.

4. OCC derives the isolated Agent host

apps/controller/src/gateway/native-admin.ts:nativeAdminTarget apps/controller/src/gateway/native-admin-host.ts:deriveNativeAdminHost

Overlong suffixes refuse native targets after authorization; opt-in fails before persistence.

deriveNativeAdminHost hashes scoped Agent identity under the configured domain. nativeAdminTarget returns /.

nativeAdminGatewayHttpBase converts wss: to https:, preserving authority and Agent path, rejecting userinfo, query or hash. Workspace-file WSS is unchanged.

apps/controller/src/auth/index.ts:createControllerAuth

deriveSharedCookieDomain computes the longest common DNS-label suffix of the HTTPS Console origin and Agent domain. It rejects IP addresses, missing hosts, and public suffixes. In shared-cookie mode this applies even when native access is disabled and uses __Secure-openclaw_occ_shared.session_token. Sign-out revokes the current session and expires its shared cookie. There is no obsolete-cookie cleanup. See the session contract.

5a. Host-only exchange binds access to the parent session

apps/controller/src/http/native-admin.ts:launch apps/controller/src/http/native-admin.ts:confirm apps/controller/src/http/native-browser-bridge.ts:landingPage packages/occ/src/state/native-browser-access.ts:PostgresNativeBrowserAccess

createControllerAuth and createNativeAdminAccess restrict host-only exchange to guarded OIDC without Google.

Host-only launch requires the Console session, x-occ-session-key, Console Origin, and exact-Agent operate or administer. An intent binds that session, Principal, Agent, Namespace, revision and origin. The landing page sets its challenge cookie; the opener confirms it. Redemption checks challenge and exchange, consumes and audits the intent, and sets the access cookie. It reuses an unexpired handle only when Installation, parent session, Principal, Agent, Namespace, revision, origin and parent expiry match; otherwise it replaces it. Reuse preserves existing WebSockets without allocating another handle. Stored secrets are hashed; expired, replayed or mismatched exchanges fail closed.

Human authentication owns parent-session currentness. Admission and renewal recheck that session, exact-Agent access and revision. The exchange creates no account or grant. See session limits.

6. OCC intercepts native-host HTTP requests

apps/controller/src/http/native-admin.ts:interceptNativeAdminHttp apps/controller/src/http/native-admin.ts:resolveNativeAdminAgentHost

The onRequest hook keeps ordinary controller routes off subdomains of the configured Agent domain, except the Console host. This also applies when native access is disabled and the domain is retained.

For proxy admission, OCC authenticates the session or handle and resolves the IAM identity. Before Agent-reference lookup, OCC requires a normalized hostname of agent- followed by exactly 32 lowercase hexadecimal characters, a dot, and the exact configured domain. Malformed hostnames fail before lookup or proxying. For well-formed keys, the same resolver scans Installation Agent references and refuses unknown, deleted or ambiguous hosts. OCC selects admin or chat and checks version and configuration.

Attributable IAM denials get one audit attempt, even after the five-second admission wait, disconnect or shutdown. Timely denials await that append before returning 403. Classified dependency failures, append failures and admission timeouts return 503; unavailable Agents remain 403. For HTTP admission refusals, http.completed records a fixed nativeAdmissionReason, without URLs, cookies, identities or error text. Early guards and post-admission failures may omit it.

Without a session, supported top-level HTML navigations with Sec-Fetch-User: ?1 redirect to the Console, carrying only the opaque host key. Without that header (including Safari), they get a 403 Sign in link. Link previews, APIs, writes and WebSockets remain refused. See the supported paths and session segments.

Different-version refusals return eligible navigations an HTTP 403 Agent version changed page linking to the Console. Other refusals retain their handling; cookies remain unchanged and no automatic relaunch occurs.

Browser permission denials return HTTP 403 linking to /console/agents on the configured public origin. JSON refusals name the required permission. Neither names the Agent; unknown hosts get identical text. WebSocket refusals remain unchanged.

Status and proxy checks reuse one Better Auth lookup for identity, expiry and attribution. Each WebSocket lease checks the original session, selected mode and Agent again. After handling stopped, replacing or unsupported Agents, admission rechecks both Compute owner fields. A mismatch returns HTTP 403 or closes an upgrading socket without an HTTP response, before gateway-key lookup or connection. A failed lease closes both sockets.

apps/controller/src/gateway/native-admin-proxy.ts:proxyNativeAdminHttp

The HTTP proxy normalizes URLs up to 4,096 characters, checks exact Agent Origin (optional for GET/HEAD), and strips cookies, service keys and forwarding, identity, and scope headers. Over private HTTPS it preserves surviving Content-Length; otherwise, it uses chunked framing only when inbound headers specify framing.

Both modes use occ-workspace-files. Chat sets x-openclaw-scopes: operator.sessions.write, capping runtime authority alongside identity and device grants.

The proxy blocks service-worker scripts, adds CSP worker-src 'none', rewrites same-upstream redirects to the Agent origin, and strips upstream Set-Cookie and Clear-Site-Data.

An upstream transfer failure stops the request. Before sending response headers, the proxy returns 503; afterwards it aborts the response. An avatar 502 becomes an empty 404 for the initials fallback, and the proxy cancels its upstream body.

7. OCC proxies native WebSocket upgrades

apps/controller/src/http/native-admin.ts:handleNativeAdminUpgrade

Before Fastify routing, the API intercepts derived-host upgrades and tracks sockets for preClose. Socket error handling covers admission-time resets. Refusals close before denial audits settle; those audits survive resets. Admission checks destruction and shutdown before and after transport resolution; late success cannot connect.

apps/controller/src/gateway/native-admin-proxy.ts:proxyNativeAdminWebSocket

The proxy requires non-null exact Agent Origin, sanitizes the private HTTPS upgrade, and connects after upstream 101. onConnect and onClose append openclaw.agents.native_admin.websocket.connect and .close with the same connectionId. closeReason records lifecycle, revocation, dependency, client, upstream or shutdown. Admission renews every 25 seconds with a five-second timeout. Failed, denied, timed-out, session-changed, mode-changed, or version-changed lease checks close both sockets. Denial closure does not wait for the append; a late attributable lease denial still gets its single audit attempt. Reconnects authenticate the current cookie and use the selected version. Cookie replacement cannot transfer a socket to another session. Native chat does not renew the OCE session.

apps/controller/src/http/native-admin.ts:drainNativeAdminWork

Bridge requests, admission, connect and close audits share one five-second shutdown drain. preClose destroys sockets and drains batches, including follow-on close audits, under one deadline. Composition then closes storage in onClose. Bridge handlers stop before subsequent phases or secret delivery after shutdown or disconnect. Unsettled work logs native_admin.pending_work_unresolved with its count and retains its audit attempt while the process lives. The drain neither cancels producers nor guarantees durable delivery; process exit can interrupt attempts.

8. Runtime renders HTML on a separate origin

apps/controller/src/drivers/compute/kubernetes/index.ts:gatewaySandboxConfiguration apps/controller/src/drivers/compute/kubernetes/index.ts:reconcileGatewayRoute

In shared-cookie mode with sandbox routing configured, preparation and activation render the Agent's stable preview origin and adjacent sandbox port in its native document. The worker reconciles its GET/HEAD route, public-shell SecurityPolicy and Agent-scoped backend ingress policy under the serving version. A separate Helm listener admits only configured ingress peers, and the backend targets the sandbox port instead of the admin port. The preview domain is outside the shared session cookie scope. Native UI still reads private file content through its authenticated connection, then delivers it to the runtime's isolated iframe. The sandbox listener serves public shell and registered renderer assets; runtime CSP and nested frame isolation remain responsible for executing generated content. See the routing contract.

For opted-in Kubernetes gateways, KubernetesComputeDriver.privateStateInitContainer copies the admitted configuration from /etc/openclaw-managed/openclaw.json to /runtime-state/home/.openclaw/openclaw.json on the init container's writable volume. The gateway mounts that same home subdirectory at /home/node, so native edits remain Pod-local and the next Pod starts from the admitted configuration. The init container cannot write through the gateway's later mount path.

apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts:configureNativeWorkerProfile reuses only the exact generated worker profile after a container restart; conflicting settings fail. KubernetesComputeDriver.gatewayPrivateStateVolumeMounts retains client/ and its migration receipt with the Gateway database. Native admin Configuration remains Pod-local.

Debugging and Verification

Search documentation