# AgentTeam Desktop Architecture

> Canonical path: `specs/daemon/desktop-architecture.md`
> Scope: current AgentTeam Desktop daemon architecture, package ownership, runtime communication boundaries, and dependency direction.

## Purpose

AgentTeam Desktop consists of a headless Go daemon/CLI on every supported platform
and an Electron native host on macOS and Windows. Linux ships the daemon/CLI
only. The daemon connects a user's machine to AgentTeam Server, owns local agent
runtime processes, serves the Desktop gateway/API surface, and exposes scoped
outbound tools to those runtimes. Electron owns application, window, tray,
menu, deep-link, renderer, and whole-application update behavior.

The architecture has one process shell and three product layers:

1. **Communication layer** — inbound and outbound protocols between Desktop, AgentTeam Server, and runtime tools.
2. **Dispatcher layer** — the routing and session hub that converts normalized channel events into runtime work and visible AgentTeam events.
3. **Agent/runtime layer** — local runtime configuration, process lifecycle, driver adapters, delivery, and recovery.

The root `web/` application is the only business UI. AgentTeam Server serves its
Browser build, while Electron packages its Desktop renderer. Local Desktop API
routes are side surfaces over the same daemon state. They must not become a
separate owner of server, channel, or runtime behavior.

## Architecture At A Glance

```mermaid
flowchart TD
    server[AgentTeam Server]
    daemon[Desktop daemon process]
    channel[Communication layer\ninternal/channel + internal/channel/agentteam]
    dispatcher[Dispatcher layer\ninternal/dispatcher]
    runtime[Agent/runtime layer\ninternal/agent + subpackages]
    api[Desktop gateway and local API\ninternal/api]
    electron[Electron main\nmacOS and Windows]
    preload[Typed preload facade]
    web[Desktop renderer\napp://agentteam]
    mcp[Runtime outbound tools\nMCP + agent tools CLI]
    process[Runtime process\nCodex / OpenCode / Claude]

    server <-->|daemon WebSocket, HTTP agent API| channel
    daemon --> channel
    channel -->|normalized messages, lifecycle commands| dispatcher
    dispatcher -->|launch config, runtime input| runtime
    runtime --> process
    process -->|events, usage, delivery ack| runtime
    runtime --> dispatcher
    dispatcher -->|agent events, outgoing messages| channel
    channel -->|HTTP send/upload, daemon WS status| server
    process -->|MCP tools/call or agentteam exec| mcp
    mcp -->|scoped tool dispatch| channel
    mcp -->|session/runtime lifecycle| dispatcher
    electron -->|receipt control capability| daemon
    electron --> preload
    preload -->|bounded typed host operations| web
    web -->|Bearer UI session and explicit daemon URLs| api
    api -->|read/control local state| dispatcher
    api -->|channel enable/disable and MCP routes| channel
```

The daemon process composes the Go layers. Electron is a separate native host;
it does not own daemon business services. Cross-layer calls should use narrow
interfaces owned by the consumer rather than package-wide facades.

## Process Shell

`desktop/internal/cmd/daemon.go` is the CLI entry. `desktop/internal/daemon`
owns headless process startup, config loading, single-instance checks, service
helpers, profile watching, runtime detection startup, runtime receipt
lifecycle, graceful shutdown, and HTTP server wiring.

`agentteam daemon` has the same headless behavior on macOS, Windows, and Linux. It
does not create windows or tray state and does not serve a local browser
application. On macOS and Windows, Electron main owns the single native window,
tray, menus, and external navigation.

Electron computes the main window's initial size once using the primary display
work area and bounded desktop layout defaults. Small displays start maximized.
Hiding and revealing the window reuses the existing native window and must not
reapply the initial sizing policy after the user resizes it.

During service setup, the daemon:

1. loads and validates local profile/config state;
2. creates the Dispatcher with the local API base URL;
3. starts the channel manager owned by the Dispatcher;
4. wires Memory, Skill, App build, runtime detection, and metrics integrations;
5. serves the authenticated local API, selected-server gateway, and daemon
   control plane with the Dispatcher and channel accessor.

The process shell must remain composition-oriented. Product behavior belongs in the owning layer below.

### Daemon Receipt And Desktop Control

`desktop/internal/daemoncontrol` owns the canonical per-user daemon runtime
receipt and the in-memory control controller. The permission-restricted,
atomically written receipt records the daemon instance and API version, PID plus
process-start identity, exact numeric loopback base URL, owner kind and process
identity, and a random control capability. Native process-start identity must
remain stable for one process lifetime across wall-clock corrections; Linux
uses the boot ID plus process start ticks. Status and Desktop attachment verify
the receipt against native process identity and the authenticated control
identity endpoint; a PID or readable receipt alone is never proof of a live
instance or ownership.
Daemon startup may recognize a retired v1 receipt only during stale-receipt
reconciliation. It removes that receipt only after the recorded process identity
is proven stale; ordinary Go receipt consumers remain v2-only.

The private `/api/internal/control/v2` surface is intentionally separate from
business API authentication. Its bounded operations are identity inspection,
renderer UI-session creation/reconciliation/renewal/revocation, adoption of an
orphaned Electron-owned daemon, and owner-safe graceful shutdown. The control
capability is accepted only in its dedicated header and remains in the receipt,
daemon controller, and trusted native client memory. It never enters process
arguments, logs, URLs, renderer bootstrap/storage, preload responses, or
general IPC.

Attaching does not imply ownership. Electron may attach to a compatible
CLI/service-owned daemon but cannot adopt or stop it. Adoption is limited to an
orphaned Electron owner and atomically rotates both owner identity and control
capability. Every mutating operation validates daemon instance, current owner,
PID plus process-start identity, and capability.

Electron recognizes a retired v1 receipt only for authenticated upgrade
replacement. It may adopt and stop an orphaned Electron-owned v1 daemon before
starting the packaged v2 daemon. On macOS it may also reinstall a daemon proven
to be the current user's canonical AgentTeam LaunchAgent by matching the plist,
loaded job PID, profile, process identity, and authenticated control identity.
A v1 service is migrated for API compatibility; a compatible v2 service is
migrated only when the packaged AgentTeam SemVer is strictly newer than the canonical
managed binary. Equal, newer, unknown, or non-canonical external daemons remain
unchanged. Electron waits for the replacement v2 service identity and attaches
without entering the Electron-owned spawn path. Other CLI/service-owned v1
daemons are never replaced. If the old Electron owner is still alive, startup
requires the user to quit it and retry rather than attaching to an incompatible
daemon. Canonical LaunchAgent replacement is the sole narrow exception to the
rule that attachment cannot stop an external daemon, and it may interrupt
active runtimes.

Explicit Electron quit closes new reconciliation admission, joins recovery
already in progress, and then revokes its renderer UI sessions. By default an
Electron-owned daemon receives authenticated graceful shutdown and a bounded
drain; the keep-running setting leaves it alive. An external daemon is never
stopped by Electron. Electron crashes and forced exits leave the daemon and
agent runtimes running. Update installation uses the same authenticated
owner-safe shutdown, even when keep-running is enabled; installation is
deferred while Electron is attached to an external daemon. No quit path falls
back to an unauthenticated process kill.

### Daemon-Owned Memory Search Derivative

`desktop/internal/memorysearch` is the daemon-owned owner of the local
full-text search derivative over a bound managed Memory canonical clone. It is
a side service of the Memory binding lifecycle, not part of the session-bound
memory watcher that owns commit and push. It owns scanning, chunking, indexing,
querying, reconciliation, garbage collection, and lifecycle of the derivative
under each agent workspace.

The dispatcher consumes only a narrow eligibility and service interface from
this owner: cached binding availability is an input to runtime-start
fingerprinting and to dynamic injection of the Memory MCP endpoint and the
`memory-maintenance` runtime skill. Indexing, path resolution, and
canonical-source validation must not leak into dispatcher, channel, or runtime
driver code.

Further detail on authorization, state, and response semantics lives in the
[Memory spec](../memory/memory.md) and [Runtime Outbound Tools](./runtime-outbound-tools.md).

## Layer 1: Communication

The communication layer owns protocol-specific inbound/outbound details. It must normalize external events before handing them to the Dispatcher and must hide server protocol details from runtime drivers.

### Channel Manager

`desktop/internal/channel` owns the running channel set. `channel.Manager` creates channel implementations from profile config, starts/stops/reconciles them, and exposes a bounded `types.ChannelAccessor` for lookup.

The channel manager receives only `types.ChannelRuntime`, a narrow dispatcher-facing port with message routing, session lookup, and inbox reads. It must not depend on concrete dispatcher internals for ordinary channel work.

### AgentTeam Channel

`desktop/internal/channel/agentteam` is the first-party AgentTeam channel implementation. It owns:

- authenticated AgentTeam daemon WebSocket connection lifecycle;
- daemon command dispatch such as `agent:start`, `agent:deliver`, `agent:stop`, workspace requests, review requests, Memory/Skill access-change pushes, and Elastic Request Links;
- AgentTeam HTTP outbound calls for message send, attachment upload, history/search/task/review/schedule APIs, token refresh, and user session token exchange;
- MCP handlers for AgentTeam message and memory tools plus knowledge retrieval tools;
- AgentTeam-specific state such as per-agent auth tokens, dedup rings, remote endpoint registry, send limits, and workspace list request tracking.

AgentTeam channel inputs are server protocol messages. AgentTeam channel outputs are normalized `types.Message`, `types.AgentEvent`, `types.OutboundMessage`, and scoped tool calls.

### Runtime Outbound Bridge

Agent runtimes call back into Desktop through outbound tool mechanisms owned by the communication layer:

- **MCP HTTP JSON-RPC** exposes the first-party `agentteam` tool inventory and registered knowledge tools under authenticated local routes.
- **AgentTeam Exec CLI** exposes selected first-party tools through `agentteam exec <domain> <action>` commands guarded by daemon-issued process-scoped credentials.

These mechanisms are not an additional architecture layer. They are outbound ports from the runtime process back into Desktop/AgentTeam. They must share the same ownership, authorization, target-routing, result-budget, and current-session rules described in [Runtime Outbound Tools](./runtime-outbound-tools.md).

## Layer 2: Dispatcher

`desktop/internal/dispatcher` is the hub between channels and local runtimes. It owns decisions that require both the source conversation and the runtime session state.

The Dispatcher owns:

- message routing from channel events to instruction handlers or interactive sessions;
- agent/runtime resolution, overrides, runtime policy fingerprinting, and launch config assembly;
- session reuse, local inboxes, deferred delivery, context recovery, and lifecycle teardown;
- MCP config resolution for each runtime session, including the first-party `agentteam` MCP surface, knowledge MCP, external MCPs, and Exec exposure policy;
- Memory and managed Skill launch-time materialization inputs;
- runtime event fan-out to AgentTeam-visible events, dashboard console events, trajectory cache, and metrics;
- ordinary runtime process admission, delivery ownership, session recovery, and lifecycle teardown.

The Dispatcher must not own transport parsing or runtime-specific stdout/SSE/protocol parsing. Those belong to channel packages and runtime drivers respectively.

## Layer 3: Agent And Runtime

`desktop/internal/agent` owns local agent runtime orchestration. It turns a resolved launch config into a running runtime process, manages runtime input delivery, interprets driver events, and preserves daemon-local recovery state.

Key boundaries:

- root `agent` package: agent config validation, workspace-backed identity helpers, concrete runtime drivers, and `Process` lifecycle;
- `agent/drivercontract`: the narrow adapter contract between `Process` and concrete runtime drivers;
- `agent/runtimeconfig`: runtime type detection, command/env/materialization helpers, and managed runtime facts;
- `agent/runtimeevents`: daemon-facing runtime event bus model;
- `agent/runtimeproc`: process tree tracking and cleanup helpers.

Concrete runtime drivers are transport adapters. They spawn the runtime, encode runtime input, parse runtime events, and report transport-level capabilities. Prompt semantics, AgentTeam target rules, tool ownership, and delivery policy remain above the driver boundary.

Supported runtime families are:

- `builtin`: AgentTeam-managed OpenCode shipped with the first-party platform
  artifact;
- `opencode`: externally configured OpenCode;
- `claude`: externally configured Claude Code;
- `codex`: externally configured Codex app-server capable runtime;
- `kimi`: externally configured Kimi Code ACP-capable runtime (`kimi acp`);
- `pi`: externally configured Pi RPC-capable runtime (`pi --mode rpc`).

The AgentTeam-managed `builtin` OpenCode runtime uses persistent, per-agent XDG data,
state, and cache directories under
`~/.agentteam/agents/<agent-id>/.runtime/opencode/`. Its SQLite database therefore
lives under that agent's `data/opencode/` directory and never shares the native
user OpenCode database. AgentTeam also keeps builtin OpenCode config in the agent's
managed workdir config directory. The daemon passes this root explicitly as
stable per-agent runtime state; session, ephemeral, detached-recovery, and
other launch workdirs must not choose it. Builtin launch removes every
case-insensitive `OPENCODE_DB` and `OPENCODE_TEST_HOME` alias, then replaces all
managed environment keys case-insensitively with canonical values so neither
daemon inheritance nor runtime configuration can bypass the isolated database.
The external `opencode` runtime retains its native data and database storage,
including `OPENCODE_DB` semantics. AgentTeam still supplies its existing managed
workdir-local OpenCode configuration for that runtime.

OpenCode initializes SQLite during `serve` startup. The daemon therefore
serializes every builtin, external, and detached OpenCode launch from port
selection and process creation until `/global/health` succeeds or startup
fails. Healthy servers continue running concurrently; cancellation while
waiting for the startup gate aborts the queued launch before a port is selected.

Desktop detects and configures external runtimes. It does not own their package-manager installation or upgrade lifecycle.

## Side Surfaces

### Shared Web Application And Desktop Gateway

The root `web/` tree owns the Browser and Desktop business application. It has two thin build-time entries:

- the Browser entry creates a Browser runtime whose server transport uses the deployment origin;
- the Desktop entry creates a Desktop runtime from the verified daemon base URL
  and short-lived renderer UI session supplied by the typed Electron preload
  facade, then reads the local runtime descriptor and active-server selection.

Both entries render the same application and inject a typed runtime contract
through a provider. Shared pages consume that contract and the common server
API client. They must not infer the host from globals, user-agent strings,
localhost URLs, or probes of Desktop-only endpoints. The typed Electron host
facade and `/api/internal/*` are private to Desktop adapters.

Business data keeps one meaning in both builds. Machine, Agent, message, statistics, files, activity, and MeshX data use AgentTeam Server APIs. The Desktop runtime may expose explicitly capability-gated native actions, but a shared page must not replace a server read with a separate Desktop-only data model.

The Desktop Machine detail may inject a Desktop-owned presentation around the shared Machine route. Its overview reads daemon-local message and token metrics only for the current Desktop binding and the Agents synchronized through that binding. The local bundle preserves the retained Agent Dashboard contract: inbound and outbound summaries, total/input/cached-input/output token summaries, message trend and volume, token trends, and per-model token bars. Repeated `agentId` query parameters select one or more Agents for a single bounded aggregate; omitting them selects all Agents on the current Machine. The handler validates the active UI target and Machine identity, excludes other servers and Workspaces, and scans the collector once instead of issuing one statistics request per Agent.

AgentTeam Context attribution、launch-level Run evidence 和 Token comparison surface
由 [AgentTeam Token 消耗诊断](./token-optimization.md) 定义，不是 Machine
presentation 的一部分，也不增加 Desktop navigation item 或 tab，只能通过显式
`agentteam token` command 访问。

The same Desktop-owned Machine presentation consumes local runtime detection/configuration, external MCP server management, and Desktop settings bundles. Those tabs use `/api/internal/*` only through the Desktop runtime adapter. Browser Machine pages keep their server-owned presentation, do not register the Desktop presentation, and do not call Desktop-local Machine statistics, runtime, MCP, or settings endpoints.

`desktop/internal/api` is the Desktop gateway. It exposes local APIs, selects
the current upstream, and proxies server traffic; it does not serve renderer or
SPA assets. Local handlers should call Dispatcher, channel, profile, or focused
domain services instead of duplicating their behavior.

#### Local Control Plane

`/api/internal/*` is the Desktop-local HTTP namespace. The private daemon
control plane is the versioned subset `/api/internal/control/v2`; the remaining
routes use renderer UI-session authentication. The namespace is reserved even
if an upstream server exposes the same path, and the gateway must never forward
it. Stable bootstrap and server-selection routes are:

```text
GET    /api/internal/runtime
GET    /api/internal/servers/available
GET    /api/internal/servers/active
PUT    /api/internal/servers/active
DELETE /api/internal/servers/active
```

Desktop GUI onboarding additionally uses authenticated local routes under `/api/internal/onboarding/*` to probe server capabilities, perform password authentication or a daemon-owned PingCAP Auth OIDC/PKCE browser flow, list Workspaces across saved server accounts, and activate a Workspace. OIDC callbacks use a bounded one-shot loopback listener; state, nonce, verifier, authorization code, and returned tokens never enter Desktop Web storage or URLs controlled by the app. These routes are Desktop-only and must not be reused by Browser Web. `desktop/internal/userauth` owns this state; `desktop/internal/profile` remains the owner of CLI/headless machine credentials.

Desktop active-Workspace selection owns automatic first-Machine setup for both explicit onboarding activation and last-Workspace restoration. Before reusing a matching local machine credential, the daemon validates it with AgentTeam Server. A complete credential that no longer resolves to the same Machine and Workspace is deleted through the `desktop/internal/profile` runtime transaction, which disconnects the stale daemon connection. A compatibility entry that has a Machine API key but no Machine ID is resolved before any create path and patched in place so its local server ID and Agent source continuity are preserved. If that compatibility identity cannot be verified, Desktop retains the entry and requires explicit recovery instead of creating a second Machine. Automatic first-Machine setup creates only when no complete or unresolved local binding remains and the current GUI user has no Machines in the selected Workspace. The explicit current-computer action may create when no binding exists; an unresolved compatibility entry requires a separate replacement confirmation before this action may overwrite the same local entry. Both create paths transactionally persist the resolved credential, start the corresponding daemon connection, and wait for its first authenticated WebSocket handshake before returning. This readiness boundary prevents a newly created current computer from first rendering as an offline Machine. The local response exposes the Machine ID but never the Machine API key. Existing Browser connect-token/deep-link and CLI machine-key flows remain independent and unchanged.

GUI server accounts are persisted in `auth.yaml` with one account per server and encrypted refresh-token state. The file may also store the opaque ID of the most recently selected GUI server-and-Workspace target so a new Desktop UI session can restore it. Passwords are never persisted, access tokens remain in memory, and `auth.yaml` uses mode `0600`. When an authenticated GUI request receives an upstream `401`, Desktop refreshes the account session and retries the request once before requiring the user to sign in again; concurrent refresh attempts for the same account are serialized. The device key already used by Desktop local encryption protects refresh tokens at rest. Failure to initialize that key or decrypt saved state fails GUI authentication closed; plaintext fallback is forbidden.

GUI onboarding accepts both HTTP and HTTPS AgentTeam Server origins, including deployments that expose only an IP address and port. HTTPS remains the recommended deployment because HTTP transmits passwords, user tokens, and Workspace data without transport encryption. Redirects from authentication and gateway requests remain restricted to the exact configured scheme, host, and port.

The active-server response includes public server facts, the selected server's `workspaceId`/`machineId` binding, and a monotonically increasing `generation`. It must not expose machine API keys, user tokens, upstream cookies, or an arbitrary upstream URL. A GUI user-auth target may have an empty `machineId` when the Workspace already has another Machine but the current computer is not bound.

The Desktop Machine dialog is host-injected and separate from the Browser Machine dialog. It always shows the current-computer option: the option is disabled and marked as already added only after a matching local server/workspace Machine binding is validated against server truth. When an unresolved compatibility binding exists, the dialog first directs the user to restore the old Machine and requires a second explicit confirmation before replacing the local binding with a new Machine; otherwise it can bind the current computer directly through the authenticated local Machine API. Adding another machine creates the normal server-side Machine and presents the CLI/API-key setup path; Desktop does not offer its current-device deep link for another machine. Shared Workspace components emit an add-Machine intent and do not branch on the runtime kind.

Workspace activation is response ordered: the authenticated local activation response switches the Desktop runtime epoch and gateway credentials before the shared application resolves the selected recent Workspace. Onboarding uses HTTP only. The active-selection stream is scoped to the mounted Workspace shell so it starts after Workspace entry and is aborted when that shell unmounts.

Active server state belongs to the authenticated, revocable UI session issued
for an Electron window, not to the daemon process. The daemon assigns a stable
logical session identity and issues short-lived bearer lease generations for
it. Electron main creates and reconciles this least-privilege session through
the authenticated daemon control plane; only the current bearer and public
bootstrap facts cross preload. The logical session identity is implicit in the
local credential and is never a request-body parameter. When a new session has
no active selection, Desktop may seed it from the durable last-target ID after
confirming that the saved account and target still exist; an invalid target is
cleared and onboarding remains the fallback.
A switch atomically advances the session generation, invalidates the old
generation's in-flight traffic and streams, and causes the Desktop runtime to
isolate cached queries and reconnect realtime traffic. A bearer expiry alone
does not clear a recoverable logical session; explicit revocation, expiry beyond
the bounded recovery window, and deleted or invalid servers clear the session
selection. Logout revokes the logical session and removes a matching durable
last target.

Electron main is the sole UI-session lifecycle owner. It reconciles before
returning renderer bootstrap, on a main-process timer, after system resume or
screen unlock, on window focus, and when the renderer reports a typed local
`401` or active-selection `4401`. These triggers share one in-flight
reconciliation. Renderer timers are not a correctness dependency.

Reconciliation is idempotent. A live lease with sufficient remaining lifetime
is returned unchanged; a near-expiry lease is rotated; an expired lease may be
reissued for the same recoverable logical session. Rotation and reissue preserve
the window-scoped active selection and selection generation. A replaced bearer
remains valid only until its original expiry so already-dispatched requests can
finish while the renderer installs the new transport. All bearer generations
map to the same logical session; logout and window revocation invalidate every
generation. Bearer replacement is not a Desktop runtime epoch change: the
renderer hot-replaces local and server transports in the existing epoch,
reconnects streams, invalidates realtime-derived history that may have missed
increments, and preserves the shared Router, query cache, and mounted business
UI. Requests that observed a typed session-authentication failure are not
automatically replayed; callers may retry reads or mutations according to their
existing semantics after transport replacement.

If reconciliation proves that the attached daemon is unavailable, Electron
main revalidates the receipt and process identity. An Electron-owned daemon may
be restarted once per failure cycle through the supervised startup path. Merely
reattaching does not consume that restart budget; only an actual spawn opens
the breaker, and a successfully ready replacement must remain stable before
the breaker is rearmed. An external daemon is reattached only if its
authenticated receipt becomes available and is never spawned or stopped by
Electron. A daemon instance or base-URL change is a host epoch change and
performs one controlled renderer reload so the bootstrap, packaged CSP, and
native download authority all move together. Ordinary bearer replacement never
reloads the renderer. Reconciliation failures use bounded backoff and expose
an explicit renderer retry instead of silently logging out.

If suspension crosses the bearer expiry boundary, owner-authenticated
reconciliation can reissue the same logical window session only within the
bounded recovery window. Beyond that window it creates a new logical session;
expired bearer credentials never authorize recovery themselves. Before
committing a reconciled credential, the renderer reloads the authoritative
active selection with it. An unchanged selection replaces transports in the
mounted epoch, while a missing or changed selection follows the normal
epoch-switch path.

Saved GUI logins distinguish credential rejection from temporary availability.
Only an explicit upstream `401` requires reauthentication. Network failures,
timeouts, `429`, `5xx`, and local persistence failures retain the saved account
and expose an explicit retry instead of reporting that the login expired. Once
the Workspace has rendered, either condition preserves the mounted Workspace
and active selection: temporary availability is shown as a reconnecting notice,
while credential rejection asks the user to reauthenticate in place. Only an
explicit user logout removes the saved login or active selection.

#### Renderer And Gateway Routing

Electron registers the internal privileged `app` scheme before readiness and
serves the packaged renderer at `app://agentteam/`. The handler uses a closed build
catalog for immutable assets and SPA navigation fallback inside `app.asar`; it
does not read arbitrary files or forward requests. Only `agentteam://` is registered
externally for application deep links.

Linux installers may register `agentteam://` with a lightweight scheme handler that
dispatches to the CLI connect flow. That handler does not create a renderer or
local browser UI.

The production renderer obtains an explicit daemon base URL and UI session from
preload. Its runtime transport resolves HTTP, WebSocket, and browser-managed
resource URLs against that verified daemon authority rather than
`window.location`:

- `/api/internal/*` enters the authenticated local API or private control
  namespace and is never forwarded upstream;
- all other authenticated HTTP, WebSocket, and event-stream requests retain
  their path and query through the selected-server gateway;
- ordinary HTTP uses a Bearer UI session, while the specifically allowlisted
  WebSocket/read-resource routes may carry the same short-lived session in
  `accessToken` because those browser APIs cannot set headers.

The remote catch-all is intentionally not limited to `/api/v1/*`: AgentTeam Server
also fronts MeshX, Git, downloads, and future routes. No `/api/remote/*` URL
rewriting or server prefix belongs in shared clients.

`desktop/internal/api/localsecurity` owns the renderer-to-daemon CORS
contract. Trusted origins are the packaged `app://agentteam` authority and the
explicit numeric-loopback development renderer origins configured by the
daemon; wildcard origins and credentialed CORS are forbidden. The request
allow-list and response expose-list are centralized, least-privilege
transport contracts derived from headers the renderer actually sends or
reads. Standard conditional and request-metadata headers such as `ETag`,
`If-Match`, and `Idempotency-Key` remain HTTP headers rather than being moved
into business DTOs. Adding or removing a renderer-consumed header requires an
exact Go policy-test update and a real Electron custom-scheme regression.
Originless native and CLI callers do not expand the renderer CORS contract.

Every proxied request snapshots the current server and generation before
dialing. No active server produces a local `409 ACTIVE_SERVER_REQUIRED`; an
unreachable upstream produces `502 REMOTE_UNREACHABLE`. Upstream business
statuses and bodies otherwise remain transparent.

The upstream origin comes only from the authenticated server registry.
Client-provided `Authorization`, `Cookie`, `Host`, routing overrides, and
hop-by-hop headers are removed; the gateway injects the selected server's user
session. Upstream cookies remain in a server-isolated jar. HTTP bodies and
downloads stream, SSE flushes without buffering, and WebSocket
close/cancellation semantics propagate in both directions. Redirects stay
within the verified daemon transport and cross-origin navigation is not loaded
inside the privileged Electron renderer.

The authenticated server registry is the union of daemon machine-auth targets and GUI user-auth targets. Machine-auth targets reuse the running daemon connection. GUI targets use the per-server user session managed by `desktop/internal/userauth` and do not register a parallel daemon channel or messaging connection.

### Profile And Supporting Packages

`desktop/internal/profile` owns persisted Desktop configuration and reload behavior for agents, channels, providers, runtimes, external MCP servers, skills, shell environment imports, and file watching.

While the daemon is running, server identity mutations are transactions owned
by the profile server manager. Dashboard writes, protocol-discovered binding
changes, and accepted file reloads serialize per server before persistence and
runtime reconciliation. External file edits may update display metadata, but
must not replace an enabled server's upstream, credentials, binding, or
lifecycle outside that transaction. A failed mutation restores both persisted
and live identity; if either rollback is untrustworthy, the server is
quarantined, its active UI selections are invalidated, and gateway traffic
remains closed.

Quarantine is cleared only by an explicit authenticated repair operation. Repair
must stop and remove any residual runtime for that server ID, start the
persisted identity, and verify that the registered runtime still matches that
identity before reopening it. During startup and shutdown the manager remains in
a non-offline lifecycle mode until the local API can no longer accept writes.
CLI tooling may edit profile files directly only when no daemon process is
running; a running but unhealthy daemon is a fail-closed condition.

Small boundary packages such as Desktop `provider` and `promptcompiler`, shared `internal/core/rewindcaps`, `memoryprovider`, and `skillprovider` should stay narrow. Prefer extending the owner that already has the required invariants over creating pass-through packages.

## Core Flows

### Server Delivery To Runtime

```mermaid
sequenceDiagram
    participant S as AgentTeam Server
    participant C as AgentTeam Channel
    participant D as Dispatcher
    participant A as Agent Process
    participant R as Runtime Driver

    S->>C: agent:deliver / agent:start over daemon WebSocket
    C->>C: parse protocol, validate agent token/session metadata
    C->>D: Dispatch normalized delivery or lifecycle request
    D->>D: agent:deliver selects current accepting Process only
    D->>D: agent:start resolves launch config and owns start/replacement
    D->>A: hand off to current Process or start via agent:start
    A->>R: encode runtime-specific input
    R-->>A: parsed runtime events and delivery acknowledgements
    A-->>D: normalized runtime events, wake status, recovery points
    D-->>C: AgentTeam-visible events or outbound messages
    C-->>S: daemon WebSocket status or HTTP send/upload calls
```

### Runtime Outbound Tool Call

```mermaid
sequenceDiagram
    participant R as Runtime Process
    participant T as Outbound Tool Bridge
    participant D as Desktop Owner
    participant S as AgentTeam Server

    R->>T: MCP tools/call or agentteam exec command
    T->>T: authenticate session token or Exec lease
    T->>D: dispatch to agentteam, knowledge, or execution owner
    D->>S: call AgentTeam API when the tool needs server state
    S-->>D: bounded result or authorization failure
    D-->>T: canonical tool result with result budget applied
    T-->>R: runtime-specific tool response
```

### Shared Web Read Through Desktop

```mermaid
flowchart LR
    web[Shared Web application]
    preload[Typed Electron preload]
    runtime[Desktop runtime transport]
    gateway[Desktop gateway]
    server[Active AgentTeam Server]
    daemon[Target daemon via server transport]

    preload -->|base URL and UI session| runtime
    web -->|server API| runtime
    runtime --> gateway
    gateway -->|same path and query| server
    server -->|typed remote endpoint when local data is required| daemon
    daemon --> server
    server --> gateway
    gateway --> runtime
    runtime --> web
```

The same server API contract serves Browser and Desktop. Remote Machine data
that requires daemon-local facts travels through the Server's typed daemon
transport; a browser or Electron renderer must never call another machine's
`/api/internal/*`.

A Machine is the current local machine only when the runtime is Desktop and the active server binding matches both the current workspace and Machine ID. That predicate gates native actions; it does not change the source of shared business data. Machine and Agent pages may compose server-authorized facts, but they must not persist live runtime telemetry or create a second source of truth.

## Ownership Rules

- Daemon process code wires components; it does not own channel, dispatcher, or runtime domain behavior.
- Channels own source protocol details and outbound source calls.
- Dispatcher owns routing, session, launch, recovery, and runtime event fan-out decisions.
- Agent/runtime code owns process execution, driver contracts, runtime-specific transport adapters, and local recovery mechanics.
- Runtime outbound tools are communication-layer bridges with strict session authorization; tool semantics live with the channel/daemon/provider/knowledge owner behind the bridge.
- The root `web/` application owns shared business UI. Electron is a thin
  native host and capability adapter, not a second frontend product.
- Shared pages use one server API contract in Browser and Desktop; only capability-gated native actions may use the local API adapter.
- `/api/internal/*` is Desktop-local and must never be referenced by shared business pages or forwarded upstream.
- Active server and generation are stable logical local-UI-session state, never
  bearer-keyed or process-global state.
- Receipt/control capability and daemon ownership stay in the Go daemon and
  Electron main; the renderer receives only a revocable UI session.
- Preload exposes purpose-specific typed operations. The renderer has no Node
  integration, Electron objects, generic IPC, process access, or filesystem
  access.
- Local API code reads or controls existing owners; it should not become a parallel owner.
- Shared `types` should stay value-oriented and interface-oriented. Do not turn it into a behavior dump.
- Avoid re-export aliases and forwarding packages after package moves. Import the owning package directly unless an interface boundary intentionally hides the owner.

## Packaging And Updates

macOS and Windows package Electron main, preload, and the renderer into
`app.asar`. The pure Go `agentteam[.exe]` sidecar and AgentTeam-managed OpenCode binary
plus model catalog are staged as `extraResources` at
`resources/bin/agentteam[.exe]` and `resources/runtimes/opencode/`.
The application release matrix is macOS arm64/x64 and Windows arm64/x64. Linux
amd64/arm64 releases contain the Go daemon/CLI and managed runtime payload only.

Each published Desktop version manifest lists exactly six user-downloadable
packages: two Linux daemon archives, two macOS DMGs, and two Windows NSIS EXEs.
The Desktop `index.json` indexes versions rather than architectures. The shell
installer and Go updater read Linux assets from the same Desktop version
manifest. Electron `latest-mac.yml` and `latest.yml` live in the Desktop release
namespace as updater metadata and are not additional user installation
packages. The legacy daemon manifest is only a bounded migration fallback for
Linux clients that predate the unified catalog.

Electron's updater replaces the signed native application, renderer, sidecar,
and managed first-party runtime as one versioned unit on macOS and Windows. The
renderer exposes one update action after discovery. Electron main owns the
download, active-agent confirmation, owned-daemon shutdown verification,
application replacement, and restart sequence; the renderer cannot separately
request download or installation, and an update must not replace the App while
an external or unverified daemon still has packaged resources loaded. The
Go updater is reserved for Linux/headless installations. External OpenCode,
Codex, Claude, and Kimi installations remain outside both update mechanisms.

## Source Of Truth

- First-party daemon contract: [AgentTeam Daemon](./daemon.md)
- Desktop module architecture: this document
- Runtime outbound tools: [Runtime Outbound Tools](./runtime-outbound-tools.md)
- Daemon transport/session link: [Daemon Transport](./daemon-transport.md)
- Prompt and delivery semantics: [Prompt Compiler Architecture](./prompt-compiler-architecture.md) and [Delivery Runtime](../message/delivery-runtime.md)
- Runtime/session authority for MaaS: [MaaS Runtime Session Authority](../maas/runtime-session-authority.md)

When code and docs disagree, inspect the current implementation and update the canonical spec in the same change that intentionally changes the stable contract.
