Proposal 052: Tauri-Hosted Node UI¶
Based on:
doc/project/20-memos/node-ui-htmx-hateoas-architecture.mddoc/project/30-stories/story-008-cool-site-comment.mddoc/project/40-proposals/006-pod-access-layer-for-thin-clients.mddoc/project/40-proposals/026-resource-opinions-and-discussion-surfaces.mddoc/project/40-proposals/050-local-readiness-gate.mddoc/project/60-solutions/001-node-ui/001-node-ui.mdnode/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
/appsurface 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
/appsurface 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:
- 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.
- Localhost control surfaces are easy to overexpose by accident.
- 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:
- Resolve
data_dirfrom an explicit argument, a profile, or the existing Node default used by local control tooling. - Query launcher status for the daemon target.
- Start the daemon through the launcher if policy says the desktop owns this session.
- Query launcher status for the
node-uitarget. - Start
node-uithroughorbiplex-node-ui-launcherif it is not running. - Read
<data_dir>/node-ui/bind. - Load the user webview from
/appthrough 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, andHX-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
localhostexceptions, - 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.v1MUST remain an audit sink only; it MUST NOT authorize, block, or otherwise change UI actions.
Credential and Cookie Rules¶
The desktop shell should not introduce a new privileged browser credential.
Rules:
X-Orbiplex-Authtokremains 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.v1JSONL 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.v1fact. 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.sqliteas 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-actionsview with bounded filters for Unix millisecond time range, action kind, trust tier, result, participant id, and limit.
Navigation Policy¶
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
userwebview may receive only the minimal commands needed for the desktop shell, - an optional
operatorwebview must receive only the minimal commands needed for the explicit operator desktop surface, external-previewreceives 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-previewURL 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-localmode.
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-Authtokinto 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
devUrlor 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-getfragment load,hx-postform submit,- non-2xx response rendering,
HX-Redirect,HX-Location,HX-Push-Urland 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
/appwindow, - 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
invokeis appropriate for host-local facts and actions, but it must not become a parallel router for Node UI operator workflows. - component
configureactions should open the operator console in the system browser; componentdiractions may open only existing configuration directories that canonicalize under the activedata_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
423withstatus = "key_locked"andhint = "POST /v1/host/capabilities/signer.unlock"; - participant identity flows returning HTTP
423withstatus = "participant_key_locked"orstatus = "key_locked"andhint = "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/5locally for the current prompt session, while still respecting daemon-providedunlock_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¶
- User starts Orbiplex Desktop.
- Tauri host starts or discovers the daemon.
- Tauri host starts or discovers Node UI.
- The main
userwebview loads the app-owned loading URL. - The desktop bootstrap waits for Node UI readiness and transitions into
/app. - Node UI renders the user shell or setup wizard according to local identity and operator-binding state.
Comment on an External Resource¶
- Operator opens or pastes an external URL.
- Tauri host loads that URL in
external-previewor the system browser. - Operator invokes "New opinion" from the trusted Node UI surface.
- Node UI receives the resource reference as data.
- Node UI follows the existing Agora/resource-opinion flow.
- External content never receives authority to submit the opinion itself.
Local Readiness Gate¶
- Daemon starts in
local_readiness_gate. - Tauri host still opens the trusted Node UI surface.
- Node UI renders blockers and safe actions.
- Operator resolves or rejects blockers.
- 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-desktopororbiplex-desktopTauri crate/binary in the Node workspace. - Accept
--data-dir, optional--profile, and lifecycle modeattach|supervise-local. - Reuse launcher contracts to start or discover daemon and Node UI.
- Wait for
<data_dir>/node-ui/bindand load the user-facing/appshell. - 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-previewwebview/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=urlandresource/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/loadingand/__orbiplex/settingsas 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-desktopunder an app-owned host path such as/__orbiplex/settings. - Implement read-only
profile,state, andpathstabs first. - Use Tauri
invokefor 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, oridentity.
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.mdoperational commands,node/DEV-GUIDELINES.mdonly 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://localhostor 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-uigrow 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¶
- Tauri v2 configuration:
devUrl,frontendDist, and app/custom URL shapes: https://v2.tauri.app/reference/config/ - Tauri v2 frontend-to-Rust commands and
invoke: https://v2.tauri.app/develop/calling-rust/ - Tauri v2 frontend model: Tauri acts as a static web host for web assets: https://v2.tauri.app/start/frontend/
- Tauri v2 capability ACL: permissions can be scoped to windows and webviews: https://v2.tauri.app/reference/acl/capability/
- Tauri runtime authority for command/capability checks: https://v2.tauri.app/security/runtime-authority/
- Tauri v2 CSP guidance: https://v2.tauri.app/security/csp/
- Tauri v2 webview versions: WebView2 on Windows, WebKit/WKWebView family on Apple platforms, WebKitGTK on Linux: https://v2.tauri.app/reference/webview-versions/