From b11d019c91378874e0104080df4ba06d7fe9c57a Mon Sep 17 00:00:00 2001 From: "K. O. A." Date: Sun, 5 Jul 2026 22:04:55 -0400 Subject: [PATCH] best-practices: add "Server-side robustness" section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add server-side robustness rules found in the completeness sweep, ground-truthed against the loader. Rules (34-37): - Declare Python deps in requirements.txt (no manifest field): installs are hash-keyed and persistent, but sequential and slow — they delay later plugins, so keep them minimal and pinned. A failed install is non-fatal (Host still loads routes), so guard heavy/optional imports and degrade if missing. - Don't block the event loop: setup() runs on the loop thread and is killed at a ~60s timeout — wire only, defer heavy work. A blocking async def handler freezes the whole server; use non-blocking I/O or a plain def (threadpool). - Split routes.py with context["load_sibling"], not bare imports — two plugins shipping a top-level util.py collide in sys.modules (first wins). load_sibling namespaces per plugin and enables relative imports; don't mix the two. - Log through context["log"], never print(); namespace routes under /api/plugins//. Renumber Shipping to 38-42 and add a checklist block. Docs only. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: K. O. A. --- CHANGELOG.md | 9 +++++ spec/best-practices.md | 76 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce6268..0a01670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,15 @@ for how the document, manifest, and per-plugin versions relate. `currentSong`/`playQueue`) **rather than its DOM controls**, **wrapper discipline** for hooking Host globals (call and `await` the original, install once, clean up, no load-order assumptions), and the **v2/v3 player-chrome** mount contract. Added a matching checklist block. Docs only. +- Best-practices guide: added a **"Server-side robustness"** section for `routes`-shipping plugins, + ground-truthed against the loader. Covers **declaring Python deps in `requirements.txt`** (no + manifest field; installs are hash-keyed, sequential, and delay later plugins — keep them minimal + and pinned; a failed install is non-fatal so guard heavy/optional imports), **not blocking the + event loop** (a fast `setup()` killed at a ~60s timeout; a blocking `async def` handler freezes the + server — use non-blocking I/O or a plain `def` that runs in the threadpool), **splitting server + code via `context["load_sibling"]`** rather than bare imports that collide across plugins in + `sys.modules`, and **logging through `context["log"]` (never `print()`)** plus route namespacing. + Added a matching checklist block. Docs only. ## [0.1.0] - 2026-07-05 diff --git a/spec/best-practices.md b/spec/best-practices.md index 75a9595..14a165f 100644 --- a/spec/best-practices.md +++ b/spec/best-practices.md @@ -544,9 +544,65 @@ it's a migration aid, not a contract. --- +## Server-side robustness + +Your `routes` module runs inside the Host's server process, sharing its event loop and startup +sequence with every other plugin. A slow or misbehaving plugin doesn't just hurt itself — it can +stall the server or delay every plugin that loads after it. These rules keep a backend plugin a good +tenant. + +### 34. Declare Python dependencies in `requirements.txt` — and keep them light + +A plugin's Python dependencies go in a **`requirements.txt`** file in the plugin directory (there is +no manifest field for this). On first load the Host `pip install`s them into a persistent location +and adds it to `sys.path`; the install is keyed by a hash of the file, so unchanged requirements +don't reinstall on later boots. + +Two consequences shape good practice: + +- **Installs are sequential and can be slow, and they delay *later* plugins.** While your deps + install (potentially minutes) your plugin shows as "installing…", and plugins after you in load + order wait their turn. Keep the dependency set **small**, prefer wheels, and **pin versions** for + reproducibility. Don't pull a huge library for a small need. +- **A failed install is non-fatal — the Host still tries to load your routes.** So `import` a heavy + or optional dependency **defensively** (guard it and degrade if it's missing) rather than assuming + it installed; if a genuinely required dep fails, your routes import will fail and the Host shows + your plugin as "failed" rather than crashing. + +### 35. Don't block the event loop; keep `setup()` fast + +The Host calls your `setup(app, context)` on the **server's event-loop thread**, and it is killed if +it takes too long (on the order of a minute). Do only wiring in `setup()` — register routes, read a +small config — and defer any heavy work (scanning, model loading, large I/O) to a background task or +the first request that needs it. + +The same discipline applies to your handlers. An `async def` handler that performs **blocking** work +(synchronous file, network, or CPU-bound work) freezes the whole server for *every* request while it +runs. Either use non-blocking I/O in an `async def`, or write the handler as a **plain `def`** — the +Host runs synchronous handlers in a threadpool where blocking is safe. + +### 36. Split `routes.py` with `load_sibling`, not bare imports + +The Host puts each plugin's directory on `sys.path`, and Python caches modules by bare name in +`sys.modules` — so if two plugins each ship a top-level `util.py`, whichever loads first wins and the +other silently gets the wrong module. To split your server code across files, load your own modules +through **`context["load_sibling"]("name")`**, which imports them under a per-plugin namespace +(`plugin_.`) so they can't collide, and lets your siblings use relative imports +(`from .shared import x`). Don't mix a bare `import util` and `load_sibling("util")` for the same +file — that executes it twice and splits its module-level state. + +### 37. Log through `context["log"]`, never `print()` + +Use the logger the Host hands you in `context["log"]`. It carries the Host's correlation context and +lands in the rotated log stream under your plugin's namespace; `print()` bypasses both and is easy to +lose. (And, per rule 7, mount every route under `/api/plugins//…` — route paths aren't namespaced +for you, and a collision with the Host or another plugin is silent and, per rule 6, permanent.) + +--- + ## Shipping & good citizenship -### 34. Fail soft, log clearly +### 38. Fail soft, log clearly - Use `context["log"]` (server) so your messages land in the Host log under your plugin's namespace. @@ -555,25 +611,25 @@ it's a migration aid, not a contract. - If a surface can't initialise, degrade to a reduced-but-working state rather than taking the whole plugin down. -### 35. Degrade gracefully across Host versions +### 39. Degrade gracefully across Host versions A plugin may run on a Host older than the one you developed against. Don't assume a `context` key or a client runtime API exists without a documented Host version guaranteeing it. If an optional surface isn't supported, your plugin's other surfaces must still work. -### 36. Only declare capabilities you actually implement +### 40. Only declare capabilities you actually implement `capabilities` and `standards` wire you into cross-plugin pipelines (diagnostics, capability inspection). Declaring a capability you don't service registers a phantom participant and breaks the pipeline. If you don't participate, omit both keys entirely. -### 37. Mind the security boundary +### 41. Mind the security boundary Your `routes` run arbitrary Python in the server process and your `script` runs in the app's renderer. Validate every route input, don't shell out on user data, and don't reach outside your plugin directory. Users installing your plugin are trusting it like an app extension — earn it. -### 38. Ship a README and a changelog +### 42. Ship a README and a changelog A plugin folder should carry a short `README.md` (what it does, which Host version it targets) and note changes per version. It costs little and saves every future reader — including you. @@ -594,6 +650,16 @@ note changes per version. It costs little and saves every future reader — incl - [ ] Routes, CSS, and persisted files are namespaced under the `id`. - [ ] `version` is set and follows semver. +**Server-side robustness (if you ship `routes`):** + +- [ ] Python deps are in `requirements.txt`, pinned and minimal; heavy/optional imports are guarded + and degrade if missing. +- [ ] `setup()` only wires things (fast); no blocking work on the event loop — blocking handlers are + plain `def`, not `async def`. +- [ ] Server code is split via `context["load_sibling"]`, not bare `import`, and routes are + namespaced under `/api/plugins//`. +- [ ] Logging goes through `context["log"]`, never `print()`. + **Client-screen performance (if you ship a `script`):** - [ ] No `querySelector`/layout reads/style writes inside `requestAnimationFrame`, `draw()`, short