From e057d9570eec072d8b745ebe5f37dfe33552f6e0 Mon Sep 17 00:00:00 2001 From: Jesse Merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:12:54 +1000 Subject: [PATCH] fix(security): re-evaluate forced policy warnings --- docs/cli/plugins.md | 2 +- docs/cli/skills.md | 5 +++-- docs/gateway/security/index.md | 2 +- docs/tools/skills-config.md | 3 ++- .../install-security-scan.runtime.test.ts | 18 ++++++++++++++++++ src/plugins/install-security-scan.runtime.ts | 18 ++++++++---------- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index ac4e52dd4d99..8e45c707d6fc 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -198,7 +198,7 @@ non-npm sources are not rewritten. `--pin` applies to npm installs only and records the resolved exact `@`. It is not supported with `git:` installs (pin the ref in the spec instead, e.g. `git:github.com/acme/plugin@v1.2.3`) or with `--marketplace` (marketplace installs persist marketplace source metadata instead of an npm spec). - When `security.installPolicy` returns `warn` in an interactive terminal, OpenClaw prints the reason and findings, then uses the same acknowledgement copy as a suspicious ClawHub release: `type: '' to install anyway`. A matching answer re-evaluates the staged source before continuing. A declined or non-interactive install stops before commit; after review, it can be rerun with `--dangerously-force-unsafe-install`. Neither form overrides `block` or a policy failure. + When `security.installPolicy` returns `warn` in an interactive terminal, OpenClaw prints the reason and findings, then uses the same acknowledgement copy as a suspicious ClawHub release: `type: '' to install anyway`. A matching answer re-evaluates the staged source before continuing. A declined or non-interactive install stops before commit; after review, `--dangerously-force-unsafe-install` is the explicit noninteractive approval. Every approved warning is re-evaluated before continuing. Neither form overrides `block` or a policy failure. 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. diff --git a/docs/cli/skills.md b/docs/cli/skills.md index 367d2f5b8102..9349b4bfb42b 100644 --- a/docs/cli/skills.md +++ b/docs/cli/skills.md @@ -104,8 +104,9 @@ When `security.installPolicy` returns `warn` in an interactive terminal, OpenClaw prints the reason and findings, then asks `type: '' to install anyway` (or `update anyway`). A matching answer evaluates the staged skill again before continuing. Declined and non-interactive commands stop before -commit; after review, rerun with `--dangerously-force-unsafe-install`. `block` -and policy failures remain terminal. +commit; after review, `--dangerously-force-unsafe-install` is the explicit +noninteractive approval, and every approved warning is re-evaluated before +continuing. `block` and policy failures remain terminal. Notes: diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index 7290209e9281..8a3369bc43e5 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -340,7 +340,7 @@ Plugins run in-process with the Gateway - treat them as trusted code. - npm and git plugin installs run package-manager dependency convergence only during the explicit install/update flow. Local paths and archives are treated as self-contained packages; OpenClaw copies/references them without running `npm install`. - Prefer pinned exact versions (`@scope/pkg@1.2.3`) and inspect the unpacked code before enabling. - `security.installPolicy` lets operators run a trusted local command to return `allow`, `warn`, or `block` for skill and plugin installs. It runs after source material is staged but before install continues and applies to ClawHub skills too. - - A `warn` result stops before commit. Interactive CLI commands ask the operator to type the plugin or skill name using the same wording as suspicious ClawHub releases, then re-evaluate policy before continuing. Declined and non-interactive commands can be rerun with `--dangerously-force-unsafe-install` after review. `block` and policy failures remain terminal. + - A `warn` result stops before commit. Interactive CLI commands ask the operator to type the plugin or skill name using the same wording as suspicious ClawHub releases, then re-evaluate policy before continuing. Declined and non-interactive commands can use `--dangerously-force-unsafe-install` as explicit approval after review; every approved warning is re-evaluated before continuing. `block` and policy failures remain terminal. Details: [Plugins](/tools/plugin) diff --git a/docs/tools/skills-config.md b/docs/tools/skills-config.md index 62076a4ce0a4..9c14e4ab8c9a 100644 --- a/docs/tools/skills-config.md +++ b/docs/tools/skills-config.md @@ -181,7 +181,8 @@ write one JSON object on stdout with an `allow`, `warn`, or `block` decision. plugin and skill commands ask the operator to type the target name using the same `install anyway` or `update anyway` copy as suspicious ClawHub releases, then run policy again before continuing. Declined and non-interactive commands -may be rerun with `--dangerously-force-unsafe-install` after review. +may use `--dangerously-force-unsafe-install` as explicit approval after review; +every approved warning is re-evaluated before continuing. Gateway-backed and automatic installs remain blocked on warnings because they have no operator-confirmation flow. A `block`, non-zero exit, timeout, malformed JSON, missing field, or unsupported protocol version diff --git a/src/plugins/install-security-scan.runtime.test.ts b/src/plugins/install-security-scan.runtime.test.ts index 7dd5cab10c33..60615d0131ae 100644 --- a/src/plugins/install-security-scan.runtime.test.ts +++ b/src/plugins/install-security-scan.runtime.test.ts @@ -393,6 +393,24 @@ describe("legacy file install scan compatibility", () => { }), }); expect(acknowledgedAttempt).toBeUndefined(); + expect(runInstallPolicyMock).toHaveBeenCalledTimes(3); + }); + + it("keeps a block from forced policy re-evaluation terminal", async () => { + runInstallPolicyMock + .mockResolvedValueOnce({ warning: { reason: "review this plugin" } }) + .mockResolvedValueOnce({ + blocked: { code: "security_scan_blocked", reason: "now blocked" }, + }); + + const result = await scanFileInstallSourceRuntime({ + dangerouslyForceUnsafeInstall: true, + filePath: "/tmp/payload.js", + logger: {}, + pluginId: "payload", + }); + + expect(result?.blocked).toEqual({ code: "security_scan_blocked", reason: "now blocked" }); expect(runInstallPolicyMock).toHaveBeenCalledTimes(2); }); diff --git a/src/plugins/install-security-scan.runtime.ts b/src/plugins/install-security-scan.runtime.ts index b8b9eea852fa..d7a72c2aeab4 100644 --- a/src/plugins/install-security-scan.runtime.ts +++ b/src/plugins/install-security-scan.runtime.ts @@ -739,11 +739,7 @@ async function runOperatorInstallPolicy(params: { logPolicyResult(result); return undefined; } - if (params.dangerouslyForceUnsafeInstall) { - logPolicyResult(result); - return undefined; - } - if (!params.onInstallPolicyWarning) { + if (!params.dangerouslyForceUnsafeInstall && !params.onInstallPolicyWarning) { return { blocked: { code: "security_scan_blocked", @@ -763,11 +759,13 @@ async function runOperatorInstallPolicy(params: { }; } logPolicyResult(result); - const acknowledged = await params.onInstallPolicyWarning({ - targetName: params.targetName, - targetType: params.targetType, - requestMode: params.requestMode, - }); + const acknowledged = + params.dangerouslyForceUnsafeInstall || + (await params.onInstallPolicyWarning?.({ + targetName: params.targetName, + targetType: params.targetType, + requestMode: params.requestMode, + })); if (acknowledged) { const reevaluated = await evaluatePolicy(); if (reevaluated?.blocked) {