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
No plugin loaded
Drop a manifest and two files into plugins/ to fill it
Beat 1284
every 2s
beat 1283
4 min ago
0 · 0
pointer, in a 0×0 viewport
Overview
Plugins run in a sandbox and never hold key material.
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
No plugin loaded
Drop a manifest and two files into plugins/ to fill it
Beat 1284
every 2s
beat 1283
4 min ago
0 · 0
pointer, in a 0×0 viewport
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.
Five crates: protocol, plugin, jsrt, host and pi
Plugins get fetch, log, config and scoped secrets, and nothing else
PKCE authorisation and secret storage on the host; the plugin spends a token it cannot read
A framed serial protocol that negotiates its baud rate and version, or refuses to talk
A plugin that fails repeatedly is killed, its retry budget reset after five minutes
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
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.
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.
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.