fix(skills): harden ClawHub update policy

Pass runtime config into CLI ClawHub skill updates so install policy sees configured safety rules, and update the bundled ClawHub skill docs to prefer openclaw skills for normal skill management. Keeps update-all limited to tracked ClawHub installs and intentionally leaves bundled-skill deprecation, legacy bootstrap, and Sherpa packaging for separate follow-up. Proof: focused ClawHub/CLI tests passed, autoreview clean, remote check:changed passed on Blacksmith Testbox tbx_01kvq0ywztsvw9vdc8zz1xktea; wrapper install/build/check passed, with full local pnpm test failing in unrelated baseline areas already reproduced on latest origin/main.
This commit is contained in:
Vincent Koc 2026-06-22 16:03:19 +08:00 committed by GitHub
parent 1711d0123c
commit 4e9dc6b5d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 118 additions and 70 deletions

View file

@ -1,77 +1,64 @@
---
name: clawhub
description: "Search, install, update, sync, or publish agent skills with the ClawHub CLI and registry."
metadata:
{
"openclaw":
{
"requires": { "bins": ["clawhub"] },
"install":
[
{
"id": "node",
"kind": "node",
"package": "clawhub",
"bins": ["clawhub"],
"label": "Install ClawHub CLI (npm)",
},
],
},
}
description: "Search ClawHub for skills when a requested capability is not already available; install, verify, update, publish, or sync skills."
---
# ClawHub CLI
# ClawHub
Install
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.
## Discover skills
Search before claiming that a requested capability is unavailable:
```bash
openclaw skills search "postgres backups"
```
Before installing, verify the selected skill and treat third-party skills as
untrusted. Obtain user approval before installation.
```bash
openclaw skills verify my-skill
openclaw skills install my-skill
openclaw skills install my-skill --version 1.2.3
```
## Manage installed skills
```bash
openclaw skills list
openclaw skills check
openclaw skills update my-skill
openclaw skills update --all
```
Use `--global` with `install` or `update` to manage skills shared by all local
agents.
## Publish skills
Install the standalone ClawHub CLI for publisher workflows:
```bash
npm i -g clawhub
```
Auth (publish)
```bash
clawhub login
clawhub whoami
```
Search
Publish or sync skills:
```bash
clawhub search "postgres backups"
clawhub skill publish ./my-skill
clawhub skill publish ./my-skill --version 1.2.3
clawhub sync --all
```
Install
## Notes
```bash
clawhub install my-skill
clawhub install my-skill --version 1.2.3
```
Update (hash-based match + upgrade)
```bash
clawhub update my-skill
clawhub update my-skill --version 1.2.3
clawhub update --all
clawhub update my-skill --force
clawhub update --all --no-input --force
```
List
```bash
clawhub list
```
Publish
```bash
clawhub publish ./my-skill --slug my-skill --name "My Skill" --version 1.2.0 --changelog "Fixes + docs"
```
Notes
- Default registry: https://clawhub.com (override with CLAWHUB_REGISTRY or --registry)
- Default workdir: cwd (falls back to OpenClaw workspace); install dir: ./skills (override with --workdir / --dir / CLAWHUB_WORKDIR)
- Update command hashes local files, resolves matching version, and upgrades to latest unless --version is set
- Public registry: https://clawhub.ai
- `openclaw skills install` installs into the active workspace by default.
- Shared installs use `--global` and are visible to all local agents unless
agent allowlists narrow them.

View file

@ -680,6 +680,7 @@ describe("skills cli commands", () => {
workspaceDir: "/tmp/workspace",
slug: undefined,
});
expect(updateAllArgs.config).toEqual({});
expectLogger(updateAllArgs.logger);
expect(
runtimeLogs.some((line) => line.includes("Updated calendar: 1.2.2 -> 1.2.3")),
@ -688,6 +689,24 @@ describe("skills cli commands", () => {
expect(runtimeErrors).toStrictEqual([]);
});
it("does not bootstrap configured skills during update all", async () => {
loadConfigMock.mockReturnValueOnce({
agents: {
defaults: {
skills: ["apple-notes"],
},
},
});
readTrackedClawHubSkillSlugsMock.mockResolvedValue([]);
await runCommand(["skills", "update", "--all"]);
expect(readTrackedClawHubSkillSlugsMock).toHaveBeenCalledWith("/tmp/workspace");
expect(updateSkillsFromClawHubMock).not.toHaveBeenCalled();
expect(runtimeLogs).toContain("No tracked ClawHub skills to update.");
expect(runtimeErrors).toStrictEqual([]);
});
it("passes --force-install through for ClawHub skill updates", async () => {
readTrackedClawHubSkillSlugsMock.mockResolvedValue(["calendar"]);
updateSkillsFromClawHubMock.mockResolvedValue([
@ -791,6 +810,7 @@ describe("skills cli commands", () => {
workspaceDir: "/tmp/openclaw-config",
slug: undefined,
logger: expect.any(Object),
config: {},
});
});
@ -817,6 +837,7 @@ describe("skills cli commands", () => {
workspaceDir: "/tmp/openclaw-config",
slug: "calendar",
logger: expect.any(Object),
config: {},
});
});

View file

@ -149,10 +149,6 @@ async function runSkillsAction(
}
}
function resolveActiveWorkspaceDir(options?: ResolveSkillsWorkspaceOptions): string {
return resolveSkillsWorkspace(options).workspaceDir;
}
function resolveSkillsWorkspaceForCommand(
command: Command | null | undefined,
opts?: { agent?: string },
@ -164,6 +160,13 @@ function resolveClawHubTargetWorkspaceDir(
command: Command | undefined,
opts: { agent?: string; global?: boolean },
): string | undefined {
return resolveClawHubTargetWorkspace(command, opts)?.workspaceDir;
}
function resolveClawHubTargetWorkspace(
command: Command | undefined,
opts: { agent?: string; global?: boolean },
): Pick<ResolvedSkillsWorkspace, "config" | "workspaceDir"> | undefined {
const agentId = resolveAgentOption(command, opts);
if (opts.global && normalizeOptionalString(agentId)) {
defaultRuntime.error("Use either --global or --agent, not both.");
@ -171,9 +174,9 @@ function resolveClawHubTargetWorkspaceDir(
return undefined;
}
if (opts.global) {
return CONFIG_DIR;
return { config: getRuntimeConfig(), workspaceDir: CONFIG_DIR };
}
return resolveActiveWorkspaceDir({ agentId });
return resolveSkillsWorkspace({ agentId });
}
function shouldFailSkillVerification(result: ClawHubSkillVerificationResponse): boolean {
@ -438,22 +441,23 @@ export function registerSkillsCli(program: Command) {
defaultRuntime.exit(1);
return;
}
const workspaceDir = resolveClawHubTargetWorkspaceDir(command, opts);
if (!workspaceDir) {
const target = resolveClawHubTargetWorkspace(command, opts);
if (!target) {
return;
}
const tracked = await readTrackedClawHubSkillSlugs(workspaceDir);
const tracked = await readTrackedClawHubSkillSlugs(target.workspaceDir);
if (opts.all && tracked.length === 0) {
defaultRuntime.log("No tracked ClawHub skills to update.");
return;
}
const results = await updateSkillsFromClawHub({
workspaceDir,
workspaceDir: target.workspaceDir,
slug,
...(opts.forceInstall ? { forceInstall: true } : {}),
logger: {
info: (message) => defaultRuntime.log(message),
},
config: target.config,
});
let failed = false;
for (const result of results) {

View file

@ -1104,6 +1104,42 @@ describe("skills-clawhub", () => {
}
});
it("does not install configured skills during update all without ClawHub tracking", async () => {
const workspaceDir = await tempDirs.make("openclaw-configured-update-");
const results = await updateSkillsFromClawHub({
workspaceDir,
config: {
agents: {
defaults: {
skills: ["apple-notes", "sherpa-onnx-tts"],
},
},
},
});
expect(results).toEqual([]);
expect(fetchClawHubSkillInstallResolutionMock).not.toHaveBeenCalled();
expect(installPackageDirMock).not.toHaveBeenCalled();
});
it("rejects untracked requested updates instead of installing by slug", async () => {
const workspaceDir = await tempDirs.make("openclaw-untracked-update-");
const results = await updateSkillsFromClawHub({
workspaceDir,
slug: "calendar",
});
expect(results).toEqual([
{
ok: false,
error: 'Skill "calendar" is not tracked as a ClawHub install.',
},
]);
expect(fetchClawHubSkillInstallResolutionMock).not.toHaveBeenCalled();
expect(installPackageDirMock).not.toHaveBeenCalled();
});
it("updates a legacy Unicode slug when requested explicitly", async () => {
const slug = "re\u0430ct";
const { workspaceDir } = await createLegacyTrackedSkillFixture(slug);