ADR-0014: unisona.ai desktop — a thin signed launcher over the one Core, not an Electron repackage
Status
Accepted — approved by Alex Place (2026-07-02).
Context
We want a non-developer to run unisona.ai locally by double-clicking something, instead of git clone + .env + make quickstart. Today unisona.ai is a brand token hardcoded across ~49 public/*.html titles and a domain that fronts the local Core; it is not a separate program. The app is already a plain Node HTTP server (apps/lantern-garage/server.js) that binds 127.0.0.1:4177, serves static HTML, streams LLM replies over SSE, and reads/writes JSONL under data/.
This forces one honest question against the North Star (CONVERGANCE-SIGMA0-BRIEFING.md): a desktop shell improves no loop stage — it is a delivery channel, so the Feature Gate ("name the loop stage you improve, or don't add it") does not by itself justify it. It earns its place on a different basis: it is the first real delivery of foundational principle [12], local-first ownership (user owns memory, keys, model choice, machine). And the work it forces — relocating writable state, storing keys in the OS vault, fixing the loopback privilege model — strengthens the Core's Remember (durable per-user memory location) and Act (secure credential handling) stages for every deployment, not just the desktop one.
A three-seat Σ₀ council (codebase cartographer, external-packaging researcher, adversarial skeptic) reviewed this. The decisive facts:
- The UI is already a browser talking to a localhost server, so we do not
need a framework that renders UI — the user's browser already does.
- The Core depends on native modules (
sharp,tesseract.js), which an
Electron bundle would force us to rebuild against Electron's V8 ABI — for a ~150 MB Chromium payload that adds nothing here.
- The proposal's real cost is not "wrap it in an exe"; it is the boring 80% that
breaks on a virgin machine (no repo-root .env, no Windows-env keys, no data/ tree, a hardcoded port, and a loopback-equals-admin model that is safe behind a server front door but dangerous on an end-user's box).
Loop stage this touches: primarily delivery of principle [12]; the required hardening touches Remember (memory persistence location) and Act (secure key handling).
Decision
We will ship unisona.ai desktop as a thin, signed launcher over the one, unmodified Convergence Core, staged, under fixed guardrails. It is a build artifact of this monorepo, never a fork.
Packaging choice: a native launcher that boots the existing server.js and opens the user's default browser — not Electron, not Tauri (for now), not a pkg bundle. Electron is rejected (native-module ABI friction + Chromium bloat for a UI the browser already renders). A branded Tauri/WebView2 window is a possible Phase 2, only if a first-class window is later judged worth the Rust toolchain + sidecar cost.
Phasing:
- Phase— the launcher (this ADR's implementation).
apps/lantern-garage/desktop/launcher.js: dependency-free (Node builtins only); picks a free loopback port; spawns server.js in clean chat-only mode; waits for readiness; opens the default browser; tears down the child-process tree on exit. Runs today via node against a checkout.
- Phase
.exe— Core hardening (must land before any public.exe). Relocate
writable state to %APPDATA%\unisona\; first-run key onboarding storing secrets in the Windows Credential Manager / DPAPI (safeStorage); require an explicit local token so loopback is no longer implicitly admin. These are Core changes that benefit all deployments.
- Phase 1-package — the signed
.exe. Ship a plain Node runtime + app
directory + a Node-SEA-compiled unisona.exe; sign + distribute via the $0 channels in the 2026-07-04 Update below (MSIX-via-Store primary + SignPath Foundation for direct download — not Azure); wrap in an installer targeting %LOCALAPPDATA%\unisona.
Guardrails (binding conditions):
- G1 — One Core. The launcher boots unmodified
server.js. No forked
server. If it needs Core changes, they land in the shared Core for everyone.
- G2 — Relocate state to a per-user OS app-data dir before shipping (Phase 0).
- G3 — Keys in the OS vault, user-supplied; never plaintext, never embedded
in the binary (Phase 0).
- G4 — Fix the loopback privilege model before shipping (Phase 0).
- G5 — No Electron. Reuse the user's browser; if a window is ever wanted, use
the OS WebView (Tauri), justified in a new ADR.
- G6 — Signed or no auto-update. Authenticode-signed binaries + signed update
manifests, or ship without self-update. No unsigned self-update channel.
- G7 — Bundle runtime only. Exclude git hooks, worktrees,
pr-watcher,
auto-deploy, dual-boot scripts, and the optional Python src/ children.
- G8 — Name it honestly. "unisona" here is the brand/skin of the one Core;
it must not silently become a second product, and must not be confused with the separate "unisona local model" (8 GB coder) plan.
Consequences
- Positive: delivers principle [12] to non-developers for the first time;
avoids the Electron footgun (size + native-module rebuilds); the launcher is ~200 lines of zero-dependency Node; the forced Phase-0 hardening (AppData relocation, key vault, loopback auth) improves the Core's security and portability regardless of the desktop app; a free-port + loopback-only boot removes thecollision and public-bind foot-guns.
- Negative / trade-offs: Phaseis only meaningful on a checkout until
Phase.envlands (the Core still uses repo-relative paths and repo-root .env); the signed .exe needs a paid signing subscription and a reputation-building period on SmartScreen; a browser tab is less "app-like" than a dedicated window (accepted for now; Phaseserver.js:linerevisits); the launcher must track server.js env gates (documented inline with server.js:line references so drift is visible).
- Follow-ups:
- Phasehardening issues: AppData state relocation; Credential-Manager key
onboarding UI; loopback local-token auth.
- Phase 1-package: Node SEA build (✅ done, #1992) + signing/distribution via the
2026-07-04 Update channels (MSIX-Store + SignPath) + installer.
- Optional tray icon (Stop / Restart / Open) — pure launcher polish.
- Decide Phase(Tauri window) yes/no once Phaseis in real use.
Alternatives considered
- Electron + electron-builder — rejected: bundles ~150 MB of Chromium the
user's browser already provides, and forces rebuilding sharp/tesseract.js against Electron's ABI. Its mature updater doesn't outweigh that here.
- Tauri + Node sidecar — deferred to a possible Phase 2: the small-binary
advantage largely evaporates once a full Node runtime is shipped as a sidecar, and it adds a Rust toolchain + WebView2 dependency. Reconsider only if a first-class window becomes worth it.
pkgsingle binary — rejected:vercel/pkgis deprecated, and.node
native addons (sharp/tesseract) are the exact case single-file bundlers handle worst. Node SEA (ship node + app dir) is the chosen packaging path instead.
- "Just repackage the whole app as unisona.ai" — rejected: that framing is
how a domain quietly becomes a second codebase (forbidden "independent ecosystem" / sprawl). The exe must be a build artifact of the one Core (G1).
- Do nothing (keep clone + make quickstart) — rejected: that is a developer
ritual, not a product; it never delivers principle [12] to real users.
Evidence
| Claim | Evidence (file:line / commit / PR) | Confidence | Source |
|---|---|---|---|
Core is a plain Node server binding 127.0.0.1:4177, port via LANTERN_GARAGE_PORT/PORT |
server.js:78-79 |
High | repo |
Setting PORT flips the bind to 0.0.0.0 (public) — launcher must avoid it |
server.js:79 |
High | repo |
Clean chat-only mode gates: MCP LANTERN_MCP_SERVER=false, OAuth LANTERN_MCP_OAUTH=false |
server.js:426, :435 |
High | repo |
Trading off via LANTERN_DISABLE_TRADING=1; tunnel off via LANTERN_CLOUDFLARE_TUNNEL=false |
server.js:440, :486 |
High | repo |
Core reads .env.local/.env from repo root (breaks on a virgin machine) |
server.js:45-57 |
High | repo |
Core has native-module deps that complicate bundling (sharp, tesseract.js) |
apps/lantern-garage/package.json:53-54 |
High | repo |
Node engine requirement is >=20 |
apps/lantern-garage/package.json:39-41 |
High | repo |
| Optional children (Discord/crypto-observer/pr-watcher) already default OFF | server.js:320, :609, :714 |
High | repo |
unisona.ai is a brand token / second domain over the same Core, not a program |
memory [[unisona-second-domain]]; docs/UNISONA-1.8.md |
High | repo + memory |
| Loopback requests are treated as local-admin (unsafe on an end-user box) | memory [[lantern-net-cloudflare-bypass]], [[api-auth-gaps-2026-06-20]] | Medium | memory |
| EV certs no longer bypass SmartScreen (removed 2024) | Microsoft Learn — SmartScreen reputation (learn.microsoft.com/windows/apps/package-and-deploy/smartscreen-reputation) | High | web |
vercel/pkg deprecated; Node SEA is the current single-executable path |
github.com/vercel/pkg; nodejs.org SEA docs | High | web |
Update — 2026-07-04: signing & distribution channel (research, Alex)
The original "sign via Azure Artifact Signing (~$10/mo)" follow-up is superseded. Grounded research corrected two premises and settled the channels; we ship both.
Premise corrections:
- EV certificates stopped granting instant SmartScreen reputation in 2024, so
paying for EV/Azure buys a signature + a slow reputation ramp, not a warning-free first launch. Azure Trusted Signing → renamed Artifact Signing (GA ~Apr 2026) needs a paid Azure sub (free/sponsored subs blocked) and uses that same ramp. Azure is dropped — strictly worse than the $0 options.
- Cloudflare and Google/Vertex credits cannot sign a Windows exe. Cloudflare
issues web TLS certs only; Google CAS issues private (untrusted) CA certs and Cloud HSM only stores keys. Code signing needs a cert chaining to Microsoft's Trusted Root Program, which neither provides.
Decision — two $0 channels (repo is public → both eligible):
- Microsoft Store (MSIX) — primary. $0; Microsoft **re-signs on
certification so users see no SmartScreen warning on first launch**; auto- updates; no cert to manage. Store registration is free (individuals Sep 2025, companies May 2026).
- SignPath Foundation — for direct download off unisona.ai. $0 free OSS OV
signing (key on their HSM, Sectigo cert). Caveats: SmartScreen publisher reads "SignPath Foundation", and reputation still ramps over downloads.
| Claim | Evidence | Confidence | Source |
|---|---|---|---|
| EV certs no longer grant instant SmartScreen reputation (2024) | knowledge.digicert.com — EV-signed apps still show SmartScreen warnings | High | web |
| MS Store re-signs MSIX → no first-launch warning; registration now free | learn.microsoft.com/windows/apps/package-and-deploy/code-signing-options; blogs.windows.com (free registration Sep/ May 2026) | High | web |
| SignPath Foundation = free OSS signing; public repos eligible | signpath.org ; signpath.org/terms.html | High | web |
| Cloudflare / Google credits don't cover Windows code signing | developers.cloudflare.com/ssl (web certs only); Google CAS = private CA | High | web + product scope |
Update — 2026-07-10: native WebView2 shell replaces the Edge --app window (amends G5)
Status: Proposed (pending Alex's approval per the ADR gate). Directed by Alex ("it should not be edge, I want a native desktop app").
What changed. G5 originally said: reuse the user's browser; if a window is ever wanted, use Edge/Chrome --app mode. In practice that launched msedge.exe — an actual browser process (browser profile, app-mode address bar, an Edge dependency and Edge update surface). It read as a browser, not an app.
Decision. Keep the principle behind G5 — no bundled Chromium, no Electron — but render the window with a native .NET WPF + WebView2 shell (Unisona.exe, apps/lantern-garage/desktop/shell/) instead of spawning the Edge browser. WebView2 is the Edge engine already on Win10/11 (same "reuse what's installed" spirit; still no ~150 MB Chromium download), but the window is a genuine native app — own process, title bar, taskbar entry, dark immersive chrome, no address bar, no msedge.exe. This is the same move Microsoft made for new Teams (Electron → WebView2). The one-Core rule (G1) is unchanged: the shell replaces only the window; it boots the same unmodified Core via launcher.js --embed.
Architecture. Two executables now ship (still no node.exe, no Chromium): Unisona.exe (the shell) spawns unisona-core.exe (the Node SEA backend) with --embed; the Core hands back the tokened loopback endpoint via %LOCALAPPDATA%\unisona\endpoint.json and the shell hosts it in WebView2. Tauri was reconsidered (per the original "reconsider Tauri" note) but WebView2-via-.NET builds and verifies on the existing Windows/.NET toolchain with no new Rust build system.
| Claim | Evidence | Confidence | Source |
|---|---|---|---|
Native shell boots the packaged Core with zero node.exe and serves the cockpit |
2026-07-10 install test: Unisona.exe → unisona-core.exe --embed → unisona-core.exe, http://127.0.0.1:*/ → 200, <title>unisona.ai</title> |
High (measured) | local install run |
| WebView2 engine is present on Win10/11 (no Chromium download) | WebView2 Evergreen Runtime 150.x on the build box; MS ships it in-box on Win11 | High | local + learn.microsoft.com |
| New Microsoft Teams moved Electron → WebView2 for a native window | learn.microsoft.com / Teams engineering posts | Medium | web |