Exhibit 01 / 03

desk-companion

Plugins run in a sandbox and never hold key material.

Discipline
Systems and device
Built with
Rust, Slint, JavaScript
Year
2026
Status
Active

Scroll

Desk Companion--:--
Slot 1free

No plugin loaded

Drop a manifest and two files into plugins/ to fill it

Beacon

Beat 1284

every 2s

EchoStale

beat 1283

4 min ago

Host readoutlive

0 · 0

pointer, in a 0×0 viewport

Overview

Plugins run in a sandbox and never hold key material.

Discipline
Systems and device
Year
2026
Status
Active
Stack
  • Rust
  • Slint
  • JavaScript
  • Raspberry Pi

A small information screen for my desk, running on a Raspberry Pi. A Rust application draws the interface on an 800 by 480 panel, and plugins fill it with whatever I want to see.

I wanted a screen on my desk that I could add new things to without rebuilding and reflashing the whole application every time. That means plugins, which means running code I did not write on a device I cannot easily get a console on. Most of the work went into the plugin host, the serial protocol and the update path rather than the graphics.

The interface

Desk Companion--:--
Slot 1free

No plugin loaded

Drop a manifest and two files into plugins/ to fill it

Beacon

Beat 1284

every 2s

EchoStale

beat 1283

4 min ago

Host readoutlive

0 · 0

pointer, in a 0×0 viewport

A web recreation of the device's interface at 800 by 480: the dock along the bottom, the status bar and the widget grid.

How it works

A plugin is two JavaScript files and a manifest: one half runs on the host, one on the device. The manifest names every host it will call and every secret it will use, and the host reads it before a line of plugin code runs.

  1. Five crates: protocol, plugin, jsrt, host and pi

  2. Plugins get fetch, log, config and scoped secrets, and nothing else

  3. PKCE authorisation and secret storage on the host; the plugin spends a token it cannot read

  4. A framed serial protocol that negotiates its baud rate and version, or refuses to talk

  5. A plugin that fails repeatedly is killed, its retry budget reset after five minutes

  6. A Slint interface cross-compiled to the Pi, drawing straight to linuxkms, CJK fonts bundled

When things go wrong

  • Plugin runninglinked

    The host ticks the plugin every three seconds and the device every second. The status pill's dot is green while the serial link is up.

  • Plugin failingretrying

    Errors are counted consecutively, not within a window, so one bad response does not spend the budget. The dock slot carries a fault badge.

  • Plugin killedstopped, 5 min

    After consecutive failures the plugin is stopped rather than left thrashing. The retry budget resets after five minutes.

Decisions

  1. Why a JavaScript runtime rather than WASM or native plugins?

    A plugin should be two small files anyone can read and change in a text editor. A JS runtime costs performance and type safety, and buys a plugin surface small enough to audit: fetch, log, config and scoped secrets, and nothing else.

  2. Why declare capabilities in a manifest instead of asking at runtime?

    A manifest can be read before a single line executes. Marking refreshToken as secret means the host holds it, runs the exchange, and hands back only the response, so the plugin can spend the token forever and never see it.

  3. Why framed and typed rather than JSON over newlines?

    A serial line gives you partial reads. Length-prefixed frames keep a partial read a partial read instead of a silently truncated message, and a protocol-version mismatch closes the link rather than guessing.

Send a frame yourself

Switch a module on the host and a real frame crosses the wire and repaints the device. The message names are lifted from the protocol crate; nothing here is a canned animation.