mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-22 14:17:09 +00:00
Plugins: allow unsafe-force override on update
This commit is contained in:
committed by
Peter Steinberger
parent
824ff335c6
commit
c4f40c3f7d
@@ -168,6 +168,75 @@ openclaw hooks enable <hook-name>
|
||||
|
||||
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `<workspace>/memory/YYYY-MM-DD-slug.md`. Requires `workspace.dir` to be configured.
|
||||
|
||||
- **`tool_result_persist`**: transform tool results before they are written to the session transcript. Must be synchronous; return the updated tool result payload or `undefined` to keep it as-is. See [Agent Loop](/concepts/agent-loop).
|
||||
|
||||
### Plugin Hook Events
|
||||
|
||||
#### before_tool_call
|
||||
|
||||
Runs before each tool call. Plugins can modify parameters, block the call, or request user approval.
|
||||
|
||||
Return fields:
|
||||
|
||||
- **`params`**: Override tool parameters (merged with original params)
|
||||
- **`block`**: Set to `true` to block the tool call
|
||||
- **`blockReason`**: Reason shown to the agent when blocked
|
||||
- **`requireApproval`**: Pause execution and wait for user approval via channels
|
||||
|
||||
The `requireApproval` field triggers native platform approval (Telegram buttons, Discord components, `/approve` command) instead of relying on the agent to cooperate:
|
||||
|
||||
```typescript
|
||||
{
|
||||
requireApproval: {
|
||||
title: "Sensitive operation",
|
||||
description: "This tool call modifies production data",
|
||||
severity: "warning", // "info" | "warning" | "critical"
|
||||
timeoutMs: 120000, // default: 120s
|
||||
timeoutBehavior: "deny", // "allow" | "deny" (default)
|
||||
onResolution: async (decision) => {
|
||||
// Called after the user resolves: "allow-once", "allow-always", "deny", "timeout", or "cancelled"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `onResolution` callback is invoked with the final decision string after the approval resolves, times out, or is cancelled. It runs in-process within the plugin (not sent to the gateway). Use it to persist decisions, update caches, or perform cleanup.
|
||||
|
||||
The `pluginId` field is stamped automatically by the hook runner from the plugin registration. When multiple plugins return `requireApproval`, the first one (highest priority) wins.
|
||||
|
||||
`block` takes precedence over `requireApproval`: if the merged hook result has both `block: true` and a `requireApproval` field, the tool call is blocked immediately without triggering the approval flow. This ensures a higher-priority plugin's block cannot be overridden by a lower-priority plugin's approval request.
|
||||
|
||||
If the gateway is unavailable or does not support plugin approvals, the tool call falls back to a soft block using the `description` as the block reason.
|
||||
|
||||
#### before_install
|
||||
|
||||
Runs after the built-in install security scan and before installation continues. OpenClaw fires this hook for interactive skill installs as well as plugin bundle, package, and single-file installs.
|
||||
|
||||
Default behavior differs by target type:
|
||||
|
||||
- Plugin install/update flows fail closed on built-in scan `critical` findings and scan errors unless the operator explicitly uses `openclaw plugins install --dangerously-force-unsafe-install` or `openclaw plugins update --dangerously-force-unsafe-install`.
|
||||
- Skill installs still surface built-in scan findings and scan errors as warnings and continue by default.
|
||||
|
||||
Return fields:
|
||||
|
||||
- **`findings`**: Additional scan findings to surface as warnings
|
||||
- **`block`**: Set to `true` to block the install
|
||||
- **`blockReason`**: Human-readable reason shown when blocked
|
||||
|
||||
Event fields:
|
||||
|
||||
- **`targetType`**: Install target category (`skill` or `plugin`)
|
||||
- **`targetName`**: Human-readable skill name or plugin id for the install target
|
||||
- **`sourcePath`**: Absolute path to the install target content being scanned
|
||||
- **`sourcePathKind`**: Whether the scanned content is a `file` or `directory`
|
||||
- **`origin`**: Normalized install origin when available (for example `openclaw-bundled`, `openclaw-workspace`, `plugin-bundle`, `plugin-package`, or `plugin-file`)
|
||||
- **`request`**: Provenance for the install request, including `kind`, `mode`, and optional `requestedSpecifier`
|
||||
- **`builtinScan`**: Structured result of the built-in scanner, including `status`, summary counts, findings, and optional `error`
|
||||
- **`skill`**: Skill install metadata when `targetType` is `skill`, including `installId` and the selected `installSpec`
|
||||
- **`plugin`**: Plugin install metadata when `targetType` is `plugin`, including the canonical `pluginId`, normalized `contentType`, optional `packageName` / `manifestId` / `version`, and `extensions`
|
||||
|
||||
Example event (plugin package install):
|
||||
|
||||
### bootstrap-extra-files config
|
||||
|
||||
```json
|
||||
|
||||
@@ -64,7 +64,7 @@ when the built-in scanner reports `critical` findings, but it does **not**
|
||||
bypass plugin `before_install` hook policy blocks and does **not** bypass scan
|
||||
failures.
|
||||
|
||||
This CLI flag applies to `openclaw plugins install`. Gateway-backed skill
|
||||
This CLI flag applies to plugin install/update flows. Gateway-backed skill
|
||||
dependency installs use the matching `dangerouslyForceUnsafeInstall` request
|
||||
override, while `openclaw skills install` remains a separate ClawHub skill
|
||||
download/install flow.
|
||||
@@ -185,6 +185,7 @@ openclaw plugins update <id-or-npm-spec>
|
||||
openclaw plugins update --all
|
||||
openclaw plugins update <id-or-npm-spec> --dry-run
|
||||
openclaw plugins update @openclaw/voice-call@beta
|
||||
openclaw plugins update openclaw-codex-app-server --dangerously-force-unsafe-install
|
||||
```
|
||||
|
||||
Updates apply to tracked installs in `plugins.installs` and tracked hook-pack
|
||||
@@ -203,6 +204,12 @@ When a stored integrity hash exists and the fetched artifact hash changes,
|
||||
OpenClaw prints a warning and asks for confirmation before proceeding. Use
|
||||
global `--yes` to bypass prompts in CI/non-interactive runs.
|
||||
|
||||
`--dangerously-force-unsafe-install` is also available on `plugins update` as a
|
||||
break-glass override for built-in dangerous-code scan false positives during
|
||||
plugin updates. It still does not bypass plugin `before_install` policy blocks
|
||||
or scan-failure blocking, and it only applies to plugin updates, not hook-pack
|
||||
updates.
|
||||
|
||||
### Inspect
|
||||
|
||||
```bash
|
||||
|
||||
@@ -477,12 +477,12 @@ Plugins run **in-process** with the Gateway. Treat them as trusted code:
|
||||
- Prefer explicit `plugins.allow` allowlists.
|
||||
- Review plugin config before enabling.
|
||||
- Restart the Gateway after plugin changes.
|
||||
- If you install plugins (`openclaw plugins install <package>`), treat it like running untrusted code:
|
||||
- If you install or update plugins (`openclaw plugins install <package>`, `openclaw plugins update <id>`), treat it like running untrusted code:
|
||||
- The install path is the per-plugin directory under the active plugin install root.
|
||||
- OpenClaw runs a built-in dangerous-code scan before install. `critical` findings block by default.
|
||||
- OpenClaw runs a built-in dangerous-code scan before install/update. `critical` findings block by default.
|
||||
- OpenClaw uses `npm pack` and then runs `npm install --omit=dev` in that directory (npm lifecycle scripts can execute code during install).
|
||||
- Prefer pinned, exact versions (`@scope/pkg@1.2.3`), and inspect the unpacked code on disk before enabling.
|
||||
- `--dangerously-force-unsafe-install` is break-glass only for built-in scan false positives. It does not bypass plugin `before_install` hook policy blocks and does not bypass scan failures.
|
||||
- `--dangerously-force-unsafe-install` is break-glass only for built-in scan false positives on plugin install/update flows. It does not bypass plugin `before_install` hook policy blocks and does not bypass scan failures.
|
||||
- Gateway-backed skill dependency installs follow the same dangerous/suspicious split: built-in `critical` findings block unless the caller explicitly sets `dangerouslyForceUnsafeInstall`, while suspicious findings still warn only. `openclaw skills install` remains the separate ClawHub skill download/install flow.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
|
||||
@@ -213,6 +213,7 @@ openclaw plugins install <path> # install from local path
|
||||
openclaw plugins install -l <path> # link (no copy) for dev
|
||||
openclaw plugins install <spec> --dangerously-force-unsafe-install
|
||||
openclaw plugins update <id> # update one plugin
|
||||
openclaw plugins update <id> --dangerously-force-unsafe-install
|
||||
openclaw plugins update --all # update all
|
||||
|
||||
openclaw plugins enable <id>
|
||||
@@ -220,14 +221,14 @@ openclaw plugins disable <id>
|
||||
```
|
||||
|
||||
`--dangerously-force-unsafe-install` is a break-glass override for false
|
||||
positives from the built-in dangerous-code scanner. It allows installs to
|
||||
continue past built-in `critical` findings, but it still does not bypass plugin
|
||||
`before_install` policy blocks or scan-failure blocking.
|
||||
positives from the built-in dangerous-code scanner. It allows plugin installs
|
||||
and plugin updates to continue past built-in `critical` findings, but it still
|
||||
does not bypass plugin `before_install` policy blocks or scan-failure blocking.
|
||||
|
||||
This CLI flag applies to plugin installs only. Gateway-backed skill dependency
|
||||
installs use the matching `dangerouslyForceUnsafeInstall` request override
|
||||
instead, while `openclaw skills install` remains the separate ClawHub skill
|
||||
download/install flow.
|
||||
This CLI flag applies to plugin install/update flows only. Gateway-backed skill
|
||||
dependency installs use the matching `dangerouslyForceUnsafeInstall` request
|
||||
override instead, while `openclaw skills install` remains the separate ClawHub
|
||||
skill download/install flow.
|
||||
|
||||
See [`openclaw plugins` CLI reference](/cli/plugins) for full details.
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ vi.mock("../infra/clawhub.js", () => ({
|
||||
|
||||
const { registerPluginsCli } = await import("./plugins-cli.js");
|
||||
|
||||
export { registerPluginsCli };
|
||||
|
||||
export function runPluginsCommand(argv: string[]) {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
|
||||
@@ -53,6 +53,7 @@ export type PluginInspectOptions = {
|
||||
export type PluginUpdateOptions = {
|
||||
all?: boolean;
|
||||
dryRun?: boolean;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
};
|
||||
|
||||
export type PluginMarketplaceListOptions = {
|
||||
@@ -799,6 +800,11 @@ export function registerPluginsCli(program: Command) {
|
||||
.argument("[id]", "Plugin or hook-pack id (omit with --all)")
|
||||
.option("--all", "Update all tracked plugins and hook packs", false)
|
||||
.option("--dry-run", "Show what would change without writing", false)
|
||||
.option(
|
||||
"--dangerously-force-unsafe-install",
|
||||
"Bypass built-in dangerous-code update blocking for plugins (plugin hooks may still block)",
|
||||
false,
|
||||
)
|
||||
.action(async (id: string | undefined, opts: PluginUpdateOptions) => {
|
||||
await runPluginUpdateCommand({ id, opts });
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
loadConfig,
|
||||
registerPluginsCli,
|
||||
resetPluginsCliTestState,
|
||||
runPluginsCommand,
|
||||
runtimeErrors,
|
||||
@@ -11,11 +13,43 @@ import {
|
||||
writeConfigFile,
|
||||
} from "./plugins-cli-test-helpers.js";
|
||||
|
||||
function createTrackedPluginConfig(params: {
|
||||
pluginId: string;
|
||||
spec: string;
|
||||
resolvedName?: string;
|
||||
}): OpenClawConfig {
|
||||
return {
|
||||
plugins: {
|
||||
installs: {
|
||||
[params.pluginId]: {
|
||||
source: "npm",
|
||||
spec: params.spec,
|
||||
installPath: `/tmp/${params.pluginId}`,
|
||||
...(params.resolvedName ? { resolvedName: params.resolvedName } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
describe("plugins cli update", () => {
|
||||
beforeEach(() => {
|
||||
resetPluginsCliTestState();
|
||||
});
|
||||
|
||||
it("shows the dangerous unsafe install override in update help", () => {
|
||||
const program = new Command();
|
||||
registerPluginsCli(program);
|
||||
|
||||
const pluginsCommand = program.commands.find((command) => command.name() === "plugins");
|
||||
const updateCommand = pluginsCommand?.commands.find((command) => command.name() === "update");
|
||||
const helpText = updateCommand?.helpInformation() ?? "";
|
||||
|
||||
expect(helpText).toContain("--dangerously-force-unsafe-install");
|
||||
expect(helpText).toContain("Bypass built-in dangerous-code update");
|
||||
expect(helpText).toContain("blocking for plugins");
|
||||
});
|
||||
|
||||
it("updates tracked hook packs through plugins update", async () => {
|
||||
const cfg = {
|
||||
hooks: {
|
||||
@@ -203,6 +237,34 @@ describe("plugins cli update", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes dangerous force unsafe install to plugin updates", async () => {
|
||||
const config = createTrackedPluginConfig({
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
spec: "openclaw-codex-app-server@beta",
|
||||
});
|
||||
loadConfig.mockReturnValue(config);
|
||||
updateNpmInstalledPlugins.mockResolvedValue({
|
||||
config,
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand([
|
||||
"plugins",
|
||||
"update",
|
||||
"openclaw-codex-app-server",
|
||||
"--dangerously-force-unsafe-install",
|
||||
]);
|
||||
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config,
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
dangerouslyForceUnsafeInstall: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps using the recorded npm tag when update is invoked by plugin id", async () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
|
||||
@@ -89,7 +89,7 @@ function resolveHookPackUpdateSelection(params: {
|
||||
|
||||
export async function runPluginUpdateCommand(params: {
|
||||
id?: string;
|
||||
opts: { all?: boolean; dryRun?: boolean };
|
||||
opts: { all?: boolean; dryRun?: boolean; dangerouslyForceUnsafeInstall?: boolean };
|
||||
}) {
|
||||
const sourceSnapshotPromise = readConfigFileSnapshot().catch(() => null);
|
||||
const cfg = loadConfig();
|
||||
@@ -122,6 +122,7 @@ export async function runPluginUpdateCommand(params: {
|
||||
pluginIds: pluginSelection.pluginIds,
|
||||
specOverrides: pluginSelection.specOverrides,
|
||||
dryRun: params.opts.dryRun,
|
||||
dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall,
|
||||
logger,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
const specLabel = drift.resolvedSpec ?? drift.spec;
|
||||
|
||||
@@ -605,6 +605,32 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
marketplacePlugin: "claude-bundle",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards dangerous force unsafe install to plugin update installers", async () => {
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
targetDir: "/tmp/openclaw-codex-app-server",
|
||||
version: "0.2.0-beta.4",
|
||||
}),
|
||||
);
|
||||
|
||||
await updateNpmInstalledPlugins({
|
||||
config: createCodexAppServerInstallConfig({
|
||||
spec: "openclaw-codex-app-server@beta",
|
||||
}),
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
dangerouslyForceUnsafeInstall: true,
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "openclaw-codex-app-server@beta",
|
||||
dangerouslyForceUnsafeInstall: true,
|
||||
expectedPluginId: "openclaw-codex-app-server",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncPluginsForUpdateChannel", () => {
|
||||
|
||||
@@ -258,6 +258,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
pluginIds?: string[];
|
||||
skipIds?: Set<string>;
|
||||
dryRun?: boolean;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
specOverrides?: Record<string, string>;
|
||||
onIntegrityDrift?: (params: PluginUpdateIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
}): Promise<PluginUpdateSummary> {
|
||||
@@ -359,6 +360,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
spec: effectiveSpec!,
|
||||
mode: "update",
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
expectedIntegrity,
|
||||
onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({
|
||||
@@ -375,6 +377,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
baseUrl: record.clawhubUrl,
|
||||
mode: "update",
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
logger,
|
||||
})
|
||||
@@ -383,6 +386,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
plugin: record.marketplacePlugin!,
|
||||
mode: "update",
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
logger,
|
||||
});
|
||||
@@ -456,6 +460,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
? await installPluginFromNpmSpec({
|
||||
spec: effectiveSpec!,
|
||||
mode: "update",
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
expectedIntegrity,
|
||||
onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({
|
||||
@@ -471,6 +476,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
spec: effectiveSpec ?? `clawhub:${record.clawhubPackage!}`,
|
||||
baseUrl: record.clawhubUrl,
|
||||
mode: "update",
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
logger,
|
||||
})
|
||||
@@ -478,6 +484,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
marketplace: record.marketplaceSource!,
|
||||
plugin: record.marketplacePlugin!,
|
||||
mode: "update",
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
logger,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user