WolOS
Reference contents

Reference · Decisions

0031: Plugins, themes and layouts

Folder plugins in a sandbox, themes for every toolkit, and layouts as data.

Status: accepted (lead decision 2026-11-01; drafted by main the same day). Builds on 0010 (widget packages), 0020 (view tree, Lua sandbox), 0021 (wopkg), 0027 (one app cannot take the desktop), 0029 (wolos builds nothing for release). An MIT-licensed desktop configuration project (credited in `docs/THIRD-PARTY.md`) is the reference for format and ecosystem; its palettes are already ported under `crates/core/src/theme/builtin/NOTICE`.

On this page

Context

the shell can be extended only by one Lua desktop card (crates/wol-lua), a JSON widget package (ADR 0010) or a theme JSON (crates/shared/src/theme.rs). The menu bar, Dock, Search, Control Center and hotkeys are closed; themes stop at toolkits (compositor colours are static in hyprland.conf); the bar and Dock edges are constants in crates/shell/src/kit/surface.rs. That reference project shows that a plugin ecosystem forms fast (1,700+ repositories in months) when the format is one folder, one manifest and one CLI, and that most plugins are small: a bar item, a panel, a theme, a web app launcher, a toggle. It runs plugins unsandboxed; WolOS will not. The lead wants: a powerful plugin and theme system integrated everywhere, an app to make themes and plugins, a layout abstraction so a Windows-like layout can come later, an API that AI agents can write plugins against easily, and plugin updates that do not wait for OS releases.

Decision

  • A plugin is a folder <id>/ with plugin.toml and the files it names. Search order: ~/.local/share/wolos/plugins/<id>/ (user, reloaded on change), /usr/share/wolos/plugins/<id>/ (packaged). Ids are reverse-DNS or <user>.<name>, lowercase; org.wolos.* is reserved. No symlinks, 1 MiB total, writes only under $XDG_STATE_HOME/wolos/plugins/<id>/.
  • Manifest plugin.toml, schema 1 (frozen in crates/shared/src/plugins.rs; unknown key = load error with a sentence): header schema, id, name, version, author, license, description, min_wolos, permissions; capability tables, each with an id: [[menubar]] (script, side), [[dock]] (name, icon, exec), [[widget]] (script, sizes; today's lua:<id> card), [[panel]] (script, anchor), [[search]] (script, kind, prefix), [[tile]] (icon, label, toggle), [[toggle]] (on, off argv; flag file in $XDG_STATE_HOME/wolos/toggles/), [[hotkey]] (default, exec, group; becomes a user-rebindable shortcuts::Custom), [[webapp]] (name, url, icon, profile), [[theme]] (file), [[look]] (file), [[hypr]] (file, whitelist of windowrule, layerrule, bezier, animation; never bind, exec, plugin, env, source). The schema is published as docs/plugins/plugin.schema.json and installed at /usr/share/wolos/plugins.schema.json so agents can validate; the whole API is documented in one file, docs/modules/plugins.md.
  • Code is Lua in the existing sandbox (wol-lua: 16 MiB, 5M instructions, no io/os/require). API v1 = today's wol.* plus the view nodes that exist in shared::view but not in the prelude (image, toggle, slider, list, item, text_field), wol.state.get/set (per-plugin JSON, 64 KB), wol.run{argv, timeout} (permission run, argv only, through widgets::run_limited), wol.http_get (per-host grant as ADR 0010), wol.open, wol.toggle_set, wol.clipboard.text (new capability, through wold), wol.show(panel). Anything that changes the system is wol.run{"wol", ...}: it passes controller::approve and the audit log, so plugins inherit the helper policy and appear in Settings > Assistants & Automation under the plugin id. No new privilege path. Load never acts.
  • Permissions and limits: declared in the manifest, shown before enabling (Store sheet, capability_row), stored as grants in helpers.json keyed by plugin id, revocable in Settings. Each render and click is budgeted; a plugin that fails three renders in a row is disabled with a toast saying why. Hypr fragments are parsed into hyprprefs keywords, never sourced raw.
  • Distribution, independent of OS releases: (a) folder drop; (b) wol plugin add <git-url|path|.wolplug> (stage, check, move; hooks never run); (c) a signed plugin index (index.json + Ed25519 signature, key in /usr/share/wolos/keys/) consumed by a new plugins source in wopkg (ADR 0021 recipe model) and by a Store "Plugins" page. A .wolplug is a tar.zst of the folder plus SHA256 and optional SHA256.sig (ADR 0011 form). Installs are user level; wopkg update updates plugins without touching pacman. Unsigned plugins are marked unreviewed like community-collection packages and need the typed yes. The index repository (wolos-plugins) accepts pull requests validated by wol plugin check in CI.
  • Agent-first CLI: wol plugin new <id> [--kinds ...] (scaffold with a passing check), wol plugin check <dir> [--json] (schema, ids, sizes, symlinks, Lua loads, every entry renders with sample state, theme contrast, hypr whitelist), wol plugin run <dir> --kind <k> [--click n --state f] (prints the Node tree as JSON, no display needed), list|enable|disable|remove|update|pack|preview.
  • Themes v2: theme.json stays (tokens + extends) and gains optional terminal (16 colours, derived when absent), wallpaper, iconTheme, lock, look. New renderers in crates/core/src/theme/: compositor colours (hyprbars bar and text, borders, shadow, written to hypr/wolos/theme.conf), lock screen, terminal palettes (foot exists; btop, alacritty, kitty, ghostty opt-in), zsh and PowerShell prompt colours. User templates ~/.config/wolos/themed/<name>.tpl with {{token}}, {{color3}}, {{token|strip}}, {{token|rgb}} rendered on every switch. Theme-from-wallpaper generator must pass check() (contrast). Theme picker panel on ⌘⌃T, background picker on ⌘⌃W.
  • Layouts: shared::layout::Layout { bar: Edge, bar_style: Menubar | Taskbar, dock: Option<Edge>, dock_merged_into_bar, workspaces_in_bar, tray_in_bar } saved in shell.json. Now: Spec::bar(edge) and Spec::dock(edge) take anchors from Layout; the bar becomes slots left/centre/right of Vec<BarItem> (plugin items are BarItem::Plugin); Dock::content takes Vec<DockItem> and an orientation; the hot zone and reveal direction follow the dock edge; the compositor's layer namespaces stay. Later, "Windows-like" = Layout{bar: Bottom, bar_style: Taskbar, dock: None, dock_merged_into_bar: true, workspaces_in_bar: true} plus one BarItem::Tasks and a start item; no other surface changes.
  • Plugin Maker / Theme Studio (wol-plugin-maker, GTK4): editor (TOML/Lua, GtkSourceView) with live preview reusing the widget gallery, a fake bar with the item inserted, Control Center tile preview and the theme preview with token sliders and live contrast; Check (same function as the CLI), Pack, Publish (opens the index pull request with the hash). Reached from Settings > Appearance ("Make a theme...") and Store > Plugins ("Make one").
  • Features of the reference project ported natively in v0.12 (MIT, rewritten, not loaded as QML): web apps (wol webapp add, frameless browser window, .desktop, Search and Dock entries), toggles as state with Control Center tiles (Stay Awake, Silence, Bar, Touchpad), theme-from-wallpaper and the two pickers, compositor colours from the theme, capture menu additions (colour picker, OCR to clipboard, QR decode kept out of history), clipboard history with images and 1-day expiry that skips verification codes, reminders (wol remind), monitor profiles with auto-apply on hotplug, update badge in the menu bar, and a reference bar plugin org.wolos.stats that proves the API and is the documentation example.

Consequences

one manifest, one sandbox, one view tree: plugin UI cannot leave the HIG and a broken plugin shows a sentence, not a crash. Plugins reach system services only through wol verbs, so helper policy, audit and Always-Allow cover them unchanged; a plugin that needs more is a libwol app (ADR 0020). Plugin updates ride wopkg's signed index, not OS releases, so ADR 0029 is untouched. Plugins from the reference project are ports, not loads; most are small. The layout refactor is mechanical and must land before plugin bar items, because BarItem and DockItem are the frozen seam. Memory and instruction limits of wol-lua now protect the menu bar, so the existing bomb tests are extended to every capability.

Amended 2026-11-02 (round-5 audit, C8-1, C8-2, X6-1, X6-2), what 0.12 ships against this decision:

  • [[look]] and [[hypr]] are planned: they parse (and the compositor-rule whitelist above is checked), but no host applies them yet, so wol plugin check refuses them and wol plugin new does not offer them. [[webapp]] is a launcher next to the person's own web apps (wol_core::plugins::sync_webapps) while the plugin is on.
  • wol plugin add takes a folder or a .wolplug and installs it through the Store's install step (verify_wolplug, install_plugin); unsigned needs --yes. Add by index id or git URL, wopkg update updating plugins and the update and preview verbs are planned; plugin updates come from the Store's Plugins page.
  • [[toggle]], [[dock]] and [[hotkey]] commands that are not wol verbs need the run permission, declared and granted; wol plugin check runs scripts with inert services (nothing runs or is written), as Plugin Maker does.

From the developer documentation, wonderlandos/wolos docs/adr/0031-shell-plugins-themes-layouts.md.