8 - Building a XRUIOS App

Building a XRUIOS App

An "app" on XRUIOS is anything that holds a stable id + its own app password (PSK) and talks to the Manager broker over the Eclipse channel. It never touches a worker, a file, or a key directly - it asks the broker for a capability by name, and the broker either runs it in the owning worker or refuses it. That contract is identical whether your app is a console tool, a desktop window, or a mod running inside a game engine. Only the shell around the client changes.

This page builds the same tiny app - read today's calendar, add an event - three ways: CMD, desktop, and an Aether Engine mod. Then it covers the shared pieces (initialization, capability naming, failure) that apply to all three.

The one contract every app shares

your app  ──(app id + app PSK)──►  Manager broker
                                        │  post-quantum handshake (Kyber + AES-256-GCM)
                                        │  XRUIOS.Permissions check on the capability name
                                        ▼
                                   owning worker runs it, result comes back

Three things your app always needs:

  1. An id the Manager minted for it (app:yourname). You don't invent it; the Manager registers it and mints the matching PSK.
  2. Its PSK, delivered out-of-band (a creds file the Manager writes, or an env var it sets). The PSK never ships inside your app binary.
  3. The broker address (loopback host + port the Manager is listening on).

With those, every app is four lines: connect, invoke a granted capability, handle a denied one, done.

var mgr = await EclipseSecureClient.ConnectAsync(brokerAddr, appId, appPsk, identity: appId);
string uid = await mgr.InvokeAsync<string>("AddEvent", new() { ["day"] = today, ["summary"] = "Ship it" });
string day = await mgr.InvokeAsync<string>("GetEvents", new() { ["day"] = today });
// anything you weren't granted throws before it reaches a worker

Everything below is just where those four lines live.


1. A CMD app

The console is the reference shell - the smallest thing that proves the contract. This is exactly what XRUIOS.SampleApp (the Höllvania calendar) is.

Project: a plain Microsoft.NET.Sdk exe on net10.0, referencing XRUIOS.SecureLayer (for EclipseSecureClient) and, if you want the UI, Spectre.Console.

using System.Text;
using Spectre.Console;
using XRUIOS.SecureLayer;

Console.OutputEncoding = Encoding.UTF8;

// the wordmark stays white; everything else is yours
AnsiConsole.Write(new FigletText("XRUIOS").Color(Color.White).Centered());
AnsiConsole.Write(new Rule("[green]HÖLLVANIA MUNICIPAL CALENDAR[/] [grey]· ON-LYNE BBS · est. 1999[/]").Centered());

// creds the Manager wrote for this app: broker addr + id + PSK
var (brokerAddr, appId, appPsk) = AppCreds.LoadFor("sampleapp");

string today = DateTime.Now.ToString("yyyy-MM-dd");

await AnsiConsole.Status().Spinner(Spinner.Known.Dots).StartAsync("Handshaking…", async _ =>
{
    var mgr = await EclipseSecureClient.ConnectAsync(brokerAddr, appId, appPsk, identity: appId);

    // GRANTED
    string uid = await mgr.InvokeAsync<string>("AddEvent",
        new() { ["day"] = today, ["summary"] = "Ship XRUIOS" });
    AnsiConsole.MarkupLine($"[green]added[/] event [aqua]{uid}[/]");

    string events = await mgr.InvokeAsync<string>("GetEvents", new() { ["day"] = today });
    AnsiConsole.Write(new Panel(events).Header($"[white]{today}[/]").BorderColor(Color.Green));

    // NOT GRANTED - the broker refuses before it reaches the worker
    try
    {
        await mgr.InvokeAsync<string>("DeleteEvent", new() { ["uid"] = uid });
    }
    catch (Exception ex)
    {
        AnsiConsole.Write(new Panel($"[red]{ex.Message}[/]").Header("[red]ACCESS DENIED BY THE OPERATOR[/]"));
    }
});

Run it: start an unlocked Manager, then run the exe. It prints the new event id, the day's events, and a red denied panel for delete. That denied panel isn't error handling for show - it's the permission model firing. The app was granted GetEvents + AddEvent, not DeleteEvent, so the broker never let the delete reach the Calendar worker. See 7 - Proven at Runtime for the actual run.

When to reach for this shell: services, cron-style jobs, install/setup tooling, anything headless. It's also the fastest way to smoke-test a new capability before you put a face on it.


2. A desktop app

A desktop app is the CMD app with a window bolted on. The client is UI-framework-agnostic, so the rule is: the broker connection lives in a small service, the UI binds to it, and no XAML/AXAML ever calls a worker. This holds for WPF, Avalonia, or OpenSilver - only the view layer differs.

The service (identical across frameworks - this is the whole app, really):

public sealed class CalendarService
{
    private EclipseSecureClient? _mgr;

    public async Task ConnectAsync()
    {
        var (addr, id, psk) = AppCreds.LoadFor("calendar-desktop");
        _mgr = await EclipseSecureClient.ConnectAsync(addr, id, psk, identity: id);
    }

    public Task<string> GetDayAsync(string day) =>
        _mgr!.InvokeAsync<string>("GetEvents", new() { ["day"] = day });

    public Task<string> AddAsync(string day, string summary) =>
        _mgr!.InvokeAsync<string>("AddEvent", new() { ["day"] = day, ["summary"] = summary });
}

The view model binds to it and keeps the UI thread clean - await marshals back for you, so the only rule is don't block on the connection:

public sealed class DayViewModel : ObservableObject
{
    private readonly CalendarService _cal;
    public ObservableCollection<string> Events { get; } = new();

    public async Task LoadAsync(string day)
    {
        string raw = await _cal.GetDayAsync(day);   // off the UI thread
        Events.Clear();
        foreach (var line in raw.Split('\n')) Events.Add(line);  // back on it
    }
}

The window (WPF shown; Avalonia is the same shape with axaml) is just a list bound to Events and a button that calls AddAsync then LoadAsync. The XAML never sees the broker, never holds the PSK, never names a worker - it only knows the view model.

Three things a desktop app must respect that a console app can ignore:

  • Connect once, off the constructor. Do the handshake in an async OnStartup/OnFrameworkInitializationCompleted, show a "connecting" state, and only enable the UI when it's up. A failed handshake is a normal state (Manager locked or not running), not a crash - show "XRUIOS is locked, log in first."
  • A denied capability is a UI affordance, not an exception you swallow. If the app wasn't granted DeleteEvent, don't show a delete button. The broker will refuse it regardless - the wall doesn't depend on your UI - but a good app doesn't offer what it can't do. Catch the denial and disable, don't crash.
  • The PSK still comes from the Manager, not your app settings. A desktop app is more tempting to ship with a baked-in secret. Don't - load it from the creds the Manager wrote, exactly like the console app. If someone copies your .exe, they get no id and no PSK, so they get nothing.

That's the entire difference. A desktop XRUIOS app is a normal MVVM app whose "backend" is the four-line broker client, wrapped so the connection is async and the denials are visible.


3. An Aether Engine mod

A mod is an app that runs inside a host (a HangarBay/Aether game or scene) instead of as its own process, so it can't even open a socket to the broker - CasCore made System.Net uncallable. Instead the host holds the broker connection and the mod asks the host. The permission story is identical - one XRUIOS.Permissions authority, capabilities by name - but the plumbing is the mod registry, not EclipseSecureClient. Full detail in 6 - Aether Engine Integration; here's the build.

On the host side (the game integrating XRUIOS - written once, not by the mod author):

// one registry over the host's real XRUIOS.Permissions handler + context
var mods = new ModRegistry(handler, appContext, privateRoot, memoTtl: TimeSpan.FromSeconds(5));

// enroll the mod by its Notary-verified package id -> host-assigned ModId
var widget = mods.Enroll("com.hollvania.calendarwidget");

// grant it exactly what it may do - persisted in XRUIOS.Permissions under its ModId
await mods.GrantAsync(widget, "Time.Calendar:GetEvents");
// note: no AddEvent, no DeleteEvent granted

// give it a jailed filesystem: its own private dir, plus any named roots it's granted
var fs = mods.CreateFileSystem(widget, grantableRoots);

On the mod side, the author writes against a host-provided interface - never against System.IO, System.Net, or the broker. The mod calls a host service; the host runs IsAllowed and either serves the result or refuses:

// inside the mod - the host injected this; the mod has no other way to reach data
public void OnTick(IXruiosHostServices host)
{
    // the host checks mods.IsAllowed(thisMod, "Time.Calendar:GetEvents") internally
    string today = host.Calendar.GetEvents(DateTime.Now);   // ALLOWED -> returns events
    Draw(today);

    // this is not granted; the host refuses it. the mod cannot bypass - it holds no key,
    // no socket, no file handle. all it can do is ask.
    // host.Calendar.DeleteEvent(uid);  // would throw: not allowed
}

The mod author's mental model is the same four-line contract, minus the connection: ask for a capability, get a result or a refusal. What changes is that identity is host-assigned (ModId from the Notary package id - a mod can't claim to be another), grants are persisted per-ModId, sensors and files are themselves capabilities, and an unsigned mod (EnrollEphemeral) can't bank trust across sessions. The runtime proof is in 6 - Aether Engine Integration: com.hollvania.calendarwidget ALLOW GetEvents / DENY DeleteEvent; com.evil.dataslurp DENY both.


Shared pieces (all three surfaces)

Initialization

A worker owns its store, and it sets that store up itself on first launch - the Manager doesn't reach into a worker's data. Each system's Initiate… routine (the ones from Barebones - InitiateCalendar, InitiatMusic, and so on) moves into its own worker and runs at worker startup, so by the time the broker routes your first AddEvent the Calendar worker has already created its Calendar directory. That's why the Calendar worker's Program.cs creates its data folder before it serves: an app should never have to initialize a worker it can't even see. When you stand up a new worker, put its init on the worker's boot path, not in any app.

Capability names are the whole API

Your app's surface is the set of capability names it was granted. Names read Group.Worker:Capability (Time.Calendar:GetEvents) so a grant is unambiguous about which worker it reaches - see the full list in 5 - Permission Catalog. Design your app around the names you'll request, then have the Manager grant exactly those. Anything else is denied by construction.

Failure is a first-class state

Every surface has the same three failure modes, and a good app names them:

Failure What it means What the app should do
Handshake fails Manager locked, not running, or wrong PSK "XRUIOS is locked - log in" - don't retry-storm
Capability denied You weren't granted it Don't offer the action; if you must ask, catch and explain
Worker error The granted call ran but failed inside the worker Surface the worker's message - it's a real error, not a permission one

The denial and the handshake failure are the security model doing its job. Treat them as expected states, not exceptions to bury.

The PSK never lives in your binary

Console, desktop, or mod - your app is not trusted with a secret it can leak. The console/desktop app loads its PSK from the creds the Manager wrote; the mod never has one at all (the host connects). Copy any app binary to another machine and it authenticates as nobody, because the id and PSK were provisioned by that machine's Manager. This is what makes "just send me the exe" safe.