Přeskočit obsah

Proposal 052: Tauri-Hosted Node UI

Based on:

  • doc/project/20-memos/node-ui-htmx-hateoas-architecture.md
  • doc/project/30-stories/story-008-cool-site-comment.md
  • doc/project/40-proposals/006-pod-access-layer-for-thin-clients.md
  • doc/project/40-proposals/026-resource-opinions-and-discussion-surfaces.md
  • doc/project/40-proposals/050-local-readiness-gate.md
  • doc/project/60-solutions/001-node-ui/001-node-ui.md
  • node/DEV-GUIDELINES.md

Status

Draft

Date

2026-04-24

Executive Summary

Orbiplex Node should add an optional desktop shell built with Tauri.

The shell is not a new application runtime and not a replacement for the existing Node UI architecture. It is a thin native host around the current web-based Node UI:

  • Rust host,
  • system webview,
  • host-owned window and panel management,
  • native integrations where useful,
  • strict origin and capability boundaries.

The Node already has the right shape for this:

  • the core application is Rust,
  • Node UI is served locally as server-rendered HTML,
  • the operator console remains browser-first for routine operator work,
  • the user-facing /app surface is the primary desktop window,
  • middleware modules are supervised HTTP services,
  • the browser or desktop webview is only a client of those surfaces.

Electron would introduce Node.js as another central runtime even though Orbiplex already has a Rust daemon and supervised services. Tauri fits the existing strata better: the desktop process remains a small host for a web UI whose semantic authority still lives in the daemon.

The preferred direction is:

  • keep Node UI as HTMX/HATEOAS over server-rendered HTML,
  • host the user-facing /app surface in a Tauri-managed main webview,
  • keep the operator console browser-first unless a specific host-owned surface needs to be opened in desktop mode,
  • avoid putting untrusted web content into trusted Orbiplex webviews,
  • use a separate low-privilege external-content webview or window when a user needs to inspect a public URL,
  • use or continue hardening a custom app protocol for the Orbiplex hypermedia space so the desktop UI can present one coherent origin without exposing daemon control endpoints as ordinary browser-reachable localhost APIs.

Problem

The current browser-based UI works, but it has three frictions:

  1. It looks and behaves like a browser tab even when it is the user's local node window or the operator's local control panel.
  2. Localhost control surfaces are easy to overexpose by accident.
  3. Story-008-style resource opinion flows need a convenient way to show or inspect an external web resource without merging that external page with the operator control origin.

The naive desktop migration would be to embed the existing localhost UI and call it done. That keeps the useful development path, but it does not name the security boundary. If the same webview can navigate from trusted Orbiplex UI to an arbitrary remote page, then that page can try to reach loopback endpoints, trigger browser-mediated requests, probe ports, or exploit weak CORS/CSRF settings.

This proposal separates two questions:

  • how Orbiplex presents its own user and operator hypermedia,
  • how Orbiplex previews or comments on foreign web resources.

Those are different trust domains and should be different webviews or windows.

Goals

  • Provide a native desktop host for Node UI without introducing Node.js as an application runtime.
  • Preserve the HTMX/HATEOAS architecture and server-side rendering model.
  • Keep daemon and middleware semantics outside the desktop shell.
  • Reduce reliance on browser-exposed localhost as the primary UI origin.
  • Provide a safe path for external web preview/commenting flows.
  • Use Tauri capabilities as explicit boundaries between trusted Orbiplex UI surfaces and untrusted or semi-trusted content.
  • Keep the desktop shell optional; browser access remains useful for development and diagnostics.

Non-Goals

  • This proposal does not replace the daemon HTTP API.
  • This proposal does not require rewriting Node UI as a SPA.
  • This proposal does not define a final visual design system.
  • This proposal does not make the Tauri shell a protocol authority.
  • This proposal does not give remote web content access to Tauri commands, daemon authtok, signer material, local files, or operator actions.
  • This proposal does not define mobile thin-client behavior; that belongs to the pod-backed access layer.

Decision

Orbiplex SHOULD add a Tauri desktop host for Node UI.

The first implementation SHOULD use three strata:

Tauri desktop shell
  - window lifecycle
  - split/panel layout
  - native menus, tray, notifications, file dialogs where needed
  - app protocol or controlled localhost bridge

Node UI web server
  - Axum + MiniJinja
  - HTMX fragments
  - HATEOAS navigation
  - daemon proxy
  - browser session and CSRF boundary

Node daemon
  - protocol authority
  - local readiness gate
  - middleware supervision
  - signer and capability registry
  - source of truth for node state

The shell MUST remain a host and coordinator. It MUST NOT implement protocol semantics, sign protocol artifacts directly, hold procurement state, or become a second control-plane authority.

Existing Node Contracts to Reuse

The desktop shell should reuse the current Node implementation contracts instead of inventing parallel discovery, lifecycle, or credential paths.

Existing mechanism Current owner Desktop use
<data_dir>/health/daemon-health.json daemon discover daemon control endpoint indirectly through Node UI, not in the webview
<data_dir>/authtok daemon remains server-side; read by Node UI and launcher/client code only
<data_dir>/node-ui/bind node-ui runtime discover the Node UI listening address
<data_dir>/node-ui/control/direct-spawn.log launcher/node-ui target surface startup failures without scraping daemon state
orbiplex-node-ui-launcher launcher crate start, stop, restart, and status for the UI target
orbiplex-node-launcher launcher crate start, stop, restart, and status for the daemon target
node_ui.start_with_node config behavior node control tooling preserve existing "node brings UI up" operator workflow
Node UI routes node-ui crate remain the canonical HTML/HATEOAS surface for user and operator views
middleware UI package registry node-ui crate continue loading module UI surfaces from middleware-packages

The desktop host should therefore treat data_dir as the root of local coordination. It may accept --data-dir and --profile in the same spirit as the existing CLI, but it should not create a separate desktop-only instance registry.

Discovery Contract

For the first implementation, desktop startup should follow this order:

  1. Resolve data_dir from an explicit argument, a profile, or the existing Node default used by local control tooling.
  2. Query launcher status for the daemon target.
  3. Start the daemon through the launcher if policy says the desktop owns this session.
  4. Query launcher status for the node-ui target.
  5. Start node-ui through orbiplex-node-ui-launcher if it is not running.
  6. Read <data_dir>/node-ui/bind.
  7. Load the user webview from /app through the app protocol bridge or from the discovered Node UI URL during compatibility development.

This keeps the desktop implementation close to today's browser path:

daemon -> writes health/authtok
node-ui -> reads health/authtok, writes node-ui/bind
desktop -> reads node-ui/bind, hosts webview

The browser still never receives X-Orbiplex-Authtok; Node UI remains the server-side daemon client.

Lifecycle Ownership Modes

The desktop host should support two lifecycle modes:

Mode Meaning Use case
attach use already-running daemon and Node UI; fail visibly if absent development, supervised production
supervise-local start/stop daemon and Node UI through launcher contracts packaged local desktop

supervise-local should still call the launcher crate or launcher binaries. The Tauri shell must not embed launchd, systemd --user, Windows service, or direct-spawn semantics inside UI code. Those are lower-level process lifecycle adapters already owned by launcher.

Failure Classes

The host should distinguish failures before rendering:

Failure Detection Desktop/user surface
daemon-launcher-unreachable launcher status/start fails desktop bootstrap error
daemon-control-unreachable Node UI cannot discover/read daemon health Node UI degraded status
node-ui-launcher-unreachable UI launcher operation fails desktop bootstrap error
node-ui-bind-missing no <data_dir>/node-ui/bind after timeout desktop bootstrap error with log pointer
node-ui-http-unreachable bind exists but HTTP connect fails desktop retry/error panel
local-readiness-gate daemon phase reported through Node UI normal Node UI state

This split follows the existing project rule: do not conflate launcher state, control-plane reachability, and protocol/runtime readiness.

Workspace Placement

The implementation should live in the Node workspace as a new edge crate, for example:

node/desktop

or:

node/node-desktop

Recommended ownership:

Crate/module Responsibility
desktop / node-desktop Tauri shell, window policy, app protocol bridge, native integrations
launcher daemon and node-ui lifecycle adapters
node-ui HTML rendering, HTMX routes, middleware UI registry, daemon proxy
control transport-agnostic daemon DTOs used by Node UI and launcher clients
daemon local control API, orchestration, protocol authority

The desktop crate may depend on launcher and small shared DTO crates, but it should avoid depending on daemon internals. If the desktop shell needs a control operation, the preferred path is:

desktop -> launcher/client contract -> daemon or node-ui process
desktop webview -> node-ui HTML route -> daemon control API

not:

desktop -> daemon internal module

This keeps the shell as an adapter and preserves the ability to run the same Node UI in a normal browser.

UI Origin Model

Trusted Orbiplex UI should appear under one app-owned hypermedia origin:

orbiplex://localhost/...

or the equivalent Tauri app URL shape:

tauri://localhost/...

The exact scheme is an implementation decision. The contract is that the the user and operator see one coherent Orbiplex UI space, while the host can map paths under that space to the Node UI server or to embedded static assets.

For example:

Visible UI path Host-owned mapping
orbiplex://localhost/app user-facing Node UI /app shell
orbiplex://localhost/__orbiplex/loading desktop-owned loading bootstrap
orbiplex://localhost/__orbiplex/settings desktop-owned host settings window
orbiplex://localhost/ Node UI index or compatibility redirect
orbiplex://localhost/executions/... Node UI HTMX route
orbiplex://localhost/local-readiness-gate Node UI local readiness view
orbiplex://localhost/modules/{module_id}/... Node UI module extension route
orbiplex://localhost/assets/... bundled or Node UI static asset

This preserves HATEOAS. Links and forms stay inside the Orbiplex hypermedia space. The desktop shell can still proxy or map the request to a localhost server under the hood, but the browser-visible contract is not "call the daemon on a random loopback port".

Bridge Shape

The app protocol bridge should be an HTTP-shaped adapter, not a semantic interpreter.

For each request under the app-owned UI origin, the bridge should preserve:

  • method,
  • path,
  • query string,
  • request body,
  • relevant HTMX headers such as HX-Request, HX-Target, HX-Current-URL,
  • response status,
  • response body,
  • response headers needed by HTMX such as HX-Redirect, HX-Location, HX-Push-Url, HX-Replace-Url, HX-Trigger, HX-Retarget, and HX-Reswap.

For app-protocol desktop mode, the bridge also owns a bounded server-side Node UI cookie cache. It should remember Node UI Set-Cookie values observed during readiness prefetch, ordinary proxied requests, and same-origin redirect hops, then inject the resulting Cookie header into later proxied requests. This is part of the bridge contract rather than a browser-visible credential: custom scheme webviews cannot be assumed to round-trip Node UI SameSite=Strict session and CSRF cookies reliably, while wizard POSTs and operator mutations still need the ordinary Node UI session boundary.

The bridge may rewrite the visible origin, but it should not rewrite Node UI application semantics. In particular, it should not parse operator forms, interpret daemon JSON, or inspect protocol records except for generic security policy checks.

IPC Boundary: Do Not Replace Hypermedia with invoke

Tauri IPC (invoke) should not become the primary routing mechanism for Node UI surfaces.

The user and operator surfaces' main interaction model is still:

link/form/HTMX request -> URL -> HTML fragment or page

not:

button/script -> invoke("operator_action", payload) -> JSON -> client-side render

invoke is a typed RPC channel from frontend JavaScript into Rust commands. It is useful for native host actions, but it is not a hypermedia transport. Using it as the main console API would turn Node UI from a HATEOAS/HTMX projection into a desktop-specific RPC client. That would introduce the same class of problems the project is intentionally avoiding:

  • a second UI state model,
  • a second route/action table beside Node UI routes,
  • loss of ordinary links and forms as visible state transitions,
  • weaker browser fallback and diagnostics,
  • more JavaScript glue around flows that are currently expressed as HTML,
  • harder reuse of middleware UI packages that expect Node UI routing.

The desktop rule is therefore:

orbiplex://... / app URL  = Orbiplex hypermedia space
Tauri custom/app protocol = HTTP-shaped bridge for that hypermedia
Tauri invoke              = side channel for native host capabilities

Trusted webviews may call invoke only for bounded host operations that do not duplicate Node UI or daemon semantics.

Absolute Path Discipline

Current Node UI templates use origin-relative paths such as:

<link rel="stylesheet" href="/static/node-ui.css">
<a href="/status">Status</a>

That is compatible with an app-owned origin as long as the bridge maps the root path to Node UI. The first app-protocol slice should therefore prefer preserving the existing root-mounted route layout rather than introducing a /node-ui prefix that would force broad template churn.

If a prefix becomes necessary later, it should be introduced as one explicit Node UI base-path option and tested against HTMX boosted navigation, fragment loads, and history restoration.

MVP Compatibility Mode

The first implementation MAY load the existing Node UI http://127.0.0.1:{port} directly in the main webview while the custom protocol bridge is being built.

That mode MUST be treated as a compatibility phase, not the final security posture. In that phase:

  • bind Node UI and daemon control ports to loopback only,
  • require explicit operator authentication for any non-loopback Node UI bind,
  • use unpredictable per-run ports where practical,
  • keep daemon authtok server-side in Node UI,
  • reject broad CORS,
  • use same-origin checks and an HTTP-only CSRF cookie on operator mutations,
  • prevent navigation of trusted Orbiplex webviews to remote URLs,
  • open external resources only in a separate low-privilege webview or external system browser.

Webview Partitioning

The desktop host should use separate webviews or windows for separate trust domains.

Webview Content Capability posture
user Orbiplex user /app shell local app capabilities needed for window/UI integration
operator Optional Orbiplex operator/admin UI local app capabilities only when an operator surface is intentionally opened in desktop mode
external-preview arbitrary or allowlisted public URLs no Tauri IPC, no daemon credentials, no local operator capabilities
diagnostics optional local trace/status panels read-only unless explicitly granted

Trusted Orbiplex webviews MUST NOT navigate to arbitrary remote pages. Links to external resources should either:

  • open in external-preview, or
  • open in the system browser, depending on policy and operator preference.

The external-preview webview MUST be considered hostile by default. It should not receive:

  • Tauri command permissions,
  • daemon authtok,
  • Node UI session cookies scoped to any trusted Orbiplex origin,
  • ambient access to local files,
  • broad localhost exceptions,
  • ability to call operator mutation endpoints.

When Story 008 needs to show https://randomseed.io/ while composing an opinion, the composition form remains in a trusted Node UI surface; the remote page is shown in external-preview. The action "comment on this resource" is performed by the trusted UI with an explicit resource reference, not by the remote page.

External Resource Handoff

The external preview should hand resource references to a trusted Node UI surface as data, not as authority.

Minimal handoff shape:

{
  "schema": "orbiplex.desktop.resource-ref-handoff.v1",
  "resource/kind": "url",
  "resource/id": "https://randomseed.io/",
  "source": {
    "webview": "external-preview",
    "observed/title": "optional page title",
    "observed/at": "2026-04-24T00:00:00Z"
  }
}

This handoff is not a protocol artifact. It is a desktop-local UI convenience that pre-fills the existing Node UI resource opinion form. Node UI and daemon still build and sign the real agora-record.v1 / resource-opinion.v1 artifact through the existing Story-008 path.

The handoff should be explicit: a remote page load must not automatically create an opinion draft or trigger a POST.

Security Boundary

Localhost Risk

Loopback is a transport locality, not an authority boundary.

A foreign page loaded in a browser-like renderer can attempt requests to 127.0.0.1, localhost, or private addresses. If Orbiplex exposes privileged operator actions on localhost without a strong browser boundary, then ordinary web behavior can become a confused-deputy path.

The desktop host should therefore reduce the browser-visible localhost surface for privileged UI paths.

Required Invariants

  • The daemon authtok MUST NOT be exposed to browser JavaScript.
  • Non-loopback Node UI binds MUST require explicit operator authentication.
  • Operator mutations MUST require a Node UI session boundary and CSRF defense.
  • Daemon control endpoints MUST reject direct browser access unless the caller has the expected server-side credential.
  • CORS MUST default to deny for daemon and middleware control endpoints.
  • Local services MUST bind to loopback or a stricter local transport unless explicitly configured otherwise.
  • External webviews MUST have no Tauri IPC capability by default.
  • Capabilities MUST be scoped per webview/window label, not globally granted to every renderer.
  • Content Security Policy MUST be strict for trusted Orbiplex UI surfaces.
  • Remote scripts, CDN assets, and untrusted module UI assets MUST NOT be allowed into the trusted Orbiplex origin by default.
  • User-action audit records MUST NOT contain passphrases, tokens, seeds, daemon authtok, private keys, raw client IP values, or raw user-agent values.
  • Optional audit mirroring to Memarium user-action.v1 MUST remain an audit sink only; it MUST NOT authorize, block, or otherwise change UI actions.

The desktop shell should not introduce a new privileged browser credential.

Rules:

  • X-Orbiplex-Authtok remains a server-side credential read by Node UI or launcher/control clients.
  • Agora client tokens remain server-side in Node UI when proxying Agora streams or records.
  • Trusted Orbiplex webviews may use ordinary Node UI session cookies once Node UI has them, but those cookies must be scoped to the trusted origin and not sent to external preview pages.
  • In app-protocol desktop mode, the desktop bridge may keep a host-side cache of those Node UI cookies and replay them only to the discovered Node UI loopback origin for trusted Orbiplex requests. The cache must not be exposed to webview JavaScript, external preview pages, desktop traces, or daemon-facing requests.
  • The external preview must use a separate data store / browsing context where the platform allows it.
  • If the app protocol bridge is used, it should not attach daemon authtok to requests originating from arbitrary webviews; only the Node UI server process should talk to daemon control endpoints.

User-Action Audit and Retention

Node UI owns a local, participant-scoped user-action audit boundary for user-mode actions such as session start, local key unlock, operator binding, messaging setup, and authentication failures. The audit record is a local fact, not an authority mechanism.

Rules:

  • Node UI writes node-ui-user-action-audit.local.v1 JSONL under the node data directory with action kind, participant id, trust tier, result, reason, timestamp, and salted hashes of client IP hints and user-agent values.
  • Passphrases, unlock tokens, CSRF tokens, session tokens, daemon authtok, mnemonic seeds, private keys, and raw client IP/user-agent values must never appear in the JSONL file, SQLite projection, Memarium mirror, operator HTML, or traces.
  • When the daemon Memarium host capability is available, Node UI may mirror the same event best-effort as a Personal-space Memarium user-action.v1 fact. Mirror failure is an audit-sink degradation; it must not fail, permit, deny, or retry the underlying UI action.
  • Node UI compacts the JSONL stream into <data-dir>/node-ui/security-audit.v1.sqlite as a local query projection. Startup and event writes enforce a 90-day retention window, prune older projection rows, and rewrite the JSONL stream from retained rows.
  • Malformed JSONL lines are skipped while building the projection and are not preserved by compaction.
  • Operators can inspect the retained local audit through the read-only /admin/audit/user-actions view with bounded filters for Unix millisecond time range, action kind, trust tier, result, participant id, and limit.

Trusted Orbiplex webviews should allow only:

  • app-owned URLs,
  • Node UI compatibility localhost URL in MVP mode,
  • static assets served by Node UI,
  • explicit file downloads emitted by trusted Node UI routes.

Everything else should be intercepted and routed by policy:

URL class Default action
http://127.0.0.1:{node-ui-port}/... allow only in MVP compatibility mode
orbiplex://localhost/... allow
tauri://localhost/... allow if selected as app URL form
https://... open external preview or system browser
http://... non-loopback open external preview or system browser with warning policy
file://... deny unless triggered by explicit trusted file-open flow
unknown custom scheme deny or ask the OS only through an explicit opener policy

This policy should be tested with ordinary links, HTMX boosted links, redirects, and HX-Redirect / HX-Location responses.

Capability Model

Tauri capabilities should be used as an explicit host boundary:

  • the user webview may receive only the minimal commands needed for the desktop shell,
  • an optional operator webview must receive only the minimal commands needed for the explicit operator desktop surface,
  • external-preview receives no privileged commands,
  • any future remote-content capability must be separately reviewed and allowlisted by URL pattern and operation,
  • multi-webview windows should scope capabilities by webview label rather than by broad window label.

This matches the project rule: authority is a contract at the boundary, not an ambient property of being displayed inside the application.

Appropriate Uses of invoke

Allowed invoke commands should be small native host affordances, for example:

  • open or close external-preview,
  • read the current external-preview URL as an explicit resource-reference handoff,
  • open a system file picker for an import/export flow initiated by Node UI,
  • show an OS notification,
  • update tray/window state,
  • request desktop bootstrap status,
  • ask the launcher to start or stop daemon/Node UI in supervise-local mode.

OS notifications are presentation effects only. Their semantic source should be the node-owned notification model described in Proposal 057 (057-user-and-operator-notifications.md).

invoke commands should not:

  • call daemon control endpoints directly from frontend code,
  • carry X-Orbiplex-Authtok into the webview,
  • sign Agora records or capability passports,
  • implement operator mutation semantics already owned by Node UI routes,
  • render Node UI fragments,
  • load middleware UI package manifests or route module surfaces,
  • become a generic invoke("http", { method, path, body }) tunnel available to unreviewed frontend code.

If an operation has a meaningful URL, form, or HTMX fragment in Node UI, it should stay in the hypermedia path. If it controls the desktop host itself, it belongs in invoke.

Development Model

Tauri supports a development URL for frontend work and a distribution path or URL for production assets. Orbiplex should use that pragmatically:

  • during development, the shell may load the local Node UI server through devUrl or an equivalent configured URL,
  • in packaged builds, the shell should start or discover the local daemon and Node UI process and then load the app-owned UI origin,
  • static shell assets may be bundled; dynamic operator views remain served by Node UI.

No JavaScript bundler is required by this proposal. If the existing Node UI stays HTMX plus vendored static assets, the desktop shell should preserve that simplicity.

Node UI Changes Required for Smooth Desktop Hosting

The desired migration is mostly additive. Node UI should remain browser-usable.

Recommended small changes:

Change Reason
Add an optional --base-url or --public-origin only if app-protocol history requires it avoid hard-coding localhost in generated absolute URLs
Add an optional --desktop-mode flag only for presentation affordances, not semantics allow minor UI chrome adjustments without changing routes
Add a lightweight /desktop/ready or reuse /status for shell smoke checks let desktop detect "HTML surface up" separately from daemon readiness
Keep /static/*, HTMX routes, and fragment routes root-relative minimize template churn
Preserve middleware UI package loading from <data_dir>/middleware-packages do not fork module UI extension model
Add tests for important headers through the bridge HTMX depends on response headers as part of the hypermedia contract

Avoid these changes:

  • moving daemon calls from Node UI into the Tauri frontend JavaScript,
  • exposing authtok through rendered templates,
  • adding a SPA state store to mirror daemon state,
  • duplicating middleware UI registration in the desktop crate,
  • making desktop-only routes the only way to reach operator actions.

App Protocol Migration Plan

The migration from current localhost browser UI to app-owned desktop origin should be staged so each step is reversible.

Stage A: Current Browser Contract

browser -> http://127.0.0.1:7766 -> node-ui -> daemon

No desktop shell is required. This stays supported.

Stage B: Tauri Compatibility Shell

user webview -> http://127.0.0.1:{node-ui-bind}/app -> node-ui -> daemon

New code:

  • desktop crate,
  • launcher integration,
  • navigation policy,
  • external URL interception.

No Node UI route changes should be required.

Stage C: App Protocol Reverse Proxy

user webview -> orbiplex://localhost/app -> desktop bridge
             -> http://127.0.0.1:{node-ui-bind}/app -> node-ui -> daemon

New code:

  • custom/app protocol handler,
  • request/response header preservation,
  • bridge tests for HTMX.

Node UI should still be the only HTML interpreter.

Before Stage C is promoted beyond spike status, the project should verify that HTMX behaves consistently over the selected app URL form on the supported desktop platforms. The spike must cover:

  • hx-get fragment load,
  • hx-post form submit,
  • non-2xx response rendering,
  • HX-Redirect,
  • HX-Location,
  • HX-Push-Url and browser history,
  • boosted links,
  • static assets,
  • file download initiated by a trusted Node UI route.

If a raw orbiplex://localhost/... custom scheme behaves inconsistently across WebView2, WKWebView, or WebKitGTK, the implementation may use Tauri's platform-compatible app URL form such as http://orbiplex.localhost/... or https://orbiplex.localhost/... where appropriate. The architectural requirement is app-owned origin and HATEOAS preservation, not one literal scheme spelling on every platform.

A first local harness for this check lives in:

node/spikes/tauri-htmx-app-protocol/

It keeps orbiplex://... as the HTMX/HATEOAS transport and uses invoke("host_ping") only as a separate native host side channel.

The first macOS manual pass is encouraging: startup, static HTMX loading, boosted navigation, home navigation, hx-get, hx-post, HX-Redirect, HX-Location, and HX-Push-Url work over the custom protocol without Node.js, npm, Vite, or a frontend dev server. Tauri invoke("host_ping") also works as a separate native side channel, confirming the intended split between hypermedia transport and host IPC. The default 422 response path does not swap content, which matches ordinary HTMX default behavior and should be configured explicitly where Node UI wants error fragments to render. The download route rendered inline in WKWebView, so file downloads should be treated as an explicit host-policy/native-integration surface rather than assumed to behave like ordinary browser downloads under a custom protocol.

The current hard-MVP desktop implementation uses the Stage C app-protocol shape. The configured Tauri window label is user, and the initial URL is:

orbiplex://localhost/__orbiplex/loading

The desktop host serves that bootstrap page itself, paints the Orbiplex.AI loading screen before Node UI is reachable, polls __orbiplex/desktop-ready, stores a prepared /app representation, and then performs an iframe handoff to the user /app shell. A native readiness watchdog in the Rust host repeats the readiness poll and signals the bootstrap page if WebView JavaScript timers are throttled while the window is backgrounded; it must not navigate the top-level webview directly to /app.

Stage D: Optional Local Transport Hardening

If supported cleanly by Tauri and the Node workspace, the bridge may later talk to Node UI over a less browser-addressable transport:

user webview -> orbiplex://localhost/app -> desktop bridge
             -> Unix domain socket / named pipe / in-process adapter
             -> node-ui service layer -> daemon

This is an optimization and hardening step, not an MVP dependency. It should not collapse Node UI into the Tauri crate unless the same service layer remains usable by the standalone orbiplex-node-ui binary.

Host Responsibilities

The Tauri host is responsible for:

  • starting or attaching to the local Node daemon profile,
  • starting or attaching to the Node UI server,
  • exposing the main user /app window,
  • keeping the operator console browser-first while allowing optional trusted operator/admin surfaces in desktop mode when explicitly needed,
  • managing external preview windows or panels,
  • enforcing navigation policy,
  • applying CSP and capability configuration,
  • providing native menus, tray integration, notifications, and file dialogs when those are explicitly needed,
  • surfacing daemon unreachable and local readiness gate states early.
  • recording close/quit decisions such as "leave daemon and Node UI running in the background" through host-local control files and exit codes.

The host is not responsible for:

  • protocol validation,
  • procurement decisions,
  • settlement logic,
  • signing Agora records,
  • evaluating middleware semantics,
  • storing canonical protocol state.

Desktop Host Settings Surface

The Tauri desktop host should provide a separate native settings window for host-level configuration and diagnostics. This window is not the operator console and should not inherit the user-facing terminal/BBS visual language. Its default presentation should be conventional desktop UI: plain light background, straightforward controls, and a left-hand tab rail with a main settings pane.

The settings surface exists to make local host state visible and adjustable even when the main user UI is not the right context. It should therefore be opened as a separate Tauri window from the application menu, keyboard shortcut, or a host-owned action such as the gear button in the /app titlebar. The initial shell should be served by the desktop host itself under an app-owned path such as:

orbiplex://localhost/__orbiplex/settings

The shell may then read data through Tauri invoke commands and, when available, through the daemon control plane. It should not require a healthy node-ui process just to display basic host diagnostics, because one of its jobs is to explain why node-ui is unavailable.

The first settings window should use these tabs:

Tab Initial content
profile selected profile, resolved data_dir
state daemon state, node-ui state, node-desktop state, WebView/Tauri diagnostics, component state table for built-ins and packages, bounded configure and dir actions
settings keep-node-running mode, autostart controls
paths log paths and relevant control files
control update, restart, reload, and lifecycle actions
identity local identity and operator-binding context used by the desktop UI

The boundary is important:

  • desktop/window settings, launcher state, app menu behavior, autostart, local paths, and WebView diagnostics belong here;
  • protocol, middleware, readiness policy, relationship, capability, passport, and audit state remain server-rendered Node UI/operator surfaces;
  • actions that mutate daemon or protocol state must still go through the daemon control plane or existing Node UI service layer, not through desktop-only state;
  • Tauri invoke is appropriate for host-local facts and actions, but it must not become a parallel router for Node UI operator workflows.
  • component configure actions should open the operator console in the system browser; component dir actions may open only existing configuration directories that canonicalize under the active data_dir.

The MVP version can be read-mostly: profile, state, paths, and diagnostics are safe first. Write paths such as autostart changes, restart/reload, update flow, and identity context switching should be added only after each command has a clear host contract, confirmation behavior, failure reporting, and trace event.

Desktop Host Sealed Secret Unlock Surface

The Tauri desktop host provides a small native askpass-style unlock surface for sealed local secrets needed by the user-mode UI. This is a host UI mechanism, not a new cryptographic primitive. The daemon and signer/sealer layers remain the authority for passphrase verification, rate limiting, unlock TTL, audit, and in-memory secret cache behavior.

The unlock prompt is a separate centered Tauri window, visually simple and independent from the terminal/BBS user shell:

Unlock key

<key-ref or secret-ref>

Tries: 0/5

[ passphrase input ]

The passphrase field accepts by pressing Enter. The window submits the passphrase to the appropriate host endpoint, receives only a success/failure response, updates the attempt counter, and closes only after successful unlock or explicit cancellation. The passphrase must not be persisted, logged, echoed into traces, stored in browser-accessible state, or sent to Node UI JavaScript.

The host should select the endpoint by locked-secret kind:

Secret kind Endpoint Request shape
Participant signing key used by identity flows POST /v1/host/identity/session/unlock { "participant_id": "...", "passphrase": "..." }
Generic signer key ref POST /v1/host/capabilities/signer.unlock { "key_ref": ..., "passphrase": "...", "ttl_seconds": ?, "scope": "session/per-caller/single-use" }
Sealer master key, passphrase-only envelope POST /v1/host/capabilities/sealer.unlock { "version_id": "...", "passphrase": "...", "ttl_seconds": ?, "scope": "session/per-caller/single-use" }

The sealer endpoint also supports a step_up_secret field for step-up protected master envelopes. The first desktop askpass slice intentionally keeps the descriptor passphrase-only; a step-up sealer prompt should be added as a typed extension rather than by allowing arbitrary request bodies from webview JavaScript.

The signer backend already exposes an in-memory unlock cache with default TTL and maximum TTL clamping. A successful signer unlock returns an unlock token; for session-scoped unlocks, later signer operations may use the host-held session default rather than exposing the token to browser JavaScript. The sealer backend likewise owns passphrase verification, rate limiting, and unlock cache behavior for sealer master versions. The desktop prompt should therefore be a thin requester and status presenter only.

Prompt invocation should be driven by stable locked responses:

  • signer operations returning HTTP 423 with status = "key_locked" and hint = "POST /v1/host/capabilities/signer.unlock";
  • participant identity flows returning HTTP 423 with status = "participant_key_locked" or status = "key_locked" and hint = "POST /v1/host/identity/session/unlock";
  • sealer operations returning locked or rate-limited key-source errors from the sealer capability layer.

The desktop host exposes the bounded Tauri command desktop_open_unlock_secret(request) to open this window. That command accepts only a typed locked-secret descriptor produced by trusted Node UI/daemon responses. It does not accept arbitrary URLs, arbitrary endpoint paths, or raw request bodies from page JavaScript. The command stores the descriptor in host-owned state and opens an orbiplex://localhost/__orbiplex/unlock/<prompt-id> window. The password form submits directly to the desktop app-protocol /__orbiplex/unlock/<prompt-id>/submit route, so Node UI JavaScript never reads or forwards the passphrase. The host constructs the daemon request itself, sends it through the trusted local control path with the daemon authtok kept inside the Tauri process, and returns only unlock status to the prompt window. Daemon unlock response bodies, including signer unlock tokens, are not serialized back to browser JavaScript.

Unlock UX requirements:

  • show the stable key identifier or version id being unlocked;
  • show Tries: N/5 locally for the current prompt session, while still respecting daemon-provided unlock_rate_limited / retry_after_secs;
  • submit on Enter;
  • return focus to the requesting window on success or cancellation;
  • allow cancellation without retrying the original operation;
  • avoid opening multiple unlock prompts for the same key at once;
  • never expose passphrase, unlock token, daemon authtok, or sealer material to webview JavaScript.

Trace and Diagnostics

The desktop host should produce local operational traces, but not protocol facts.

Candidate trace events:

Event Meaning
desktop/daemon-launcher-status launcher status was queried
desktop/daemon-start-requested shell requested daemon start
desktop/node-ui-start-requested shell requested Node UI start
desktop/node-ui-bind-observed bind file was read successfully
desktop/trusted-navigation-blocked trusted Orbiplex webview attempted disallowed navigation
desktop/external-preview-opened external URL opened in preview
desktop/resource-ref-handoff operator accepted a preview URL as a resource reference
desktop/app-protocol-proxy-error app URL bridge failed to reach Node UI
desktop/loading-watchdog-ready native readiness watchdog observed Node UI readiness
desktop/close-decision-recorded close/quit prompt wrote the keep-runtime decision
desktop/secret-unlock-prompt-opened native askpass prompt opened for a locked secret, with key ref redacted or hashed
desktop/secret-unlock-result native askpass prompt finished with success, cancellation, failure, or rate-limit, without passphrase or token data

These traces should live in a desktop/runtime log under the instance data_dir, for example:

<data_dir>/desktop/control/desktop.log

They should be exportable and diagnostically useful, but they must not leak daemon authtok, Agora client tokens, passphrases, seed material, request bodies from sensitive forms, or remote page contents.

Desktop Flows

Open User Node UI

  1. User starts Orbiplex Desktop.
  2. Tauri host starts or discovers the daemon.
  3. Tauri host starts or discovers Node UI.
  4. The main user webview loads the app-owned loading URL.
  5. The desktop bootstrap waits for Node UI readiness and transitions into /app.
  6. Node UI renders the user shell or setup wizard according to local identity and operator-binding state.

Comment on an External Resource

  1. Operator opens or pastes an external URL.
  2. Tauri host loads that URL in external-preview or the system browser.
  3. Operator invokes "New opinion" from the trusted Node UI surface.
  4. Node UI receives the resource reference as data.
  5. Node UI follows the existing Agora/resource-opinion flow.
  6. External content never receives authority to submit the opinion itself.

Local Readiness Gate

  1. Daemon starts in local_readiness_gate.
  2. Tauri host still opens the trusted Node UI surface.
  3. Node UI renders blockers and safe actions.
  4. Operator resolves or rejects blockers.
  5. Daemon restarts or reloads through an explicit control operation.

This is one of the strongest reasons to keep the desktop host thin: even when the full runtime is blocked, the local control surface remains small and understandable.

Proposed Implementation Slices

Slice 1: Desktop Host MVP

  • Add a node-desktop or orbiplex-desktop Tauri crate/binary in the Node workspace.
  • Accept --data-dir, optional --profile, and lifecycle mode attach|supervise-local.
  • Reuse launcher contracts to start or discover daemon and Node UI.
  • Wait for <data_dir>/node-ui/bind and load the user-facing /app shell.
  • Serve a desktop-owned loading bootstrap before Node UI is ready.
  • Ask on close/quit whether daemon and Node UI should keep running in the background, recording the decision for launcher/control tooling.
  • Deny navigation of trusted Orbiplex webviews to remote URLs.
  • Open remote links in the system browser or a separate preview window.
  • Keep all daemon credentials server-side in Node UI.
  • Add smoke tests for bind discovery and URL policy as pure Rust where possible.

Slice 2: External Preview Panel

  • Add an external-preview webview/window.
  • Give it no Tauri IPC capabilities.
  • Add explicit "use current URL as resource reference" bridge through the trusted host, not through remote page JavaScript.
  • Clear preview browsing data on request and optionally per session.
  • Pre-fill the existing Node UI Agora/resource opinion route with the selected resource/kind=url and resource/id=<url> rather than adding a desktop-only publication path.

Slice 3: App Protocol Bridge

  • Serve trusted Node UI under an app-owned scheme such as orbiplex://localhost/.
  • Map app URL paths to Node UI routes or bundled assets.
  • Keep HATEOAS links inside the app-owned origin.
  • Tighten CORS and direct localhost exposure further.
  • Preserve HTMX request and response headers end to end.
  • Test boosted navigation, fragment swaps, form posts, redirects, and downloads.
  • Confirm whether the selected app URL form is raw orbiplex://localhost/... or a Tauri/platform-specific localhost-shaped app origin.
  • Keep /__orbiplex/loading and /__orbiplex/settings as host-owned paths, not Node UI routes.

Slice 4: Native Integrations

  • Add tray/status indication.
  • Add OS notifications for readiness blockers and completed long-running tasks.
  • Add file picker integration only for explicit import/export flows.
  • Add deep-link handling only after the resource-reference security model is stable.

The readiness/task notification semantics should be consumed from Proposal 057 rather than invented inside the desktop shell.

Slice 5: Desktop Host Settings Window

  • Add a separate Tauri settings window with a conventional system-like visual style, left tab rail, and main content pane.
  • Serve the base shell from node-desktop under an app-owned host path such as /__orbiplex/settings.
  • Implement read-only profile, state, and paths tabs first.
  • Use Tauri invoke for host-local diagnostics and control files.
  • Use daemon control-plane reads for daemon/component state when available.
  • Keep Node UI operator/domain settings out of this window unless they are rendered through the existing server-side UI boundary.
  • Add confirmation, trace, and failure contracts before enabling write actions in settings, control, or identity.

Slice 6: Node-Side Ledger and Docs Alignment

When implementation begins in node, update:

  • node/docs/implementation-ledger.toml,
  • regenerated node/docs/IMPLEMENTATION-LEDGER.md,
  • node/README.md operational commands,
  • node/DEV-GUIDELINES.md only if a new reusable guideline emerges.

The ledger row should describe the desktop shell as an edge/client capability, not as a new protocol layer.

Open Questions

  • Should the first app-owned scheme be orbiplex://localhost or should the host use Tauri's default app URL form until external protocol registration is needed?
  • Does raw orbiplex://localhost/... preserve all required HTMX behavior on WebView2, WKWebView, and WebKitGTK, or should MVP use a localhost-shaped app origin while keeping the same semantic namespace?
  • Should external preview be an in-app split panel by default, or should the safer default be the system browser with in-app preview as an opt-in?
  • Should the app protocol bridge proxy to Node UI over HTTP, Unix domain socket, or an in-process service adapter?
  • Which native integrations are essential for MVP and which are merely desktop polish?
  • Should the packaged desktop host own daemon supervision, or should it delegate to the existing launcher in the first slice?
  • Should node-ui grow a shared library entry point for an eventual in-process adapter, or should the standalone HTTP process remain the only supported rendering boundary until there is measured need?
  • Should desktop traces live under <data_dir>/desktop/control/ or reuse the launcher control log layout more directly?

Acceptance Criteria

# Criterion Verification
1 User /app shell runs in a Tauri main webview without changing Node UI into a SPA. Manual desktop smoke test; HTMX flows still work.
2 Daemon authtok is never visible to browser JavaScript. Code review; browser devtools/session inspection.
3 Remote URLs cannot load inside trusted Orbiplex webviews. Navigation policy test with https://example.org/.
4 External preview receives no privileged Tauri capabilities. Tauri capability configuration review.
5 Operator mutations remain protected by Node UI session, same-origin checks, and CSRF boundary. Mutations missing same-origin evidence or a current CSRF cookie fail closed.
6 Story-008 resource opinion composition can use an external URL without giving the remote page authority. End-to-end resource opinion flow.
7 Local readiness gate remains visible in desktop mode. Start daemon with blockers and inspect desktop UI.
8 Browser access to Node UI remains available for development/diagnostics. Existing local browser workflow still works.
9 Desktop startup reuses <data_dir>/node-ui/bind rather than hard-coding the UI port. Start Node UI on a non-default port and open desktop.
10 Desktop lifecycle operations delegate to launcher contracts. Code review; launcher contract tests remain the lifecycle authority.
11 App protocol bridge preserves HTMX headers, status codes, and Node UI session/CSRF cookie continuity across prepared navigation, live form POSTs, and same-origin redirects. Bridge tests for boosted navigation, form POST, fragment swap, redirect, and cookie replay.
12 Middleware UI package surfaces still render through Node UI in desktop mode. Install a package manifest under middleware-packages and inspect nav/routes.
13 External URL handoff pre-fills an existing Node UI resource opinion flow without a desktop-only publish path. Story-008 smoke test from preview to signed opinion.
14 Tauri invoke is limited to native host affordances and does not duplicate Node UI operator routes. Code review; route/action inventory.
15 Selected app URL form supports HTMX over all target desktop webviews. Cross-platform spike covering hx-get, hx-post, HX-* headers, history, static assets, and downloads.
16 Desktop settings opens as a separate Tauri window and shows host-local profile, state, paths, and diagnostics without requiring healthy Node UI. Manual desktop smoke test; kill Node UI and verify settings still renders host diagnostics.
17 Desktop loading starts at orbiplex://localhost/__orbiplex/loading, paints Orbiplex.AI, and hands off to /app without exposing a blank window. Manual startup smoke; trace review with ORBIPLEX_NODE_DESKTOP_TRACE=1.
18 Closing or quitting the desktop asks whether daemon and Node UI should remain running and records the decision for control tooling. Manual close/quit smoke; inspect close-decision marker and controller cleanup behavior.
19 Backgrounded startup does not wait for focus before reaching /app. Start desktop in the background and verify native readiness watchdog signals the bootstrap handoff.
20 Locked participant/signer/sealer secrets can trigger a native askpass-style unlock prompt without exposing passphrases or unlock tokens to webview JavaScript. Unit-test endpoint selection and redaction; manual smoke with a locked participant key and signer.unlock.
21 User-mode actions are locally auditable without leaking secrets, queryable through /admin/audit/user-actions, bounded by 90-day retention, and optionally mirrorable to Memarium user-action.v1. Unit tests for audit redaction, SQLite retention, query filters, and fail-open Memarium sink behavior; operator route smoke.

Tracking

ID Feature Status Evidence
P052-001 Node desktop edge crate done node/node-desktop exists as a Tauri v2 crate in the Node workspace and is wired into the control tooling as node-desktop start|stop|status|restart.
P052-002 User /app as the desktop main window done node-desktop/tauri.conf.json defines the main window label as user; the desktop README states that the user-facing /app shell is the default and the operator console remains browser-first.
P052-003 App-protocol bridge done orbiplex://localhost/... is registered by node-desktop; the bridge proxies HTTP-shaped requests to the discovered node-ui bind address while preserving relevant HTMX request and response headers. The desktop host also keeps a server-side Node UI cookie cache, records Set-Cookie from prefetch/proxy/redirect responses, and replays participant session and CSRF cookies into trusted proxied requests so app-protocol wizard POSTs keep the same Node UI session boundary as browser access.
P052-004 Loading bootstrap and prepared navigation done node-desktop serves /__orbiplex/loading, polls /__orbiplex/desktop-ready, stores the prepared /app representation, and hands off to the user shell through an iframe path.
P052-005 Native readiness watchdog partial The Rust host now runs a native readiness poll and signals the bootstrap document when Node UI is ready, reducing dependence on foreground WebView timers. Remaining work: repeat the background-window smoke on each supported desktop webview.
P052-006 Desktop close/quit decision done Window close and application quit show a keep-runtime prompt, write a close-decision marker, and use the keep-runtime exit code path consumed by the control tooling.
P052-007 External preview isolation planned The proposal keeps the separate external-preview trust domain, but the current hard-MVP desktop shell does not yet implement a dedicated external preview window.
P052-008 Desktop Host Settings Surface partial node-desktop now opens a separate system-style settings window under /__orbiplex/settings from the application menu, Cmd+,, and a host-only gear button in the user shell. The first slice is read-only and host-owned: profile/data-dir, daemon/node-ui/node-desktop state, WebView diagnostics, component rows, bounded configure/dir component actions, paths, and placeholder control/identity tabs. Write actions remain planned.
P052-009 Native integrations deferred Tray/status, OS notifications, file picker integration, deep links, and update flow remain post-MVP until their host contracts are explicit.
P052-010 Local transport hardening deferred The current bridge proxies to the Node UI HTTP bind address. Unix domain socket, named pipe, or in-process service adapter remain optional hardening steps.
P052-011 Native sealed-secret unlock prompt done node-desktop now exposes desktop_open_unlock_secret and desktop_cancel_unlock_secret, serves the askpass window under /__orbiplex/unlock/<prompt-id>, accepts password form submissions through the desktop app-protocol /__orbiplex/unlock/<prompt-id>/submit route, maps participant/signer/sealer descriptors to the fixed daemon endpoints, keeps daemon authtok and unlock response bodies inside the Tauri process, and unit-tests endpoint selection plus unlock-token redaction. The user shell exposes a thin window.OrbiplexDesktop.openUnlockPrompt(...) bridge for trusted fragments.
P052-012 User-action audit view and retention done Node UI writes local redacted node-ui-user-action-audit.local.v1 events, compacts them into security-audit.v1.sqlite with 90-day retention, exposes /admin/audit/user-actions for operator inspection, and can mirror best-effort Personal-space Memarium user-action.v1 facts without changing UI action authority.

References