mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
fix(skills): accept owner-qualified verify refs (#95992)
Merged via squash.
Prepared head SHA: de9f1e566e
Co-authored-by: Patrick-Erichsen <20157849+Patrick-Erichsen@users.noreply.github.com>
Co-authored-by: Patrick-Erichsen <20157849+Patrick-Erichsen@users.noreply.github.com>
Reviewed-by: @Patrick-Erichsen
This commit is contained in:
parent
59713194fc
commit
6343e1483f
8 changed files with 293 additions and 16 deletions
|
|
@ -25,7 +25,7 @@ OpenClaw agent or Gateway.
|
|||
openclaw skills search "calendar"
|
||||
openclaw skills install @owner/<slug>
|
||||
openclaw skills update @owner/<slug>
|
||||
openclaw skills verify <slug>
|
||||
openclaw skills verify @owner/<slug>
|
||||
|
||||
openclaw plugins search "calendar"
|
||||
openclaw plugins install clawhub:<package>
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ openclaw skills update @owner/<slug> --global
|
|||
openclaw skills update --all
|
||||
openclaw skills update --all --agent <id>
|
||||
openclaw skills update --all --global
|
||||
openclaw skills verify <slug>
|
||||
openclaw skills verify <slug> --version <version>
|
||||
openclaw skills verify <slug> --tag <tag>
|
||||
openclaw skills verify <slug> --card
|
||||
openclaw skills verify <slug> --global
|
||||
openclaw skills verify @owner/<slug>
|
||||
openclaw skills verify @owner/<slug> --version <version>
|
||||
openclaw skills verify @owner/<slug> --tag <tag>
|
||||
openclaw skills verify @owner/<slug> --card
|
||||
openclaw skills verify @owner/<slug> --global
|
||||
openclaw skills list
|
||||
openclaw skills list --eligible
|
||||
openclaw skills list --json
|
||||
|
|
@ -105,8 +105,11 @@ Notes:
|
|||
target the shared managed skills directory instead of the workspace.
|
||||
- `update --all` updates tracked ClawHub installs in the selected workspace, or
|
||||
in the shared managed skills directory when combined with `--global`.
|
||||
- `verify <slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON envelope by
|
||||
default. There is no `--json` flag because JSON is already the default.
|
||||
- `verify @owner/<slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON
|
||||
envelope by default. There is no `--json` flag because JSON is already the
|
||||
default. Bare slugs remain accepted for compatibility when the skill is
|
||||
already installed or unambiguous, but owner-qualified refs avoid publisher
|
||||
ambiguity.
|
||||
- When ClawHub returns server-resolved source provenance, verify JSON also
|
||||
includes a commit-pinned `openclaw.verifiedSourceUrl`. Unavailable or
|
||||
self-declared source URLs stay only in the raw provenance envelope and are not
|
||||
|
|
|
|||
|
|
@ -152,8 +152,8 @@ publish and sync.
|
|||
| Update all workspace skills | `openclaw skills update --all` |
|
||||
| Update a shared managed skill | `openclaw skills update @owner/<slug> --global` |
|
||||
| Update all shared managed skills | `openclaw skills update --all --global` |
|
||||
| Verify a skill's trust envelope | `openclaw skills verify <slug>` |
|
||||
| Print the generated Skill Card | `openclaw skills verify <slug> --card` |
|
||||
| Verify a skill's trust envelope | `openclaw skills verify @owner/<slug>` |
|
||||
| Print the generated Skill Card | `openclaw skills verify @owner/<slug> --card` |
|
||||
| Publish / sync via ClawHub CLI | `clawhub sync --all` |
|
||||
|
||||
<AccordionGroup>
|
||||
|
|
@ -171,9 +171,11 @@ publish and sync.
|
|||
|
||||
</Accordion>
|
||||
<Accordion title="Verification and security scanning">
|
||||
`openclaw skills verify <slug>` asks ClawHub for the skill's
|
||||
`openclaw skills verify @owner/<slug>` asks ClawHub for the skill's
|
||||
`clawhub.skill.verify.v1` trust envelope. Installed ClawHub skills verify
|
||||
against the version and registry recorded in `.clawhub/origin.json`.
|
||||
Bare slugs remain accepted for existing installed or unambiguous skills, but
|
||||
owner-qualified refs avoid publisher ambiguity.
|
||||
|
||||
ClawHub skill pages expose the latest security scan state before install,
|
||||
with detail pages for VirusTotal, ClawScan, and static analysis. The
|
||||
|
|
|
|||
|
|
@ -449,6 +449,24 @@ describe("skills cli commands", () => {
|
|||
expect(help).not.toContain("openclaw skills install weather");
|
||||
});
|
||||
|
||||
it("documents owner-qualified ClawHub verify refs in command help", () => {
|
||||
const skillsCommand = createProgram().commands.find((command) => command.name() === "skills");
|
||||
const verifyCommand = skillsCommand?.commands.find((command) => command.name() === "verify");
|
||||
const output: string[] = [];
|
||||
|
||||
verifyCommand?.configureOutput({
|
||||
writeOut: (value) => output.push(value),
|
||||
writeErr: (value) => output.push(value),
|
||||
});
|
||||
verifyCommand?.outputHelp();
|
||||
const help = output.join("");
|
||||
|
||||
expect(help).toContain("<skill-ref>");
|
||||
expect(help).toContain("@owner/slug");
|
||||
expect(help).toContain("openclaw skills verify @owner/weather");
|
||||
expect(help).not.toContain("openclaw skills verify weather");
|
||||
});
|
||||
|
||||
it("installs a skill from a git source into the active workspace", async () => {
|
||||
installSkillFromSourceMock.mockResolvedValue({
|
||||
ok: true,
|
||||
|
|
@ -935,6 +953,44 @@ describe("skills cli commands", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("passes owner-qualified verify refs and selectors through the resolver", async () => {
|
||||
resolveClawHubSkillVerificationTargetMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
baseUrl: "https://private.example.com/clawhub",
|
||||
version: undefined,
|
||||
tag: "latest",
|
||||
resolution: {
|
||||
source: "registry",
|
||||
selector: "tag",
|
||||
registry: "https://private.example.com/clawhub",
|
||||
skillDir: undefined,
|
||||
installedVersion: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await runCommand(["skills", "verify", "@demo-owner/weather", "--tag", "latest", "--card"]);
|
||||
|
||||
expect(resolveClawHubSkillVerificationTargetMock).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/workspace",
|
||||
slug: "@demo-owner/weather",
|
||||
version: undefined,
|
||||
tag: "latest",
|
||||
});
|
||||
expect(fetchClawHubSkillVerificationMock).toHaveBeenCalledWith({
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
version: undefined,
|
||||
tag: "latest",
|
||||
baseUrl: "https://private.example.com/clawhub",
|
||||
});
|
||||
expect(fetchClawHubSkillCardMock).toHaveBeenCalledWith({
|
||||
url: "https://private.example.com/clawhub/api/v1/skills/agentreceipt/card?version=1.2.3",
|
||||
baseUrl: "https://private.example.com/clawhub",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes explicit verify selectors and shared workspace options to the resolver", async () => {
|
||||
await runCommand(["skills", "verify", "agentreceipt", "--version", "2.0.0", "--global"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ export function registerSkillsCli(program: Command) {
|
|||
skills
|
||||
.command("update")
|
||||
.description("Update ClawHub-installed skills in the active or shared managed directory")
|
||||
.argument("[slug]", "Single skill slug")
|
||||
.argument("[skill-ref]", "Single ClawHub skill ref (@owner/slug)")
|
||||
.option("--all", "Update all tracked ClawHub skills", false)
|
||||
.option(
|
||||
"--force-install",
|
||||
|
|
@ -487,7 +487,7 @@ export function registerSkillsCli(program: Command) {
|
|||
skills
|
||||
.command("verify")
|
||||
.description("Verify a ClawHub skill with ClawHub")
|
||||
.argument("<slug>", "ClawHub skill slug")
|
||||
.argument("<skill-ref>", "ClawHub skill ref (@owner/slug)")
|
||||
.option("--version <version>", "Verify a specific version")
|
||||
.option("--tag <tag>", "Verify a dist tag")
|
||||
.option("--card", "Print the generated Skill Card Markdown", false)
|
||||
|
|
@ -497,6 +497,7 @@ export function registerSkillsCli(program: Command) {
|
|||
false,
|
||||
)
|
||||
.option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)")
|
||||
.addHelpText("after", "\nExamples:\n openclaw skills verify @owner/weather\n")
|
||||
.action(
|
||||
async (
|
||||
slug: string,
|
||||
|
|
|
|||
|
|
@ -194,6 +194,80 @@ describe("skills verify CLI", () => {
|
|||
expect(mocks.runtimeErrors).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("verifies owner-qualified registry refs with explicit versions", async () => {
|
||||
mocks.fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
|
||||
schema: "clawhub.skill.verify.v1",
|
||||
ok: true,
|
||||
decision: "pass",
|
||||
reasons: [],
|
||||
skill: { slug: "weather" },
|
||||
publisher: { handle: "demo-owner" },
|
||||
version: { version: "2.0.0" },
|
||||
card: { available: true },
|
||||
artifact: { sourceFingerprint: "source-fp" },
|
||||
provenance: null,
|
||||
security: { status: "clean" },
|
||||
signature: { status: "unsigned" },
|
||||
});
|
||||
|
||||
await runCommand(["skills", "verify", "@demo-owner/weather", "--version", "2.0.0"]);
|
||||
|
||||
expect(mocks.fetchClawHubSkillVerificationMock).toHaveBeenCalledWith({
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
version: "2.0.0",
|
||||
tag: undefined,
|
||||
baseUrl: "https://clawhub.ai",
|
||||
});
|
||||
const payload = JSON.parse(mocks.runtimeStdout.at(-1) ?? "{}") as {
|
||||
openclaw?: { resolution?: { source?: string; selector?: string } };
|
||||
};
|
||||
expect(payload.openclaw?.resolution).toMatchObject({
|
||||
source: "registry",
|
||||
selector: "version",
|
||||
});
|
||||
expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
|
||||
expect(mocks.runtimeErrors).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("prints cards for owner-qualified tag verification", async () => {
|
||||
mocks.fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
|
||||
schema: "clawhub.skill.verify.v1",
|
||||
ok: true,
|
||||
decision: "pass",
|
||||
reasons: [],
|
||||
skill: { slug: "weather" },
|
||||
publisher: { handle: "demo-owner" },
|
||||
version: { version: "2.0.0" },
|
||||
card: {
|
||||
available: true,
|
||||
url: "https://cards.example.test/generated/weather.md",
|
||||
},
|
||||
artifact: { sourceFingerprint: "source-fp" },
|
||||
provenance: null,
|
||||
security: { status: "clean" },
|
||||
signature: { status: "unsigned" },
|
||||
});
|
||||
mocks.fetchClawHubSkillCardMock.mockResolvedValueOnce("# Weather\n");
|
||||
|
||||
await runCommand(["skills", "verify", "@demo-owner/weather", "--tag", "latest", "--card"]);
|
||||
|
||||
expect(mocks.fetchClawHubSkillVerificationMock).toHaveBeenCalledWith({
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
version: undefined,
|
||||
tag: "latest",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
});
|
||||
expect(mocks.fetchClawHubSkillCardMock).toHaveBeenCalledWith({
|
||||
url: "https://cards.example.test/generated/weather.md",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
});
|
||||
expect(mocks.runtimeStdout.at(-1)).toBe("# Weather");
|
||||
expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
|
||||
expect(mocks.runtimeErrors).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("surfaces only server-verified source provenance in verify JSON", async () => {
|
||||
const sourceUrl = "https://github.com/openclaw/skills/tree/main/agentreceipt";
|
||||
const verifiedSourceUrl =
|
||||
|
|
|
|||
|
|
@ -1329,6 +1329,66 @@ describe("skills-clawhub", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("accepts owner-qualified installed verification targets", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
|
||||
try {
|
||||
await writeClawHubOriginFixture({
|
||||
workspaceDir,
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
registry: "https://private.example.com/clawhub",
|
||||
installedVersion: "2.0.0",
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveClawHubSkillVerificationTarget({
|
||||
workspaceDir,
|
||||
slug: "@demo-owner/weather",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
ok: true,
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
baseUrl: "https://private.example.com/clawhub",
|
||||
version: "2.0.0",
|
||||
tag: undefined,
|
||||
resolution: {
|
||||
source: "installed",
|
||||
selector: "installed-version",
|
||||
registry: "https://private.example.com/clawhub",
|
||||
installedVersion: "2.0.0",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects owner-qualified installed verification when the owner differs", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
|
||||
try {
|
||||
await writeClawHubOriginFixture({
|
||||
workspaceDir,
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
});
|
||||
|
||||
const result = await resolveClawHubSkillVerificationTarget({
|
||||
workspaceDir,
|
||||
slug: "@other-owner/weather",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) {
|
||||
throw new Error("expected owner mismatch failure");
|
||||
}
|
||||
expect(result.error).toContain("is tracked as @demo-owner/weather");
|
||||
expect(result.error).toContain("not @other-owner/weather");
|
||||
} finally {
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the installed registry when an explicit version overrides the installed version", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skill-verify-"));
|
||||
try {
|
||||
|
|
@ -1634,6 +1694,79 @@ describe("skills-clawhub", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("uses owner-qualified registry verification targets", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
|
||||
resolveClawHubBaseUrlMock.mockReturnValueOnce("https://configured.example.com/clawhub");
|
||||
try {
|
||||
await expect(
|
||||
resolveClawHubSkillVerificationTarget({
|
||||
workspaceDir,
|
||||
slug: "@demo-owner/weather",
|
||||
baseUrl: "https://configured.example.com/clawhub/",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
baseUrl: "https://configured.example.com/clawhub",
|
||||
version: undefined,
|
||||
tag: undefined,
|
||||
resolution: {
|
||||
source: "registry",
|
||||
selector: "latest",
|
||||
registry: "https://configured.example.com/clawhub",
|
||||
skillDir: undefined,
|
||||
installedVersion: undefined,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps owner-qualified registry selectors for explicit versions and tags", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
|
||||
try {
|
||||
await expect(
|
||||
resolveClawHubSkillVerificationTarget({
|
||||
workspaceDir,
|
||||
slug: "@demo-owner/weather",
|
||||
version: "2.0.0",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
ok: true,
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
version: "2.0.0",
|
||||
tag: undefined,
|
||||
resolution: {
|
||||
source: "registry",
|
||||
selector: "version",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveClawHubSkillVerificationTarget({
|
||||
workspaceDir,
|
||||
slug: "@demo-owner/weather",
|
||||
tag: "beta",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
ok: true,
|
||||
slug: "weather",
|
||||
ownerHandle: "demo-owner",
|
||||
version: undefined,
|
||||
tag: "beta",
|
||||
resolution: {
|
||||
source: "registry",
|
||||
selector: "tag",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("fails clearly when installed origin metadata is malformed", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skill-verify-"));
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -956,7 +956,8 @@ export async function resolveClawHubSkillVerificationTarget(params: {
|
|||
return { ok: false, error: "Use either --version or --tag." };
|
||||
}
|
||||
|
||||
const trackedSlug = normalizeTrackedSkillSlug(params.slug);
|
||||
const requestedRef = parseRequestedClawHubSkillRef(params.slug);
|
||||
const trackedSlug = requestedRef.slug;
|
||||
const skillDir = resolveWorkspaceSkillInstallDir(params.workspaceDir, trackedSlug);
|
||||
const originRead = await readClawHubSkillOriginStrict(skillDir);
|
||||
if (originRead.kind === "malformed") {
|
||||
|
|
@ -997,6 +998,13 @@ export async function resolveClawHubSkillVerificationTarget(params: {
|
|||
error: `Skill "${trackedSlug}" ClawHub origin metadata does not match the workspace ClawHub lockfile. Reinstall it from ClawHub before verifying it as an installed ClawHub skill.`,
|
||||
};
|
||||
}
|
||||
if (requestedRef.ownerHandle && lockedOwnerHandle !== requestedRef.ownerHandle) {
|
||||
const trackedRef = lockedOwnerHandle ? `@${lockedOwnerHandle}/${trackedSlug}` : trackedSlug;
|
||||
return {
|
||||
ok: false,
|
||||
error: `Skill "${trackedSlug}" is tracked as ${trackedRef}, not @${requestedRef.ownerHandle}/${trackedSlug}.`,
|
||||
};
|
||||
}
|
||||
const selector: ClawHubSkillVerificationSelector = version
|
||||
? "version"
|
||||
: tag
|
||||
|
|
@ -1033,12 +1041,12 @@ export async function resolveClawHubSkillVerificationTarget(params: {
|
|||
};
|
||||
}
|
||||
|
||||
const slug = validateRequestedSkillSlug(params.slug);
|
||||
const registry = resolveClawHubBaseUrl(params.baseUrl);
|
||||
const selector: ClawHubSkillVerificationSelector = version ? "version" : tag ? "tag" : "latest";
|
||||
return {
|
||||
ok: true,
|
||||
slug,
|
||||
slug: requestedRef.slug,
|
||||
...(requestedRef.ownerHandle ? { ownerHandle: requestedRef.ownerHandle } : {}),
|
||||
baseUrl: registry,
|
||||
version,
|
||||
tag,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue