plugins: a plugin's tasks are read where they live
the last piece of docs/plugin-capabilities.md. skills, affordances and notification catalogues already loaded from each installed and enabled plugin's own directory; tasks did not, so the transcribe plugin shipped a task nothing could see — right-clicking an .m4a offered no Run Task at all. it turned out to be one function rather than several. every path that runs a task resolves it through getTaskByDirName and then reads implementation off the record, so the sources go in there and in listTasks and a plugin's task is runnable everywhere at once, identity unchanged: still a directory name, still what pipeline_jobs stores. the record carries `plugin` and that one field does the rest. updateTask and deleteTask throw on it, DELETE /api/tasks/:name answers 409 naming the plugin, and the capability screen shows whose it is instead of an edit and a delete that cannot work. without the delete guard it was quietly a no-op: rm -rf on a path in the owner's store that does not exist, then ok: true, and the task still in the menu. owner's store first, so a plugin cannot take over a name already in use. getTaskByDirName now refuses a name containing a separator. it always joined a caller-supplied string onto a path — from a websocket message and a job row — and searching several roots is the moment that stops being one directory nobody could escape. capabilityRootsFrom is gone; it was dead. capabilitySources replaces it and carries the provenance, with capabilityRoots the thin map over it that the loaders that only want paths still use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# A plugin's capabilities are LOADED, never copied
|
||||
|
||||
**Status: designed, not built.** What exists today is the copying this replaces.
|
||||
**Status: built.** Skills, affordances and notification catalogues landed in `2026.08.23.06`; tasks — the
|
||||
read-write one, and the last piece — landed on 2026-08-24. The four publishers are gone.
|
||||
|
||||
## The problem, as it is on disk right now
|
||||
|
||||
@@ -32,8 +33,8 @@ plugins/<name>/ the plugin's own repository, cloned on instal
|
||||
```
|
||||
|
||||
Nothing is copied. Nothing needs a namespace to keep the two apart, because they are already apart —
|
||||
which is the whole reason this beats scoping the paths. A namespace makes provenance *readable*; separate
|
||||
directories make it *structural*.
|
||||
which is the whole reason this beats scoping the paths. A namespace makes provenance _readable_; separate
|
||||
directories make it _structural_.
|
||||
|
||||
Four consequences, and each of them is a bug that stops existing:
|
||||
|
||||
@@ -64,6 +65,21 @@ silently writes into a directory the next `plugins:update` overwrites.
|
||||
A task's directory name is also stored data — `taskDirName` in `pipeline_jobs` — so lookup by name has to
|
||||
search every source rather than assuming one root.
|
||||
|
||||
**How it went in, since it turned out to be one function rather than several.** Every path that runs a
|
||||
task — the file browser's context menu, both runner sockets, the job queue, the pipeline executor —
|
||||
resolves it through `getTaskByDirName` and then reads `implementation` off the record. So the sources go
|
||||
in there and in `listTasks`, and a plugin's task becomes runnable everywhere at once with its identity
|
||||
unchanged: still a directory name, still what `pipeline_jobs` stores.
|
||||
|
||||
The record carries `plugin` — the app name, or null for the owner's own. That one field does the rest:
|
||||
`updateTask` and `deleteTask` throw on it, `DELETE /api/tasks/:name` answers 409 naming the plugin, and
|
||||
the capability screen replaces the edit and delete controls with a line saying whose it is. Without the
|
||||
delete guard the old code was quietly a no-op — `rm -rf` on a path in the owner's store that does not
|
||||
exist, then `{ ok: true }`, and the task still in the menu.
|
||||
|
||||
`getTaskByDirName` also refuses a name containing a separator. It always joined a caller-supplied string
|
||||
onto a path; searching several roots is the moment that stops being one directory nobody could escape.
|
||||
|
||||
**The four publishers are deleted**, along with their install and uninstall wiring:
|
||||
`affordances/publish.ts`, `notifications/publish.ts`, `skills/publish.ts`, `api/tasks/publish.ts`. The
|
||||
last was written on 2026-08-23 and is superseded before it ever shipped.
|
||||
@@ -87,6 +103,7 @@ what happens today and needs no change. Scoping them is a separate question and
|
||||
force it either way.
|
||||
|
||||
**Whether a plugin's capabilities should be namespaced among themselves.** Two plugins can still ship a
|
||||
task with the same directory name, and the loader has to break that tie — first-come, as the items store
|
||||
has always done, or by qualifying with the plugin name at read time. Worth settling when a second plugin
|
||||
ships a task; not before.
|
||||
task with the same directory name. The tie-break shipped as first-come with the owner's store searched
|
||||
first, which settles the case that matters — a plugin cannot take over a name its owner is already
|
||||
using — and leaves two plugins colliding with each other resolved by discovery order, which is arbitrary.
|
||||
Worth revisiting when a second plugin ships a task; not before.
|
||||
|
||||
Reference in New Issue
Block a user