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:
@@ -15,6 +15,7 @@ tasksRouter.get('/', async (ctx) => {
|
||||
category: row.category,
|
||||
triggers: (row.trigger as TriggerConfig[]) ?? [],
|
||||
mode: row.mode ?? 'agentic',
|
||||
plugin: row.plugin,
|
||||
}));
|
||||
|
||||
return ctx.json(tasks);
|
||||
@@ -47,6 +48,7 @@ tasksRouter.get('/:name', async (ctx) => {
|
||||
config: task.config,
|
||||
version: task.version,
|
||||
filePath: task.filePath,
|
||||
plugin: task.plugin,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +76,8 @@ tasksRouter.delete('/:name', async (ctx) => {
|
||||
|
||||
const task = await getTaskByDirName(name);
|
||||
if (!task) return ctx.text('Not found', 404);
|
||||
// A plugin's task is its own file in its own checkout. Uninstalling the plugin is how it goes away.
|
||||
if (task.plugin) return ctx.text(`${name} belongs to the ${task.plugin} plugin — uninstall it instead`, 409);
|
||||
|
||||
await deleteTask(task.dirName);
|
||||
return ctx.json({ ok: true });
|
||||
|
||||
Reference in New Issue
Block a user