Skip to content

Latest commit

 

History

History
37 lines (29 loc) · 9.08 KB

0010-plugins.md

File metadata and controls

37 lines (29 loc) · 9.08 KB

Plugins

Status: Accepted

Problem

Aurral removed most of its integrations to keep core small and reliable, and users still want them back, along with new ones for the clients they run. Integrations that live in core slow it down and break with other apps' updates. Arbitrary code from other people also makes bug reports hard: a crash, a hang, or a bad value from a plugin looks like an Aurral bug.

Decision

A plugin is a folder in <data dir>/plugins/<id> with a manifest.json and a bundled index.js. It fills typed capabilities that Aurral calls, and it can call Aurral's public API. It cannot hook into Aurral's internals.

  • Capabilities, not hooks. API 1 has menu actions for playlists, albums, artists, and tracks, events, a schedule, a download source for tracks and albums, a playlist import list, and a settings test. The manifest declares them, along with the settings Aurral renders on the plugin's page. Plugins ship no frontend code, so Aurral draws every menu item and settings row, and a page that breaks is Aurral's bug. plugin-api.md and aurral-plugin.d.ts beside the code are the reference; the docs site and the MCP server both read them.
  • App-wide and admin-only. Admins install, configure, and turn on plugins for everyone. There are no per-user plugin settings. An admin can turn off each capability on its own, stored as keys in plugins.disabled.
  • One process per plugin, per Aurral process. pluginHost.ts forks pluginRunner.ts, which imports index.js and answers calls over IPC. The main process runs actions, and the downloads worker runs download sources, so each forks its own copy. A process starts on its first call and stops after 5 idle minutes. It gets a 256 MB heap, a short list of environment variables, and its stdout and stderr go to the app log under plugin:<id>.
  • One boundary. callPlugin checks every answer with a Valibot schema. A failure before that check is the plugin's: its error carries the plugin's name in the UI and its id in the log, and its page shows the last error with the manifest's repository link. A failure after the check is Aurral's.
  • Broken plugins turn off. A crash, a timeout, a failed start, or an answer the schema rejects counts as broken. After three in a row, Aurral turns the plugin off and records why. The count lives in plugins.broken_in_a_row, because worker processes come and go between calls. An error the plugin throws, such as its service being down, never counts, because that is the plugin working.
  • State in SQLite. The plugins table holds enabled, priority, encrypted settings, last_error, and a snapshot of the manifest. Opening Settings > Plugins and starting the server rescan the folder and update the snapshot. Worker processes read only the table, so the download pipeline's synchronous source list stays one query. The snapshot is parsed again when read, as in 0007.
  • Off until turned on. A new folder never runs until an admin turns it on. Actions and sources show only while the plugin is on and its required settings are filled in. The one exception is an install converted from Aurral 2's Lidarr mode: the first admin's Set up Lidarr plugin installs the official Lidarr plugin, fills in the old connection, and turns it on once its test passes. That admin already chose Lidarr, so one click is both the install and the consent.
  • Safe mode. The pluginSafeMode setting or AURRAL_SAFE_MODE makes every plugin inactive without changing their own settings. It is the first step for telling a plugin bug from an Aurral bug.
  • Download sources fill a staging folder. A source's id is plugin:<id>. It searches, Aurral matches the results with the plugin capabilities in candidateNormalizer.ts, the plugin downloads into an empty staging folder Aurral gives it, and Aurral checks and commits the files like any other source. A track path outside that folder is refused. An album source finishes through finishAlbumGrab, which judges the release by its files. A download can return pending for a client that works on its own, and the pipeline polls status every 15 seconds for up to a day instead of holding a call open. A client that files its own album downloads, like Lidarr, has status report how many files it filed instead of filling the staging folder. It names no paths, so the two apps never have to agree on where the music is mounted. Aurral finds the album in its read-only folders through the Library index, by album identity and not by folder, because one folder holds many albums. It waits for the index to catch up, scans the read-only folders once when file events don't arrive, checks the files the same way, and uses them where they are, so it never moves or changes a file another app manages. The plugin is one implementation of the DownloadSource contract in download/downloadSource.ts, and native sources like qBittorrent and yt-dlp implement the same contract in process. The contract is the one model for every source, and the plugin API is the part of it a plugin can use. A new ability a native source gets from the pipeline, like upgrade searches or keeping one track's file from an album, reaches plugins too. Inputs only an in-process source can use, like the matcher's view of the track or the results the user denied, stay internal. An optional extra, like a wider manual Search, reaches plugins once one needs it.
  • Events ride the notification outbox. An event is queued once per subscribed plugin as a plugin notification, so it never runs in the request that caused it. The outbox is serial and also delivers native notifications, so it hands each event to the plugin and moves on without waiting. Events get no retries in exchange, and each has 10 seconds. Nothing is queued when no active plugin subscribes.
  • The public API instead of internals. A plugin with apiAccess gets the loopback API URL and a key with every call. The key is an HMAC of the plugin id with the settings encryption key, so every process derives it without storing it, and auth accepts it as an admin only while the plugin is on, being tested, and not in safe mode. A plugin that changes Aurral goes through the same versioned API apps use, so refactors don't break it.
  • Schedules ride the maintenance worker. One system task every 15 minutes calls scheduled on each plugin whose interval has passed since plugins.scheduled_at. It marks the run before calling, so a failing plugin waits for its next turn instead of being retried.
  • A data folder outside the plugin folder. <data dir>/plugin-data/<id> survives updates, which replace the plugin folder, and is deleted with the plugin.
  • Long actions answer later. A background action returns right away and its result reaches the user over the plugins websocket channel.
  • Install from a link in two steps. A GitHub repo link resolves to the latest release's .zip asset, else the release's source, else the default branch. Aurral unpacks it into staging, finds every plugin folder up to two levels down, and returns their manifests with a token. Installing moves the chosen folders into place. The link is kept as source_url, so Update fetches it again and takes the folder with the same id. A daily maintenance task inspects each source_url once and stores a newer manifest in plugins.update_manifest, so the list can show what the new version adds before an admin installs it. Nothing updates on its own, because an update can ask for more, like apiAccess.
  • Agents use developer keys. /api/mcp is a small MCP server for building plugins. It skips session auth and accepts only a developer key, which an admin creates for 24 hours and Aurral stores hashed. Its tools write plugin folders and call plugin functions while the plugin is off, without counting failures, so an agent can iterate without turning anything on.

Traps

  • Plugins are not sandboxed. A plugin runs as the Aurral user and can read the data dir. The env filter only keeps Aurral's secrets out of its environment by accident. The docs tell users to trust plugins like Docker images.
  • Turning a plugin off stops its process only in the process that saved the change. Another process keeps its copy until it goes idle and starts no new call, because every call reads the table first. A call already running there, such as a download, finishes or times out.
  • New code reaches every process through the file, not a message. Each call compares index.js's inode, size, and mtime with the ones its process started from and restarts the process when they differ, which also stops calls still running in the old one. An edit only to another file the plugin imports needs a restart.
  • A plugin's API key is as strong as an admin, and it is stable for an id because it is derived. It stops working when the plugin turns off, but the only way to change it is to change the settings encryption key.
  • A developer key is as strong as an admin: write_plugin and test_plugin run any code as Aurral. Keys expire after 24 hours and stay revocable for that reason.
  • Changing a capability's input or output shape breaks plugins. Add a field, or raise PLUGIN_API_VERSION and keep reading the old version.