diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index 14b8006c5ee8..694f72f1fa93 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, `--acknowledge-install-policy-warning` is the explicit noninteractive approval. Every approved warning is re-evaluated before continuing. 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, `--acknowledge-install-policy-warning` is the explicit noninteractive approval. The flag is consumed by the first warning in one command; a later warning fails closed and requires interactive review. 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). This flag does not ask ClawHub to rescan the plugin or make a blocked release public. The deprecated `--dangerously-force-unsafe-install` flag remains a no-op. diff --git a/docs/cli/skills.md b/docs/cli/skills.md index de8d29fd7e15..03078701cb27 100644 --- a/docs/cli/skills.md +++ b/docs/cli/skills.md @@ -106,7 +106,9 @@ anyway` (or `update anyway`). A matching answer evaluates the staged skill again before continuing. Declined and non-interactive commands stop before commit; after review, `--acknowledge-install-policy-warning` is the explicit noninteractive approval, and every approved warning is re-evaluated before -continuing. `block` and policy failures remain terminal. The deprecated +continuing. The flag is consumed by the first warning in one command; a later +warning fails closed and requires interactive review. `block` and policy +failures remain terminal. The deprecated `--dangerously-force-unsafe-install` flag remains a no-op. Notes: diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index c5f206476a66..b95113e8b102 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 use `--acknowledge-install-policy-warning` as explicit approval after review; every approved warning is re-evaluated before continuing. `block` and policy failures remain terminal. The deprecated `--dangerously-force-unsafe-install` flag remains a no-op. + - 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 `--acknowledge-install-policy-warning` as explicit approval after review; the flag is consumed by the first warning in one command, and a later warning fails closed and requires interactive review. Every approved warning is re-evaluated before continuing. `block` and policy failures remain terminal. The deprecated `--dangerously-force-unsafe-install` flag remains a no-op. Details: [Plugins](/tools/plugin) diff --git a/docs/tools/skills-config.md b/docs/tools/skills-config.md index be4a95fb6aff..d9319b8cdd46 100644 --- a/docs/tools/skills-config.md +++ b/docs/tools/skills-config.md @@ -183,6 +183,8 @@ same `install anyway` or `update anyway` copy as suspicious ClawHub releases, then run policy again before continuing. Declined and non-interactive commands may use `--acknowledge-install-policy-warning` as explicit approval after review; every approved warning is re-evaluated before continuing. +The flag is consumed by the first warning in one command; a later warning +fails closed and requires interactive review. Gateway-backed and automatic installs remain blocked on warnings because they have no operator-confirmation flow. The deprecated `--dangerously-force-unsafe-install` flag remains a no-op. A `block`, non-zero diff --git a/src/cli/install-policy-warning-acknowledgement.test.ts b/src/cli/install-policy-warning-acknowledgement.test.ts index d43e91c54c3f..788ea32444e2 100644 --- a/src/cli/install-policy-warning-acknowledgement.test.ts +++ b/src/cli/install-policy-warning-acknowledgement.test.ts @@ -98,6 +98,13 @@ describe("resolveInstallPolicyWarningAcknowledgementCliOptions", () => { requestMode: "install", }), ).resolves.toBe(true); + await expect( + options.onInstallPolicyWarning?.({ + targetName: "demo-dependency", + targetType: "plugin", + requestMode: "install", + }), + ).resolves.toBe(false); expect(promptTextMock).not.toHaveBeenCalled(); }); }); diff --git a/src/cli/install-policy-warning-acknowledgement.ts b/src/cli/install-policy-warning-acknowledgement.ts index 0f6d7d7df2dd..6a6c315a3d8e 100644 --- a/src/cli/install-policy-warning-acknowledgement.ts +++ b/src/cli/install-policy-warning-acknowledgement.ts @@ -18,10 +18,19 @@ export function resolveInstallPolicyWarningAcknowledgementCliOptions(params: { !params.acknowledgeInstallPolicyWarning && params.allowPrompt !== false && canPromptForInstallPolicyWarning(); + let explicitAcknowledgementAvailable = params.acknowledgeInstallPolicyWarning === true; return { ...(params.dangerouslyForceUnsafeInstall ? { dangerouslyForceUnsafeInstall: true } : {}), ...(params.acknowledgeInstallPolicyWarning - ? { onInstallPolicyWarning: async () => true } + ? { + onInstallPolicyWarning: async () => { + if (!explicitAcknowledgementAvailable) { + return false; + } + explicitAcknowledgementAvailable = false; + return true; + }, + } : canPrompt ? { onInstallPolicyWarning: async (request: InstallPolicyWarningAcknowledgementRequest) => { diff --git a/src/cli/plugins-cli.install.test.ts b/src/cli/plugins-cli.install.test.ts index aadc9e6c5539..d4b8937399b6 100644 --- a/src/cli/plugins-cli.install.test.ts +++ b/src/cli/plugins-cli.install.test.ts @@ -1644,7 +1644,24 @@ describe("plugins cli install", () => { "--acknowledge-install-policy-warning", ]); - expect(clawHubInstallCall().onInstallPolicyWarning).toEqual(expect.any(Function)); + const acknowledgement = clawHubInstallCall().onInstallPolicyWarning; + if (typeof acknowledgement !== "function") { + throw new Error("expected ClawHub install-policy acknowledgement callback"); + } + await expect( + acknowledgement({ + targetName: "demo", + targetType: "plugin", + requestMode: "install", + }), + ).resolves.toBe(true); + await expect( + acknowledgement({ + targetName: "demo-dependency", + targetType: "plugin", + requestMode: "install", + }), + ).resolves.toBe(false); }); it("does not report a ClawHub install when durable persistence fails", async () => { diff --git a/src/cli/plugins-cli.update.test.ts b/src/cli/plugins-cli.update.test.ts index 38bc89c8dd8f..a8669c77a4ae 100644 --- a/src/cli/plugins-cli.update.test.ts +++ b/src/cli/plugins-cli.update.test.ts @@ -1457,6 +1457,55 @@ describe("plugins cli update", () => { expect(updateParams.onInstallPolicyWarning).toEqual(expect.any(Function)); }); + it("shares one noninteractive install-policy acknowledgement across bulk plugin and hook updates", async () => { + setTty(false); + const config = createTrackedPluginConfig({ + pluginId: "openclaw-codex-app-server", + spec: "openclaw-codex-app-server", + }); + loadConfig.mockReturnValue(config); + setInstalledPluginIndexInstallRecords(config.plugins?.installs ?? {}); + setHookInstallRecords({ + "demo-hooks": { + source: "npm", + spec: "@acme/demo-hooks@1.0.0", + installPath: "/tmp/hooks/demo-hooks", + }, + }); + primePluginUpdate(config); + updateNpmInstalledHookPacks.mockResolvedValue({ + config, + changed: false, + outcomes: [], + }); + + await runPluginsCommand(["plugins", "update", "--all", "--acknowledge-install-policy-warning"]); + + const pluginAcknowledgement = + expectSingleCallParams(updateNpmInstalledPlugins).onInstallPolicyWarning; + const hookAcknowledgement = expectSingleCallParams( + updateNpmInstalledHookPacks, + ).onInstallPolicyWarning; + if (typeof pluginAcknowledgement !== "function") { + throw new Error("expected plugin install-policy acknowledgement callback"); + } + expect(hookAcknowledgement).toBe(pluginAcknowledgement); + await expect( + pluginAcknowledgement({ + targetName: "openclaw-codex-app-server", + targetType: "plugin", + requestMode: "update", + }), + ).resolves.toBe(true); + await expect( + pluginAcknowledgement({ + targetName: "demo-hooks", + targetType: "plugin", + requestMode: "update", + }), + ).resolves.toBe(false); + }); + it("writes updated config when updater reports changes", async () => { const cfg = { plugins: { diff --git a/src/cli/plugins-update-command.ts b/src/cli/plugins-update-command.ts index cf28e1a79be3..dd99723df840 100644 --- a/src/cli/plugins-update-command.ts +++ b/src/cli/plugins-update-command.ts @@ -342,6 +342,11 @@ async function runPluginUpdateCommandUnlocked(params: RunPluginUpdateCommandPara } } + const installPolicyWarningAcknowledgement = resolveInstallPolicyWarningAcknowledgementCliOptions({ + acknowledgeInstallPolicyWarning: params.opts.acknowledgeInstallPolicyWarning, + dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall, + allowPrompt: !params.opts.dryRun, + }); const pluginResult = pluginSelection.pluginIds.length > 0 ? await updateNpmInstalledPlugins({ @@ -353,11 +358,7 @@ async function runPluginUpdateCommandUnlocked(params: RunPluginUpdateCommandPara officialPluginUpdateChannel, syncOfficialPluginInstalls: params.opts.all ? true : undefined, coreVersion: VERSION, - ...resolveInstallPolicyWarningAcknowledgementCliOptions({ - acknowledgeInstallPolicyWarning: params.opts.acknowledgeInstallPolicyWarning, - dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall, - allowPrompt: !params.opts.dryRun, - }), + ...installPolicyWarningAcknowledgement, ...resolveClawHubRiskAcknowledgementCliOptions({ acknowledgeClawHubRisk: params.opts.acknowledgeClawHubRisk, action: "updating", @@ -384,11 +385,7 @@ async function runPluginUpdateCommandUnlocked(params: RunPluginUpdateCommandPara hookSelection.hookIds.length > 0 ? await updateNpmInstalledHookPacks({ config: pluginResult.config, - ...resolveInstallPolicyWarningAcknowledgementCliOptions({ - acknowledgeInstallPolicyWarning: params.opts.acknowledgeInstallPolicyWarning, - dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall, - allowPrompt: !params.opts.dryRun, - }), + ...installPolicyWarningAcknowledgement, hookIds: hookSelection.hookIds, specOverrides: hookSelection.specOverrides, dryRun: params.opts.dryRun,