mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(skills): provide safe workspace-specific malicious skill remediation (#118969)
This commit is contained in:
committed by
GitHub
parent
7daf990688
commit
108cec388e
+26
-3
@@ -1,8 +1,9 @@
|
||||
---
|
||||
summary: "ClawHub CLI entry points for discovering, installing, publishing, and verifying OpenClaw skills and plugins."
|
||||
summary: "ClawHub CLI entry points for discovering, installing, removing, publishing, and verifying OpenClaw skills and plugins."
|
||||
read_when:
|
||||
- You want to use ClawHub from the command line
|
||||
- You want to install ClawHub skills or plugins through OpenClaw
|
||||
- You need to remove an installed ClawHub skill
|
||||
- You want to publish ClawHub packages
|
||||
title: "ClawHub CLI"
|
||||
---
|
||||
@@ -13,8 +14,8 @@ Two command-line surfaces talk to ClawHub:
|
||||
|
||||
- `openclaw skills` / `openclaw plugins` - discover, install, and update
|
||||
packages for a local OpenClaw agent or Gateway.
|
||||
- The standalone `clawhub` CLI - publisher workflows: login, publish, sync,
|
||||
and transfer.
|
||||
- The standalone `clawhub` CLI - remove installed skills and handle publisher
|
||||
workflows including login, publish, sync, and transfer.
|
||||
|
||||
## Discover and install
|
||||
|
||||
@@ -60,6 +61,28 @@ enforces scan and force-install policy before returning a pinned commit.
|
||||
- **Official ClawHub publishers/packages and bundled OpenClaw sources** skip
|
||||
the trust prompt and security-verdict fetch entirely.
|
||||
|
||||
## Remove an installed skill
|
||||
|
||||
If the standalone ClawHub CLI is not already installed, install it explicitly:
|
||||
|
||||
```bash
|
||||
npm i -g clawhub
|
||||
clawhub uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
The command asks for confirmation, then removes the installed skill directory
|
||||
and its ClawHub lockfile entry. Select the original agent workspace or shared
|
||||
OpenClaw state directory when the installation is outside the current workdir:
|
||||
|
||||
```bash
|
||||
clawhub --workdir /path/to/agent-workspace uninstall @owner/my-skill
|
||||
clawhub --workdir ~/.openclaw uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
For a custom `OPENCLAW_STATE_DIR`, replace `~/.openclaw` with that configured
|
||||
directory. See [Remove a ClawHub skill](/cli/skills#remove-a-clawhub-skill) for
|
||||
workspace targeting and skill refresh behavior.
|
||||
|
||||
## Publish and maintain
|
||||
|
||||
Install the standalone CLI once, then log in:
|
||||
|
||||
@@ -3,6 +3,7 @@ summary: "CLI reference for `openclaw skills` (search/install/update/verify/list
|
||||
read_when:
|
||||
- You want to see which skills are available and ready to run
|
||||
- You want to search ClawHub or install skills from ClawHub, Git, or local directories
|
||||
- You need to remove an installed ClawHub skill
|
||||
- You want to verify a ClawHub skill with ClawHub
|
||||
- You want to debug missing binaries/env/config for skills
|
||||
title: "Skills"
|
||||
@@ -134,6 +135,39 @@ non-interactive command should continue after that review. Official ClawHub
|
||||
skill publishers and bundled OpenClaw skill sources bypass this release-trust
|
||||
prompt.
|
||||
|
||||
## Remove a ClawHub skill
|
||||
|
||||
Use the standalone ClawHub CLI to remove a ClawHub-tracked skill. If the CLI
|
||||
is not installed, install it explicitly first:
|
||||
|
||||
```bash
|
||||
npm i -g clawhub
|
||||
clawhub uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
The CLI asks for confirmation before deleting the skill directory and its
|
||||
`.clawhub/lock.json` entry. Use the installed skill's owner-qualified name or
|
||||
bare slug, not its original `skills-sh:` reference.
|
||||
|
||||
Select the same root where the skill was installed: the agent workspace for an
|
||||
agent-specific skill, or the OpenClaw state directory for a shared skill
|
||||
installed with `--global`:
|
||||
|
||||
```bash
|
||||
clawhub --workdir /path/to/agent-workspace uninstall @owner/my-skill
|
||||
clawhub --workdir ~/.openclaw uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
If `OPENCLAW_STATE_DIR` is set, use that configured state directory for shared
|
||||
skills instead:
|
||||
|
||||
```bash
|
||||
clawhub --workdir "$OPENCLAW_STATE_DIR" uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
The default [skills watcher](/tools/skills#snapshots-and-refresh) picks up the
|
||||
removal on the next agent turn. If watching is disabled, start a new session.
|
||||
|
||||
## Skill Workshop
|
||||
|
||||
`openclaw skills workshop` manages pending skill proposals in the selected
|
||||
|
||||
+31
-3
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: clawhub
|
||||
description: "Search ClawHub for skills when a requested capability is not already available; install, verify, update, publish, or sync skills."
|
||||
description: "Search ClawHub for skills when a requested capability is not already available; install, verify, update, uninstall, publish, or sync skills."
|
||||
---
|
||||
|
||||
# ClawHub
|
||||
|
||||
Use `openclaw skills` to discover and manage skills for the current OpenClaw
|
||||
agent. Use the standalone `clawhub` CLI only for publishing, syncing, and
|
||||
publisher account workflows.
|
||||
agent. Use the standalone `clawhub` CLI to uninstall installed ClawHub skills
|
||||
and for publishing, syncing, and publisher account workflows.
|
||||
|
||||
## Discover skills
|
||||
|
||||
@@ -38,6 +38,34 @@ openclaw skills update --all
|
||||
Use `--global` with `install` or `update` to manage skills shared by all local
|
||||
agents.
|
||||
|
||||
## Remove an installed skill
|
||||
|
||||
Obtain user approval before uninstalling. If the standalone ClawHub CLI is not
|
||||
installed, install it explicitly:
|
||||
|
||||
```bash
|
||||
npm i -g clawhub
|
||||
clawhub uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
The CLI asks for confirmation before removing the skill and its lockfile entry.
|
||||
Use the original agent workspace for agent-specific skills or the OpenClaw
|
||||
state directory for skills installed with `--global`:
|
||||
|
||||
```bash
|
||||
clawhub --workdir /path/to/agent-workspace uninstall @owner/my-skill
|
||||
clawhub --workdir ~/.openclaw uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
If `OPENCLAW_STATE_DIR` is set, use its value instead of `~/.openclaw`:
|
||||
|
||||
```bash
|
||||
clawhub --workdir "$OPENCLAW_STATE_DIR" uninstall @owner/my-skill
|
||||
```
|
||||
|
||||
The default skills watcher refreshes the available skills on the next agent
|
||||
turn. If watching is disabled, start a new session.
|
||||
|
||||
## Publish skills
|
||||
|
||||
Install the standalone ClawHub CLI for publisher workflows:
|
||||
|
||||
@@ -4,6 +4,7 @@ import { stripAnsi, visibleWidth } from "../../packages/terminal-core/src/ansi.j
|
||||
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
||||
import { formatTerminalLink } from "../../packages/terminal-core/src/terminal-link.js";
|
||||
import { theme } from "../../packages/terminal-core/src/theme.js";
|
||||
import { quoteCliArg } from "../cli/quote-cli-arg.js";
|
||||
import {
|
||||
fetchClawHubPackageSecurity,
|
||||
fetchClawHubSkillVerification,
|
||||
@@ -63,11 +64,9 @@ type ClawHubInstallLogger = {
|
||||
terminalLinks?: boolean;
|
||||
};
|
||||
|
||||
type ClawHubTrustSubject = {
|
||||
kind: "plugin" | "skill";
|
||||
packageName: string;
|
||||
ownerHandle?: string;
|
||||
};
|
||||
type ClawHubTrustSubject =
|
||||
| { kind: "plugin"; packageName: string }
|
||||
| { kind: "skill"; packageName: string; workspaceDir: string; ownerHandle?: string };
|
||||
|
||||
type ClawHubSkillSecurityLinks = {
|
||||
subject: string;
|
||||
@@ -536,6 +535,15 @@ function formatClawHubTrustWarning(params: {
|
||||
params.assessment.disposition,
|
||||
),
|
||||
formatClawHubRawLinks({ subject: params.subject, links }),
|
||||
...(params.subject.kind === "skill" && malicious && params.mode === "update"
|
||||
? [
|
||||
`Remove installed skill: clawhub --workdir ${quoteCliArg(
|
||||
sanitizeTerminalText(params.subject.workspaceDir),
|
||||
)} uninstall ${quoteCliArg(
|
||||
sanitizeTerminalText(formatClawHubSubjectPackageName(params.subject)),
|
||||
)}`,
|
||||
]
|
||||
: []),
|
||||
].join("\n");
|
||||
}
|
||||
if (params.assessment.disposition === "review-required") {
|
||||
|
||||
@@ -430,6 +430,7 @@ export async function ensureClawHubSkillTrustAcknowledged(
|
||||
subject: {
|
||||
kind: "skill",
|
||||
packageName: params.slug,
|
||||
workspaceDir: params.workspaceDir,
|
||||
...(params.ownerHandle ? { ownerHandle: params.ownerHandle } : {}),
|
||||
},
|
||||
version: params.version,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// ClawHub lifecycle tests cover registry metadata lookup and error handling.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
@@ -1935,58 +1936,135 @@ describe("skills-clawhub", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("explains that a malicious skill update will not be downloaded", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-malicious-update-");
|
||||
const warnings: string[] = [];
|
||||
await writeClawHubOriginFixture({
|
||||
workspaceDir,
|
||||
slug: "agentreceipt",
|
||||
installedVersion: "0.9.0",
|
||||
});
|
||||
fetchClawHubSkillSecurityVerdictsMock.mockResolvedValueOnce({
|
||||
schema: "clawhub.skill.security-verdicts.v1",
|
||||
items: [
|
||||
{
|
||||
ok: false,
|
||||
decision: "fail",
|
||||
reasons: ["scan:malicious"],
|
||||
requestedSlug: "agentreceipt",
|
||||
requestedVersion: "1.0.0",
|
||||
slug: "agentreceipt",
|
||||
version: "1.0.0",
|
||||
security: {
|
||||
status: "malicious",
|
||||
passed: false,
|
||||
it.each([
|
||||
{
|
||||
ownerHandle: undefined,
|
||||
skillRef: "agentreceipt",
|
||||
shellArg: "agentreceipt",
|
||||
workspaceName: "agent-workspace",
|
||||
},
|
||||
{
|
||||
ownerHandle: "acme",
|
||||
skillRef: "@acme/agentreceipt",
|
||||
shellArg: "@acme/agentreceipt",
|
||||
workspaceName: "different agent workspace",
|
||||
},
|
||||
{
|
||||
ownerHandle: undefined,
|
||||
skillRef: "agentreceipt",
|
||||
shellArg: "agentreceipt",
|
||||
workspaceName: ".openclaw",
|
||||
},
|
||||
{
|
||||
ownerHandle: undefined,
|
||||
skillRef: "a;printf X",
|
||||
shellArg: "'a;printf X'",
|
||||
workspaceName: "shared state;printf PWN",
|
||||
},
|
||||
{
|
||||
ownerHandle: undefined,
|
||||
skillRef: "b$(printf X)",
|
||||
shellArg: "'b$(printf X)'",
|
||||
workspaceName: "agent$(printf PWN)",
|
||||
},
|
||||
{
|
||||
ownerHandle: undefined,
|
||||
skillRef: "c`printf X`",
|
||||
shellArg: "'c`printf X`'",
|
||||
workspaceName: "agent`printf PWN`",
|
||||
},
|
||||
{
|
||||
ownerHandle: undefined,
|
||||
skillRef: "white space",
|
||||
shellArg: "'white space'",
|
||||
workspaceName: "custom state directory",
|
||||
},
|
||||
{
|
||||
ownerHandle: undefined,
|
||||
skillRef: "d'$(printf X)",
|
||||
shellArg: "'d'\\''$(printf X)'",
|
||||
workspaceName: "agent's $(printf PWN) state",
|
||||
},
|
||||
])(
|
||||
"explains that a malicious skill update will not be downloaded ($skillRef)",
|
||||
async ({ ownerHandle, skillRef, shellArg, workspaceName }) => {
|
||||
const tempRoot = await tempDirs.make("openclaw-skill-malicious-update-");
|
||||
const workspaceDir = path.join(tempRoot, workspaceName);
|
||||
const warnings: string[] = [];
|
||||
const slug = ownerHandle ? skillRef.slice(skillRef.indexOf("/") + 1) : skillRef;
|
||||
await writeClawHubOriginFixture({
|
||||
workspaceDir,
|
||||
slug,
|
||||
ownerHandle,
|
||||
installedVersion: "0.9.0",
|
||||
});
|
||||
fetchClawHubSkillSecurityVerdictsMock.mockResolvedValueOnce({
|
||||
schema: "clawhub.skill.security-verdicts.v1",
|
||||
items: [
|
||||
{
|
||||
ok: false,
|
||||
decision: "fail",
|
||||
reasons: ["scan:malicious"],
|
||||
requestedSlug: slug,
|
||||
requestedVersion: "1.0.0",
|
||||
slug,
|
||||
version: "1.0.0",
|
||||
...(ownerHandle ? { publisherHandle: ownerHandle } : {}),
|
||||
security: {
|
||||
status: "malicious",
|
||||
passed: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const results = await updateSkillsFromClawHub({
|
||||
workspaceDir,
|
||||
slug: skillRef,
|
||||
logger: {
|
||||
warn: (message) => warnings.push(message),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const results = await updateSkillsFromClawHub({
|
||||
workspaceDir,
|
||||
slug: "agentreceipt",
|
||||
logger: {
|
||||
warn: (message) => warnings.push(message),
|
||||
},
|
||||
});
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
ok: false,
|
||||
code: "clawhub_download_blocked",
|
||||
error: "ClawHub blocked this release; update was not started.",
|
||||
}),
|
||||
]);
|
||||
expect(warnings.join("\n")).toContain(
|
||||
"Latest skill version is marked malicious; OpenClaw will not download it.",
|
||||
);
|
||||
const workspaceArg = /^[A-Za-z0-9_/:=.,@%+-]+$/.test(workspaceDir)
|
||||
? workspaceDir
|
||||
: `'${workspaceDir.replaceAll("'", "'\\''")}'`;
|
||||
const uninstallCommand = `clawhub --workdir ${workspaceArg} uninstall ${shellArg}`;
|
||||
const actionLine = expectDefined(
|
||||
warnings
|
||||
.join("\n")
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("Remove installed skill: ")),
|
||||
"malicious skill warning remediation",
|
||||
);
|
||||
expect(actionLine).toBe(`Remove installed skill: ${uninstallCommand}`);
|
||||
expect(warnings.join("\n")).toContain("independently reviewed it.");
|
||||
expect(warnings.join("\n")).not.toContain("Choose a different version");
|
||||
expect(downloadClawHubSkillArchiveUrlMock).not.toHaveBeenCalled();
|
||||
expect(downloadClawHubSkillArchiveMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
ok: false,
|
||||
code: "clawhub_download_blocked",
|
||||
error: "ClawHub blocked this release; update was not started.",
|
||||
}),
|
||||
]);
|
||||
expect(warnings.join("\n")).toContain(
|
||||
"Latest skill version is marked malicious; OpenClaw will not download it.",
|
||||
);
|
||||
expect(warnings.join("\n")).toContain(
|
||||
"Uninstall the installed skill unless you have independently reviewed it.",
|
||||
);
|
||||
expect(warnings.join("\n")).not.toContain("Choose a different version");
|
||||
expect(downloadClawHubSkillArchiveUrlMock).not.toHaveBeenCalled();
|
||||
expect(downloadClawHubSkillArchiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
const shellResult = spawnSync(
|
||||
"sh",
|
||||
[
|
||||
"-c",
|
||||
`clawhub() { printf '%s\\n' "$@"; }\n${actionLine.slice("Remove installed skill: ".length)}`,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
expect(shellResult.status).toBe(0);
|
||||
expect(shellResult.stdout).toBe(`--workdir\n${workspaceDir}\nuninstall\n${skillRef}\n`);
|
||||
},
|
||||
);
|
||||
|
||||
it("updates owner-qualified ClawHub skills when the requested owner matches tracking", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-owner-update-request-");
|
||||
|
||||
Reference in New Issue
Block a user