nanobot/desktop/docs/host-contract.md
Xubin Ren ab9f49970d
feat(desktop): polish desktop shell and shared WebUI surfaces (#4195)
* feat(desktop): add native host scaffold

* feat(webui): track turns and usage in gateway

* feat(webui): polish desktop chat experience

* feat(apps): add ArcGIS and Joplin logos

* feat(desktop): polish shell and shared surfaces

* fix(webui): avoid preview chips for glob references

* test: align CI expectations for token fallback

* feat(webui): preview prompt rail entries

* feat(webui): add prompt navigator drawer

* style(webui): refine prompt navigator placement

* style(webui): align prompt navigator with header actions

* style(webui): simplify prompt navigator header

* refactor(webui): clean thread resource refresh

* feat(desktop): add native reply notifications

* fix(webui): preserve desktop restart and replay state

* fix(desktop): harden gateway proxy startup

* fix(web): fall back when readability is unavailable

* fix(desktop): hide window instead of closing on macos

* fix(webui): unify desktop header actions

* fix(webui): simplify prompt history rows

* fix(desktop): log notification delivery failures

* chore(desktop): clean source package artifacts

* fix(cron): support one-time relative reminders

* fix(webui): reveal scroll button in place

* Revert "fix(cron): support one-time relative reminders"

This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b.

* refactor(webui): extract token usage heatmap

* docs(desktop): clarify contributor guides

---------

Co-authored-by: chengyongru <2755839590@qq.com>
2026-06-06 19:49:33 +08:00

3.4 KiB

Native Host Contract

This is a contributor reference for the boundary between the shared WebUI and the native desktop host. Users should not need this contract to run the app, but it explains why the desktop app can use native capabilities without turning the WebUI into Electron-specific code.

desktop is a native host shell around the shared WebUI build. The renderer must not import Electron directly. It receives a minimal bridge at window.nanobotHost.

Runtime API

type HostRuntimeInfo = {
  surface: "native";
  app_version: string;
  engine_status: "starting" | "ready" | "restarting" | "stopped" | "crashed";
  data_dir: string;
  logs_dir: string;
  config_path: string;
  workspace_path: string;
  python: string;
  engine_transport?: "unix_socket";
};

type HostSocketEvent =
  | { id: string; type: "open" }
  | { id: string; type: "message"; data: string }
  | { id: string; type: "error"; message: string }
  | { id: string; type: "close"; code?: number; reason?: string };

type NanobotHost = {
  getRuntimeInfo(): Promise<HostRuntimeInfo>;
  restartEngine(): Promise<void>;
  pickFolder(): Promise<string | null>;
  openLogs(): Promise<void>;
  exportDiagnostics(): Promise<string>;
  checkForUpdates(): Promise<{ supported: boolean; message?: string }>;
  openSocket(url: string): Promise<string>;
  sendSocket(id: string, data: string): Promise<void>;
  closeSocket(id: string): Promise<void>;
  onSocketEvent(listener: (event: HostSocketEvent) => void): () => void;
  onRuntimeStatus(listener: (status: HostRuntimeInfo["engine_status"]) => void): () => void;
};

First Run

The desktop host starts the private engine immediately. If the native data directory has no config.json, nanobot desktop-gateway creates one with defaults before serving the shared WebUI. Provider, model, credential, and login setup stay in WebUI settings instead of Electron-owned HTML.

Socket Bridge

The engine listens on a per-user Unix socket under the app data directory. /webui/bootstrap returns runtime_surface: "native" and a WebSocket URL in the nanobot-host://engine/... scheme. WebUI never opens that URL directly in the browser runtime; it hands the URL to window.nanobotHost.openSocket.

The native host then performs the WebSocket handshake against the Unix socket and forwards events over Electron IPC.

Host Security Boundary

The host bridge is intentionally narrower than a general Electron preload:

  • IPC calls are accepted only from renderer frames loaded from nanobot-app://app/....
  • openSocket accepts only nanobot-host://engine/... URLs.
  • External navigation is denied in the app window; safe web links are opened by the operating system.
  • Native WebUI responses carry a restrictive Content Security Policy and X-Content-Type-Options: nosniff.
  • The renderer runs with nodeIntegration: false, contextIsolation: true, sandbox: true, and webSecurity: true.

Security-sensitive tool behavior still belongs in nanobot core. The host protects the native app boundary; the engine protects file, network, and tool permissions.

Data Directory

The host stores config, workspace, sessions, logs, and transient socket files under Electron's platform app data directory. In development on macOS this is usually:

~/Library/Application Support/@nanobot/desktop/

Packaged builds use the packaged app name.

The app bundle is replaceable. User data is not stored in the bundle.