From 3cf06f7939578541120712947a7d6b30561b4477 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 13:15:46 +0000 Subject: [PATCH] docs(plugins): clarify workspace shadowing --- docs/tools/plugin.md | 18 ++++++++++++++++++ src/plugins/loader.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 7dd6a045c15..5455bb2b38d 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -85,6 +85,13 @@ Implications: Use allowlists and explicit install/load paths for non-bundled plugins. Treat workspace plugins as development-time code, not production defaults. +Important trust note: + +- `plugins.allow` trusts **plugin ids**, not source provenance. +- A workspace plugin with the same id as a bundled plugin intentionally shadows + the bundled copy when that workspace plugin is enabled/allowlisted. +- This is normal and useful for local development, patch testing, and hotfixes. + ## Available plugins (official) - Microsoft Teams is plugin-only as of 2026.1.15; install `@openclaw/msteams` if you use Teams. @@ -363,6 +370,14 @@ manifest. If multiple plugins resolve to the same id, the first match in the order above wins and lower-precedence copies are ignored. +That means: + +- workspace plugins intentionally shadow bundled plugins with the same id +- `plugins.allow: ["foo"]` authorizes the active `foo` plugin by id, even when + the active copy comes from the workspace instead of the bundled extension root +- if you need stricter provenance control, use explicit install/load paths and + inspect the resolved plugin source before enabling it + ### Enablement rules Enablement is resolved after discovery: @@ -372,6 +387,7 @@ Enablement is resolved after discovery: - `plugins.entries..enabled: false` disables that plugin - workspace-origin plugins are disabled by default - allowlists restrict the active set when `plugins.allow` is non-empty +- allowlists are **id-based**, not source-based - bundled plugins are disabled by default unless: - the bundled id is in the built-in default-on set, or - you explicitly enable it, or @@ -1322,6 +1338,8 @@ Plugins run in-process with the Gateway. Treat them as trusted code: - Only install plugins you trust. - Prefer `plugins.allow` allowlists. +- Remember that `plugins.allow` is id-based, so an enabled workspace plugin can + intentionally shadow a bundled plugin with the same id. - Restart the Gateway after changes. ## Testing plugins diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 95b790b69fd..031d75b31b7 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1528,6 +1528,44 @@ describe("loadOpenClawPlugins", () => { expect(workspacePlugin?.status).toBe("loaded"); }); + it("lets an explicitly trusted workspace plugin shadow a bundled plugin with the same id", () => { + const bundledDir = makeTempDir(); + writePlugin({ + id: "shadowed", + body: `module.exports = { id: "shadowed", register() {} };`, + dir: bundledDir, + filename: "index.cjs", + }); + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; + + const workspaceDir = makeTempDir(); + const workspaceExtDir = path.join(workspaceDir, ".openclaw", "extensions", "shadowed"); + mkdirSafe(workspaceExtDir); + writePlugin({ + id: "shadowed", + body: `module.exports = { id: "shadowed", register() {} };`, + dir: workspaceExtDir, + filename: "index.cjs", + }); + + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir, + config: { + plugins: { + enabled: true, + allow: ["shadowed"], + }, + }, + }); + + const entries = registry.plugins.filter((entry) => entry.id === "shadowed"); + const loaded = entries.find((entry) => entry.status === "loaded"); + const overridden = entries.find((entry) => entry.status === "disabled"); + expect(loaded?.origin).toBe("workspace"); + expect(overridden?.origin).toBe("bundled"); + }); + it("warns when loaded non-bundled plugin has no install/load-path provenance", () => { useNoBundledPlugins(); const stateDir = makeTempDir();