diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index c05a23758e2b..e5645ff817a7 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -159,7 +159,7 @@ is available, then fall back to `latest`. `--dangerously-force-unsafe-install` is deprecated and is now a no-op. OpenClaw no longer runs built-in install-time dangerous-code blocking for plugin installs. - Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks and `security.installPolicy` can still block installs. + Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks are plugin-runtime lifecycle hooks and are not the primary policy boundary for CLI installs. If a plugin you published on ClawHub is hidden or blocked by a registry scan, use the publisher steps in [ClawHub publishing](/clawhub/publishing). `--dangerously-force-unsafe-install` does not ask ClawHub to rescan the plugin or make a blocked release public. @@ -405,7 +405,7 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked - `--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` and plugin `before_install` hooks can still block updates. + `--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` can still block updates; plugin `before_install` hooks only apply in processes where plugin hooks are loaded. diff --git a/docs/concepts/agent-loop.md b/docs/concepts/agent-loop.md index f4235ae6bedc..7adcb4403a98 100644 --- a/docs/concepts/agent-loop.md +++ b/docs/concepts/agent-loop.md @@ -97,7 +97,7 @@ These run inside the agent loop or gateway pipeline: - **`agent_end`**: inspect the final message list and run metadata after completion. - **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles. - **`before_tool_call` / `after_tool_call`**: intercept tool params/results. -- **`before_install`**: inspect install context and optionally block skill or plugin installs after operator install policy runs. +- **`before_install`**: inspect staged skill or plugin install material after operator install policy runs, when plugin hooks are loaded in the current OpenClaw process. - **`tool_result_persist`**: synchronously transform tool results before they are written to an OpenClaw-owned session transcript. - **`message_received` / `message_sending` / `message_sent`**: inbound + outbound message hooks. - **`session_start` / `session_end`**: session lifecycle boundaries. @@ -109,6 +109,7 @@ Hook decision rules for outbound/tool guards: - `before_tool_call`: `{ block: false }` is a no-op and does not clear a prior block. - `before_install`: `{ block: true }` is terminal and stops lower-priority handlers. - `before_install`: `{ block: false }` is a no-op and does not clear a prior block. +- Use `security.installPolicy`, not `before_install`, for operator-owned install allow/block decisions that must cover CLI install and update paths. - `message_sending`: `{ cancel: true }` is terminal and stops lower-priority handlers. - `message_sending`: `{ cancel: false }` is a no-op and does not clear a prior cancel. diff --git a/docs/plugins/hooks.md b/docs/plugins/hooks.md index d18d9dc68a83..374b6f9e19a8 100644 --- a/docs/plugins/hooks.md +++ b/docs/plugins/hooks.md @@ -152,7 +152,8 @@ observation-only. - `gateway_start` / `gateway_stop` - start or stop plugin-owned services with the Gateway - `deactivate` - deprecated compatibility alias for `gateway_stop`; use `gateway_stop` in new plugins - `cron_changed` - observe gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled) -- **`before_install`** - inspect skill or plugin install context and optionally block +- **`before_install`** - inspect staged skill or plugin install material from a loaded + plugin runtime ## Debug runtime hooks @@ -462,11 +463,19 @@ Decision rules: ## Install hooks -`before_install` runs after the operator-owned `security.installPolicy` check -when one is configured. The `builtinScan` field remains in the event payload for -compatibility, but OpenClaw no longer runs built-in install-time dangerous-code -blocking, so it is an empty `ok` result. Return additional findings or -`{ block: true, blockReason }` to stop the install. +Use `security.installPolicy` for operator-owned allow/block decisions. That +policy runs from OpenClaw config, covers CLI install and update paths, and fails +closed when enabled but unavailable. + +`before_install` is a plugin-runtime lifecycle hook. It runs after +`security.installPolicy` only in the OpenClaw process where plugin hooks have +already been loaded, such as Gateway-backed install flows. It is useful for +plugin-owned observations, warnings, and compatibility checks, but it is not the +primary enterprise or host security boundary for installs. The `builtinScan` +field remains in the event payload for compatibility, but OpenClaw no longer +runs built-in install-time dangerous-code blocking, so it is an empty `ok` +result. Return additional findings or `{ block: true, blockReason }` to stop the +install in that process. `block: true` is terminal. `block: false` is treated as no decision. Handler failures block the install fail-closed. diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 512209b1930f..601de43a3767 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -431,6 +431,10 @@ semantics. ### Hook decision semantics +`before_install` is a plugin-runtime lifecycle hook, not the operator install +policy surface. Use `security.installPolicy` when an allow/block decision must +cover CLI and Gateway-backed install or update paths. + - `before_tool_call`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped. - `before_tool_call`: returning `{ block: false }` is treated as no decision (same as omitting `block`), not as an override. - `before_install`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped. diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 9ce999105d61..a38234d6957d 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -147,10 +147,12 @@ such as `@beta` stay pinned to the selected package and fail when incompatible. Configure `security.installPolicy` to run a trusted local policy command before plugin install or update proceeds. The policy receives metadata plus the staged -source path and can allow or block the install. It runs before plugin -`before_install` hooks. The deprecated `--dangerously-force-unsafe-install` -flag is accepted for compatibility but does not bypass install policy, hooks, or -OpenClaw's built-in plugin dependency denylist. +source path and can allow or block the install. It covers CLI and Gateway-backed +plugin install/update paths. Plugin `before_install` hooks run later only in +OpenClaw processes where plugin hooks are loaded, so use `security.installPolicy` +for operator-owned install decisions. The deprecated +`--dangerously-force-unsafe-install` flag is accepted for compatibility but does +not bypass install policy or OpenClaw's built-in plugin dependency denylist. See [Skills config](/tools/skills-config#operator-install-policy-securityinstallpolicy) for the shared `security.installPolicy` exec schema used by both skills and diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index f09cb0cd332e..cc0457d37488 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -153,7 +153,7 @@ export function registerPluginsCli(program: Command) { .option("--pin", "Record npm installs as exact resolved @", false) .option( "--dangerously-force-unsafe-install", - "Deprecated no-op; install policy and plugin hooks may still block", + "Deprecated no-op; security.installPolicy may still block", false, ) .option( @@ -184,7 +184,7 @@ export function registerPluginsCli(program: Command) { .option("--dry-run", "Show what would change without writing", false) .option( "--dangerously-force-unsafe-install", - "Deprecated no-op; install policy and plugin hooks may still block", + "Deprecated no-op; security.installPolicy may still block", false, ) .action(async (id: string | undefined, opts: PluginUpdateOptions) => { diff --git a/src/cli/plugins-cli.update.test.ts b/src/cli/plugins-cli.update.test.ts index c9a23a8c6db5..0870b2fc9412 100644 --- a/src/cli/plugins-cli.update.test.ts +++ b/src/cli/plugins-cli.update.test.ts @@ -78,8 +78,8 @@ describe("plugins cli update", () => { expect(helpText).toContain("--dangerously-force-unsafe-install"); expect(helpText).toContain("Deprecated no-op"); - expect(helpText).toContain("install policy and"); - expect(helpText).toContain("plugin hooks may still block"); + expect(helpText).toContain("security.installPolicy"); + expect(helpText).toContain("may still block"); }); it("refuses plugin updates in Nix mode before package-manager work", async () => { diff --git a/src/plugins/install-policy-context.ts b/src/plugins/install-policy-context.ts index 3426dffd701f..976c9fb07c9e 100644 --- a/src/plugins/install-policy-context.ts +++ b/src/plugins/install-policy-context.ts @@ -1,4 +1,4 @@ -// Builds install policy context for plugin install checks. +// Builds payloads for the plugin-runtime before_install lifecycle hook. import type { PluginHookBeforeInstallBuiltinScan, PluginHookBeforeInstallContext, diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index d8b19c62b0c1..3ee417d2bc16 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -1970,7 +1970,7 @@ describe("installPluginFromArchive", () => { ); }); - it("surfaces plugin scanner findings from before_install", async () => { + it("surfaces plugin lifecycle findings from before_install", async () => { const handler = vi.fn().mockReturnValue({ findings: [ { @@ -2052,10 +2052,10 @@ describe("installPluginFromArchive", () => { expect(requests[1]?.request.requestedSpecifier).toBe(pluginDir); }); - it("blocks plugin install when before_install rejects dangerous-looking source", async () => { + it("blocks plugin install when before_install rejects the staged source", async () => { const handler = vi.fn().mockReturnValue({ block: true, - blockReason: "Blocked by enterprise policy", + blockReason: "Blocked by plugin lifecycle hook", }); initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }])); @@ -2078,7 +2078,7 @@ describe("installPluginFromArchive", () => { expect(result.ok).toBe(false); if (!result.ok) { - expect(result.error).toBe("Blocked by enterprise policy"); + expect(result.error).toBe("Blocked by plugin lifecycle hook"); expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); } expect(handler).toHaveBeenCalledTimes(1); @@ -2098,14 +2098,14 @@ describe("installPluginFromArchive", () => { extensions: ["index.js"], }); expect( - warnings.some((w) => w.includes("blocked by plugin hook: Blocked by enterprise policy")), + warnings.some((w) => w.includes("blocked by plugin hook: Blocked by plugin lifecycle hook")), ).toBe(true); }); it("keeps before_install hook blocks even when dangerous force unsafe install is set", async () => { const handler = vi.fn().mockReturnValue({ block: true, - blockReason: "Blocked by enterprise policy", + blockReason: "Blocked by plugin lifecycle hook", }); initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }])); @@ -2132,7 +2132,7 @@ describe("installPluginFromArchive", () => { expect(result.ok).toBe(false); if (!result.ok) { - expect(result.error).toBe("Blocked by enterprise policy"); + expect(result.error).toBe("Blocked by plugin lifecycle hook"); expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); } expect( @@ -2144,7 +2144,7 @@ describe("installPluginFromArchive", () => { ).toBe(false); expect( warnings.some((warning) => - warning.includes("blocked by plugin hook: Blocked by enterprise policy"), + warning.includes("blocked by plugin hook: Blocked by plugin lifecycle hook"), ), ).toBe(true); }); diff --git a/src/skills/lifecycle/install.test.ts b/src/skills/lifecycle/install.test.ts index 457feadd7755..b12f3a140a49 100644 --- a/src/skills/lifecycle/install.test.ts +++ b/src/skills/lifecycle/install.test.ts @@ -109,7 +109,7 @@ async function withWorkspaceCase( } } -describe("installSkill install policy hooks", () => { +describe("installSkill before_install hooks", () => { beforeEach(() => { resetGlobalHookRunner(); runCommandWithTimeoutMock.mockClear(); @@ -273,7 +273,7 @@ describe("installSkill install policy hooks", () => { it("blocks install when before_install rejects the skill", async () => { const handler = vi.fn().mockReturnValue({ block: true, - blockReason: "Blocked by enterprise policy", + blockReason: "Blocked by plugin lifecycle hook", }); initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }])); @@ -287,7 +287,7 @@ describe("installSkill install policy hooks", () => { }); expect(result.ok).toBe(false); - expect(result.message).toBe("Blocked by enterprise policy"); + expect(result.message).toBe("Blocked by plugin lifecycle hook"); expect(runCommandWithTimeoutMock).not.toHaveBeenCalled(); }); });