fix(security): consume CLI policy approval once

This commit is contained in:
jesse-merhi
2026-08-11 01:37:39 +10:00
parent d6b8648a69
commit 2ebb077ae5
9 changed files with 98 additions and 15 deletions
+1 -1
View File
@@ -198,7 +198,7 @@ non-npm sources are not rewritten.
`--pin` applies to npm installs only and records the resolved exact `<name>@<version>`. 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).
</Accordion>
<Accordion title="--acknowledge-install-policy-warning">
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: '<plugin>' 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: '<plugin>' 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.
+3 -1
View File
@@ -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:
+1 -1
View File
@@ -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)
+2
View File
@@ -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
@@ -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();
});
});
@@ -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) => {
+18 -1
View File
@@ -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 () => {
+49
View File
@@ -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: {
+7 -10
View File
@@ -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,