# Web runtime and host boundaries

> Canonical path: `specs/web/runtime-host-boundaries.md`
> Status: active
> Scope: the root `web/` application across AgentTeam Server Browser deployments and the native Desktop host.

## One business application

The root `web/` tree is the only AgentTeam business UI. It produces:

- a Browser build served by AgentTeam Server;
- a Desktop renderer packaged into Electron's `app.asar`.

Machine, Agent, conversation, task, settings, and MeshX behavior must not be
implemented twice.

Electron is the native Desktop host on macOS and Windows. Linux publishes the
headless Go daemon/CLI and does not build or serve the Desktop renderer.

## Entries and runtime injection

`web/src/main.tsx` delegates to a build-selected entry:

```text
web/src/entries/browser.tsx
web/src/entries/desktop.tsx
```

Both entries call the same render bootstrap. The Browser entry constructs a
Browser runtime. The Desktop entry obtains the verified daemon base URL,
short-lived UI session, platform, and build metadata from the typed Electron
preload facade, then reads `/api/internal/runtime` and
`/api/internal/servers/active` through that daemon transport.

The runtime contract describes host kind, capabilities, the server transport,
and optional native adapters. Shared modules use the provider and capability
hooks. They must fail explicitly when a required capability is absent rather
than probing the environment. Before an active server exists, the Desktop build
owns an independent centered selection surface. A native Desktop window has two
visible states: authentication and Workspace selection. Authentication presents
the official AgentTeam server (`https://agentteam.pingcap.com`) first and keeps the
other-server address input on that surface. Workspace selection groups
saved-server Workspaces and offers another-server authentication as the
alternate action. Browser Web keeps its own authentication and Machine
connect-token flows; after Desktop Workspace activation, both hosts render the
shared application.

Shared App, route, page, and business-domain modules must not:

- inspect `window.agentteamDesktop`, user-agent strings, localhost, or injected
  globals to decide whether they run in Desktop;
- import Desktop runtime adapters or Electron preload types;
- construct or fetch `/api/internal/*`;
- make Desktop and Browser use different DTO meanings for the same product data.

The Desktop root intercepts user-initiated anchor navigation to external
`http` and `https` origins and delegates it to the native-window adapter so the
system default browser opens the destination. Internal `app://agentteam` navigation
stays in the renderer, and download links and non-HTTP protocols retain their
explicit handlers. This policy is owned by the Desktop host and is not installed
in the Browser entry.

Host detection and local authentication stay behind the Desktop entry and
adapters. Electron details remain behind the typed preload facade; control
tokens, Node/Electron objects, and generic IPC never enter the renderer.

Desktop onboarding may call `/api/internal/onboarding/*` through the Desktop local runtime adapter. It must not store user tokens in browser storage or expose Machine IDs, machine API keys, or daemon connect-token exchange fields. The local daemon injects the selected server's user token into gateway traffic.

Desktop bootstrap reads the optional active-server form and treats an unselected session as a normal `null` result, avoiding an expected `409` during first paint. Onboarding uses authenticated local HTTP only. A successful Workspace activation response replaces the Desktop runtime epoch before shared Workspace routing and server requests proceed. The active-selection stream is a local WebSocket at `/api/internal/servers/active/stream`; it starts with the mounted Workspace shell and closes when that shell unmounts. Onboarding does not depend on that stream.

After Workspace selection, the shared Workspace shell owns routing, queries, realtime lifecycle, and business overlays while the host injects the outer presentation. Browser uses the Browser host frame with sidebar search. Desktop uses a dedicated host frame with a native-titlebar-safe top region, top-level search entry, and no duplicate sidebar search. Host differences must be expressed through the shell host contract or injected components rather than host checks spread through shared business code.

## Server and local transports

The server API is the product data plane. Browser resolves it against its
deployment origin; Desktop resolves it against the verified daemon `baseURL`
and calls the same paths through the local gateway. `RuntimeTransport` is the
only owner of HTTP, WebSocket, and browser-managed resource authorities. This
includes AgentTeam APIs, MeshX APIs, realtime, files, downloads, and any future
server route. Shared code must not derive these authorities from
`window.location`.

Desktop HTTP requests authenticate with a Bearer UI session and omit
credentials. Apart from normal HTTP metadata such as JSON `Content-Type`, the
renderer does not send Workspace, route-selection, or observability headers to
the daemon. The daemon derives Workspace identity from the active selection and
request path, replaces the renderer session with the selected server's user
credential, and generates any upstream routing headers authoritatively.
WebSocket and DOM-managed read resources use the same short-lived session only
in the `accessToken` query field because those browser APIs cannot set headers.
`resolveResourceURL` accepts only the verified daemon authority and allowlisted
read routes; mutations continue to use authenticated fetch.
Explicit downloads use authenticated fetch to a Blob and never send a
token-bearing daemon URL to the system browser.

The Desktop local API is a separate adapter for the reserved
`/api/internal/*` namespace. Shared pages may reach it only through a narrow
runtime capability whose meaning is inherently local, such as daemon runtime
configuration, diagnostics, onboarding, or current-machine operations.
Window/tray/menu actions and macOS/Windows application updates use the typed
Electron host facade instead of daemon HTTP routes.

Machine, Agent, message, statistics, activity, and file reads remain server API operations in both runtimes. A runtime may later optimize a read internally only if it preserves the same authorization, DTO, error, ordering, and freshness contract.

HTTP、gateway、protocol、token refresh 或远端授权失败本身不拥有登出语义。
Browser 和 Desktop 都必须保留持久化登录、已提交的 Workspace 产品面，以及未被
确认失效的 Desktop active server selection。Workspace shell 一旦提交，临时连接
或 Workspace 列表失败就留在该 shell 内，由头像连接状态表示，并通过后台
retry/backoff 自动恢复；不能用全屏恢复页替换 shell，也不提供全局手动重试入口。

自动连接和重连状态只使用头像连接状态，不显示 Workspace banner。紧凑型全局
banner 仅用于已经确认、需要用户关注或决策的跨路由状态，例如物理网络断开、远端
session 失效、active Workspace 已不可用，或可关闭的客户端更新。成员和权限决定、
资源加载失败，以及 Machine 或 Agent 空集合，都由所属路由内联呈现，不产生全局
Workspace banner。

服务端明确确认 session 已失效时，Browser 继续使用现有站内登录流程；Desktop
自动委托 host 重新认证：只清除 active server selection，不删除已保存账号，并回到
既有的服务器 / Workspace 选择页，由对应账号条目提示用户重新登录。冷启动期间确认
session 失效时也进入同一 Desktop 账号恢复流程，不能显示独立的 session-expired
中转页或启动另一套重登录弹窗。用户完成登录后继续通过既有 Workspace 选择流程进入
产品。

未确认 session 失效的冷启动 transport failure 可以在尚无 Workspace shell 时显示
紧凑的被动连接提示，但不能提供账号退出，也不能变成全屏错误页；这类 bootstrap
恢复不能自动转入 OIDC、Browser 登录或 Desktop onboarding。后台恢复、明确的
Workspace 恢复或重新认证成功后，都从既有流程继续。若刷新后的 Workspace 列表暂时
遗漏已提交的 Workspace，当前产品面保持挂载，直到 Workspace 恢复或用户明确切换。
只有用户显式登出才清除持久化凭据和选择状态；Desktop 为重新认证清除 active
selection 不属于登出，其他 Browser 标签页仍可通过共享凭据存储同步显式登出。

## Electron host boundary

Electron registers `app` as an internal privileged scheme and serves only the
packaged renderer catalog and SPA navigations from `app://agentteam/`. `app://` is
never registered with the operating system. The external application scheme is
`agentteam://`; Electron validates and dispatches those links in main without
requiring renderer ownership of the credential exchange.

Production renderer windows use context isolation and sandboxing with Node
integration disabled. Preload exposes one exact, typed `window.agentteamDesktop`
facade for bootstrap/session renewal, bounded window and tray actions, safe
external URL opening, application settings, quit, updater operations, and
validated events. It must not expose `ipcRenderer`, Electron or Node objects,
filesystem/process access, channel names, or a generic invoke/send method.

The daemon receipt and its control capability stay in the Go daemon and trusted
native client boundary, including Electron main. Only the least-privilege,
revocable UI session issued for the renderer window crosses preload. Destroying
that window/session, quitting, or replacing the daemon instance revokes or
replaces the session.

## Current-machine predicate

A selected Machine is the Desktop's current machine only when all three facts match:

```ts
runtime.kind === "desktop" &&
activeServer.binding.workspaceId === workspace.id &&
activeServer.binding.machineId === machine.id
```

The predicate gates local capabilities. It must not cause shared data panels to switch from the server API to a second Desktop-only data source. A remote Machine never exposes another Desktop's local control plane; server APIs obtain remote facts through the typed daemon transport.

When GUI onboarding activates a Workspace without associating a Machine, `activeServer.binding.machineId` is empty. The predicate is therefore false by construction while ordinary shared server-backed Workspace UI remains available.

## Build and dependency boundaries

Vite selects the entry at build time. Browser and Desktop adapters may depend on the shared runtime contract, but shared App and business modules do not depend back on either adapter.

```text
shared App/pages -> runtime contract + server API
Browser adapter  -> runtime contract
Desktop adapter  -> runtime contract + local API/typed host boundary
```

Desktop-only modules must stay out of the Browser entry graph. Both builds use the same i18n, routing, query, UI primitives, and business components.

The macOS/Windows package places Electron main, preload, and the renderer in
`app.asar`. The pure Go `agentteam[.exe]` sidecar and managed first-party OpenCode
runtime/model catalog are staged as `extraResources`; they are not renderer
assets and are not importable by Web code. Electron targets macOS arm64/x64 and
Windows arm64/x64. Linux amd64/arm64 builds only the Go daemon/CLI and managed
runtime payload and have no Electron or renderer dependency.

## Verification

Changes to this boundary must cover:

- entry selection and the shared render bootstrap;
- fail-fast provider and missing-capability behavior;
- Browser and Desktop builds;
- the Desktop bootstrap error state;
- source/dependency guards that prevent shared business modules from importing
  Desktop adapters or `/api/internal/*`, deriving API/media/WS hosts from
  `window.location`, opening raw WebSockets outside transport-owned modules, or
  bypassing the server transport with bare relative API fetches.
