hraness

a desktop app around a headless rust core

the menu bar is a shell too

Drafted by an AI agent at Ben Guo's direct request from the Hraness source repositories, and checked against those sources before publication.

Most desktop applications are built UI-first: a window, a framework, then enough logic behind it to make the buttons work. The interesting test of a headless core is the opposite: can you put a real interface on top of a system that was designed with no interface at all, without the interface becoming a second place where policy lives?

Valhalla’s desktop story is two different answers to that question. One shipped: vhalla-menubar, a macOS status-item companion that is deliberately too thin to have opinions. One is still being qualified: a shared Dioxus UI intended to run the same Rust on web and desktop. Both treat the interface as an adapter over authorities it does not own, and both had to earn the right to exist by proving they hold no authority themselves.

the menu bar is a shell

desktop/menubar is 174 lines of main.rs. Its doc comment says what it is and, more important, what it is not: “a disposable client… Valhalla identities, stores, and rooms remain the authorities; this binary holds no privilege of its own and reads only the outputs directory.”

That is the whole contract. Agents drop descriptively named files into an outputs directory (vhalla outputs creates it under the platform state directory); the menu bar lists them newest-first with thumbnails and open/reveal actions; a “Support” item opens a fixed account URL in the browser; a Quit item quits. The binary never opens an identity file, never reads a store, never sees a key. It holds one flock on menubar.lock so two instances don’t double-register a status item, and that is the extent of its state.

Two details make it a shell rather than a program. It sits in desktop/, a separate Cargo workspace: desktop/AGENTS.md is explicit that Tauri’s build dependencies must never enter the protocol crates’ Linux or WASM checks. And it is built on desktop-foundation, a shared Hraness crate pinned by immutable tag (v0.7.0), so the adapter adds no product state to the menu model. It implements snapshot() and dispatch_result() and lets the foundation own validation, cancellation, and refresh coalescing. The 174 lines are what is left when every decision belongs to something else.

Even installation keeps the shell thin. vhalla menubar install copies a release build into bin/ under the state directory and registers a per-user LaunchAgent (com.hraness.valhalla.menubar, RunAtLoad): no system daemon, no root, and uninstall boots the agent out and removes both files. The one outbound handoff (a “Support” menu item that opens a fixed account.hraness.com URL) passes no identity, email, session, or store data to the browser; the AGENTS.md requires it. The binary also holds a single-instance flock and exits quietly on a second launch, because a status item is a singleton and a second one is a bug, not a feature.

a replica, not a client

The heavier answer is vhalla-rooms-app. It is the renderer-agnostic service that a desktop or browser UI would actually talk to, and it is built as a read replica of a running node rather than a client of one.

The doc comment describes the split: a Service absorbs committed journal bundles out of the node’s home directory (immutable, content-addressed files safe to read while the node runs) into its own stores under replica_home, verifies every certificate against the trusted validator sets, and answers typed Screen projections. Submissions go the other direction: a *.body file dropped into the node’s intake/ directory, where the node assembles the batch against its own frontier. The service “never fabricates parent or result claims,” and signing stays outside entirely: callers pass canonical signed record bytes, and “no private key enters the service.”

That is the headless-core pattern at the application layer. The node’s journal is the authority on what committed. The replica derives a view of it. The UI, when it exists, reads projections of the replica. At no point does anything with a screen hold a key, assemble a batch, or decide what committed. The chain of custody is: consensus decides, the journal records, the replica projects, the interface renders.

The projection types are the interface contract: Screen, Projection, RoomRow, Pending, PendingState, Error are all platform-neutral on purpose, so “browser and native renderers share them.” A screen is a request for a named view of committed state; the service answers with data, not a promise about the node. And because the replica reads the node’s journal rather than trusting a live channel, it can verify every certificate it absorbs against the trusted validator set; the replica holds the node to the same standard a voter did, using verify_canonical_certificate from the portable cert module.

The PendingState enum is the same honesty applied to submissions. A dropped body resolves to Queued (still in the node’s intake), Submitted (in flight toward a decision), Committed (the registry cites the record), Collision (a different record claimed the slug first), or Rejected (intake refused the drop). There is no “success”: the UI watches the committed record arrive through the journal, and a collision is a first-class outcome, not an exception. The submission is a marker; the resolution is read back from authority.

the renderer is interchangeable

vhalla-rooms-tui shows what that buys. The terminal UI is built Elm-style: App is a pure model, key events fold through App::key, and a view module renders the model. The model talks to a Source trait (sync(), project(screen), pending(), submit(...)) which mirrors the vhalla-rooms-app service boundary exactly. The doc comment notes this was deliberate: “the same contract the Dioxus spike defined, so renderers stay interchangeable.” Terminal setup and identity custody are cfg(unix); signing happens “in the CLI’s own trust domain, never inside the service.”

Because the model is pure, the TUI is fully testable without a terminal: feed KeyEvents through App::key, assert on the model. Because Source is a trait, the same model runs over a real replica or a fixture. And because the projections come from vhalla-rooms-app, the terminal shows exactly what a future Dioxus app would show. The UI changed, the evidence did not.

what the shell cannot do

The design plan’s Dioxus section is unusually explicit about the boundaries a UI must respect, and reading it is a reminder that “the UI is a shell” is a security statement, not an aesthetic one. The proposed ownership table puts the pure vhalla-* crates on one side (no Dioxus, DOM, filesystem, or platform-clock dependency) and layered shells on the other: a vhalla-client-api of inert presentation data, a vhalla-client with injected storage/transport/clock/identity interfaces, a vhalla-ui of Dioxus components, and separate vhalla-web/vhalla-desktop launchers.

The hard rules: untrusted message text renders as text; remote content cannot supply component code, raw HTML, scripts, or IPC method names; view code emits bounded typed intentions and “never mints execution capabilities”; keys and private reader data never go in route parameters. And the caveat that keeps it honest: “Compiler boundaries prevent UI code from accidentally importing signing or generic effect APIs; they do not isolate a compromised native renderer in the same process.” A WebView is a dependency to be threat-modeled, not a sandbox.

the unfinished parts

The honesty matters because the desktop story is genuinely partial. The menu bar shipped and does what it says. The Dioxus work is an accepted framework decision with shared prototypes (the plan’s own words), not a delivered application. The spikes found real problems (a stock WebView loader that opens external links before the navigation handler runs, injected interpreter scripts, no default CSP) and the qualification gates in the plan (shared screens, platform services, renderer boundary, packaging, integration) are tracked as evidence, several still in progress. The pure crates that a UI would sit on are tested and compile to wasm32-unknown-unknown; the shared UI itself is still being proven on both renderers.

That is the shape worth copying: the interface is allowed to be unfinished because the authorities it fronts are not. The menu bar can be 174 lines and disposable because everything it displays is owned by something that is not.

sources

  • vhalla
  • desktop/menubar/src/main.rs: the 174-line shell, and desktop/AGENTS.md for the workspace boundary
  • crates/vhalla-rooms-app/src/lib.rs: the renderer-agnostic read replica
  • crates/vhalla-rooms-tui/src/lib.rs: the Elm-style App/Source split
  • valhalla-security-first-design.md: the Dioxus ownership table and renderer caveats