Files
openclaw/docs/web/urls.md
T
Peter Steinberger cc2fc55f9b feat: portals — expose agent-run dev servers to the operator (#122536)
* feat(protocol): add portal methods and event

Bump the reviewed protocol owner-module count from 55 to 56.

* feat(gateway): add portal service and reverse proxy

* feat(agents): add portal tool

* fix(gateway): refine portal URL and proxy auth

* refactor(gateway): keep portal helper types private

* fix(gateway): declare portal transport service

* test(gateway): satisfy portal proxy lint

* test(gateway): narrow websocket payload types

* refactor(protocol): compact portal schema exports

* fix(gateway): export portal protocol types

* feat(ui): add portals page

* docs(gateway): add portals guide

* fix(gateway): dial portal targets via localhost dual-stack

Vite and other Node >=17 dev servers bind ::1 only for localhost, so a
fixed 127.0.0.1 dial 502s on the default path. Use hostname localhost
with family autoselection and rewrite Host to match.

* fix(gateway): type portal dual-stack connection

* fix: satisfy portal integration gates

* fix(gateway): isolate portal cookie jars per target

Cookies are hostname-scoped, not port-scoped, so the per-port origin
split alone let Gateway plugin-auth cookies reach agent-run targets.
Forward only cookies carrying this portal's own name prefix (stripped),
rewrite target Set-Cookie names to the prefixed form incl. the WS 101
handshake, and drop Domain attributes.

* fix(ui): detect unreachable portals behind proxied gateways

Probe the portal origin from the browser (no-cors, 4s timeout) and show
a recovery notice with the gateway-host URL instead of a dead iframe
when only the gateway port is exposed (Serve/Funnel/reverse proxy).
Docs: cookie isolation + reachability; zh-CN glossary entry.

* test(ui): satisfy portal reachability lint

* test(gateway): provide control UI request hosts

* chore(protocol): regenerate after rebase

* fix(gateway): namespace portal auth cookies by listener

* fix(gateway): scope portal token URLs to write-capable clients

The portal bearer token rides in the summary url/tokenQuery; portal.list
is operator.read and portal.changed fans out to read subscribers, so a
read-only client could harvest an openable URL. Make those fields
optional, redact them from read-scope list responses, and drop them from
every portal.changed broadcast; write/admin clients still receive them
and the UI refetches the list on change.

* docs(web): list the portals route

* fix(gateway): type portal open credentials

* docs(gateway): clarify portals PORT/PUBLIC_URL are agent-set

Opening a portal creates only the proxy listener; the agent sets PORT
and PUBLIC_URL in its own exec command, matching the portal tool
contract. Removes the implication of an automatic env handoff.

* chore(protocol): regenerate portal models

* style(gateway): format portal method-order assertions

Rebase union-merge left the portal.list assertion wrapped; oxfmt fits it
on one line.

* chore(plugin-sdk): refresh API baseline after rebase

* chore(plugin-sdk): refresh API baseline after rebase

* chore(protocol): refresh portal event order after rebase

* chore(plugin-sdk): refresh API baseline after rebase

* fix(gateway): pin portal referrer policy to no-referrer

The portal URL carries its bearer token in the query, and upstream
response headers are copied verbatim, so a target answering with
Referrer-Policy: unsafe-url could leak that URL to every third-party
origin it references. Force no-referrer after the copy and drop any
inbound Referer that still carries the token before forwarding.
2026-08-13 00:46:11 -07:00

16 KiB

summary, read_when, title
summary read_when title
Control UI URL routes, stable session-link grammar, and connection handoff parameters
You need to bookmark or share a Control UI session
You are adding or changing a Control UI route
You need a terminal, approval, onboarding, or remote Gateway URL
Control UI URLs

The Control UI uses readable paths for pages and session links. A configured gateway.controlUi.basePath prefixes every path below. For example, /chat/main becomes /openclaw/chat/main when the base path is /openclaw.

Session and dashboard URLs

Chat and dashboard views are parallel route namespaces:

/chat/main/deploy-monitor-6db92d48
/dashboard/main/deploy-monitor-6db92d48
/chat/main/telegram/12345
/chat/main/cron/nightly/run/8821
/chat/main

The path grammar is:

/<namespace>/<agentId>
/<namespace>/<agentId>/<sessionRef>
/<namespace>/<agentId>/<restSegment>/<restSegment>...

<namespace> is either /chat or /dashboard. The first form opens that agent's main session. The other forms encode one immutable session key in one of two ways.

The short-id form applies when the session key's rest, everything after agent:<agentId>:, ends in a UUID. <sessionRef> is an optional display-name slug plus a short id, such as deploy-monitor-6db92d48. The short id is the authoritative part: at least eight lowercase hexadecimal characters from the start of the key's trailing UUID, with UUID dashes omitted. Longer prefixes up to all 32 hexadecimal characters are accepted. The row's rotating sessionId is not part of the URL identity.

Every other key uses the literal-key form. Each colon-delimited segment after agent:<agentId>: becomes one URL-encoded path segment. For example, agent:main:telegram:12345 becomes /chat/main/telegram/12345, and agent:main:cron:nightly:run:8821 becomes /chat/main/cron/nightly/run/8821.

Literal rest segments exactly equal to . or .. use ~dot and ~dotdot so browsers cannot collapse them as relative path segments. A literal segment that starts with ~ doubles that leading character to keep the encoding reversible. When an otherwise literal one-segment rest could be mistaken for a short id, the builder inserts ~key before it, for example agent:main:release-deadbeef becomes /chat/main/~key/release-deadbeef. The marker forces literal interpretation and appears only when the unescaped form would be ambiguous.

The reserved single-segment literal rest names are main, global, boot, and sessions. The configured session.mainKey joins that set at runtime. Exactly one segment after the agent id is literal when it is reserved or does not contain a valid short id; otherwise it is a short reference. Two or more segments after the agent id are always literal.

Only the configured session.mainKey collapses to the agent-only main-session path. With session.mainKey: "workspace", agent:research:workspace becomes /chat/research, while the distinct key agent:research:main remains the literal path /chat/research/main.

Stability contract

The following parts are stable URL contracts:

  • The /chat and /dashboard namespace words.
  • The key UUID short id in short-id URLs.
  • The arity and short-versus-literal parsing rules above.

In short-id form, the agent segment is decorative and the slug is almost decorative. Neither identifies the session on its own, and both may change without notice. The one exception is a tie: if the short id matches more than one session and exactly one of them still carries the slug in the link, that session is used, so a generated link keeps working even when two ids happen to share a prefix. A slug that matches none or several of the tied sessions is ignored and the disambiguation view is shown. After resolution, the Control UI replaces the address bar with the current agent id and current display-name slug without adding a browser-history entry.

In literal-key form, the agent segment is authoritative because it is part of the reconstructed session key. The remaining literal segments are authoritative too. A slug, when present, is always decorative; literal-key forms do not synthesize one.

As a best-effort convenience, an unescaped one-segment literal that does not resolve as an exact session key is also checked against display-name slugs. One exact slug match is replaced in the address bar with its full /<namespace>/<agentId>/<slug>-<shortId> reference. If several sessions share the slug, the UI shows the same disambiguation view used for short-id ties instead of guessing. Exact short-id and literal-key references always win over slug matching.

If one short id matches more than one session and the slug does not settle it, the UI does not guess. It shows a small disambiguation view with the matching display names, agents, and longer id prefixes. Use a longer prefix to make the URL unique. Current Gateways return at most ten recent candidates; when that bound is reached, the view treats the result as incomplete instead of guessing. Against an older Gateway that predates short-id resolve support, the UI falls back to the prior bounded list search, scanning at most five pages of results. It likewise reports an incomplete search instead of guessing when that fallback cannot prove uniqueness.

To continue one of these links in the terminal or attach a coding harness, see Session synchronization and attachment.

Canonical links do not use ?session= or ?face=. Released links such as /chat?session=<sessionKey> are accepted only at the application boundary as a migration aid and immediately rewritten, without adding browser history, to the canonical path. The released ?face=dashboard companion selects the /dashboard namespace during that rewrite. Loaders and page code never read the query-form identity, and new links must not emit it. The Sessions list keeps its own ?session= parameter because that parameter expands a row; it is not a session deep link. The one-shot composer value ?draft= remains supported on chat and dashboard session paths.

Route table

This table lists every Control UI application route. A dash means the route has no route-specific URL parameters.

Page Canonical path Aliases Parameters or dynamic forms
Chat /chat - Key-backed session forms above; ?draft=<text>
Dashboard /dashboard - Key-backed session forms above; ?draft=<text>
Dashboards /dashboards - -
Ask OpenClaw /custodian - ?intent=new-agent, ?onboarding=1
New session /new - ?agent=<agentId>, ?catalog=<catalogId>
Activity /activity - ?view=run&run=<run-id>, ?view=run&execution=<execution-id>
Apps /apps - -
Portals /portals - -
Agents /settings/agents /agents /settings/agents/<agentId>[/<panel>]
Channels /settings/channels /channels Shared settings parameters below
Connection /settings/connection - Shared settings parameters below
Legacy General /settings/general /config Redirects to Appearance → Language
Profile /settings/profile /profile Shared settings parameters below
Communications /settings/communications /communications Shared settings parameters below
Appearance /settings/appearance /appearance Shared settings parameters below
Notifications /settings/notifications - Shared settings parameters below
Security /settings/security - Shared settings parameters below
Secrets /settings/secrets - Shared settings parameters below
Advanced /settings/advanced - Shared settings parameters below
Approvals /settings/approvals - Shared settings parameters below
Automation settings /settings/automation /automation Shared settings parameters below
MCP /settings/mcp /mcp Shared settings parameters below
Memory /settings/memory - /settings/memory/memories|dreams|settings
Infrastructure /settings/infrastructure /infrastructure Shared settings parameters below
Labs /settings/labs - Shared settings parameters below
About /settings/about - Shared settings parameters below
AI and agents /settings/ai-agents /ai-agents Shared settings parameters below
Model setup /settings/model-setup /model-setup ?firstRun=1
Model providers /settings/model-providers /model-providers Shared settings parameters below
Import memory /memory-import /settings/memory-import -
Workboard /workboard - /workboard/<boardId>
Worktrees /worktrees /settings/worktrees -
Sessions /sessions /settings/sessions ?session=<sessionKey>, ?status=archived|all
Usage /usage - -
Debug /debug - -
Logs /logs - -
Skill Workshop /skills/workshop - -
Skills /skills - -
Plugins /settings/plugins - /settings/plugins/discover
Automations /cron - -
Tasks /tasks - -
Devices /settings/devices /nodes Shared settings parameters below
Plugin tab host /plugin - ?plugin=<pluginId>&id=<tabId>

Settings routes that use schema-backed deep links accept ?section=<section>, ?advanced=1, and #<setting-id>. These values select content within the page; they do not change the route identity.

The retired General route and its /config alias are replaced once with /settings/appearance?section=__appearance__#settings-language. The historical #settings-general-model target instead lands on the Models behavior section.

Memory tabs use the paths in the table instead of ?tab=. Older Memory links with ?tab=memories|dreams|settings, ?tab=dreaming, ?tab=search, or ?section=memory are replaced once with the corresponding path while keeping any setting anchor.

Plugin catalog tabs also use paths instead of ?tab=. Older links with ?tab=discover|installed are replaced once with the corresponding path while keeping other query parameters and the fragment.

Agent selection and its overview|files|tools|skills|channels|cron|memory panels use paths. Older links with ?agent=<agentId> are replaced once with the agent path while keeping other query parameters and the fragment.

Special documents and startup modes

These Gateway-served documents sit outside the application route table:

  • /?onboarding=1 opens the first-run onboarding presentation.
  • /terminal opens the user-facing full-screen terminal. With a base path, use <basePath>/terminal.
  • /?view=terminal opens the same terminal-only document in the WebView/embed form used by the mobile apps. Terminal availability in either form still requires gateway.terminal.enabled and operator.admin.
  • /approve/<approvalId> opens a standalone approval document. With a base path, use <basePath>/approve/<approvalId>. The id identifies an approval but never authorizes it; normal Gateway authentication still applies.

The approval namespace is reserved ahead of plugin HTTP routes for all HTTP methods. When Control UI serving is disabled, it returns 404 instead of falling through to a plugin route.

Remote Gateway handoff

The Vite development UI can connect to a different Gateway:

http://localhost:5173/?gatewayUrl=ws%3A%2F%2F<gateway-host>%3A18789
http://localhost:5173/?gatewayUrl=wss%3A%2F%2F<gateway-host>%3A18789#token=<gateway-token>

URL-encode a full ws:// or wss:// value. gatewayUrl is accepted only in a top-level window, stored after load, and removed from the address bar. Prefer #token= because fragments do not enter HTTP request logs or Referer headers. The legacy ?token= handoff remains a bootstrap-only credential fallback and is stripped immediately. Passwords stay in memory only.

When gatewayUrl selects another Gateway, the UI does not fall back to local configuration or environment credentials. Provide the remote Gateway's token or password explicitly, and use wss:// behind TLS.