diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 741f28ec..a8556687 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -132,6 +132,7 @@ import type * as lib_skills_index from "../lib/skills/index.js"; import type * as lib_skills_slugResolution from "../lib/skills/slugResolution.js"; import type * as lib_skillsShCatalogEnvironment from "../lib/skillsShCatalogEnvironment.js"; import type * as lib_skillsShCatalogFixtures from "../lib/skillsShCatalogFixtures.js"; +import type * as lib_skillsShCatalogPublication from "../lib/skillsShCatalogPublication.js"; import type * as lib_staticPublishScan from "../lib/staticPublishScan.js"; import type * as lib_testSeed from "../lib/testSeed.js"; import type * as lib_tokens from "../lib/tokens.js"; @@ -307,6 +308,7 @@ declare const fullApi: ApiFromModules<{ "lib/skills/slugResolution": typeof lib_skills_slugResolution; "lib/skillsShCatalogEnvironment": typeof lib_skillsShCatalogEnvironment; "lib/skillsShCatalogFixtures": typeof lib_skillsShCatalogFixtures; + "lib/skillsShCatalogPublication": typeof lib_skillsShCatalogPublication; "lib/staticPublishScan": typeof lib_staticPublishScan; "lib/testSeed": typeof lib_testSeed; "lib/tokens": typeof lib_tokens; diff --git a/convex/fixtures/patrick-html-canary-SKILL.txt b/convex/fixtures/patrick-html-canary-SKILL.txt new file mode 100644 index 00000000..af9931e3 --- /dev/null +++ b/convex/fixtures/patrick-html-canary-SKILL.txt @@ -0,0 +1,87 @@ +--- +name: html +description: Use when the user invokes /html, wants to choose an HTML artifact pattern, or wants a visual or interactive single-file HTML format for planning, review, explanation, reporting, or lightweight editing. +--- + +# HTML Artifact Chooser + +Use this as a menu for picking an HTML artifact shape. It is based on Thariq Shihipar's "The unreasonable effectiveness of HTML" examples. + +Source index: https://thariqs.github.io/html-effectiveness/ + +## How to Use + +When the user asks for `/html`, show the options below and ask which one they want to explore or create. + +After the user chooses an option, open the linked example and read it before designing the artifact. Treat the example as pattern inspiration, not a template to copy blindly. + +Keep outputs as a single self-contained `.html` file unless the user asks for something else. For interactive tools, include a copy/export affordance when useful. + +## Default Location + +When working inside a repo or workspace, write generated HTML artifacts under +gitignored `.artifacts/` by default so they are easy to open locally without +polluting source docs or PR diffs. For planning or review artifacts, prefer +`.artifacts//index.html` or `.artifacts/.html` unless +the user asks for a different path. + +If the user is using the Codex app, open the generated HTML artifact in the +in-app browser when practical and report the local file path. Keep committed +`docs/` or `specs/` for durable repo documentation, not temporary HTML review +artifacts. + +## Options + +| Option | Use When | Full Example | +| --- | --- | --- | +| Three code approaches | Comparing multiple implementation strategies side by side with tradeoffs | https://thariqs.github.io/html-effectiveness/01-exploration-code-approaches.html | +| Visual design directions | Reviewing layout, palette, or visual direction options as rendered screens | https://thariqs.github.io/html-effectiveness/02-exploration-visual-designs.html | +| Annotated pull request | Turning a diff or PR review into a scannable annotated artifact | https://thariqs.github.io/html-effectiveness/03-code-review-pr.html | +| Module map | Explaining an unfamiliar package, dependency graph, hot path, or entry points | https://thariqs.github.io/html-effectiveness/04-code-understanding.html | +| Living design system | Showing tokens, colors, type, spacing, and components from a repo | https://thariqs.github.io/html-effectiveness/05-design-system.html | +| Component variants | Reviewing one component across sizes, states, intents, and edge cases | https://thariqs.github.io/html-effectiveness/06-component-variants.html | +| Animation sandbox | Tuning motion with sliders for duration, easing, delay, or intensity | https://thariqs.github.io/html-effectiveness/07-prototype-animation.html | +| Clickable flow | Trying a lightweight multi-screen interaction before implementation | https://thariqs.github.io/html-effectiveness/08-prototype-interaction.html | +| Arrow-key slide deck | Turning a short narrative, update, or meeting brief into browser slides | https://thariqs.github.io/html-effectiveness/09-slide-deck.html | +| SVG figure sheet | Creating editable inline vector figures for docs, posts, or explainers | https://thariqs.github.io/html-effectiveness/10-svg-illustrations.html | +| Weekly status | Making a recurring status update skimmable with sections and small charts | https://thariqs.github.io/html-effectiveness/11-status-report.html | +| Incident timeline | Reconstructing an incident or debugging story with logs and follow-ups | https://thariqs.github.io/html-effectiveness/12-incident-report.html | +| Annotated flowchart | Explaining a process, workflow, pipeline, or failure path interactively | https://thariqs.github.io/html-effectiveness/13-flowchart-diagram.html | +| Feature explainer | Teaching how a repo feature works with paths, snippets, FAQ, and glossary | https://thariqs.github.io/html-effectiveness/14-research-feature-explainer.html | +| Concept explainer | Teaching a general concept with an interactive model and glossary | https://thariqs.github.io/html-effectiveness/15-research-concept-explainer.html | +| Implementation plan | Turning a selected approach into milestones, risks, flows, and handoff notes | https://thariqs.github.io/html-effectiveness/16-implementation-plan.html | +| PR writeup for reviewers | Preparing reviewer context with motivation, before/after, and file tour | https://thariqs.github.io/html-effectiveness/17-pr-writeup.html | +| Ticket triage board | Sorting issues or tasks visually, then exporting the final order | https://thariqs.github.io/html-effectiveness/18-editor-triage-board.html | +| Feature flag editor | Editing flags with dependency warnings and a copyable diff | https://thariqs.github.io/html-effectiveness/19-editor-feature-flags.html | +| Prompt tuner | Editing a prompt template while live-rendering sample inputs | https://thariqs.github.io/html-effectiveness/20-editor-prompt-tuner.html | + +## Default Response Shape + +If the user invokes `/html` without a choice, reply with: + +```text +Pick one HTML artifact pattern: + +1. Three code approaches +2. Visual design directions +3. Annotated pull request +4. Module map +5. Living design system +6. Component variants +7. Animation sandbox +8. Clickable flow +9. Arrow-key slide deck +10. SVG figure sheet +11. Weekly status +12. Incident timeline +13. Annotated flowchart +14. Feature explainer +15. Concept explainer +16. Implementation plan +17. PR writeup for reviewers +18. Ticket triage board +19. Feature flag editor +20. Prompt tuner +``` + +If the user names a choice, say which source example you are reading, then inspect that URL before producing the artifact. diff --git a/convex/http.ts b/convex/http.ts index c6f434b3..e73fffc7 100644 --- a/convex/http.ts +++ b/convex/http.ts @@ -60,6 +60,7 @@ import { whoamiV1Http, contentRightsV1Http, skillsShCatalogTestV1Http, + skillsShCatalogPublicV1Http, } from "./httpApiV1"; import { preflightHandler } from "./httpPreflight"; import { installRateLimitedRoutes } from "./lib/httpRouteRateLimit"; @@ -331,6 +332,12 @@ http.route({ handler: skillsShCatalogTestV1Http, }); +http.route({ + pathPrefix: "/api/v1/skills-sh/", + method: "GET", + handler: skillsShCatalogPublicV1Http, +}); + http.route({ path: "/api/v1/operator/skills-sh/catalog-test", method: "POST", diff --git a/convex/httpApi.handlers.test.ts b/convex/httpApi.handlers.test.ts index b7c7c085..38185166 100644 --- a/convex/httpApi.handlers.test.ts +++ b/convex/httpApi.handlers.test.ts @@ -327,6 +327,7 @@ describe("httpApi handlers", () => { event: "install", slug: "weather", ownerHandle: "openclaw", + sourceRef: "skills-sh/openclaw/skills/weather", version: "1.0.0", rootId: "abc", rootLabel: "~/skills", @@ -339,6 +340,7 @@ describe("httpApi handlers", () => { userId: "users:1", slug: "weather", ownerHandle: "openclaw", + sourceRef: "skills-sh/openclaw/skills/weather", version: "1.0.0", }); }); diff --git a/convex/httpApi.ts b/convex/httpApi.ts index 1dc2285c..0c5468e9 100644 --- a/convex/httpApi.ts +++ b/convex/httpApi.ts @@ -332,6 +332,7 @@ async function cliTelemetryInstallHandler(ctx: ActionCtx, request: Request) { userId, slug: args.slug, ownerHandle: args.ownerHandle, + sourceRef: args.sourceRef, version: args.version, }); } diff --git a/convex/httpApiV1.handlers.test.ts b/convex/httpApiV1.handlers.test.ts index 7020a991..7790bc32 100644 --- a/convex/httpApiV1.handlers.test.ts +++ b/convex/httpApiV1.handlers.test.ts @@ -2864,6 +2864,285 @@ describe("httpApiV1 handlers", () => { }); }); + it("skill install resolver returns the exact approved skills.sh GitHub descriptor", async () => { + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + expect(args).toEqual({ + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }); + return { + install: { + ok: true, + slug: "skills-sh/patrick-erichsen/skills/html", + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit: "050daba89f6b6636470add5cb300aac46a412cf8", + contentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + sourceUrl: + "https://github.com/patrick-erichsen/skills/tree/050daba89f6b6636470add5cb300aac46a412cf8/skills/html", + }, + }, + }; + }); + const runMutation = vi.fn().mockResolvedValue(okRate()); + + const response = await __handlers.skillsGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request( + "https://example.com/api/v1/skills/html/install?reference=skills-sh%2Fpatrick-erichsen%2Fskills%2Fhtml", + ), + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + ok: true, + slug: "skills-sh/patrick-erichsen/skills/html", + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit: "050daba89f6b6636470add5cb300aac46a412cf8", + }, + }); + expect(runQuery).toHaveBeenCalledTimes(1); + }); + + it.each(["skills-sh:patrick-erichsen/skills/html", "skills-sh/patrick-erichsen/skills/weather"])( + "skill install resolver rejects invalid exact catalog reference %s", + async (reference) => { + const runQuery = vi.fn(); + const runMutation = vi.fn().mockResolvedValue(okRate()); + + const response = await __handlers.skillsGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request( + `https://example.com/api/v1/skills/html/install?reference=${encodeURIComponent(reference)}`, + ), + ); + + expect(response.status).toBe(400); + expect(runQuery).not.toHaveBeenCalled(); + }, + ); + + it("skill install resolver never falls back to a native slug for a hidden catalog reference", async () => { + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + expect(args).toEqual({ + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }); + return null; + }); + const runMutation = vi.fn().mockResolvedValue(okRate()); + + const response = await __handlers.skillsGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request( + "https://example.com/api/v1/skills/html/install?reference=skills-sh%2Fpatrick-erichsen%2Fskills%2Fhtml", + ), + ); + + expect(response.status).toBe(404); + await expect(response.text()).resolves.toBe("Skill not found"); + expect(runQuery).toHaveBeenCalledTimes(1); + }); + + it("skill verification returns the exact approved skills.sh scan envelope", async () => { + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + expect(args).toEqual({ + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }); + return { + ref: "skills-sh/patrick-erichsen/skills/html", + route: "/skills-sh/patrick-erichsen/skills/html", + displayName: "HTML Artifact Chooser", + repository: "patrick-erichsen/skills", + githubPath: "skills/html", + githubCommit: "1".repeat(40), + githubContentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + security: { + verdict: "clean", + source: "clawhub", + attemptId: "skillsShCatalogScanAttempts:canary", + scannedAt: 123, + }, + artifact: { + contentHash: "c".repeat(64), + files: [ + { + path: "SKILL.md", + size: 42, + sha256: "d".repeat(64), + contentType: "text/markdown", + }, + ], + }, + }; + }); + const runMutation = vi.fn().mockResolvedValue(okRate()); + + const response = await __handlers.skillsGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request( + "https://example.com/api/v1/skills/html/verify?reference=skills-sh%2Fpatrick-erichsen%2Fskills%2Fhtml", + ), + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + schema: "clawhub.skill.verify.v1", + ok: true, + decision: "pass", + reasons: [], + slug: "skills-sh/patrick-erichsen/skills/html", + displayName: "HTML Artifact Chooser", + pageUrl: "https://example.com/skills-sh/patrick-erichsen/skills/html", + publisherHandle: null, + publisherDisplayName: null, + publisherProfileUrl: null, + version: "1".repeat(40), + resolvedFrom: "latest", + tag: null, + createdAt: 123, + card: { + available: false, + path: "skill-card.md", + url: null, + sha256: null, + size: null, + contentType: null, + }, + artifact: { + sourceFingerprint: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + bundleFingerprints: ["c".repeat(64)], + files: [ + { + path: "SKILL.md", + size: 42, + sha256: "d".repeat(64), + contentType: "text/markdown", + }, + ], + }, + provenance: { + source: "skills-sh-catalog", + reference: "skills-sh/patrick-erichsen/skills/html", + repository: "patrick-erichsen/skills", + path: "skills/html", + commit: "1".repeat(40), + contentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + scanAttemptId: "skillsShCatalogScanAttempts:canary", + artifactContentHash: "c".repeat(64), + }, + security: { + status: "clean", + passed: true, + rawStatus: "clean", + verdict: "clean", + source: "clawhub", + attemptId: "skillsShCatalogScanAttempts:canary", + checkedAt: 123, + }, + signature: { + status: "unsigned", + }, + }); + expect(runQuery).toHaveBeenCalledTimes(1); + }); + + it("skill verification preserves suspicious publication while failing the security decision", async () => { + const runQuery = vi.fn(async () => ({ + ref: "skills-sh/patrick-erichsen/skills/html", + route: "/skills-sh/patrick-erichsen/skills/html", + displayName: "HTML Artifact Chooser", + repository: "patrick-erichsen/skills", + githubPath: "skills/html", + githubCommit: "1".repeat(40), + githubContentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + security: { + verdict: "suspicious", + source: "clawhub", + attemptId: "skillsShCatalogScanAttempts:canary", + scannedAt: 123, + }, + artifact: { + contentHash: "c".repeat(64), + files: [{ path: "SKILL.md", size: 42, sha256: "d".repeat(64) }], + }, + })); + const runMutation = vi.fn().mockResolvedValue(okRate()); + + const response = await __handlers.skillsGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request( + "https://example.com/api/v1/skills/html/verify?reference=skills-sh%2Fpatrick-erichsen%2Fskills%2Fhtml", + ), + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + schema: "clawhub.skill.verify.v1", + ok: false, + decision: "fail", + reasons: ["security.status_not_clean"], + slug: "skills-sh/patrick-erichsen/skills/html", + security: { + status: "suspicious", + passed: false, + verdict: "suspicious", + attemptId: "skillsShCatalogScanAttempts:canary", + }, + }); + expect(runQuery).toHaveBeenCalledTimes(1); + }); + + it.each(["skills-sh:patrick-erichsen/skills/html", "skills-sh/patrick-erichsen/skills/weather"])( + "skill verification rejects invalid exact catalog reference %s", + async (reference) => { + const runQuery = vi.fn(); + const runMutation = vi.fn().mockResolvedValue(okRate()); + + const response = await __handlers.skillsGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request( + `https://example.com/api/v1/skills/html/verify?reference=${encodeURIComponent(reference)}`, + ), + ); + + expect(response.status).toBe(400); + expect(runQuery).not.toHaveBeenCalled(); + }, + ); + + it("skill verification never falls back to a native slug for a hidden catalog reference", async () => { + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + expect(args).toEqual({ + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }); + return null; + }); + const runMutation = vi.fn().mockResolvedValue(okRate()); + + const response = await __handlers.skillsGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request( + "https://example.com/api/v1/skills/html/verify?reference=skills-sh%2Fpatrick-erichsen%2Fskills%2Fhtml", + ), + ); + + expect(response.status).toBe(404); + await expect(response.text()).resolves.toBe("Skill not found"); + expect(runQuery).toHaveBeenCalledTimes(1); + }); + it("skill install resolver returns a pinned GitHub descriptor for scan-clean source-backed skills", async () => { const runQuery = makeInstallResolverRunQuery({ skill: { diff --git a/convex/httpApiV1.ts b/convex/httpApiV1.ts index 2a1a12b5..97d36cee 100644 --- a/convex/httpApiV1.ts +++ b/convex/httpApiV1.ts @@ -27,7 +27,10 @@ import { promotionsPostRouterV1Handler, } from "./httpApiV1/promotionsV1"; import { createPublisherV1Handler } from "./httpApiV1/publishersV1"; -import { skillsShCatalogTestV1Handler } from "./httpApiV1/skillsShCatalogV1"; +import { + skillsShCatalogPublicV1Handler, + skillsShCatalogTestV1Handler, +} from "./httpApiV1/skillsShCatalogV1"; import { exportSkillsV1Handler, listSkillsV1Handler, @@ -69,6 +72,7 @@ export const verifyDocsSessionV1Http = httpAction(verifyDocsSessionV1Handler); export const createPublisherV1Http = httpAction(createPublisherV1Handler); export const contentRightsV1Http = httpAction(contentRightsV1Handler); export const skillsShCatalogTestV1Http = httpAction(skillsShCatalogTestV1Handler); +export const skillsShCatalogPublicV1Http = httpAction(skillsShCatalogPublicV1Handler); export const catalogFeedV1Http = httpAction(catalogFeedV1Handler); export const catalogSkillsFeedV1Http = httpAction(catalogSkillsFeedV1Handler); export const promotionsFeedV1Http = httpAction(promotionsFeedV1Handler); @@ -119,6 +123,7 @@ export const __handlers = { createPublisherV1Handler, contentRightsV1Handler, skillsShCatalogTestV1Handler, + skillsShCatalogPublicV1Handler, catalogFeedV1Handler, catalogSkillsFeedV1Handler, searchSkillsV1Handler, diff --git a/convex/httpApiV1/skillsShCatalogV1.test.ts b/convex/httpApiV1/skillsShCatalogV1.test.ts index 5043c703..08bae6ba 100644 --- a/convex/httpApiV1/skillsShCatalogV1.test.ts +++ b/convex/httpApiV1/skillsShCatalogV1.test.ts @@ -30,8 +30,11 @@ vi.mock("./shared", async (importOriginal) => { const { requireAdminOrResponse, requireApiTokenUserOrResponse } = await import("./shared"); const { buildGitHubApiHeaders } = await import("../lib/githubAuth"); const { computeGitHubSkillFolderContentHash } = await import("../lib/githubSkillSync"); -const { skillsShCatalogTestV1Handler, verifyControlledCanaryGitHubSource } = - await import("./skillsShCatalogV1"); +const { + skillsShCatalogPublicV1Handler, + skillsShCatalogTestV1Handler, + verifyControlledCanaryGitHubSource, +} = await import("./skillsShCatalogV1"); function sha256(value: string) { return createHash("sha256").update(value).digest("hex"); @@ -565,4 +568,202 @@ describe("skills.sh catalog Test HTTP API", () => { }); expect(JSON.stringify(runMutation.mock.calls[0]?.[1])).not.toContain("client-controlled"); }); + + it.each([ + { + operation: "start-canary-scan", + input: { reason: "scan the exact canary" }, + expected: { + actor: "catalog-operator", + reason: "scan the exact canary", + }, + }, + { + operation: "set-publication", + input: { + enabled: true, + reason: "enable exact-version publication", + confirm: "set-skills-sh-test-publication", + }, + expected: { + enabled: true, + actor: "catalog-operator", + reason: "enable exact-version publication", + confirm: "set-skills-sh-test-publication", + }, + }, + { + operation: "set-pause", + input: { + paused: true, + reason: "pause catalog-only work", + confirm: "set-skills-sh-test-pause", + }, + expected: { + paused: true, + actor: "catalog-operator", + reason: "pause catalog-only work", + confirm: "set-skills-sh-test-pause", + }, + }, + { + operation: "rollback-publication", + input: { + externalId: "patrick-erichsen/skills/html", + attemptId: "skillsShCatalogScanAttempts:canary", + reason: "hide the exact published attempt", + confirm: "rollback-skills-sh-test-publication", + }, + expected: { + externalId: "patrick-erichsen/skills/html", + attemptId: "skillsShCatalogScanAttempts:canary", + actor: "catalog-operator", + reason: "hide the exact published attempt", + confirm: "rollback-skills-sh-test-publication", + }, + }, + ])("forwards the $operation operator command", async ({ operation, input, expected }) => { + const runMutation = vi.fn(async (_ref: unknown, _args: unknown) => ({ ok: true })); + const ctx = { + runQuery: vi.fn(async () => ({ + environment: "test", + deploymentName: "academic-chihuahua-392", + buildSha: "test-sha", + control: {}, + })), + runMutation, + } as never; + const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", { + method: "POST", + body: JSON.stringify({ operation, ...input }), + }); + + const response = await skillsShCatalogTestV1Handler(ctx, request); + + expect(response.status).toBe(200); + expect(runMutation).toHaveBeenCalledOnce(); + expect(runMutation.mock.calls[0]?.[1]).toEqual(expected); + }); + + it.each([ + { operation: "set-publication", field: "enabled", value: undefined }, + { operation: "set-publication", field: "enabled", value: "true" }, + { operation: "set-publication", field: "enabled", value: null }, + { operation: "set-pause", field: "paused", value: undefined }, + { operation: "set-pause", field: "paused", value: "false" }, + { operation: "set-pause", field: "paused", value: 0 }, + ])("rejects malformed $operation $field controls", async ({ operation, field, value }) => { + const runMutation = vi.fn(async (_ref: unknown, _args: unknown) => ({ ok: true })); + const ctx = { + runQuery: vi.fn(async () => ({ + environment: "test", + deploymentName: "academic-chihuahua-392", + buildSha: "test-sha", + control: {}, + })), + runMutation, + } as never; + const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", { + method: "POST", + body: JSON.stringify({ + operation, + ...(value === undefined ? {} : { [field]: value }), + reason: "exercise strict operator input validation", + confirm: + operation === "set-publication" + ? "set-skills-sh-test-publication" + : "set-skills-sh-test-pause", + }), + }); + + const response = await skillsShCatalogTestV1Handler(ctx, request); + + expect(response.status).toBe(400); + expect(await response.text()).toBe(`${field} is required`); + expect(runMutation).not.toHaveBeenCalled(); + }); +}); + +describe("skills.sh public HTTP API", () => { + const publicEntry = { + ref: "skills-sh/patrick-erichsen/skills/html", + route: "/skills-sh/patrick-erichsen/skills/html", + displayName: "HTML Artifact Chooser", + security: { + verdict: "clean", + source: "clawhub", + attemptId: "skillsShCatalogScanAttempts:canary", + }, + install: { + ok: true, + slug: "skills-sh/patrick-erichsen/skills/html", + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit: "050daba89f6b6636470add5cb300aac46a412cf8", + contentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + sourceUrl: + "https://github.com/patrick-erichsen/skills/tree/050daba89f6b6636470add5cb300aac46a412cf8/skills/html", + }, + }, + }; + + it.each([ + { + suffix: "", + expected: publicEntry, + }, + { + suffix: "/install", + expected: publicEntry.install, + }, + ])("serves an approved slash route$suffix", async ({ suffix, expected }) => { + const runQuery = vi.fn(async () => publicEntry); + const ctx = { runQuery } as never; + const response = await skillsShCatalogPublicV1Handler( + ctx, + new Request( + `https://academic-chihuahua-392.convex.site/api/v1/skills-sh/patrick-erichsen/skills/html${suffix}`, + ), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual(expected); + expect(runQuery).toHaveBeenCalledWith(expect.anything(), { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }); + }); + + it.each([ + "/api/v1/skills-sh:patrick-erichsen/skills/html/install", + "/api/v1/skills-sh/patrick-erichsen/skills/html/extra", + "/api/v1/skills-sh/patrick-erichsen/skills%3Ahtml/install", + "/api/v1/skills-sh/patrick-erichsen/%/html/install", + ])("rejects malformed or colon-form references at %s", async (path) => { + const runQuery = vi.fn(); + const ctx = { runQuery } as never; + const response = await skillsShCatalogPublicV1Handler( + ctx, + new Request(`https://academic-chihuahua-392.convex.site${path}`), + ); + + expect(response.status).toBe(404); + expect(runQuery).not.toHaveBeenCalled(); + }); + + it("returns 404 while the exact entry is not public", async () => { + const ctx = { runQuery: vi.fn(async () => null) } as never; + const response = await skillsShCatalogPublicV1Handler( + ctx, + new Request( + "https://academic-chihuahua-392.convex.site/api/v1/skills-sh/patrick-erichsen/skills/html/install", + ), + ); + + expect(response.status).toBe(404); + expect(await response.text()).toBe("Skill not found"); + }); }); diff --git a/convex/httpApiV1/skillsShCatalogV1.ts b/convex/httpApiV1/skillsShCatalogV1.ts index 5a8d206a..42e06cce 100644 --- a/convex/httpApiV1/skillsShCatalogV1.ts +++ b/convex/httpApiV1/skillsShCatalogV1.ts @@ -1,4 +1,4 @@ -import { internal } from "../_generated/api"; +import { api, internal } from "../_generated/api"; import type { Id } from "../_generated/dataModel"; import type { ActionCtx } from "../_generated/server"; import { buildGitHubApiHeaders } from "../lib/githubAuth"; @@ -20,6 +20,10 @@ const internalRefs = internal as unknown as { processStagingLiveBatchInternal: unknown; resolveKnownGitHubOwnersInternal: unknown; rollbackFixtureRunInternal: unknown; + rollbackPublicationInternal: unknown; + setCatalogPausedInternal: unknown; + setPublicationEnabledInternal: unknown; + startControlledCanaryScanRunInternal: unknown; startFixtureRunInternal: unknown; startStagingLiveRunInternal: unknown; }; @@ -29,6 +33,19 @@ const GITHUB_OWNER_RESOLUTION_CONCURRENCY = 8; const CONTROLLED_CANARY_FIXTURE_ID = "patrick-html-canary-v1"; const MAX_CONTROLLED_CANARY_FILES = 100; +export function parseSkillsShCatalogReference(value: string) { + const segments = value + .trim() + .split("/") + .map((segment) => segment.trim().toLowerCase()); + if (segments.length !== 4 || segments[0] !== "skills-sh") return null; + const [owner, repo, slug] = segments.slice(1); + if (!owner || !repo || !slug || [owner, repo, slug].some((part) => part.includes(":"))) { + return null; + } + return { owner, repo, slug }; +} + async function runMutationRef( ctx: ActionCtx, ref: unknown, @@ -67,6 +84,12 @@ function requireNumber(record: Record, key: string) { return value; } +function requireBoolean(record: Record, key: string) { + const value = record[key]; + if (typeof value !== "boolean") throw new Error(`${key} is required`); + return value; +} + async function sha256Hex(bytes: Uint8Array) { const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer); return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); @@ -423,6 +446,57 @@ export async function skillsShCatalogTestV1Handler(ctx: ActionCtx, request: Requ ); return json({ ...result, sourceVerification: verification }, 200, rate.headers); } + if (operation === "start-canary-scan") { + return json( + await runMutationRef( + ctx, + internalRefs.skillsShCatalog.startControlledCanaryScanRunInternal, + { + actor: auth.user.handle, + reason: requireString(body, "reason"), + }, + ), + 200, + rate.headers, + ); + } + if (operation === "set-publication") { + return json( + await runMutationRef(ctx, internalRefs.skillsShCatalog.setPublicationEnabledInternal, { + enabled: requireBoolean(body, "enabled"), + actor: auth.user.handle, + reason: requireString(body, "reason"), + confirm: requireString(body, "confirm"), + }), + 200, + rate.headers, + ); + } + if (operation === "set-pause") { + return json( + await runMutationRef(ctx, internalRefs.skillsShCatalog.setCatalogPausedInternal, { + paused: requireBoolean(body, "paused"), + actor: auth.user.handle, + reason: requireString(body, "reason"), + confirm: requireString(body, "confirm"), + }), + 200, + rate.headers, + ); + } + if (operation === "rollback-publication") { + return json( + await runMutationRef(ctx, internalRefs.skillsShCatalog.rollbackPublicationInternal, { + externalId: requireString(body, "externalId"), + attemptId: requireString(body, "attemptId"), + actor: auth.user.handle, + reason: requireString(body, "reason"), + confirm: requireString(body, "confirm"), + }), + 200, + rate.headers, + ); + } if (operation === "process-fixture") { return json( await runMutationRef(ctx, internalRefs.skillsShCatalog.processFixtureBatchInternal, { @@ -521,3 +595,33 @@ export async function skillsShCatalogTestV1Handler(ctx: ActionCtx, request: Requ return text(unavailable ? "Not found" : message, unavailable ? 404 : 400, rate.headers); } } + +export async function skillsShCatalogPublicV1Handler(ctx: ActionCtx, request: Request) { + const rate = await applyRateLimit(ctx, request, "read"); + if (!rate.ok) return rate.response; + if (request.method !== "GET") return text("Not found", 404, rate.headers); + const prefix = "/api/v1/skills-sh/"; + const pathname = new URL(request.url).pathname; + if (!pathname.startsWith(prefix)) return text("Not found", 404, rate.headers); + let segments: string[]; + try { + segments = pathname + .slice(prefix.length) + .split("/") + .filter(Boolean) + .map((segment) => decodeURIComponent(segment).trim().toLowerCase()); + } catch { + return text("Not found", 404, rate.headers); + } + const install = segments.at(-1) === "install"; + if ((install && segments.length !== 4) || (!install && segments.length !== 3)) { + return text("Not found", 404, rate.headers); + } + const [owner, repo, slug] = segments; + if (!owner || !repo || !slug || [owner, repo, slug].some((part) => part.includes(":"))) { + return text("Not found", 404, rate.headers); + } + const entry = await ctx.runQuery(api.skillsShCatalog.getPublicEntry, { owner, repo, slug }); + if (!entry) return text("Skill not found", 404, rate.headers); + return json(install ? entry.install : entry, 200, rate.headers); +} diff --git a/convex/httpApiV1/skillsV1.ts b/convex/httpApiV1/skillsV1.ts index 7391137a..b91b4611 100644 --- a/convex/httpApiV1/skillsV1.ts +++ b/convex/httpApiV1/skillsV1.ts @@ -81,6 +81,7 @@ import { text, toOptionalNumber, } from "./shared"; +import { parseSkillsShCatalogReference } from "./skillsShCatalogV1"; const MAX_EXPORT_FILE_COUNT = 10_000; const MAX_EXPORT_PAGE_LIMIT = 250; @@ -1828,6 +1829,16 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) if (second === "install" && segments.length === 2) { const installUrl = new URL(request.url); + if (installUrl.searchParams.has("reference")) { + const reference = installUrl.searchParams.get("reference") ?? ""; + const catalogRef = parseSkillsShCatalogReference(reference); + if (!catalogRef || catalogRef.slug !== slug) { + return text("Invalid skills.sh reference", 400, rate.headers); + } + const entry = await ctx.runQuery(api.skillsShCatalog.getPublicEntry, catalogRef); + if (!entry) return text("Skill not found", 404, rate.headers); + return json(entry.install, 200, rate.headers); + } const forceInstall = parseBooleanQueryParam(installUrl.searchParams.get("forceInstall")); const skill = (await runQueryRef< | (InstallResolverSkill & { @@ -2269,6 +2280,72 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) const versionParam = verifyUrl.searchParams.get("version")?.trim(); const tagParam = verifyUrl.searchParams.get("tag")?.trim(); if (versionParam && tagParam) return text("Use either version or tag", 400, rate.headers); + if (verifyUrl.searchParams.has("reference")) { + const reference = verifyUrl.searchParams.get("reference") ?? ""; + const catalogRef = parseSkillsShCatalogReference(reference); + if (!catalogRef || catalogRef.slug !== slug || versionParam || tagParam) { + return text("Invalid skills.sh reference", 400, rate.headers); + } + const entry = await ctx.runQuery(api.skillsShCatalog.getPublicEntry, catalogRef); + if (!entry?.artifact) return text("Skill not found", 404, rate.headers); + const securityPassed = entry.security.verdict === "clean"; + const reasons = securityPassed ? [] : ["security.status_not_clean"]; + return json( + { + schema: "clawhub.skill.verify.v1", + ok: securityPassed, + decision: securityPassed ? "pass" : "fail", + reasons, + slug: entry.ref, + displayName: entry.displayName, + pageUrl: `${publicApiOrigin(request)}${entry.route}`, + publisherHandle: null, + publisherDisplayName: null, + publisherProfileUrl: null, + version: entry.githubCommit, + resolvedFrom: "latest", + tag: null, + createdAt: entry.security.scannedAt, + card: { + available: false, + path: "skill-card.md", + url: null, + sha256: null, + size: null, + contentType: null, + }, + artifact: { + sourceFingerprint: entry.githubContentHash, + bundleFingerprints: [entry.artifact.contentHash], + files: entry.artifact.files, + }, + provenance: { + source: "skills-sh-catalog", + reference: entry.ref, + repository: entry.repository, + path: entry.githubPath, + commit: entry.githubCommit, + contentHash: entry.githubContentHash, + scanAttemptId: entry.security.attemptId, + artifactContentHash: entry.artifact.contentHash, + }, + security: { + status: entry.security.verdict, + passed: securityPassed, + rawStatus: entry.security.verdict, + verdict: entry.security.verdict, + source: entry.security.source, + attemptId: entry.security.attemptId, + checkedAt: entry.security.scannedAt, + }, + signature: { + status: "unsigned", + }, + }, + 200, + rate.headers, + ); + } const skillResult = (await runQueryRef( ctx, diff --git a/convex/lib/skillsShCatalogPublication.test.ts b/convex/lib/skillsShCatalogPublication.test.ts new file mode 100644 index 00000000..c85280bf --- /dev/null +++ b/convex/lib/skillsShCatalogPublication.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import { + buildSkillsShCatalogInstallResolution, + shouldPublishSkillsShCatalogEntry, +} from "./skillsShCatalogPublication"; + +const entry = { + externalId: "patrick-erichsen/skills/html", + githubOwnerId: 20_157_849, + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + githubPath: "skills/html", + githubCommit: "050daba89f6b6636470add5cb300aac46a412cf8", + githubContentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + sourceContentHash: "source-hash", +}; + +const attempt = { + externalId: entry.externalId, + githubOwnerId: entry.githubOwnerId, + owner: entry.owner, + repo: entry.repo, + slug: entry.slug, + githubPath: entry.githubPath, + githubCommit: entry.githubCommit, + githubContentHash: entry.githubContentHash, + sourceContentHash: entry.sourceContentHash, + dispatchKind: "real" as const, + source: "skills-sh-catalog-test" as const, +}; + +const control = { + mode: "staging-live" as const, + paused: false, + publicVisibilityEnabled: true, + realScanAllowlist: [entry.externalId], +}; + +describe("skills.sh catalog publication", () => { + it.each(["clean", "suspicious"] as const)( + "publishes an exact %s result while visibility is enabled", + (verdict) => { + expect(shouldPublishSkillsShCatalogEntry({ control, entry, attempt, verdict })).toBe(true); + }, + ); + + it("rejects a stale callback whose commit no longer matches the entry", () => { + expect( + shouldPublishSkillsShCatalogEntry({ + control, + entry: { ...entry, githubCommit: "1".repeat(40) }, + attempt, + verdict: "clean", + }), + ).toBe(false); + }); + + it.each(["malicious", "failed"] as const)("never publishes a %s result", (verdict) => { + expect(shouldPublishSkillsShCatalogEntry({ control, entry, attempt, verdict })).toBe(false); + }); + + it("rejects a deterministic fixture attempt", () => { + expect( + shouldPublishSkillsShCatalogEntry({ + control, + entry, + attempt: { + ...attempt, + dispatchKind: "deterministic", + source: "skills-sh-catalog-fixture", + }, + verdict: "clean", + }), + ).toBe(false); + }); + + it("rejects an exact attempt removed from the active allowlist", () => { + expect( + shouldPublishSkillsShCatalogEntry({ + control: { ...control, realScanAllowlist: [] }, + entry, + attempt, + verdict: "clean", + }), + ).toBe(false); + }); + + it("returns the approved commit-pinned GitHub descriptor with slash identity", () => { + expect(buildSkillsShCatalogInstallResolution(entry)).toEqual({ + ok: true, + slug: "skills-sh/patrick-erichsen/skills/html", + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit: "050daba89f6b6636470add5cb300aac46a412cf8", + contentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + sourceUrl: + "https://github.com/patrick-erichsen/skills/tree/050daba89f6b6636470add5cb300aac46a412cf8/skills/html", + }, + }); + }); +}); diff --git a/convex/lib/skillsShCatalogPublication.ts b/convex/lib/skillsShCatalogPublication.ts new file mode 100644 index 00000000..8d139a10 --- /dev/null +++ b/convex/lib/skillsShCatalogPublication.ts @@ -0,0 +1,77 @@ +export type SkillsShCatalogPublicationControl = { + mode: "off" | "fixture" | "staging-live"; + paused: boolean; + publicVisibilityEnabled: boolean; + realScanAllowlist: string[]; +}; + +export type SkillsShCatalogIdentity = { + externalId: string; + githubOwnerId: number; + owner: string; + repo: string; + slug: string; + githubPath?: string; + githubCommit?: string; + githubContentHash?: string; + sourceContentHash: string; +}; + +export type SkillsShCatalogVerdict = "clean" | "suspicious" | "malicious" | "failed"; + +export type SkillsShCatalogPublicationAttempt = SkillsShCatalogIdentity & { + dispatchKind: "deterministic" | "real"; + source: "skills-sh-catalog-fixture" | "skills-sh-catalog-test"; +}; + +export function isExactSkillsShCatalogAttempt( + entry: SkillsShCatalogIdentity, + attempt: SkillsShCatalogIdentity, +) { + return ( + attempt.externalId === entry.externalId && + attempt.githubOwnerId === entry.githubOwnerId && + attempt.owner === entry.owner && + attempt.repo === entry.repo && + attempt.slug === entry.slug && + attempt.githubPath === entry.githubPath && + attempt.githubCommit === entry.githubCommit && + attempt.githubContentHash === entry.githubContentHash && + attempt.sourceContentHash === entry.sourceContentHash + ); +} + +export function shouldPublishSkillsShCatalogEntry(args: { + control: SkillsShCatalogPublicationControl | null; + entry: SkillsShCatalogIdentity; + attempt: SkillsShCatalogPublicationAttempt; + verdict: SkillsShCatalogVerdict; +}) { + return ( + args.control?.mode === "staging-live" && + !args.control.paused && + args.control.publicVisibilityEnabled && + args.control.realScanAllowlist.includes(args.attempt.externalId) && + args.attempt.dispatchKind === "real" && + args.attempt.source === "skills-sh-catalog-test" && + (args.verdict === "clean" || args.verdict === "suspicious") && + isExactSkillsShCatalogAttempt(args.entry, args.attempt) + ); +} + +export function buildSkillsShCatalogInstallResolution(entry: SkillsShCatalogIdentity) { + if (!entry.githubPath || !entry.githubCommit || !entry.githubContentHash) return null; + const repo = `${entry.owner}/${entry.repo}`; + return { + ok: true as const, + slug: `skills-sh/${entry.externalId}`, + installKind: "github" as const, + github: { + repo, + path: entry.githubPath, + commit: entry.githubCommit, + contentHash: entry.githubContentHash, + sourceUrl: `https://github.com/${repo}/tree/${entry.githubCommit}/${entry.githubPath}`, + }, + }; +} diff --git a/convex/schema.ts b/convex/schema.ts index 42a786fe..511f9f6b 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -2899,6 +2899,7 @@ const skillsShCatalogEntries = defineTable({ }), ), publicVisible: v.boolean(), + publishedScanAttemptId: v.optional(v.id("skillsShCatalogScanAttempts")), scanStatus: v.union( v.literal("not-planned"), v.literal("planned"), @@ -2923,6 +2924,13 @@ const skillsShCatalogScanAttempts = defineTable({ entryId: v.id("skillsShCatalogEntries"), runId: v.id("skillsShCatalogRuns"), externalId: v.string(), + githubOwnerId: v.optional(v.number()), + owner: v.optional(v.string()), + repo: v.optional(v.string()), + slug: v.optional(v.string()), + githubPath: v.optional(v.string()), + githubCommit: v.optional(v.string()), + githubContentHash: v.optional(v.string()), sourceContentHash: v.string(), artifactContentHash: v.optional(v.string()), skillScanRequestId: v.optional(v.id("skillScanRequests")), @@ -2945,6 +2953,7 @@ const skillsShCatalogScanAttempts = defineTable({ v.literal("failed"), ), ), + publicationRolledBackAt: v.optional(v.number()), completedAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), diff --git a/convex/securityScan.ts b/convex/securityScan.ts index 6d1ce558..ede08760 100644 --- a/convex/securityScan.ts +++ b/convex/securityScan.ts @@ -22,6 +22,10 @@ import { serializedSkillScanRequestFilesBytes, } from "./lib/skillScanRequestFiles"; import { getSkillsShFixtureEnvironmentPolicy } from "./lib/skillsShCatalogEnvironment"; +import { + isExactSkillsShCatalogAttempt, + shouldPublishSkillsShCatalogEntry, +} from "./lib/skillsShCatalogPublication"; import { redactWorkerPublicText } from "./lib/workerTextRedaction"; import { requestSecurityScanDispatch } from "./securityScanDispatch"; @@ -1994,7 +1998,12 @@ export const completeCatalogSkillScanJobInternal = internalMutation({ (request.lastError === "Catalog scan analysis failed" && job.lastError === "Catalog scan analysis failed")) ) { - return { ok: true as const, applied: true as const, publicVisible: false as const }; + const terminalEntry = await ctx.db.get(attempt.entryId); + return { + ok: true as const, + applied: true as const, + publicVisible: terminalEntry?.publicVisible === true, + }; } if ( attempt.status === "canceled" && @@ -2019,9 +2028,13 @@ export const completeCatalogSkillScanJobInternal = internalMutation({ throw new ConvexError("Catalog scan job lease mismatch"); } - const [run, entry] = await Promise.all([ + const [run, entry, control] = await Promise.all([ ctx.db.get(attempt.runId), ctx.db.get(attempt.entryId), + ctx.db + .query("skillsShCatalogControls") + .withIndex("by_key", (q) => q.eq("key", "global")) + .unique(), ]); const now = Date.now(); const terminalizeWithoutResult = async (reason: "run-canceled" | "stale-attempt") => { @@ -2091,11 +2104,38 @@ export const completeCatalogSkillScanJobInternal = internalMutation({ if (run?.status === "canceling" || run?.status === "canceled") { return await terminalizeWithoutResult("run-canceled"); } - if (!entry || entry.sourceContentHash !== attempt.sourceContentHash) { + const attemptIdentity = + attempt.githubOwnerId !== undefined && + attempt.owner !== undefined && + attempt.repo !== undefined && + attempt.slug !== undefined + ? { + externalId: attempt.externalId, + githubOwnerId: attempt.githubOwnerId, + owner: attempt.owner, + repo: attempt.repo, + slug: attempt.slug, + githubPath: attempt.githubPath, + githubCommit: attempt.githubCommit, + githubContentHash: attempt.githubContentHash, + sourceContentHash: attempt.sourceContentHash, + dispatchKind: attempt.dispatchKind, + source: attempt.source, + } + : null; + if (!entry || !attemptIdentity || !isExactSkillsShCatalogAttempt(entry, attemptIdentity)) { return await terminalizeWithoutResult("stale-attempt"); } const scanFailed = args.verdict === "failed"; + const publicVisible = + attempt.publicationRolledBackAt === undefined && + shouldPublishSkillsShCatalogEntry({ + control, + entry, + attempt: attemptIdentity, + verdict: args.verdict, + }); await ctx.db.patch(attempt._id, { status: scanFailed ? "failed" : "succeeded", verdict: args.verdict, @@ -2104,7 +2144,8 @@ export const completeCatalogSkillScanJobInternal = internalMutation({ }); await ctx.db.patch(entry._id, { scanStatus: args.verdict, - publicVisible: false, + publicVisible, + publishedScanAttemptId: publicVisible ? attempt._id : undefined, updatedAt: now, }); await ctx.db.patch(request._id, { @@ -2142,7 +2183,7 @@ export const completeCatalogSkillScanJobInternal = internalMutation({ updatedAt: now, }); } - return { ok: true as const, applied: true as const, publicVisible: false as const }; + return { ok: true as const, applied: true as const, publicVisible }; }, }); diff --git a/convex/skillsShCatalog.test.ts b/convex/skillsShCatalog.test.ts index db0c8bc1..654dd5f7 100644 --- a/convex/skillsShCatalog.test.ts +++ b/convex/skillsShCatalog.test.ts @@ -103,6 +103,28 @@ async function storeTestArtifactFiles( }; } +async function storeAuthenticatedTestArtifact< + T extends { externalId: string; githubContentHash?: string }, +>(t: CatalogTest, row: T, content: string, path = "SKILL.md") { + return await storeAuthenticatedTestArtifactFiles(t, row, [{ path, content }]); +} + +async function storeAuthenticatedTestArtifactFiles< + T extends { externalId: string; githubContentHash?: string }, +>(t: CatalogTest, row: T, inputs: Array<{ path: string; content: string }>) { + const artifact = await storeTestArtifactFiles(t, row.externalId, inputs); + const manifest = artifact.files + .map((file) => `${file.path}\0${file.size}\0${file.sha256.toLowerCase()}`) + .join("\n"); + return { + artifact, + row: { + ...row, + githubContentHash: await sha256Hex(manifest), + }, + }; +} + async function processToTerminal( t: CatalogTest, runId: Id<"skillsShCatalogRuns">, @@ -590,9 +612,14 @@ describe("skills.sh catalog overload control plane", () => { it("replans unchanged staging content after its prior attempt was canceled", async () => { useEnvironment(TEST_ENV); const t = convexTest(schema, modules); - const row = frozenSnapshot.rows.find( + const sourceRow = frozenSnapshot.rows.find( (candidate) => candidate.externalId === "nvidia/skills/aiq-deploy", )!; + const { artifact, row } = await storeAuthenticatedTestArtifact( + t, + sourceRow, + "staging retry artifact", + ); const actorUserId = await t.run(async (ctx) => { return await ctx.db.insert("users", { handle: "catalog-retry-operator", @@ -600,7 +627,6 @@ describe("skills.sh catalog overload control plane", () => { role: "admin", }); }); - const artifact = await storeTestArtifact(t, row.externalId, "staging retry artifact"); await t.mutation(internal.skillsShCatalog.configureFixtureControlInternal, { ...BASE_CONTROL, mode: "staging-live", @@ -1656,7 +1682,17 @@ describe("skills.sh catalog overload control plane", () => { realScanAllowlist: ["nvidia/skills/aiq-deploy"], }); - const rows = frozenSnapshot.rows.map((row) => ({ ...row })); + const sourceRow = frozenSnapshot.rows.find( + (row) => row.externalId === "nvidia/skills/aiq-deploy", + )!; + const { artifact, row: authenticatedRow } = await storeAuthenticatedTestArtifact( + t, + sourceRow, + "hello catalog", + ); + const rows = frozenSnapshot.rows.map((row) => + row.externalId === authenticatedRow.externalId ? authenticatedRow : { ...row }, + ); const { runId } = await t.mutation(internal.skillsShCatalog.startStagingLiveRunInternal, { actor: "catalog-test-operator", reason: "prove exact live Test batching", @@ -1686,7 +1722,6 @@ describe("skills.sh catalog overload control plane", () => { }); expect((await collectEntries(t)).every((entry) => !entry.publicVisible)).toBe(true); - const artifact = await storeTestArtifact(t, "nvidia/skills/aiq-deploy", "hello catalog"); const admitted = await t.action(internal.skillsShCatalog.admitRealScansInternal, { runId, externalIds: ["nvidia/skills/aiq-deploy"], @@ -1804,6 +1839,14 @@ describe("skills.sh catalog overload control plane", () => { maxCatalogInFlight: 1, realScanAllowlist: ["nvidia/skills/aiq-deploy"], }); + const sourceRow = frozenSnapshot.rows.find( + (row) => row.externalId === "nvidia/skills/aiq-deploy", + )!; + const { artifact, row } = await storeAuthenticatedTestArtifact( + t, + sourceRow, + "active expiry artifact", + ); const { runId } = await t.mutation(internal.skillsShCatalog.startStagingLiveRunInternal, { actor: "catalog-expiry-operator", reason: "prove active request expiry is deferred", @@ -1815,13 +1858,8 @@ describe("skills.sh catalog overload control plane", () => { await t.mutation(internal.skillsShCatalog.processStagingLiveBatchInternal, { runId, cursor: 0, - rows: [frozenSnapshot.rows.find((row) => row.externalId === "nvidia/skills/aiq-deploy")!], + rows: [row], }); - const artifact = await storeTestArtifact( - t, - "nvidia/skills/aiq-deploy", - "active expiry artifact", - ); await t.action(internal.skillsShCatalog.admitRealScansInternal, { runId, externalIds: ["nvidia/skills/aiq-deploy"], @@ -1927,6 +1965,14 @@ describe("skills.sh catalog overload control plane", () => { maxCatalogInFlight: 1, realScanAllowlist: ["nvidia/skills/aiq-deploy"], }); + const sourceRow = frozenSnapshot.rows.find( + (row) => row.externalId === "nvidia/skills/aiq-deploy", + )!; + const { artifact, row } = await storeAuthenticatedTestArtifact( + t, + sourceRow, + "running cancellation artifact", + ); const { runId } = await t.mutation(internal.skillsShCatalog.startStagingLiveRunInternal, { actor: "catalog-cancel-operator", reason: "defer running real cancellation", @@ -1938,13 +1984,8 @@ describe("skills.sh catalog overload control plane", () => { await t.mutation(internal.skillsShCatalog.processStagingLiveBatchInternal, { runId, cursor: 0, - rows: [frozenSnapshot.rows.find((row) => row.externalId === "nvidia/skills/aiq-deploy")!], + rows: [row], }); - const artifact = await storeTestArtifact( - t, - "nvidia/skills/aiq-deploy", - "running cancellation artifact", - ); await t.action(internal.skillsShCatalog.admitRealScansInternal, { runId, externalIds: ["nvidia/skills/aiq-deploy"], @@ -2017,6 +2058,14 @@ describe("skills.sh catalog overload control plane", () => { maxCatalogInFlight: 1, realScanAllowlist: ["nvidia/skills/aiq-deploy"], }); + const sourceRow = frozenSnapshot.rows.find( + (row) => row.externalId === "nvidia/skills/aiq-deploy", + )!; + const { artifact, row } = await storeAuthenticatedTestArtifact( + t, + sourceRow, + "expired running cancellation artifact", + ); const { runId } = await t.mutation(internal.skillsShCatalog.startStagingLiveRunInternal, { actor: "catalog-expired-cancel-operator", reason: "terminalize expired running real cancellation", @@ -2028,13 +2077,8 @@ describe("skills.sh catalog overload control plane", () => { await t.mutation(internal.skillsShCatalog.processStagingLiveBatchInternal, { runId, cursor: 0, - rows: [frozenSnapshot.rows.find((row) => row.externalId === "nvidia/skills/aiq-deploy")!], + rows: [row], }); - const artifact = await storeTestArtifact( - t, - "nvidia/skills/aiq-deploy", - "expired running cancellation artifact", - ); await t.action(internal.skillsShCatalog.admitRealScansInternal, { runId, externalIds: ["nvidia/skills/aiq-deploy"], @@ -2290,6 +2334,10 @@ describe("skills.sh catalog overload control plane", () => { maxCatalogInFlight: 1, realScanAllowlist: ["nvidia/skills/aiq-deploy"], }); + const sourceRow = frozenSnapshot.rows.find( + (row) => row.externalId === "nvidia/skills/aiq-deploy", + )!; + const { artifact, row } = await storeAuthenticatedTestArtifact(t, sourceRow, "budget artifact"); const { runId } = await t.mutation(internal.skillsShCatalog.startStagingLiveRunInternal, { actor: "catalog-budget-operator", reason: "prove admission write reservation", @@ -2301,9 +2349,8 @@ describe("skills.sh catalog overload control plane", () => { await t.mutation(internal.skillsShCatalog.processStagingLiveBatchInternal, { runId, cursor: 0, - rows: [frozenSnapshot.rows.find((row) => row.externalId === "nvidia/skills/aiq-deploy")!], + rows: [row], }); - const artifact = await storeTestArtifact(t, "nvidia/skills/aiq-deploy", "budget artifact"); await expect( t.action(internal.skillsShCatalog.admitRealScansInternal, { @@ -2342,6 +2389,13 @@ describe("skills.sh catalog overload control plane", () => { maxCatalogInFlight: 1, realScanAllowlist: ["nvidia/skills/aiq-deploy"], }); + const sourceRow = frozenSnapshot.rows.find( + (row) => row.externalId === "nvidia/skills/aiq-deploy", + )!; + const { artifact, row } = await storeAuthenticatedTestArtifactFiles(t, sourceRow, [ + { path: "SKILL.md", content: "six write artifact" }, + { path: "references/context.md", content: "second embedded artifact file" }, + ]); const { runId } = await t.mutation(internal.skillsShCatalog.startStagingLiveRunInternal, { actor: "catalog-six-write-operator", reason: "prove exact admission write reservation", @@ -2353,12 +2407,8 @@ describe("skills.sh catalog overload control plane", () => { await t.mutation(internal.skillsShCatalog.processStagingLiveBatchInternal, { runId, cursor: 0, - rows: [frozenSnapshot.rows.find((row) => row.externalId === "nvidia/skills/aiq-deploy")!], + rows: [row], }); - const artifact = await storeTestArtifactFiles(t, "nvidia/skills/aiq-deploy", [ - { path: "SKILL.md", content: "six write artifact" }, - { path: "references/context.md", content: "second embedded artifact file" }, - ]); const result = await t.action(internal.skillsShCatalog.admitRealScansInternal, { runId, @@ -2432,7 +2482,7 @@ describe("skills.sh catalog overload control plane", () => { { runId: unchanged.runId, cursor: 0, - rows: [frozenSnapshot.rows.find((row) => row.externalId === "nvidia/skills/aiq-deploy")!], + rows: [row], }, ); expect(unchangedRun.counts).toMatchObject({ diff --git a/convex/skillsShCatalog.ts b/convex/skillsShCatalog.ts index 366a1dde..923dc583 100644 --- a/convex/skillsShCatalog.ts +++ b/convex/skillsShCatalog.ts @@ -3,7 +3,7 @@ import { ConvexError, type Infer, v } from "convex/values"; import { internal } from "./_generated/api"; import type { Doc, Id } from "./_generated/dataModel"; import type { ActionCtx, MutationCtx, QueryCtx } from "./_generated/server"; -import { internalAction, internalMutation, internalQuery } from "./functions"; +import { internalAction, internalMutation, internalQuery, query } from "./functions"; import { assertSkillsShCatalogControlMutationAllowed, assertSkillsShFixtureEnvironmentAllowed, @@ -13,6 +13,11 @@ import { getSkillsShCatalogFixture, type SkillsShCatalogFixtureRow, } from "./lib/skillsShCatalogFixtures"; +import { + buildSkillsShCatalogInstallResolution, + isExactSkillsShCatalogAttempt, + shouldPublishSkillsShCatalogEntry, +} from "./lib/skillsShCatalogPublication"; import { validateFilePath } from "./lib/skillZip"; import { enqueueSkillsShCatalogScanRequest } from "./securityScan"; @@ -20,6 +25,9 @@ const CONTROL_KEY = "global"; const ENABLE_FIXTURE_CONFIRM = "enable-skills-sh-fixture-control"; const DISABLE_CATALOG_CONFIRM = "disable-skills-sh-catalog"; const ROLLBACK_CONTROLLED_CANARY_CONFIRM = "rollback-skills-sh-controlled-canary"; +const SET_PUBLICATION_CONFIRM = "set-skills-sh-test-publication"; +const SET_CATALOG_PAUSE_CONFIRM = "set-skills-sh-test-pause"; +const ROLLBACK_PUBLICATION_CONFIRM = "rollback-skills-sh-test-publication"; const CONTROLLED_CANARY_FIXTURE_ID = "patrick-html-canary-v1"; const STATUS_LIMIT = 50; const MAX_DISCOVERY_ROWS = 20_000; @@ -309,6 +317,7 @@ export const configureFixtureControlInternal = internalMutation({ writesEnabled: v.boolean(), scanPlanningEnabled: v.boolean(), scanAdmissionEnabled: v.boolean(), + publicVisibilityEnabled: v.optional(v.boolean()), maxEntriesPerRun: v.number(), maxEntriesPerBatch: v.number(), maxWritesPerBatch: v.number(), @@ -393,6 +402,17 @@ export const configureFixtureControlInternal = internalMutation({ ) { throw new ConvexError("skills.sh Test controls are capped at 500 discoveries and 10 scans"); } + if ( + args.publicVisibilityEnabled && + (mode !== "staging-live" || + !args.scanAdmissionEnabled || + realScanAllowlist.length < 1 || + realScanAllowlist.length > 3) + ) { + throw new ConvexError( + "skills.sh Test publication requires staging-live admission and an allowlist of 1-3 entries", + ); + } const now = Date.now(); const existing = await getControlDoc(ctx); @@ -402,7 +422,7 @@ export const configureFixtureControlInternal = internalMutation({ writesEnabled: args.writesEnabled, scanPlanningEnabled: args.scanPlanningEnabled, scanAdmissionEnabled: args.scanAdmissionEnabled, - publicVisibilityEnabled: false, + publicVisibilityEnabled: args.publicVisibilityEnabled ?? false, paused: false, maxEntriesPerRun: args.maxEntriesPerRun, maxEntriesPerBatch: args.maxEntriesPerBatch, @@ -470,6 +490,244 @@ export const disableCatalogInternal = internalMutation({ }, }); +export const setPublicationEnabledInternal = internalMutation({ + args: { + enabled: v.boolean(), + actor: v.string(), + reason: v.string(), + confirm: v.string(), + }, + handler: async (ctx, args) => { + const environment = assertSkillsShFixtureEnvironmentAllowed(); + if (environment.environment !== "test") { + throw new ConvexError( + "skills.sh publication controls require the permanent Test environment", + ); + } + if (args.confirm !== SET_PUBLICATION_CONFIRM) { + throw new ConvexError(`Pass confirm="${SET_PUBLICATION_CONFIRM}" to change publication.`); + } + const control = await getControlDoc(ctx); + if (!control) throw new ConvexError("skills.sh catalog controls are not configured"); + if ( + args.enabled && + (control.mode !== "staging-live" || + control.paused || + !control.scanAdmissionEnabled || + control.realScanAllowlist.length < 1 || + control.realScanAllowlist.length > 3) + ) { + throw new ConvexError( + "skills.sh publication requires active staging-live controls and an allowlist of 1-3 entries", + ); + } + const now = Date.now(); + await ctx.db.patch(control._id, { + publicVisibilityEnabled: args.enabled, + updatedBy: args.actor.trim(), + reason: args.reason.trim(), + updatedAt: now, + }); + return { enabled: args.enabled, updatedAt: now }; + }, +}); + +export const setCatalogPausedInternal = internalMutation({ + args: { + paused: v.boolean(), + actor: v.string(), + reason: v.string(), + confirm: v.string(), + }, + handler: async (ctx, args) => { + const environment = assertSkillsShFixtureEnvironmentAllowed(); + if (environment.environment !== "test") { + throw new ConvexError("skills.sh pause controls require the permanent Test environment"); + } + if (args.confirm !== SET_CATALOG_PAUSE_CONFIRM) { + throw new ConvexError(`Pass confirm="${SET_CATALOG_PAUSE_CONFIRM}" to change pause state.`); + } + const control = await getControlDoc(ctx); + if (!control) throw new ConvexError("skills.sh catalog controls are not configured"); + if ( + !args.paused && + (control.mode !== "staging-live" || + !control.scanAdmissionEnabled || + control.realScanAllowlist.length < 1 || + control.realScanAllowlist.length > 3) + ) { + throw new ConvexError( + "skills.sh resume requires staging-live admission and an allowlist of 1-3 entries", + ); + } + const now = Date.now(); + await ctx.db.patch(control._id, { + paused: args.paused, + updatedBy: args.actor.trim(), + reason: args.reason.trim(), + updatedAt: now, + }); + return { paused: args.paused, updatedAt: now }; + }, +}); + +export const startControlledCanaryScanRunInternal = internalMutation({ + args: { + actor: v.string(), + reason: v.string(), + }, + handler: async (ctx, args) => { + const environment = assertSkillsShFixtureEnvironmentAllowed(); + const control = assertScanAdmissionEnabled(await getControlDoc(ctx)); + if ( + environment.environment !== "test" || + control.mode !== "staging-live" || + control.realScanAllowlist.length !== 1 + ) { + throw new ConvexError("controlled canary scan requires one-entry permanent Test controls"); + } + const fixture = getSkillsShCatalogFixture(CONTROLLED_CANARY_FIXTURE_ID); + const expected = normalizeIdentity(fixture.rowAt(0)); + if (control.realScanAllowlist[0] !== expected.externalId) { + throw new ConvexError( + "controlled canary scan allowlist does not match the committed fixture", + ); + } + const entry = await ctx.db + .query("skillsShCatalogEntries") + .withIndex("by_external_id", (q) => q.eq("externalId", expected.externalId)) + .unique(); + if ( + !entry || + entry.githubOwnerId !== expected.githubOwnerId || + entry.githubPath !== expected.githubPath || + entry.githubCommit !== expected.githubCommit || + entry.githubContentHash !== expected.githubContentHash || + entry.sourceContentHash !== expected.sourceContentHash + ) { + throw new ConvexError("controlled canary row does not match the committed fixture"); + } + const existingAttempt = await ctx.db + .query("skillsShCatalogScanAttempts") + .withIndex("by_entry_and_source_content_hash", (q) => + q.eq("entryId", entry._id).eq("sourceContentHash", entry.sourceContentHash), + ) + .filter((q) => + q.and( + q.eq(q.field("dispatchKind"), "real"), + q.eq(q.field("source"), "skills-sh-catalog-test"), + ), + ) + .order("desc") + .first(); + const existingAttemptIsExact = + existingAttempt?.githubOwnerId !== undefined && + existingAttempt.owner !== undefined && + existingAttempt.repo !== undefined && + existingAttempt.slug !== undefined && + isExactSkillsShCatalogAttempt(entry, { + externalId: existingAttempt.externalId, + githubOwnerId: existingAttempt.githubOwnerId, + owner: existingAttempt.owner, + repo: existingAttempt.repo, + slug: existingAttempt.slug, + githubPath: existingAttempt.githubPath, + githubCommit: existingAttempt.githubCommit, + githubContentHash: existingAttempt.githubContentHash, + sourceContentHash: existingAttempt.sourceContentHash, + }); + if ( + existingAttemptIsExact && + (existingAttempt.status === "queued" || existingAttempt.status === "running") + ) { + throw new ConvexError("controlled canary scan attempt is already active"); + } + if ( + existingAttemptIsExact && + existingAttempt.status === "succeeded" && + (existingAttempt.verdict === "clean" || existingAttempt.verdict === "suspicious") && + existingAttempt.publicationRolledBackAt === undefined + ) { + const shouldPublish = shouldPublishSkillsShCatalogEntry({ + control, + entry, + attempt: { + externalId: existingAttempt.externalId, + githubOwnerId: existingAttempt.githubOwnerId!, + owner: existingAttempt.owner!, + repo: existingAttempt.repo!, + slug: existingAttempt.slug!, + githubPath: existingAttempt.githubPath, + githubCommit: existingAttempt.githubCommit, + githubContentHash: existingAttempt.githubContentHash, + sourceContentHash: existingAttempt.sourceContentHash, + dispatchKind: existingAttempt.dispatchKind, + source: existingAttempt.source, + }, + verdict: existingAttempt.verdict, + }); + if ( + shouldPublish && + (!entry.publicVisible || + entry.publishedScanAttemptId !== existingAttempt._id || + entry.scanStatus !== existingAttempt.verdict) + ) { + await ctx.db.patch(entry._id, { + scanStatus: existingAttempt.verdict, + publicVisible: true, + publishedScanAttemptId: existingAttempt._id, + updatedAt: Date.now(), + }); + } + return { + runId: existingAttempt.runId, + externalId: expected.externalId, + reused: true as const, + }; + } + const now = Date.now(); + const runId = await ctx.db.insert("skillsShCatalogRuns", { + fixtureId: CONTROLLED_CANARY_FIXTURE_ID, + snapshotId: fixture.snapshotId, + sourceKind: fixture.sourceKind, + ...(fixture.capturedAt ? { sourceCapturedAt: fixture.capturedAt } : {}), + snapshotCaptureFetches: fixture.snapshotCaptureFetches, + dryRun: false, + status: "completed", + cursor: 1, + scanCursor: 0, + fixtureLength: 1, + counts: { ...emptyCounts(), observed: 1, unchanged: 1, scansPlanned: 1 }, + budgets: { + maxEntriesPerRun: control.maxEntriesPerRun, + maxEntriesPerBatch: control.maxEntriesPerBatch, + maxWritesPerBatch: control.maxWritesPerBatch, + maxPlannedScans: control.maxPlannedScans, + maxScanAdmissionsPerBatch: control.maxScanAdmissionsPerBatch, + maxScanAdmissionsPerRun: control.maxScanAdmissionsPerRun, + maxScanAdmissionsPerDay: control.maxScanAdmissionsPerDay, + }, + operations: { functionCalls: 1, dbReads: 2, dbWrites: 2 }, + actor: args.actor.trim(), + reason: args.reason.trim(), + batchesProcessed: 0, + scanAdmissionBatches: 0, + lastBatchWrites: 2, + lastBatchReads: 2, + startedAt: now, + completedAt: now, + updatedAt: now, + }); + await ctx.db.patch(entry._id, { + scanStatus: "planned", + publicVisible: false, + publishedScanAttemptId: undefined, + updatedAt: now, + }); + return { runId, externalId: expected.externalId, reused: false as const }; + }, +}); + export const startFixtureRunInternal = internalMutation({ args: { fixtureId: fixtureIdValidator, @@ -1119,7 +1377,12 @@ async function admitScans(ctx: MutationCtx, args: AdmitScansArgs) { if (control.mode !== "staging-live") { throw new ConvexError("real skills.sh scan admission requires staging-live controls"); } - if (run.sourceKind !== "staging-live" || run.fixtureId !== "skills-sh-test-live-500") { + const controlledCanaryRun = + run.fixtureId === CONTROLLED_CANARY_FIXTURE_ID && run.fixtureLength === 1; + if ( + !controlledCanaryRun && + (run.sourceKind !== "staging-live" || run.fixtureId !== "skills-sh-test-live-500") + ) { throw new ConvexError("real skills.sh scan admission requires a staging-live run"); } if (environment.environment !== "test") { @@ -1206,11 +1469,44 @@ async function admitScans(ctx: MutationCtx, args: AdmitScansArgs) { .withIndex("by_entry_and_source_content_hash", (q) => q.eq("entryId", entry._id).eq("sourceContentHash", entry.sourceContentHash), ) - .filter((q) => q.neq(q.field("status"), "canceled")) + .filter((q) => { + const expectedSource = + args.dispatchKind === "real" ? "skills-sh-catalog-test" : "skills-sh-catalog-fixture"; + return q.and( + q.neq(q.field("status"), "canceled"), + q.eq(q.field("dispatchKind"), args.dispatchKind), + q.eq(q.field("source"), expectedSource), + ); + }) .order("desc") .first(); readsUsed += 1; - if (existingAttempt) { + const existingAttemptIsExact = + existingAttempt?.githubOwnerId !== undefined && + existingAttempt.owner !== undefined && + existingAttempt.repo !== undefined && + existingAttempt.slug !== undefined && + isExactSkillsShCatalogAttempt(entry, { + externalId: existingAttempt.externalId, + githubOwnerId: existingAttempt.githubOwnerId, + owner: existingAttempt.owner, + repo: existingAttempt.repo, + slug: existingAttempt.slug, + githubPath: existingAttempt.githubPath, + githubCommit: existingAttempt.githubCommit, + githubContentHash: existingAttempt.githubContentHash, + sourceContentHash: existingAttempt.sourceContentHash, + }); + const existingAttemptBlocksAdmission = + existingAttemptIsExact && + (existingAttempt.status === "queued" || + existingAttempt.status === "running" || + (existingAttempt.status === "succeeded" && + (existingAttempt.verdict === "clean" || existingAttempt.verdict === "suspicious") && + existingAttempt.publicationRolledBackAt === undefined && + entry.publicVisible && + entry.publishedScanAttemptId === existingAttempt._id)); + if (existingAttemptBlocksAdmission) { skipped += 1; continue; } @@ -1234,6 +1530,15 @@ async function admitScans(ctx: MutationCtx, args: AdmitScansArgs) { `real Test scan admission requires a fetched artifact: ${externalId}`, ); } + const authenticatedContentHash = await computeGitHubArtifactContentHash(artifact.files); + if ( + !entry.githubContentHash || + authenticatedContentHash !== entry.githubContentHash.toLowerCase() + ) { + throw new ConvexError( + `real Test scan artifact does not match authenticated GitHub content: ${externalId}`, + ); + } } // skillScanRequests embeds its validated file manifest in one document, so file count // does not change the six real-admission writes before the final run patch. @@ -1245,6 +1550,13 @@ async function admitScans(ctx: MutationCtx, args: AdmitScansArgs) { entryId: entry._id, runId: run._id, externalId, + githubOwnerId: entry.githubOwnerId, + owner: entry.owner, + repo: entry.repo, + slug: entry.slug, + githubPath: entry.githubPath, + githubCommit: entry.githubCommit, + githubContentHash: entry.githubContentHash, sourceContentHash: entry.sourceContentHash, dispatchKind: args.dispatchKind, artifactContentHash: artifact?.artifactContentHash.toLowerCase(), @@ -1269,6 +1581,7 @@ async function admitScans(ctx: MutationCtx, args: AdmitScansArgs) { await ctx.db.patch(entry._id, { scanStatus: "queued", publicVisible: false, + publishedScanAttemptId: undefined, updatedAt: now, }); writesUsed += 2; @@ -2004,7 +2317,7 @@ export const getRunReconciliationInternal = internalQuery({ ...entry, resolution: { externalRoute: `/skills-sh/${entry.externalId}`, - installRef: `skills-sh:${entry.externalId}`, + installRef: `skills-sh/${entry.externalId}`, installable: false, }, }); @@ -2201,7 +2514,7 @@ export const getStatusInternal = internalQuery({ ...entry, resolution: { externalRoute: `/skills-sh/${entry.externalId}`, - installRef: `skills-sh:${entry.externalId}`, + installRef: `skills-sh/${entry.externalId}`, installable: false, }, })), @@ -2215,6 +2528,198 @@ export const getStatusInternal = internalQuery({ }, }); +export const getPublicEntry = query({ + args: { + owner: v.string(), + repo: v.string(), + slug: v.string(), + }, + handler: async (ctx, args) => { + const environment = getSkillsShFixtureEnvironmentPolicy(); + if (!environment.allowed) return null; + const externalId = `${args.owner.trim().toLowerCase()}/${args.repo + .trim() + .toLowerCase()}/${args.slug.trim().toLowerCase()}`; + const [control, entry] = await Promise.all([ + getControlDoc(ctx), + ctx.db + .query("skillsShCatalogEntries") + .withIndex("by_external_id", (q) => q.eq("externalId", externalId)) + .unique(), + ]); + if ( + !control || + control.mode !== "staging-live" || + control.paused || + !control.publicVisibilityEnabled || + !entry?.publicVisible || + (entry.scanStatus !== "clean" && entry.scanStatus !== "suspicious") + ) { + return null; + } + const attempt = entry.publishedScanAttemptId + ? await ctx.db.get(entry.publishedScanAttemptId) + : null; + if ( + !attempt || + attempt.status !== "succeeded" || + attempt.publicationRolledBackAt !== undefined || + attempt.verdict !== entry.scanStatus || + !shouldPublishSkillsShCatalogEntry({ + control, + entry, + attempt: { + externalId: attempt.externalId, + githubOwnerId: attempt.githubOwnerId ?? 0, + owner: attempt.owner ?? "", + repo: attempt.repo ?? "", + slug: attempt.slug ?? "", + githubPath: attempt.githubPath, + githubCommit: attempt.githubCommit, + githubContentHash: attempt.githubContentHash, + sourceContentHash: attempt.sourceContentHash, + dispatchKind: attempt.dispatchKind, + source: attempt.source, + }, + verdict: attempt.verdict, + }) + ) { + return null; + } + const install = buildSkillsShCatalogInstallResolution(entry); + if (!install) return null; + const scanRequest = attempt.skillScanRequestId + ? await ctx.db.get(attempt.skillScanRequestId) + : null; + const artifact = + attempt.artifactContentHash && + scanRequest?.sourceKind === "skills-sh-catalog" && + scanRequest.status === "succeeded" && + scanRequest.skillsShCatalogAttemptId === attempt._id && + scanRequest.securityScanJobId === attempt.securityScanJobId && + scanRequest.sha256hash === attempt.artifactContentHash + ? { + contentHash: attempt.artifactContentHash, + files: scanRequest.files.map(({ path, size, sha256, contentType }) => ({ + path, + size, + sha256, + ...(contentType ? { contentType } : {}), + })), + } + : null; + return { + ref: `skills-sh/${entry.externalId}`, + route: `/skills-sh/${entry.externalId}`, + displayName: entry.displayName, + summary: + entry.scanStatus === "suspicious" + ? "GitHub-backed skill indexed by skills.sh and flagged as suspicious by ClawHub." + : "GitHub-backed skill indexed by skills.sh and verified by ClawHub.", + owner: { + handle: entry.owner, + githubUrl: `https://github.com/${entry.owner}`, + }, + repository: `${entry.owner}/${entry.repo}`, + githubPath: entry.githubPath, + githubCommit: entry.githubCommit, + githubContentHash: entry.githubContentHash, + sourceUrl: entry.sourceUrl, + installs: entry.installs, + security: { + verdict: entry.scanStatus, + source: "clawhub" as const, + attemptId: attempt._id, + scannedAt: attempt.completedAt ?? attempt.updatedAt, + }, + artifact, + install, + }; + }, +}); + +export const rollbackPublicationInternal = internalMutation({ + args: { + externalId: v.string(), + attemptId: v.id("skillsShCatalogScanAttempts"), + actor: v.string(), + reason: v.string(), + confirm: v.string(), + }, + handler: async (ctx, args) => { + const environment = assertSkillsShFixtureEnvironmentAllowed(); + if (environment.environment !== "test") { + throw new ConvexError("skills.sh publication rollback requires permanent Test"); + } + if (args.confirm !== ROLLBACK_PUBLICATION_CONFIRM) { + throw new ConvexError( + `Pass confirm="${ROLLBACK_PUBLICATION_CONFIRM}" to roll back publication.`, + ); + } + const entry = await ctx.db + .query("skillsShCatalogEntries") + .withIndex("by_external_id", (q) => q.eq("externalId", args.externalId.trim().toLowerCase())) + .unique(); + const attempt = await ctx.db.get(args.attemptId); + const attemptIdentity = + attempt?.githubOwnerId !== undefined && + attempt.owner !== undefined && + attempt.repo !== undefined && + attempt.slug !== undefined + ? { + externalId: attempt.externalId, + githubOwnerId: attempt.githubOwnerId, + owner: attempt.owner, + repo: attempt.repo, + slug: attempt.slug, + githubPath: attempt.githubPath, + githubCommit: attempt.githubCommit, + githubContentHash: attempt.githubContentHash, + sourceContentHash: attempt.sourceContentHash, + } + : null; + if ( + !entry || + !attempt || + attempt.entryId !== entry._id || + !attemptIdentity || + !isExactSkillsShCatalogAttempt(entry, attemptIdentity) || + attempt.status !== "succeeded" || + attempt.dispatchKind !== "real" || + attempt.source !== "skills-sh-catalog-test" || + (attempt.verdict !== "clean" && attempt.verdict !== "suspicious") + ) { + throw new ConvexError("skills.sh publication rollback identity mismatch"); + } + if (attempt.publicationRolledBackAt !== undefined) { + return { + externalId: entry.externalId, + publicVisible: entry.publicVisible, + alreadyRolledBack: true, + actor: args.actor.trim(), + reason: args.reason.trim(), + }; + } + if (entry.publishedScanAttemptId !== attempt._id) { + throw new ConvexError("skills.sh publication rollback attempt is not currently published"); + } + const now = Date.now(); + await ctx.db.patch(attempt._id, { publicationRolledBackAt: now, updatedAt: now }); + await ctx.db.patch(entry._id, { + publicVisible: false, + publishedScanAttemptId: undefined, + updatedAt: now, + }); + return { + externalId: entry.externalId, + publicVisible: false, + alreadyRolledBack: false, + actor: args.actor.trim(), + reason: args.reason.trim(), + }; + }, +}); + function emptyCounts() { return { observed: 0, @@ -2284,6 +2789,14 @@ async function sha256Hex(bytes: Uint8Array) { return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); } +async function computeGitHubArtifactContentHash(files: StagingLiveArtifact["files"]) { + const manifest = [...files] + .sort((left, right) => left.path.localeCompare(right.path)) + .map((file) => `${file.path}\0${file.size}\0${file.sha256.toLowerCase()}`) + .join("\n"); + return await sha256Hex(new TextEncoder().encode(manifest)); +} + async function validateRealScanArtifacts( ctx: ActionCtx, externalIds: string[], @@ -2349,6 +2862,13 @@ async function insertCatalogScanAttempt( entryId: Id<"skillsShCatalogEntries">; runId: Id<"skillsShCatalogRuns">; externalId: string; + githubOwnerId: number; + owner: string; + repo: string; + slug: string; + githubPath?: string; + githubCommit?: string; + githubContentHash?: string; sourceContentHash: string; dispatchKind: "deterministic" | "real"; artifactContentHash?: string; @@ -2359,6 +2879,13 @@ async function insertCatalogScanAttempt( entryId: args.entryId, runId: args.runId, externalId: args.externalId, + githubOwnerId: args.githubOwnerId, + owner: args.owner, + repo: args.repo, + slug: args.slug, + ...(args.githubPath ? { githubPath: args.githubPath } : {}), + ...(args.githubCommit ? { githubCommit: args.githubCommit } : {}), + ...(args.githubContentHash ? { githubContentHash: args.githubContentHash } : {}), sourceContentHash: args.sourceContentHash, ...(args.artifactContentHash ? { artifactContentHash: args.artifactContentHash } : {}), source: args.dispatchKind === "real" ? "skills-sh-catalog-test" : "skills-sh-catalog-fixture", diff --git a/convex/skillsShCatalogCanary.test.ts b/convex/skillsShCatalogCanary.test.ts index 37c94f99..8586df24 100644 --- a/convex/skillsShCatalogCanary.test.ts +++ b/convex/skillsShCatalogCanary.test.ts @@ -2,8 +2,9 @@ /* @vitest-environment edge-runtime */ import { convexTest } from "convex-test"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { internal } from "./_generated/api"; -import type { Id } from "./_generated/dataModel"; +import { api, internal } from "./_generated/api"; +import type { Doc, Id } from "./_generated/dataModel"; +import canarySkillMarkdown from "./fixtures/patrick-html-canary-SKILL.txt?raw"; import schema from "./schema"; const modules = import.meta.glob("./**/*.ts"); @@ -12,6 +13,13 @@ const LOCAL_ENV = { CONVEX_CLOUD_URL: "http://127.0.0.1:3210", }; +const TEST_ENV = { + CLAWHUB_DEPLOYMENT_NAME: "academic-chihuahua-392", + CLAWHUB_DISABLE_CRONS: "1", + CLAWHUB_ENV: "test", + CONVEX_CLOUD_URL: "https://academic-chihuahua-392.convex.cloud", +}; + const CANARY_EXTERNAL_ID = "patrick-erichsen/skills/html"; const CANARY_COMMIT = "050daba89f6b6636470add5cb300aac46a412cf8"; const CANARY_CONTENT_HASH = "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f"; @@ -49,8 +57,8 @@ const SOURCE_VERIFICATION = { type CatalogTest = ReturnType; -function useLocalEnvironment() { - for (const [name, value] of Object.entries(LOCAL_ENV)) vi.stubEnv(name, value); +function useEnvironment(env: Record) { + for (const [name, value] of Object.entries(env)) vi.stubEnv(name, value); } async function configureCanary(t: CatalogTest) { @@ -70,6 +78,118 @@ async function runCanary(t: CatalogTest) { return { runId: started.runId, run }; } +async function sha256Hex(value: Blob | string) { + const bytes = + typeof value === "string" + ? new TextEncoder().encode(value) + : new Uint8Array(await value.arrayBuffer()); + const digest = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +async function storeCanaryArtifact(t: CatalogTest, content = canarySkillMarkdown) { + const blob = new Blob([content], { type: "text/markdown" }); + const storageId = await t.run(async (ctx) => await ctx.storage.store(blob)); + const sha256 = await sha256Hex(blob); + return { + externalId: CANARY_EXTERNAL_ID, + artifactContentHash: await sha256Hex(`SKILL.md\0${sha256}\n`), + files: [ + { + path: "SKILL.md", + size: blob.size, + storageId, + sha256, + contentType: "text/markdown", + }, + ], + }; +} + +async function prepareScannedCanary(t: CatalogTest) { + await configureCanary(t); + await runCanary(t); + await t.mutation(internal.skillsShCatalog.configureFixtureControlInternal, { + ...CANARY_CONTROL, + mode: "staging-live", + scanAdmissionEnabled: true, + publicVisibilityEnabled: true, + maxWritesPerBatch: 7, + maxScanAdmissionsPerBatch: 1, + maxScanAdmissionsPerRun: 1, + maxScanAdmissionsPerDay: 1, + maxCatalogQueued: 1, + maxCatalogInFlight: 1, + realScanAllowlist: [CANARY_EXTERNAL_ID], + }); + const actorUserId = await t.run( + async (ctx) => + await ctx.db.insert("users", { + handle: "catalog-test-operator", + displayName: "Catalog Test Operator", + role: "admin", + }), + ); + const { runId } = await t.mutation( + internal.skillsShCatalog.startControlledCanaryScanRunInternal, + { + actor: "catalog-test-operator", + reason: "scan one exact controlled canary", + }, + ); + const artifact = await storeCanaryArtifact(t); + await t.action(internal.skillsShCatalog.admitRealScansInternal, { + runId, + externalIds: [CANARY_EXTERNAL_ID], + actorUserId, + artifacts: [artifact], + }); + const [attempt] = await t.run(async (ctx) => + (await ctx.db.query("skillsShCatalogScanAttempts").collect()).filter( + (candidate) => candidate.runId === runId && candidate.status === "queued", + ), + ); + if (!attempt?.skillScanRequestId || !attempt.securityScanJobId || !attempt.artifactContentHash) { + throw new Error("controlled canary scan admission did not create linked work"); + } + await t.run(async (ctx) => { + await ctx.db.patch(attempt._id, { status: "running", updatedAt: Date.now() }); + await ctx.db.patch(attempt.skillScanRequestId!, { + status: "running", + updatedAt: Date.now(), + }); + await ctx.db.patch(attempt.securityScanJobId!, { + status: "running", + leaseToken: "canary-lease", + leaseExpiresAt: Date.now() + 60_000, + workerId: "canary-worker", + updatedAt: Date.now(), + }); + }); + return attempt as Doc<"skillsShCatalogScanAttempts"> & { + skillScanRequestId: Id<"skillScanRequests">; + securityScanJobId: Id<"securityScanJobs">; + artifactContentHash: string; + }; +} + +async function completeScannedCanary( + t: CatalogTest, + attempt: Awaited>, + verdict: "clean" | "suspicious" | "malicious" | "failed", +) { + return await t.mutation(internal.securityScan.completeCatalogSkillScanJobInternal, { + attemptId: attempt._id, + scanId: attempt.skillScanRequestId, + jobId: attempt.securityScanJobId, + leaseToken: "canary-lease", + artifactContentHash: attempt.artifactContentHash, + verdict, + runId: "canary-clawscan-run", + llmAnalysis: { status: verdict, checkedAt: Date.now() }, + }); +} + async function seedNativeSkill( t: CatalogTest, options: { @@ -135,7 +255,7 @@ describe("skills.sh controlled hidden metadata canary", () => { }); it("records a new external skill without creating native state", async () => { - useLocalEnvironment(); + useEnvironment(LOCAL_ENV); const t = convexTest(schema, modules); await configureCanary(t); @@ -186,7 +306,7 @@ describe("skills.sh controlled hidden metadata canary", () => { }); it("records an exact native match and preserves its downloads", async () => { - useLocalEnvironment(); + useEnvironment(LOCAL_ENV); const t = convexTest(schema, modules); const nativeSkillId = await seedNativeSkill(t, { exactSource: true, downloads: 143 }); await configureCanary(t); @@ -220,7 +340,7 @@ describe("skills.sh controlled hidden metadata canary", () => { }); it("records a route collision without changing or attaching the native skill", async () => { - useLocalEnvironment(); + useEnvironment(LOCAL_ENV); const t = convexTest(schema, modules); const nativeSkillId = await seedNativeSkill(t, { exactSource: false, downloads: 77 }); await configureCanary(t); @@ -253,7 +373,7 @@ describe("skills.sh controlled hidden metadata canary", () => { }); it("reruns idempotently and rolls back only the hidden canary metadata", async () => { - useLocalEnvironment(); + useEnvironment(LOCAL_ENV); const t = convexTest(schema, modules); const nativeSkillId = await seedNativeSkill(t, { exactSource: false, downloads: 91 }); await configureCanary(t); @@ -293,4 +413,573 @@ describe("skills.sh controlled hidden metadata canary", () => { }); expect(first.runId).not.toBe(repeated.runId); }); + + it.each(["clean", "suspicious"] as const)( + "publishes only the exact %s canary attempt and resolves a pinned GitHub install", + async (verdict) => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + const attempt = await prepareScannedCanary(t); + + await expect(completeScannedCanary(t, attempt, verdict)).resolves.toEqual({ + ok: true, + applied: true, + publicVisible: true, + }); + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toMatchObject({ + ref: "skills-sh/patrick-erichsen/skills/html", + route: "/skills-sh/patrick-erichsen/skills/html", + artifact: { + contentHash: attempt.artifactContentHash, + files: [ + { + path: "SKILL.md", + size: expect.any(Number), + sha256: expect.stringMatching(/^[a-f0-9]{64}$/), + contentType: "text/markdown", + }, + ], + }, + security: { + verdict, + source: "clawhub", + attemptId: attempt._id, + }, + install: { + ok: true, + slug: "skills-sh/patrick-erichsen/skills/html", + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit: CANARY_COMMIT, + contentHash: CANARY_CONTENT_HASH, + }, + }, + }); + }, + ); + + it("omits verification artifacts when the scan request no longer matches the approved attempt", async () => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + const attempt = await prepareScannedCanary(t); + await completeScannedCanary(t, attempt, "clean"); + await t.run(async (ctx) => { + await ctx.db.patch(attempt.skillScanRequestId, { + sha256hash: "0".repeat(64), + }); + }); + + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toMatchObject({ + ref: "skills-sh/patrick-erichsen/skills/html", + artifact: null, + security: { attemptId: attempt._id, verdict: "clean" }, + }); + }); + + it("reuses an exact completed canary scan without hiding the published entry", async () => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + const attempt = await prepareScannedCanary(t); + await completeScannedCanary(t, attempt, "clean"); + + await expect( + t.mutation(internal.skillsShCatalog.startControlledCanaryScanRunInternal, { + actor: "catalog-test-operator", + reason: "repeat the exact approved canary", + }), + ).resolves.toEqual({ + runId: attempt.runId, + externalId: CANARY_EXTERNAL_ID, + reused: true, + }); + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toMatchObject({ + ref: "skills-sh/patrick-erichsen/skills/html", + security: { attemptId: attempt._id, verdict: "clean" }, + }); + const attempts = await t.run(async (ctx) => + ctx.db.query("skillsShCatalogScanAttempts").collect(), + ); + expect(attempts).toHaveLength(1); + }); + + it("does not reuse or block on an exact deterministic fixture verdict", async () => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + await configureCanary(t); + const { runId: fixtureRunId } = await runCanary(t); + await t.run(async (ctx) => { + const entry = await ctx.db + .query("skillsShCatalogEntries") + .withIndex("by_external_id", (q) => q.eq("externalId", CANARY_EXTERNAL_ID)) + .unique(); + if (!entry) throw new Error("controlled canary entry was not created"); + await ctx.db.insert("skillsShCatalogScanAttempts", { + entryId: entry._id, + runId: fixtureRunId, + externalId: entry.externalId, + githubOwnerId: entry.githubOwnerId, + owner: entry.owner, + repo: entry.repo, + slug: entry.slug, + githubPath: entry.githubPath, + githubCommit: entry.githubCommit, + githubContentHash: entry.githubContentHash, + sourceContentHash: entry.sourceContentHash, + source: "skills-sh-catalog-fixture", + dispatchKind: "deterministic", + priority: "low", + status: "succeeded", + verdict: "clean", + completedAt: 1, + createdAt: 1, + updatedAt: 1, + }); + }); + await t.mutation(internal.skillsShCatalog.configureFixtureControlInternal, { + ...CANARY_CONTROL, + mode: "staging-live", + scanAdmissionEnabled: true, + publicVisibilityEnabled: true, + maxWritesPerBatch: 7, + maxScanAdmissionsPerBatch: 1, + maxScanAdmissionsPerRun: 1, + maxScanAdmissionsPerDay: 1, + maxCatalogQueued: 1, + maxCatalogInFlight: 1, + realScanAllowlist: [CANARY_EXTERNAL_ID], + }); + const actorUserId = await t.run( + async (ctx) => + await ctx.db.insert("users", { + handle: "catalog-test-operator", + displayName: "Catalog Test Operator", + role: "admin", + }), + ); + + const started = await t.mutation( + internal.skillsShCatalog.startControlledCanaryScanRunInternal, + { + actor: "catalog-test-operator", + reason: "replace deterministic evidence with a real catalog scan", + }, + ); + expect(started).toMatchObject({ + externalId: CANARY_EXTERNAL_ID, + reused: false, + }); + expect(started.runId).not.toBe(fixtureRunId); + await expect( + t.action(internal.skillsShCatalog.admitRealScansInternal, { + runId: started.runId, + externalIds: [CANARY_EXTERNAL_ID], + actorUserId, + artifacts: [await storeCanaryArtifact(t)], + }), + ).resolves.toMatchObject({ admitted: 1, skipped: 0 }); + }); + + it("does not let a stale real verdict block an exact replacement scan", async () => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + await configureCanary(t); + const { runId: fixtureRunId } = await runCanary(t); + await t.run(async (ctx) => { + const entry = await ctx.db + .query("skillsShCatalogEntries") + .withIndex("by_external_id", (q) => q.eq("externalId", CANARY_EXTERNAL_ID)) + .unique(); + if (!entry) throw new Error("controlled canary entry was not created"); + await ctx.db.insert("skillsShCatalogScanAttempts", { + entryId: entry._id, + runId: fixtureRunId, + externalId: entry.externalId, + githubOwnerId: entry.githubOwnerId, + owner: entry.owner, + repo: entry.repo, + slug: entry.slug, + githubPath: entry.githubPath, + githubCommit: "1".repeat(40), + githubContentHash: entry.githubContentHash, + sourceContentHash: entry.sourceContentHash, + source: "skills-sh-catalog-test", + dispatchKind: "real", + priority: "low", + status: "succeeded", + verdict: "clean", + completedAt: 1, + createdAt: 1, + updatedAt: 1, + }); + }); + await t.mutation(internal.skillsShCatalog.configureFixtureControlInternal, { + ...CANARY_CONTROL, + mode: "staging-live", + scanAdmissionEnabled: true, + publicVisibilityEnabled: true, + maxWritesPerBatch: 7, + maxScanAdmissionsPerBatch: 1, + maxScanAdmissionsPerRun: 1, + maxScanAdmissionsPerDay: 2, + maxCatalogQueued: 1, + maxCatalogInFlight: 1, + realScanAllowlist: [CANARY_EXTERNAL_ID], + }); + const actorUserId = await t.run( + async (ctx) => + await ctx.db.insert("users", { + handle: "catalog-test-operator", + displayName: "Catalog Test Operator", + role: "admin", + }), + ); + const started = await t.mutation( + internal.skillsShCatalog.startControlledCanaryScanRunInternal, + { + actor: "catalog-test-operator", + reason: "replace stale real evidence with an exact scan", + }, + ); + + await expect( + t.action(internal.skillsShCatalog.admitRealScansInternal, { + runId: started.runId, + externalIds: [CANARY_EXTERNAL_ID], + actorUserId, + artifacts: [await storeCanaryArtifact(t)], + }), + ).resolves.toMatchObject({ admitted: 1, skipped: 0 }); + }); + + it("rejects a scan artifact that differs from the authenticated GitHub folder", async () => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + await configureCanary(t); + await runCanary(t); + await t.mutation(internal.skillsShCatalog.configureFixtureControlInternal, { + ...CANARY_CONTROL, + mode: "staging-live", + scanAdmissionEnabled: true, + publicVisibilityEnabled: true, + maxWritesPerBatch: 7, + maxScanAdmissionsPerBatch: 1, + maxScanAdmissionsPerRun: 1, + maxScanAdmissionsPerDay: 1, + maxCatalogQueued: 1, + maxCatalogInFlight: 1, + realScanAllowlist: [CANARY_EXTERNAL_ID], + }); + const actorUserId = await t.run( + async (ctx) => + await ctx.db.insert("users", { + handle: "catalog-test-operator", + displayName: "Catalog Test Operator", + role: "admin", + }), + ); + const { runId } = await t.mutation( + internal.skillsShCatalog.startControlledCanaryScanRunInternal, + { + actor: "catalog-test-operator", + reason: "reject changed canary content", + }, + ); + const changedArtifact = await storeCanaryArtifact(t, `${canarySkillMarkdown}\nchanged\n`); + + await expect( + t.action(internal.skillsShCatalog.admitRealScansInternal, { + runId, + externalIds: [CANARY_EXTERNAL_ID], + actorUserId, + artifacts: [changedArtifact], + }), + ).rejects.toThrow("real Test scan artifact does not match authenticated GitHub content"); + const attempts = await t.run(async (ctx) => + ctx.db.query("skillsShCatalogScanAttempts").collect(), + ); + expect(attempts).toHaveLength(0); + }); + + it.each(["malicious", "failed"] as const)( + "keeps a %s canary hidden and non-installable", + async (verdict) => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + const attempt = await prepareScannedCanary(t); + + await expect(completeScannedCanary(t, attempt, verdict)).resolves.toEqual({ + ok: true, + applied: true, + publicVisible: false, + }); + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toBeNull(); + }, + ); + + it.each(["malicious", "failed"] as const)( + "admits a fresh exact attempt after a %s canary scan", + async (verdict) => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + const blockedAttempt = await prepareScannedCanary(t); + await completeScannedCanary(t, blockedAttempt, verdict); + await t.mutation(internal.skillsShCatalog.configureFixtureControlInternal, { + ...CANARY_CONTROL, + mode: "staging-live", + scanAdmissionEnabled: true, + publicVisibilityEnabled: true, + maxWritesPerBatch: 7, + maxScanAdmissionsPerBatch: 1, + maxScanAdmissionsPerRun: 1, + maxScanAdmissionsPerDay: 2, + maxCatalogQueued: 1, + maxCatalogInFlight: 1, + realScanAllowlist: [CANARY_EXTERNAL_ID], + }); + const retry = await t.mutation( + internal.skillsShCatalog.startControlledCanaryScanRunInternal, + { + actor: "catalog-test-operator", + reason: `retry the exact canary after a ${verdict} scan`, + }, + ); + expect(retry).toMatchObject({ + externalId: CANARY_EXTERNAL_ID, + reused: false, + }); + expect(retry.runId).not.toBe(blockedAttempt.runId); + const actorUserId = await t.run( + async (ctx) => + (await ctx.db + .query("users") + .filter((q) => q.eq(q.field("handle"), "catalog-test-operator")) + .unique())!._id, + ); + const artifact = await storeCanaryArtifact(t); + await expect( + t.action(internal.skillsShCatalog.admitRealScansInternal, { + runId: retry.runId, + externalIds: [CANARY_EXTERNAL_ID], + actorUserId, + artifacts: [artifact], + }), + ).resolves.toMatchObject({ admitted: 1, skipped: 0 }); + }, + ); + + it.each([ + ["githubPath", "skills/changed"], + ["githubCommit", "1".repeat(40)], + ["githubContentHash", "2".repeat(64)], + ] as const)("rejects a stale callback after the entry %s changes", async (field, value) => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + const attempt = await prepareScannedCanary(t); + await t.run(async (ctx) => { + await ctx.db.patch(attempt.entryId, { [field]: value, updatedAt: Date.now() }); + }); + + await expect(completeScannedCanary(t, attempt, "clean")).resolves.toEqual({ + ok: true, + applied: false, + reason: "stale-attempt", + }); + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toBeNull(); + }); + + it("blocks promotion while paused, then supports idempotent publication rollback", async () => { + useEnvironment(TEST_ENV); + const t = convexTest(schema, modules); + const pausedAttempt = await prepareScannedCanary(t); + await t.mutation(internal.skillsShCatalog.setCatalogPausedInternal, { + paused: true, + actor: "catalog-test-operator", + reason: "prove catalog-only pause", + confirm: "set-skills-sh-test-pause", + }); + + await expect(completeScannedCanary(t, pausedAttempt, "clean")).resolves.toEqual({ + ok: true, + applied: true, + publicVisible: false, + }); + await t.mutation(internal.skillsShCatalog.setCatalogPausedInternal, { + paused: false, + actor: "catalog-test-operator", + reason: "resume after paused callback proof", + confirm: "set-skills-sh-test-pause", + }); + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toBeNull(); + + await expect( + t.mutation(internal.skillsShCatalog.startControlledCanaryScanRunInternal, { + actor: "catalog-test-operator", + reason: "publish the exact completed canary after resume", + }), + ).resolves.toEqual({ + runId: pausedAttempt.runId, + externalId: CANARY_EXTERNAL_ID, + reused: true, + }); + const publishedAttempt = pausedAttempt; + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toMatchObject({ + security: { attemptId: publishedAttempt._id, verdict: "clean" }, + }); + await t.mutation(internal.skillsShCatalog.rollbackPublicationInternal, { + externalId: CANARY_EXTERNAL_ID, + attemptId: publishedAttempt._id, + actor: "catalog-test-operator", + reason: "prove exact publication rollback", + confirm: "rollback-skills-sh-test-publication", + }); + await expect( + t.run(async (ctx) => await ctx.db.get(publishedAttempt._id)), + ).resolves.toMatchObject({ + publicationRolledBackAt: expect.any(Number), + }); + await expect(completeScannedCanary(t, publishedAttempt, "clean")).resolves.toEqual({ + ok: true, + applied: true, + publicVisible: false, + }); + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toBeNull(); + await t.mutation(internal.skillsShCatalog.configureFixtureControlInternal, { + ...CANARY_CONTROL, + mode: "staging-live", + scanAdmissionEnabled: true, + publicVisibilityEnabled: true, + maxWritesPerBatch: 7, + maxScanAdmissionsPerBatch: 1, + maxScanAdmissionsPerRun: 1, + maxScanAdmissionsPerDay: 2, + maxCatalogQueued: 1, + maxCatalogInFlight: 1, + realScanAllowlist: [CANARY_EXTERNAL_ID], + }); + const replacementRun = await t.mutation( + internal.skillsShCatalog.startControlledCanaryScanRunInternal, + { + actor: "catalog-test-operator", + reason: "publish a replacement after rollback", + }, + ); + const actorUserId = await t.run( + async (ctx) => + (await ctx.db + .query("users") + .filter((q) => q.eq(q.field("handle"), "catalog-test-operator")) + .first())!._id, + ); + await t.action(internal.skillsShCatalog.admitRealScansInternal, { + runId: replacementRun.runId, + externalIds: [CANARY_EXTERNAL_ID], + actorUserId, + artifacts: [await storeCanaryArtifact(t)], + }); + const replacementAttempt = await t.run(async (ctx) => { + const attempt = await ctx.db + .query("skillsShCatalogScanAttempts") + .withIndex("by_run", (q) => q.eq("runId", replacementRun.runId)) + .unique(); + if (!attempt?.skillScanRequestId || !attempt.securityScanJobId) { + throw new Error("replacement canary scan admission did not create linked work"); + } + await ctx.db.patch(attempt._id, { status: "running", updatedAt: Date.now() }); + await ctx.db.patch(attempt.skillScanRequestId, { + status: "running", + updatedAt: Date.now(), + }); + await ctx.db.patch(attempt.securityScanJobId, { + status: "running", + leaseToken: "replacement-lease", + leaseExpiresAt: Date.now() + 60_000, + workerId: "replacement-worker", + updatedAt: Date.now(), + }); + return attempt; + }); + await t.mutation(internal.securityScan.completeCatalogSkillScanJobInternal, { + attemptId: replacementAttempt._id, + scanId: replacementAttempt.skillScanRequestId!, + jobId: replacementAttempt.securityScanJobId!, + leaseToken: "replacement-lease", + artifactContentHash: replacementAttempt.artifactContentHash!, + verdict: "clean", + runId: "replacement-clawscan-run", + llmAnalysis: { status: "clean", checkedAt: Date.now() }, + }); + + await expect( + t.mutation(internal.skillsShCatalog.rollbackPublicationInternal, { + externalId: CANARY_EXTERNAL_ID, + attemptId: publishedAttempt._id, + actor: "catalog-test-operator", + reason: "retry the old rollback after replacement publication", + confirm: "rollback-skills-sh-test-publication", + }), + ).resolves.toMatchObject({ + externalId: CANARY_EXTERNAL_ID, + publicVisible: true, + alreadyRolledBack: true, + }); + await expect( + t.query(api.skillsShCatalog.getPublicEntry, { + owner: "patrick-erichsen", + repo: "skills", + slug: "html", + }), + ).resolves.toMatchObject({ + security: { attemptId: replacementAttempt._id, verdict: "clean" }, + }); + }); }); diff --git a/convex/telemetry.test.ts b/convex/telemetry.test.ts index a845a8a9..3eb461ea 100644 --- a/convex/telemetry.test.ts +++ b/convex/telemetry.test.ts @@ -36,7 +36,13 @@ const reportCliInstallHandler = ( reportCliInstallInternal as unknown as { _handler: ( ctx: unknown, - args: { userId: string; slug: string; ownerHandle?: string; version?: string }, + args: { + userId: string; + slug: string; + ownerHandle?: string; + sourceRef?: string; + version?: string; + }, ) => Promise; } )._handler; @@ -187,6 +193,22 @@ describe("telemetry install events", () => { ); }); + it("does not attribute an unclaimed skills.sh install to a same-slug native skill", async () => { + const query = vi.fn(); + const insert = vi.fn(); + const ctx = { db: { query, insert, patch: vi.fn() } }; + + await reportCliInstallHandler(ctx, { + userId: "users:one", + slug: "demo", + sourceRef: "skills-sh/alice/skills/demo", + version: "a".repeat(40), + }); + + expect(query).not.toHaveBeenCalled(); + expect(insert).not.toHaveBeenCalled(); + }); + it("uses owner identity when recording an owner-qualified install", async () => { const publisher = { _id: "publishers:alice", diff --git a/convex/telemetry.ts b/convex/telemetry.ts index 411fd4eb..dfeb7c20 100644 --- a/convex/telemetry.ts +++ b/convex/telemetry.ts @@ -24,9 +24,13 @@ export const reportCliInstallInternal = internalMutation({ userId: v.id("users"), slug: v.string(), ownerHandle: v.optional(v.string()), + sourceRef: v.optional(v.string()), version: v.optional(v.string()), }, handler: async (ctx, args) => { + // Unclaimed catalog installs have no native skill row yet. Keep the source + // identity in the request without guessing from a same-slug native skill. + if (args.sourceRef?.trim().toLowerCase().startsWith("skills-sh/")) return; await upsertUserSkillInstall(ctx, args); }, }); diff --git a/packages/clawhub/src/cli/commands/inspect.test.ts b/packages/clawhub/src/cli/commands/inspect.test.ts index 0329605b..b5008567 100644 --- a/packages/clawhub/src/cli/commands/inspect.test.ts +++ b/packages/clawhub/src/cli/commands/inspect.test.ts @@ -360,6 +360,52 @@ describe("cmdInspect", () => { }); describe("cmdVerifySkill", () => { + it("prints exact skills.sh catalog verification from the standard verify route", async () => { + const sourceRef = "skills-sh/patrick-erichsen/skills/html"; + const payload = { + schema: "clawhub.skill.verify.v1", + ok: true, + decision: "pass", + reasons: [], + slug: sourceRef, + displayName: "HTML Artifact Chooser", + pageUrl: "https://clawhub.ai/skills-sh/patrick-erichsen/skills/html", + publisherHandle: null, + publisherDisplayName: null, + publisherProfileUrl: null, + version: "a".repeat(40), + resolvedFrom: "latest", + tag: null, + createdAt: 123, + card: { + available: false, + }, + artifact: { + sourceFingerprint: "b".repeat(64), + bundleFingerprints: ["c".repeat(64)], + files: [{ path: "SKILL.md", size: 42, sha256: "d".repeat(64) }], + }, + provenance: { source: "skills-sh-catalog" }, + security: { status: "clean", passed: true }, + signature: { status: "unsigned" }, + }; + httpMocks.apiRequest.mockResolvedValueOnce(payload); + + await cmdVerifySkill(makeGlobalOpts(), sourceRef); + + const request = httpMocks.apiRequest.mock.calls[0]?.[1]; + const url = new URL(String(request?.url)); + expect(url.pathname).toBe(`${ApiRoutes.skills}/html/verify`); + expect(url.searchParams.get("reference")).toBe(sourceRef); + expect(JSON.parse(String(mockLog.mock.calls[0]?.[0]))).toEqual(payload); + }); + + it("rejects colon-form skills.sh verification references", async () => { + await expect( + cmdVerifySkill(makeGlobalOpts(), "skills-sh:patrick-erichsen/skills/html"), + ).rejects.toThrow("Invalid skills.sh ref: use skills-sh/owner/repo/slug"); + }); + it("fetches and prints JSON verification by default", async () => { const payload = { schema: "clawhub.skill.verify.v1", diff --git a/packages/clawhub/src/cli/commands/inspect.ts b/packages/clawhub/src/cli/commands/inspect.ts index 0053923d..e092dbdb 100644 --- a/packages/clawhub/src/cli/commands/inspect.ts +++ b/packages/clawhub/src/cli/commands/inspect.ts @@ -261,7 +261,14 @@ export async function cmdVerifySkill( slug: string, options: VerifySkillOptions = {}, ) { - const requested = parseSkillRef(slug); + if (slug.trim().toLowerCase().startsWith("skills-sh:")) { + fail("Invalid skills.sh ref: use skills-sh/owner/repo/slug"); + } + const skillsShRef = parseSkillsShCatalogRef(slug); + if (skillsShRef && (options.version || options.tag || options.card)) { + fail("skills.sh verification does not support --version, --tag, or --card"); + } + const requested = skillsShRef ? { slug: skillsShRef.slug } : parseSkillRef(slug); const trimmed = requested.slug; if (!trimmed) fail("Skill required"); if (options.version && options.tag) fail("Use either --version or --tag"); @@ -271,7 +278,11 @@ export async function cmdVerifySkill( const spinner = createCrabLoader("Fetching skill verification"); try { const url = registryUrl(`${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/verify`, registry); - if (requested.ownerHandle) url.searchParams.set("ownerHandle", requested.ownerHandle); + if (skillsShRef) { + url.searchParams.set("reference", slug.trim().toLowerCase()); + } else if (requested.ownerHandle) { + url.searchParams.set("ownerHandle", requested.ownerHandle); + } if (options.version) { url.searchParams.set("version", options.version); } else if (options.tag) { @@ -306,6 +317,24 @@ export async function cmdVerifySkill( } } +function parseSkillsShCatalogRef(raw: string) { + const value = raw.trim().toLowerCase(); + if (!value.startsWith("skills-sh/")) return null; + const segments = value.split("/"); + if ( + segments.length !== 4 || + segments[0] !== "skills-sh" || + segments.slice(1).some((segment) => !segment || segment.includes(":") || segment.includes("..")) + ) { + fail("Invalid skills.sh ref: use skills-sh/owner/repo/slug"); + } + return { + owner: segments[1]!, + repo: segments[2]!, + slug: segments[3]!, + }; +} + function parseSkillRef(raw: string) { const value = raw.trim(); if (!value) fail("Skill required"); diff --git a/packages/clawhub/src/cli/commands/installTelemetry.ts b/packages/clawhub/src/cli/commands/installTelemetry.ts index d9de2c16..181a4e8f 100644 --- a/packages/clawhub/src/cli/commands/installTelemetry.ts +++ b/packages/clawhub/src/cli/commands/installTelemetry.ts @@ -6,6 +6,7 @@ export async function reportInstalledSkillsTelemetryIfEnabled(params: { registry: string; slug: string; ownerHandle?: string | null; + sourceRef?: string | null; version?: string | null; }) { if (!params.token || isTelemetryDisabled()) return; @@ -23,6 +24,7 @@ export async function reportInstalledSkillsTelemetryIfEnabled(params: { event: "install", slug, ownerHandle: params.ownerHandle ?? undefined, + sourceRef: params.sourceRef ?? undefined, version: params.version ?? undefined, }, }, diff --git a/packages/clawhub/src/cli/commands/skills.test.ts b/packages/clawhub/src/cli/commands/skills.test.ts index d5545d23..237c5a5b 100644 --- a/packages/clawhub/src/cli/commands/skills.test.ts +++ b/packages/clawhub/src/cli/commands/skills.test.ts @@ -61,6 +61,7 @@ vi.mock("../ui.js", () => ({ const extractZipToDirMock = vi.spyOn(skillStore, "extractZipToDir"); const extractGitHubZipPathToDirMock = vi.spyOn(skillStore, "extractGitHubZipPathToDir"); const hashSkillFilesMock = vi.spyOn(skillStore, "hashSkillFiles"); +const listManualSkillsMock = vi.spyOn(skillStore, "listManualSkills"); const listTextFilesMock = vi.spyOn(skillStore, "listSkillFiles"); const readLockfileMock = vi.spyOn(skillStore, "readLockfile"); const readSkillOriginMock = vi.spyOn(skillStore, "readSkillOrigin"); @@ -114,6 +115,7 @@ beforeEach(() => { extractZipToDirMock.mockResolvedValue(undefined); extractGitHubZipPathToDirMock.mockResolvedValue(undefined); hashSkillFilesMock.mockReturnValue({ fingerprint: "hash", files: [] }); + listManualSkillsMock.mockResolvedValue([]); listTextFilesMock.mockResolvedValue([]); readLockfileMock.mockResolvedValue({ version: 1, skills: {} }); readSkillOriginMock.mockResolvedValue(null); @@ -129,6 +131,7 @@ afterAll(() => { extractZipToDirMock.mockRestore(); extractGitHubZipPathToDirMock.mockRestore(); hashSkillFilesMock.mockRestore(); + listManualSkillsMock.mockRestore(); listTextFilesMock.mockRestore(); readLockfileMock.mockRestore(); readSkillOriginMock.mockRestore(); @@ -441,6 +444,85 @@ describe("skill moderation commands", () => { }); describe("cmdUpdate", () => { + it("updates a legacy slug-keyed skills.sh install through its stored sourceRef", async () => { + const sourceRef = "skills-sh/patrick-erichsen/skills/html"; + const previousCommit = "a".repeat(40); + const nextCommit = "b".repeat(40); + const installedFiles = [{ path: "SKILL.md", sha256: "c".repeat(64), size: 1 }]; + const contentHash = skillStore.buildGitHubFolderContentHash(installedFiles); + mockApiRequest.mockResolvedValueOnce({ + ok: true, + slug: sourceRef, + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit: nextCommit, + contentHash, + sourceUrl: `https://github.com/patrick-erichsen/skills/tree/${nextCommit}/skills/html`, + }, + }); + mockFetchBinary.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + html: { + version: previousCommit, + installedAt: 123, + sourceRef, + }, + }, + }); + vi.mocked(readSkillOrigin).mockResolvedValue({ + version: 1, + registry: "https://clawhub.ai", + slug: "html", + sourceRef, + installedVersion: previousCommit, + installedAt: 123, + fingerprint: "hash", + }); + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); + vi.mocked(listSkillFiles).mockResolvedValue([ + { relPath: "SKILL.md", bytes: new Uint8Array([1]) }, + ]); + hashSkillFilesMock.mockReturnValue({ fingerprint: "hash", files: installedFiles }); + + await cmdUpdate(makeOpts(), sourceRef, {}, false); + + expect(mockApiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "GET", + path: `${ApiRoutes.skillsSh}/patrick-erichsen/skills/html/install`, + token: undefined, + }, + expect.anything(), + ); + expect(mockFetchBinary).toHaveBeenCalledWith("https://clawhub.ai", { + url: `https://codeload.github.com/patrick-erichsen/skills/zip/${nextCommit}`, + }); + expect(writeSkillOrigin).toHaveBeenCalledWith("/work/skills/html", { + version: 1, + registry: "https://clawhub.ai", + slug: "html", + sourceRef, + installedVersion: nextCommit, + installedAt: 123, + fingerprint: "hash", + }); + expect(writeLockfile).toHaveBeenCalledWith("/work", { + version: 1, + skills: { + html: { + version: nextCommit, + installedAt: 123, + sourceRef, + }, + }, + }); + }); + it("fails when directly updating a pinned skill", async () => { vi.mocked(readLockfile).mockResolvedValue({ version: 1, @@ -1288,6 +1370,24 @@ describe("pin commands", () => { }); describe("cmdList", () => { + it("does not report a tracked skills.sh install as a manual skill", async () => { + const sourceRef = "skills-sh/patrick-erichsen/skills/html"; + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + [sourceRef]: { + version: "a".repeat(40), + installedAt: 123, + sourceRef, + }, + }, + }); + await cmdList(makeOpts()); + + expect(skillStore.listManualSkills).toHaveBeenCalledWith("/work/skills", new Set(["html"])); + expect(mockLog).toHaveBeenCalledWith(`${sourceRef} ${"a".repeat(40)}`); + }); + it("shows pinned state in list output", async () => { vi.mocked(readLockfile).mockResolvedValue({ version: 1, @@ -1305,6 +1405,142 @@ describe("cmdList", () => { }); describe("cmdInstall", () => { + it("installs a skills.sh catalog ref from the approved pinned GitHub resolver", async () => { + const sourceRef = "skills-sh/patrick-erichsen/skills/html"; + const commit = "a".repeat(40); + const installedFiles = [{ path: "SKILL.md", sha256: "b".repeat(64), size: 1 }]; + const contentHash = skillStore.buildGitHubFolderContentHash(installedFiles); + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest + .mockResolvedValueOnce({ + ok: true, + slug: sourceRef, + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit, + contentHash, + sourceUrl: `https://github.com/patrick-erichsen/skills/tree/${commit}/skills/html`, + }, + }) + .mockResolvedValueOnce({ ok: true }); + mockFetchBinary.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + vi.mocked(listSkillFiles).mockResolvedValue([ + { relPath: "SKILL.md", bytes: new Uint8Array([1]) }, + ]); + hashSkillFilesMock.mockReturnValue({ fingerprint: "hash", files: installedFiles }); + + await cmdInstall(makeOpts(), sourceRef); + + expect(mockApiRequest).toHaveBeenNthCalledWith( + 1, + "https://clawhub.ai", + { + method: "GET", + path: `${ApiRoutes.skillsSh}/patrick-erichsen/skills/html/install`, + token: "tkn", + }, + expect.anything(), + ); + expect(mockFetchBinary).toHaveBeenCalledWith("https://clawhub.ai", { + url: `https://codeload.github.com/patrick-erichsen/skills/zip/${commit}`, + }); + expect(extractGitHubZipPathToDir).toHaveBeenCalledWith( + new Uint8Array([1, 2, 3]), + "/work/skills/html", + "skills/html", + ); + expect(writeSkillOrigin).toHaveBeenCalledWith("/work/skills/html", { + version: 1, + registry: "https://clawhub.ai", + slug: "html", + sourceRef, + installedVersion: commit, + installedAt: expect.any(Number), + fingerprint: "hash", + }); + expect(writeLockfile).toHaveBeenCalledWith("/work", { + version: 1, + skills: { + [sourceRef]: { + version: commit, + installedAt: expect.any(Number), + sourceRef, + }, + }, + }); + expect(mockApiRequest).toHaveBeenNthCalledWith( + 2, + "https://clawhub.ai", + expect.objectContaining({ + path: LegacyApiRoutes.cliTelemetryInstall, + body: { + event: "install", + slug: "html", + sourceRef, + version: commit, + }, + }), + expect.anything(), + ); + }); + + it("rejects the unsupported colon-form skills.sh reference", async () => { + await expect(cmdInstall(makeOpts(), "skills-sh:patrick-erichsen/skills/html")).rejects.toThrow( + "Invalid skills.sh ref: use skills-sh/owner/repo/slug", + ); + expect(mockApiRequest).not.toHaveBeenCalled(); + }); + + it("blocks a skills.sh install when the slug target belongs to another source", async () => { + const sourceRef = "skills-sh/patrick-erichsen/skills/html"; + statMock.mockResolvedValue({} as Awaited>); + readSkillOriginMock.mockResolvedValue({ + version: 1, + registry: "https://clawhub.ai", + slug: "html", + sourceRef: "skills-sh/other/repo/html", + installedVersion: "a".repeat(40), + installedAt: 1, + }); + + await expect(cmdInstall(makeOpts(), sourceRef, undefined, true)).rejects.toThrow( + `Install target collision: /work/skills/html is owned by skills-sh/other/repo/html`, + ); + expect(mockApiRequest).not.toHaveBeenCalled(); + }); + + it("rejects a skills.sh install whose extracted folder hash differs from the resolver", async () => { + const sourceRef = "skills-sh/patrick-erichsen/skills/html"; + const commit = "a".repeat(40); + mockApiRequest.mockResolvedValueOnce({ + ok: true, + slug: sourceRef, + installKind: "github", + github: { + repo: "patrick-erichsen/skills", + path: "skills/html", + commit, + contentHash: "b".repeat(64), + sourceUrl: `https://github.com/patrick-erichsen/skills/tree/${commit}/skills/html`, + }, + }); + mockFetchBinary.mockResolvedValue(new Uint8Array([1, 2, 3])); + listTextFilesMock.mockResolvedValue([{ relPath: "SKILL.md", bytes: new Uint8Array([1]) }]); + hashSkillFilesMock.mockReturnValue({ + fingerprint: "local-fingerprint", + files: [{ path: "SKILL.md", sha256: "c".repeat(64), size: 1 }], + }); + + await expect(cmdInstall(makeOpts(), sourceRef)).rejects.toThrow( + "Downloaded skills.sh folder hash does not match the approved ClawHub resolver", + ); + expect(writeSkillOrigin).not.toHaveBeenCalled(); + expect(writeLockfile).not.toHaveBeenCalled(); + }); + it("passes optional auth token to API + download requests", async () => { mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockImplementation(async (_registry, args) => { diff --git a/packages/clawhub/src/cli/commands/skills.ts b/packages/clawhub/src/cli/commands/skills.ts index 79d04668..5218a28f 100644 --- a/packages/clawhub/src/cli/commands/skills.ts +++ b/packages/clawhub/src/cli/commands/skills.ts @@ -19,6 +19,7 @@ import { type SkillReportStatus, } from "../../schema/index.js"; import { + buildGitHubFolderContentHash, extractGitHubZipPathToDir, extractZipToDir, hashSkillFiles, @@ -68,6 +69,11 @@ type SkillReportTriageOptions = { type SkillRef = { slug: string; ownerHandle?: string; + sourceRef?: string; + skillsSh?: { + owner: string; + repo: string; + }; }; function normalizeOwnerHandle(raw: string | null | undefined) { @@ -104,6 +110,21 @@ function normalizeSkillSlugForRemote(raw: unknown) { function parseSkillRefOrFail(raw: string): SkillRef { const ref = raw.trim(); if (!ref) fail("Slug required"); + if (ref.toLowerCase().startsWith("skills-sh:")) { + fail("Invalid skills.sh ref: use skills-sh/owner/repo/slug"); + } + if (ref.toLowerCase().startsWith("skills-sh/")) { + const segments = ref.split("/"); + if (segments.length !== 4 || segments[0]?.toLowerCase() !== "skills-sh") { + fail("Invalid skills.sh ref: use skills-sh/owner/repo/slug"); + } + const [, rawOwner, rawRepo, rawSlug] = segments; + const owner = normalizeSkillsShSegment(rawOwner, ref); + const repo = normalizeSkillsShSegment(rawRepo, ref); + const slug = normalizeSkillsShSegment(rawSlug, ref); + const sourceRef = `skills-sh/${owner}/${repo}/${slug}`; + return { slug, sourceRef, skillsSh: { owner, repo } }; + } const slashIndex = ref.indexOf("/"); if (slashIndex < 0) { return { slug: normalizeSkillSlugOrFail(ref) }; @@ -121,19 +142,43 @@ function parseSkillRefOrFail(raw: string): SkillRef { return { slug, ownerHandle }; } +function normalizeSkillsShSegment(raw: string | undefined, ref: string) { + const segment = raw?.trim().toLowerCase() ?? ""; + if ( + !segment || + segment.includes("\\") || + segment.includes(":") || + segment.includes("..") || + !isSafeSkillSlug(segment) + ) { + fail(`Invalid skills.sh ref: ${ref}`); + } + return segment; +} + function isSafeSkillSlug(slug: string) { return Boolean(slug) && !slug.includes("/") && !slug.includes("\\") && !slug.includes(".."); } function skillIdentity(ref: SkillRef) { + if (ref.sourceRef) return ref.sourceRef; return ref.ownerHandle ? `@${ref.ownerHandle}/${ref.slug}` : ref.slug; } function skillTarget(dir: string, ref: SkillRef) { + if (ref.sourceRef) return join(dir, ref.slug); return ref.ownerHandle ? join(dir, `@${ref.ownerHandle}`, ref.slug) : join(dir, ref.slug); } function isSafeSkillIdentity(value: string) { + if (value.toLowerCase().startsWith("skills-sh/")) { + const segments = value.split("/"); + return ( + segments.length === 4 && + segments[0]?.toLowerCase() === "skills-sh" && + segments.slice(1).every((segment) => isSafeSkillSlug(segment) && !segment.includes(":")) + ); + } const slashIndex = value.indexOf("/"); if (slashIndex < 0) return isSafeSkillSlug(value); if (value.indexOf("/", slashIndex + 1) >= 0) return false; @@ -143,11 +188,16 @@ function isSafeSkillIdentity(value: string) { } function findExistingLockKey( - lock: { skills: Record }, + lock: { skills: Record }, ref: SkillRef, ) { const key = skillIdentity(ref); if (lock.skills[key]) return key; + if (ref.sourceRef) { + const legacyEntry = lock.skills[ref.slug]; + if (legacyEntry?.sourceRef === ref.sourceRef) return ref.slug; + return key; + } if (ref.ownerHandle) { const legacyEntry = lock.skills[ref.slug]; if (legacyEntry && normalizeOwnerHandle(legacyEntry.ownerHandle) === ref.ownerHandle) { @@ -296,6 +346,9 @@ export async function cmdInstall( ) { const requested = parseSkillRefOrFail(slug); const trimmed = requested.slug; + if (requested.sourceRef && versionFlag) { + fail("--version is not supported for skills.sh catalog references"); + } const token = await getOptionalAuthToken(); @@ -306,6 +359,18 @@ export async function cmdInstall( const localRef = lockKey === skillIdentity(requested) ? requested : parseSkillRefOrFail(lockKey); const target = skillTarget(opts.dir, localRef); const targetExists = await fileExists(target); + const existingOrigin = requested.sourceRef && targetExists ? await readSkillOrigin(target) : null; + if (requested.sourceRef) { + assertSkillsShTargetOwnership({ + dir: opts.dir, + lock, + lockKey, + requested, + target, + targetExists, + existingOrigin, + }); + } if (!force && targetExists) { fail(`Already installed: ${target} (use --force)`); } @@ -319,6 +384,51 @@ export async function cmdInstall( const spinner = createCrabLoader(`Resolving ${trimmed}`); try { + if (requested.sourceRef && requested.skillsSh) { + const resolvedInstall = await resolveSkillsShCatalogInstall(registry, requested, token); + spinner.text = `Downloading ${trimmed} ${formatGitHubVersion(resolvedInstall.github.commit)}`; + await installSkillWithOptionalStaging(target, targetExists, (installTarget) => + installGitHubSkill(registry, resolvedInstall, installTarget, { + expectedContentHash: resolvedInstall.github.contentHash, + }), + ); + const installedFiles = await listSkillFiles(target); + const installedFingerprint = + installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; + const installedAt = Date.now(); + await writeSkillOrigin(target, { + version: 1, + registry, + slug: trimmed, + sourceRef: requested.sourceRef, + installedVersion: resolvedInstall.github.commit, + installedAt, + fingerprint: installedFingerprint, + }); + lock.skills[lockKey] = { + ...withPinnedMetadata(resolvedInstall.github.commit, installedAt, existingEntry), + sourceRef: requested.sourceRef, + }; + await writeLockfile(opts.workdir, lock); + await reportInstalledSkillsTelemetryIfEnabled({ + token, + registry, + slug: trimmed, + sourceRef: requested.sourceRef, + version: resolvedInstall.github.commit, + }); + spinner.succeed( + `${styleText("Installed", "brand")} ${styleText( + requested.sourceRef, + "strong", + )} ${styleText(formatGitHubVersion(resolvedInstall.github.commit), "muted")} -> ${styleText( + target, + "muted", + )}`, + ); + return; + } + // Fetch skill metadata including moderation status const skillMeta = await apiRequest( registry, @@ -456,6 +566,9 @@ export async function cmdUpdate( if (slug && all) fail("Use either or --all"); if (options.version && !slug) fail("--version requires a single "); if (options.version && !semver.valid(options.version)) fail("--version must be valid semver"); + if (requestedRef?.sourceRef && options.version) { + fail("--version is not supported for skills.sh catalog references"); + } const lock = await readLockfile(opts.workdir); const requestedLockKey = requestedRef ? findExistingLockKey(lock, requestedRef) : undefined; if (requestedLockKey && isPinnedSkillEntry(lock.skills[requestedLockKey])) { @@ -500,13 +613,110 @@ export async function cmdUpdate( }; for (const entry of slugs) { - const entryRef = parseSkillRefOrFail(entry); const entryLock = lock.skills[entry]; + const entryRef = parseSkillRefOrFail(entryLock?.sourceRef ?? entry); const spinner = createCrabLoader(`Checking ${entry}`); try { const target = skillTarget(opts.dir, entryRef); const exists = await fileExists(target); const existingOrigin = exists ? await readSkillOrigin(target) : null; + if (entryRef.sourceRef && entryRef.skillsSh) { + assertSkillsShTargetOwnership({ + dir: opts.dir, + lock, + lockKey: entry, + requested: entryRef, + target, + targetExists: exists, + existingOrigin, + }); + const filesOnDisk = exists ? await listSkillFiles(target) : []; + const localFingerprint = + filesOnDisk.length > 0 ? hashSkillFiles(filesOnDisk).fingerprint : null; + const latestInstall = await resolveSkillsShCatalogInstall(registry, entryRef, token); + const targetVersion = latestInstall.github.commit; + const originFingerprint = + existingOrigin?.sourceRef === entryRef.sourceRef ? existingOrigin.fingerprint : undefined; + const hasLocalChanges = Boolean( + exists && + localFingerprint && + (!originFingerprint || originFingerprint !== localFingerprint), + ); + const matched = + existingOrigin?.sourceRef === entryRef.sourceRef && + originFingerprint && + localFingerprint && + originFingerprint === localFingerprint + ? existingOrigin.installedVersion + : null; + + if (hasLocalChanges && !options.force) { + spinner.stop(); + if (!allowPrompt) { + console.log(`${entry}: local changes (no match). Use --force to overwrite.`); + continue; + } + const confirm = await promptConfirm( + `${entry}: local changes (no match). Overwrite with ${formatGitHubVersion( + targetVersion, + )}?`, + ); + if (!confirm) { + console.log(`${entry}: skipped`); + continue; + } + spinner.start(`Updating ${entry} -> ${formatGitHubVersion(targetVersion)}`); + } + + if (matched === targetVersion && !options.force && !hasLocalChanges) { + if ( + lock.skills[entry]?.version !== targetVersion || + lock.skills[entry]?.sourceRef !== entryRef.sourceRef + ) { + lock.skills[entry] = { + ...withPinnedMetadata( + targetVersion, + lock.skills[entry]?.installedAt ?? Date.now(), + lock.skills[entry], + ), + sourceRef: entryRef.sourceRef, + }; + markLockDirty(); + await flushLockfile(); + } + spinner.succeed(`${entry}: up to date (${formatGitHubVersion(targetVersion)})`); + continue; + } + + spinner.text = `Updating ${entry} -> ${formatGitHubVersion(targetVersion)}`; + await installSkillWithOptionalStaging(target, exists, (installTarget) => + installGitHubSkill(registry, latestInstall, installTarget, { + expectedContentHash: latestInstall.github.contentHash, + }), + ); + const installedFiles = await listSkillFiles(target); + const installedFingerprint = + installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; + const installedAt = existingOrigin?.installedAt ?? Date.now(); + await writeSkillOrigin(target, { + version: 1, + registry: existingOrigin?.registry ?? registry, + slug: entryRef.slug, + sourceRef: entryRef.sourceRef, + installedVersion: targetVersion, + installedAt, + fingerprint: installedFingerprint, + }); + lock.skills[entry] = { + ...withPinnedMetadata(targetVersion, installedAt, lock.skills[entry]), + sourceRef: entryRef.sourceRef, + }; + markLockDirty(); + await flushLockfile(); + spinner.succeed(`${entry}: updated -> ${formatGitHubVersion(targetVersion)}`); + continue; + } + const requestedOwnerHandle = normalizeOwnerHandle( requestedRef?.ownerHandle ?? entryRef.ownerHandle ?? @@ -812,7 +1022,13 @@ export async function cmdUpdate( export async function cmdList(opts: GlobalOpts) { const lock = await readLockfile(opts.workdir); const entries = Object.entries(lock.skills); - const manualSkills = await listManualSkills(opts.dir, new Set(Object.keys(lock.skills))); + const trackedTargets = new Set( + Object.keys(lock.skills).map((entry) => { + const ref = parseSkillRefOrFail(entry); + return ref.sourceRef ? ref.slug : entry; + }), + ); + const manualSkills = await listManualSkills(opts.dir, trackedTargets); if (entries.length === 0 && manualSkills.length === 0) { console.log("No installed skills."); return; @@ -1215,15 +1431,78 @@ async function resolveLatestSkillInstall( ); } +async function resolveSkillsShCatalogInstall(registry: string, ref: SkillRef, token?: string) { + if (!ref.sourceRef || !ref.skillsSh) { + fail("Invalid skills.sh ref: use skills-sh/owner/repo/slug"); + } + const path = `${ApiRoutes.skillsSh}/${encodeURIComponent( + ref.skillsSh.owner, + )}/${encodeURIComponent(ref.skillsSh.repo)}/${encodeURIComponent(ref.slug)}/install`; + const resolution = await apiRequest( + registry, + { method: "GET", path, token }, + ApiV1SkillInstallResolveResponseSchema, + ); + if (!resolution.ok) fail(resolution.message); + if (resolution.installKind !== "github") { + fail("skills.sh catalog resolver did not return a GitHub install"); + } + return resolution; +} + async function installGitHubSkill( registry: string, resolution: GitHubInstallResolution, target: string, + options: { expectedContentHash?: string } = {}, ) { const zip = await fetchBinary(registry, { url: gitHubZipUrl(resolution.github.repo, resolution.github.commit), }); await extractGitHubZipPathToDir(zip, target, resolution.github.path); + if (options.expectedContentHash) { + const installed = hashSkillFiles(await listSkillFiles(target)); + const actualContentHash = buildGitHubFolderContentHash(installed.files); + if (actualContentHash !== options.expectedContentHash.toLowerCase()) { + await rm(target, { recursive: true, force: true }); + fail("Downloaded skills.sh folder hash does not match the approved ClawHub resolver"); + } + } +} + +function assertSkillsShTargetOwnership(args: { + dir: string; + lock: { skills: Record }; + lockKey: string; + requested: SkillRef; + target: string; + targetExists: boolean; + existingOrigin: Awaited>; +}) { + if (!args.requested.sourceRef) return; + for (const [key, entry] of Object.entries(args.lock.skills)) { + if (key === args.lockKey) continue; + const identity = entry.sourceRef ?? key; + if (!isSafeSkillIdentity(identity)) continue; + const ref = parseSkillRefOrFail(identity); + if (skillTarget(args.dir, ref) === args.target) { + fail(`Install target collision: ${args.target} is owned by ${identity}`); + } + } + if (!args.targetExists) return; + const lockedSource = args.lock.skills[args.lockKey]?.sourceRef; + if ( + args.existingOrigin?.sourceRef && + args.existingOrigin.sourceRef !== args.requested.sourceRef + ) { + fail(`Install target collision: ${args.target} is owned by ${args.existingOrigin.sourceRef}`); + } + if (!args.existingOrigin?.sourceRef && lockedSource !== args.requested.sourceRef) { + const owner = args.existingOrigin?.ownerHandle + ? `@${args.existingOrigin.ownerHandle}/${args.existingOrigin.slug}` + : (args.existingOrigin?.slug ?? "another local skill"); + fail(`Install target collision: ${args.target} is owned by ${owner}`); + } } async function installSkillWithOptionalStaging( diff --git a/packages/clawhub/src/schema/routes.ts b/packages/clawhub/src/schema/routes.ts index 97db0473..6ae9d275 100644 --- a/packages/clawhub/src/schema/routes.ts +++ b/packages/clawhub/src/schema/routes.ts @@ -17,6 +17,7 @@ export const ApiRoutes = { download: "/api/v1/download", publishTokenMint: "/api/v1/publish/token/mint", skills: "/api/v1/skills", + skillsSh: "/api/v1/skills-sh", skillScans: "/api/v1/skills/-/scan", packages: "/api/v1/packages", codePlugins: "/api/v1/code-plugins", diff --git a/packages/clawhub/src/schema/schemas.ts b/packages/clawhub/src/schema/schemas.ts index c6e3412f..689360d6 100644 --- a/packages/clawhub/src/schema/schemas.ts +++ b/packages/clawhub/src/schema/schemas.ts @@ -24,6 +24,7 @@ export const LockfileSchema = type({ version: "string|null", installedAt: "number", ownerHandle: "string?", + sourceRef: "string?", pinned: "boolean?", pinReason: "string?", }, @@ -163,10 +164,47 @@ export const ApiV1SkillInstallResolveResponseSchema = type({ export type ApiV1SkillInstallResolveResponse = (typeof ApiV1SkillInstallResolveResponseSchema)[inferred]; +export const ApiV1SkillsShCatalogEntrySchema = type({ + ref: "string", + route: "string", + displayName: "string", + summary: "string", + owner: { + handle: "string", + githubUrl: "string", + }, + repository: "string", + githubPath: "string", + githubCommit: "string", + githubContentHash: "string", + sourceUrl: "string", + installs: "number", + security: { + verdict: '"clean"|"suspicious"', + source: '"clawhub"', + attemptId: "string", + scannedAt: "number", + }, + install: { + ok: "true", + slug: "string", + installKind: '"github"', + github: { + repo: "string", + path: "string", + commit: "string", + contentHash: "string", + sourceUrl: "string", + }, + }, +}); +export type ApiV1SkillsShCatalogEntry = (typeof ApiV1SkillsShCatalogEntrySchema)[inferred]; + export const CliTelemetryInstallRequestSchema = type({ event: '"install"', slug: "string", ownerHandle: "string?", + sourceRef: "string?", version: "string?", // Deprecated compatibility fields accepted and ignored by the backend. rootId: "string?", diff --git a/packages/clawhub/src/skills.ts b/packages/clawhub/src/skills.ts index a424d8cc..90f54b12 100644 --- a/packages/clawhub/src/skills.ts +++ b/packages/clawhub/src/skills.ts @@ -16,6 +16,7 @@ export type SkillOrigin = { registry: string; slug: string; ownerHandle?: string; + sourceRef?: string; installedVersion: string; installedAt: number; fingerprint?: string; @@ -152,6 +153,16 @@ export function hashSkillFiles(files: Array<{ relPath: string; bytes: Uint8Array return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }; } +export function buildGitHubFolderContentHash( + files: Array<{ path: string; sha256: string; size: number }>, +) { + const payload = [...files] + .sort((left, right) => left.path.localeCompare(right.path)) + .map((file) => `${file.path}\0${file.size}\0${file.sha256.toLowerCase()}`) + .join("\n"); + return createHash("sha256").update(payload).digest("hex"); +} + export function hashSkillZip(zipBytes: Uint8Array) { const entries = unzipSync(zipBytes); const hashed = Object.entries(entries) @@ -205,6 +216,7 @@ export async function readSkillOrigin(skillFolder: string): Promise; export type ApiV1SkillInstallResolveResponse = (typeof ApiV1SkillInstallResolveResponseSchema)[inferred]; +export declare const ApiV1SkillsShCatalogEntrySchema: import("arktype/internal/variants/object.ts").ObjectType<{ + ref: string; + route: string; + displayName: string; + summary: string; + owner: { + handle: string; + githubUrl: string; + }; + repository: string; + githubPath: string; + githubCommit: string; + githubContentHash: string; + sourceUrl: string; + installs: number; + security: { + verdict: "clean" | "suspicious"; + source: "clawhub"; + attemptId: string; + scannedAt: number; + }; + install: { + ok: true; + slug: string; + installKind: "github"; + github: { + repo: string; + path: string; + commit: string; + contentHash: string; + sourceUrl: string; + }; + }; +}, {}>; +export type ApiV1SkillsShCatalogEntry = (typeof ApiV1SkillsShCatalogEntrySchema)[inferred]; export declare const CliTelemetryInstallRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{ event: "install"; slug: string; ownerHandle?: string | undefined; + sourceRef?: string | undefined; version?: string | undefined; rootId?: string | undefined; rootLabel?: string | undefined; diff --git a/packages/schema/dist/schemas.js b/packages/schema/dist/schemas.js index f86dbf79..a532778c 100644 --- a/packages/schema/dist/schemas.js +++ b/packages/schema/dist/schemas.js @@ -20,6 +20,7 @@ export const LockfileSchema = type({ version: "string|null", installedAt: "number", ownerHandle: "string?", + sourceRef: "string?", pinned: "boolean?", pinReason: "string?", }, @@ -138,10 +139,45 @@ export const ApiV1SkillInstallResolveResponseSchema = type({ message: "string", status: "number", }); +export const ApiV1SkillsShCatalogEntrySchema = type({ + ref: "string", + route: "string", + displayName: "string", + summary: "string", + owner: { + handle: "string", + githubUrl: "string", + }, + repository: "string", + githubPath: "string", + githubCommit: "string", + githubContentHash: "string", + sourceUrl: "string", + installs: "number", + security: { + verdict: '"clean"|"suspicious"', + source: '"clawhub"', + attemptId: "string", + scannedAt: "number", + }, + install: { + ok: "true", + slug: "string", + installKind: '"github"', + github: { + repo: "string", + path: "string", + commit: "string", + contentHash: "string", + sourceUrl: "string", + }, + }, +}); export const CliTelemetryInstallRequestSchema = type({ event: '"install"', slug: "string", ownerHandle: "string?", + sourceRef: "string?", version: "string?", // Deprecated compatibility fields accepted and ignored by the backend. rootId: "string?", diff --git a/packages/schema/dist/schemas.js.map b/packages/schema/dist/schemas.js.map index 2ca6e8ad..ed2443ca 100644 --- a/packages/schema/dist/schemas.js.map +++ b/packages/schema/dist/schemas.js.map @@ -1 +1 @@ -{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/src/routes.ts b/packages/schema/src/routes.ts index 31efd59d..f20465d2 100644 --- a/packages/schema/src/routes.ts +++ b/packages/schema/src/routes.ts @@ -17,6 +17,7 @@ export const ApiRoutes = { download: "/api/v1/download", publishTokenMint: "/api/v1/publish/token/mint", skills: "/api/v1/skills", + skillsSh: "/api/v1/skills-sh", skillScans: "/api/v1/skills/-/scan", plugins: "/api/v1/plugins", pluginsExport: "/api/v1/plugins/export", diff --git a/packages/schema/src/schemas.test.ts b/packages/schema/src/schemas.test.ts index 532d40b0..1353877e 100644 --- a/packages/schema/src/schemas.test.ts +++ b/packages/schema/src/schemas.test.ts @@ -231,7 +231,13 @@ describe("clawhub-schema", () => { it("accepts current and legacy install telemetry payloads", () => { const current = parseArk( CliTelemetryInstallRequestSchema, - { event: "install", slug: "demo", ownerHandle: "alice", version: "1.0.0" }, + { + event: "install", + slug: "demo", + ownerHandle: "alice", + sourceRef: "skills-sh/alice/skills/demo", + version: "1.0.0", + }, "Install telemetry", ); const legacy = parseArk( @@ -248,7 +254,12 @@ describe("clawhub-schema", () => { "Install telemetry", ); - expect(current).toMatchObject({ event: "install", slug: "demo", ownerHandle: "alice" }); + expect(current).toMatchObject({ + event: "install", + slug: "demo", + ownerHandle: "alice", + sourceRef: "skills-sh/alice/skills/demo", + }); expect(legacy).toMatchObject({ roots: [{ rootId: "root" }] }); }); diff --git a/packages/schema/src/schemas.ts b/packages/schema/src/schemas.ts index 47e2c7ff..f7e18199 100644 --- a/packages/schema/src/schemas.ts +++ b/packages/schema/src/schemas.ts @@ -25,6 +25,7 @@ export const LockfileSchema = type({ version: "string|null", installedAt: "number", ownerHandle: "string?", + sourceRef: "string?", pinned: "boolean?", pinReason: "string?", }, @@ -164,10 +165,47 @@ export const ApiV1SkillInstallResolveResponseSchema = type({ export type ApiV1SkillInstallResolveResponse = (typeof ApiV1SkillInstallResolveResponseSchema)[inferred]; +export const ApiV1SkillsShCatalogEntrySchema = type({ + ref: "string", + route: "string", + displayName: "string", + summary: "string", + owner: { + handle: "string", + githubUrl: "string", + }, + repository: "string", + githubPath: "string", + githubCommit: "string", + githubContentHash: "string", + sourceUrl: "string", + installs: "number", + security: { + verdict: '"clean"|"suspicious"', + source: '"clawhub"', + attemptId: "string", + scannedAt: "number", + }, + install: { + ok: "true", + slug: "string", + installKind: '"github"', + github: { + repo: "string", + path: "string", + commit: "string", + contentHash: "string", + sourceUrl: "string", + }, + }, +}); +export type ApiV1SkillsShCatalogEntry = (typeof ApiV1SkillsShCatalogEntrySchema)[inferred]; + export const CliTelemetryInstallRequestSchema = type({ event: '"install"', slug: "string", ownerHandle: "string?", + sourceRef: "string?", version: "string?", // Deprecated compatibility fields accepted and ignored by the backend. rootId: "string?", diff --git a/specs/github-backed-skills.md b/specs/github-backed-skills.md index 243f2060..837ecd9f 100644 --- a/specs/github-backed-skills.md +++ b/specs/github-backed-skills.md @@ -250,6 +250,23 @@ returns: OpenClaw downloads the GitHub archive for that commit and extracts only the skill path. The local lock/origin version is the commit SHA. +Controlled unclaimed skills.sh catalog entries use the repository-qualified +reference `skills-sh///`. The colon form +`skills-sh://` is invalid and must be rejected by clients and +HTTP handlers. + +In Local and the permanent Test environment, an unclaimed catalog entry remains +hidden and non-installable until a real low-priority ClawHub scan completes for +the exact catalog identity, immutable GitHub owner ID, repository, path, commit, +folder content hash, source content hash, and scan attempt. Clean and suspicious +verdicts may publish only that exact attempt. Malicious, failed, canceled, or +stale callbacks cannot publish it. + +The public Test route is `/skills-sh///`. Its install resolver +returns the same commit-pinned GitHub descriptor used by native GitHub-backed +skills. Catalog pause, kill, publication disable, and exact-attempt rollback +must fail closed without disabling or mutating native scan work. + Pending verification keeps the skill visible in ClawHub search and detail UI, but normal install/update returns a structured block: diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index b0470d79..f400e82d 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,121 +9,62 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as UploadRouteImport } from './routes/upload' -import { Route as StarsRouteImport } from './routes/stars' -import { Route as SettingsRouteImport } from './routes/settings' -import { Route as SearchRouteImport } from './routes/search' -import { Route as PublishSkillRouteImport } from './routes/publish-skill' -import { Route as PublishPluginRouteImport } from './routes/publish-plugin' -import { Route as ManagementRouteImport } from './routes/management' -import { Route as ImportRouteImport } from './routes/import' -import { Route as DashboardRouteImport } from './routes/dashboard' -import { Route as AuditsRouteImport } from './routes/audits' -import { Route as AdminRouteImport } from './routes/admin' -import { Route as AddRouteImport } from './routes/add' -import { Route as AccountBannedRouteImport } from './routes/account-banned' -import { Route as SlugRouteImport } from './routes/$slug' import { Route as IndexRouteImport } from './routes/index' -import { Route as UsersIndexRouteImport } from './routes/users/index' -import { Route as SkillsIndexRouteImport } from './routes/skills/index' -import { Route as PublishersIndexRouteImport } from './routes/publishers/index' -import { Route as PluginsIndexRouteImport } from './routes/plugins/index' -import { Route as PackagesIndexRouteImport } from './routes/packages/index' -import { Route as CreatorsIndexRouteImport } from './routes/creators/index' -import { Route as UserHandleRouteImport } from './routes/user/$handle' -import { Route as UHandleRouteImport } from './routes/u/$handle' -import { Route as SkillsPublishRouteImport } from './routes/skills/publish' -import { Route as PluginsPublishRouteImport } from './routes/plugins/publish' -import { Route as PluginsNewRouteImport } from './routes/plugins/new' -import { Route as PluginsNameRouteImport } from './routes/plugins/$name' -import { Route as PackagesNewRouteImport } from './routes/packages/new' -import { Route as PackagesNameRouteImport } from './routes/packages/$name' -import { Route as PHandleRouteImport } from './routes/p/$handle' -import { Route as OrgsHandleRouteImport } from './routes/orgs/$handle' -import { Route as CliDeviceRouteImport } from './routes/cli/device' -import { Route as CliAuthRouteImport } from './routes/cli/auth' -import { Route as AuthDocsRouteImport } from './routes/auth/docs' +import { Route as SlugRouteImport } from './routes/$slug' +import { Route as AccountBannedRouteImport } from './routes/account-banned' +import { Route as AddRouteImport } from './routes/add' +import { Route as AdminRouteImport } from './routes/admin' +import { Route as AuditsRouteImport } from './routes/audits' +import { Route as DashboardRouteImport } from './routes/dashboard' +import { Route as ImportRouteImport } from './routes/import' +import { Route as ManagementRouteImport } from './routes/management' +import { Route as PublishPluginRouteImport } from './routes/publish-plugin' +import { Route as PublishSkillRouteImport } from './routes/publish-skill' +import { Route as SearchRouteImport } from './routes/search' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as StarsRouteImport } from './routes/stars' +import { Route as UploadRouteImport } from './routes/upload' import { Route as OwnerSlugRouteImport } from './routes/$owner/$slug' -import { Route as PluginsScopeNameRouteImport } from './routes/plugins/$scope/$name' -import { Route as PluginsNameSecurityAuditRouteImport } from './routes/plugins/$name/security-audit' -import { Route as PackagesScopeNameRouteImport } from './routes/packages/$scope/$name' -import { Route as OwnerSkillsSlugRouteImport } from './routes/$owner/skills/$slug' -import { Route as OwnerPluginsSlugRouteImport } from './routes/$owner/plugins/$slug' -import { Route as OwnerSlugSettingsRouteImport } from './routes/$owner/$slug/settings' +import { Route as AuthDocsRouteImport } from './routes/auth/docs' +import { Route as CliAuthRouteImport } from './routes/cli/auth' +import { Route as CliDeviceRouteImport } from './routes/cli/device' +import { Route as CreatorsIndexRouteImport } from './routes/creators/index' +import { Route as OrgsHandleRouteImport } from './routes/orgs/$handle' +import { Route as PHandleRouteImport } from './routes/p/$handle' +import { Route as PackagesIndexRouteImport } from './routes/packages/index' +import { Route as PackagesNameRouteImport } from './routes/packages/$name' +import { Route as PackagesNewRouteImport } from './routes/packages/new' +import { Route as PluginsIndexRouteImport } from './routes/plugins/index' +import { Route as PluginsNameRouteImport } from './routes/plugins/$name' +import { Route as PluginsNewRouteImport } from './routes/plugins/new' +import { Route as PluginsPublishRouteImport } from './routes/plugins/publish' +import { Route as PublishersIndexRouteImport } from './routes/publishers/index' +import { Route as SkillsIndexRouteImport } from './routes/skills/index' +import { Route as SkillsPublishRouteImport } from './routes/skills/publish' +import { Route as UHandleRouteImport } from './routes/u/$handle' +import { Route as UserHandleRouteImport } from './routes/user/$handle' +import { Route as UsersIndexRouteImport } from './routes/users/index' import { Route as OwnerSlugSecurityAuditRouteImport } from './routes/$owner/$slug/security-audit' -import { Route as PluginsScopeNameSecurityAuditRouteImport } from './routes/plugins/$scope/$name/security-audit' -import { Route as PluginsNameSecurityScannerRouteImport } from './routes/plugins/$name/security/$scanner' -import { Route as OwnerSkillsSlugSettingsRouteImport } from './routes/$owner/skills/$slug/settings' -import { Route as OwnerSkillsSlugSecurityAuditRouteImport } from './routes/$owner/skills/$slug/security-audit' -import { Route as OwnerPluginsSlugSecurityAuditRouteImport } from './routes/$owner/plugins/$slug/security-audit' +import { Route as OwnerSlugSettingsRouteImport } from './routes/$owner/$slug/settings' +import { Route as OwnerPluginsSlugRouteImport } from './routes/$owner/plugins/$slug' +import { Route as OwnerSkillsSlugRouteImport } from './routes/$owner/skills/$slug' +import { Route as PackagesScopeNameRouteImport } from './routes/packages/$scope/$name' +import { Route as PluginsNameSecurityAuditRouteImport } from './routes/plugins/$name/security-audit' +import { Route as PluginsScopeNameRouteImport } from './routes/plugins/$scope/$name' import { Route as OwnerSlugSecurityScannerRouteImport } from './routes/$owner/$slug/security/$scanner' -import { Route as PluginsScopeNameSecurityScannerRouteImport } from './routes/plugins/$scope/$name/security/$scanner' -import { Route as OwnerSkillsSlugSecurityScannerRouteImport } from './routes/$owner/skills/$slug/security/$scanner' +import { Route as OwnerPluginsSlugSecurityAuditRouteImport } from './routes/$owner/plugins/$slug/security-audit' +import { Route as OwnerSkillsSlugSecurityAuditRouteImport } from './routes/$owner/skills/$slug/security-audit' +import { Route as OwnerSkillsSlugSettingsRouteImport } from './routes/$owner/skills/$slug/settings' +import { Route as PluginsNameSecurityScannerRouteImport } from './routes/plugins/$name/security/$scanner' +import { Route as PluginsScopeNameSecurityAuditRouteImport } from './routes/plugins/$scope/$name/security-audit' +import { Route as SkillsShOwnerRepoSlugRouteImport } from './routes/skills-sh/$owner/$repo/$slug' import { Route as OwnerPluginsSlugSecurityScannerRouteImport } from './routes/$owner/plugins/$slug/security/$scanner' +import { Route as OwnerSkillsSlugSecurityScannerRouteImport } from './routes/$owner/skills/$slug/security/$scanner' +import { Route as PluginsScopeNameSecurityScannerRouteImport } from './routes/plugins/$scope/$name/security/$scanner' -const UploadRoute = UploadRouteImport.update({ - id: '/upload', - path: '/upload', - getParentRoute: () => rootRouteImport, -} as any) -const StarsRoute = StarsRouteImport.update({ - id: '/stars', - path: '/stars', - getParentRoute: () => rootRouteImport, -} as any) -const SettingsRoute = SettingsRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => rootRouteImport, -} as any) -const SearchRoute = SearchRouteImport.update({ - id: '/search', - path: '/search', - getParentRoute: () => rootRouteImport, -} as any) -const PublishSkillRoute = PublishSkillRouteImport.update({ - id: '/publish-skill', - path: '/publish-skill', - getParentRoute: () => rootRouteImport, -} as any) -const PublishPluginRoute = PublishPluginRouteImport.update({ - id: '/publish-plugin', - path: '/publish-plugin', - getParentRoute: () => rootRouteImport, -} as any) -const ManagementRoute = ManagementRouteImport.update({ - id: '/management', - path: '/management', - getParentRoute: () => rootRouteImport, -} as any) -const ImportRoute = ImportRouteImport.update({ - id: '/import', - path: '/import', - getParentRoute: () => rootRouteImport, -} as any) -const DashboardRoute = DashboardRouteImport.update({ - id: '/dashboard', - path: '/dashboard', - getParentRoute: () => rootRouteImport, -} as any) -const AuditsRoute = AuditsRouteImport.update({ - id: '/audits', - path: '/audits', - getParentRoute: () => rootRouteImport, -} as any) -const AdminRoute = AdminRouteImport.update({ - id: '/admin', - path: '/admin', - getParentRoute: () => rootRouteImport, -} as any) -const AddRoute = AddRouteImport.update({ - id: '/add', - path: '/add', - getParentRoute: () => rootRouteImport, -} as any) -const AccountBannedRoute = AccountBannedRouteImport.update({ - id: '/account-banned', - path: '/account-banned', +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', getParentRoute: () => rootRouteImport, } as any) const SlugRoute = SlugRouteImport.update({ @@ -131,104 +72,69 @@ const SlugRoute = SlugRouteImport.update({ path: '/$slug', getParentRoute: () => rootRouteImport, } as any) -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', +const AccountBannedRoute = AccountBannedRouteImport.update({ + id: '/account-banned', + path: '/account-banned', getParentRoute: () => rootRouteImport, } as any) -const UsersIndexRoute = UsersIndexRouteImport.update({ - id: '/users/', - path: '/users/', +const AddRoute = AddRouteImport.update({ + id: '/add', + path: '/add', getParentRoute: () => rootRouteImport, } as any) -const SkillsIndexRoute = SkillsIndexRouteImport.update({ - id: '/skills/', - path: '/skills/', +const AdminRoute = AdminRouteImport.update({ + id: '/admin', + path: '/admin', getParentRoute: () => rootRouteImport, } as any) -const PublishersIndexRoute = PublishersIndexRouteImport.update({ - id: '/publishers/', - path: '/publishers/', +const AuditsRoute = AuditsRouteImport.update({ + id: '/audits', + path: '/audits', getParentRoute: () => rootRouteImport, } as any) -const PluginsIndexRoute = PluginsIndexRouteImport.update({ - id: '/plugins/', - path: '/plugins/', +const DashboardRoute = DashboardRouteImport.update({ + id: '/dashboard', + path: '/dashboard', getParentRoute: () => rootRouteImport, } as any) -const PackagesIndexRoute = PackagesIndexRouteImport.update({ - id: '/packages/', - path: '/packages/', +const ImportRoute = ImportRouteImport.update({ + id: '/import', + path: '/import', getParentRoute: () => rootRouteImport, } as any) -const CreatorsIndexRoute = CreatorsIndexRouteImport.update({ - id: '/creators/', - path: '/creators/', +const ManagementRoute = ManagementRouteImport.update({ + id: '/management', + path: '/management', getParentRoute: () => rootRouteImport, } as any) -const UserHandleRoute = UserHandleRouteImport.update({ - id: '/user/$handle', - path: '/user/$handle', +const PublishPluginRoute = PublishPluginRouteImport.update({ + id: '/publish-plugin', + path: '/publish-plugin', getParentRoute: () => rootRouteImport, } as any) -const UHandleRoute = UHandleRouteImport.update({ - id: '/u/$handle', - path: '/u/$handle', +const PublishSkillRoute = PublishSkillRouteImport.update({ + id: '/publish-skill', + path: '/publish-skill', getParentRoute: () => rootRouteImport, } as any) -const SkillsPublishRoute = SkillsPublishRouteImport.update({ - id: '/skills/publish', - path: '/skills/publish', +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', getParentRoute: () => rootRouteImport, } as any) -const PluginsPublishRoute = PluginsPublishRouteImport.update({ - id: '/plugins/publish', - path: '/plugins/publish', +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', getParentRoute: () => rootRouteImport, } as any) -const PluginsNewRoute = PluginsNewRouteImport.update({ - id: '/plugins/new', - path: '/plugins/new', +const StarsRoute = StarsRouteImport.update({ + id: '/stars', + path: '/stars', getParentRoute: () => rootRouteImport, } as any) -const PluginsNameRoute = PluginsNameRouteImport.update({ - id: '/plugins/$name', - path: '/plugins/$name', - getParentRoute: () => rootRouteImport, -} as any) -const PackagesNewRoute = PackagesNewRouteImport.update({ - id: '/packages/new', - path: '/packages/new', - getParentRoute: () => rootRouteImport, -} as any) -const PackagesNameRoute = PackagesNameRouteImport.update({ - id: '/packages/$name', - path: '/packages/$name', - getParentRoute: () => rootRouteImport, -} as any) -const PHandleRoute = PHandleRouteImport.update({ - id: '/p/$handle', - path: '/p/$handle', - getParentRoute: () => rootRouteImport, -} as any) -const OrgsHandleRoute = OrgsHandleRouteImport.update({ - id: '/orgs/$handle', - path: '/orgs/$handle', - getParentRoute: () => rootRouteImport, -} as any) -const CliDeviceRoute = CliDeviceRouteImport.update({ - id: '/cli/device', - path: '/cli/device', - getParentRoute: () => rootRouteImport, -} as any) -const CliAuthRoute = CliAuthRouteImport.update({ - id: '/cli/auth', - path: '/cli/auth', - getParentRoute: () => rootRouteImport, -} as any) -const AuthDocsRoute = AuthDocsRouteImport.update({ - id: '/auth/docs', - path: '/auth/docs', +const UploadRoute = UploadRouteImport.update({ + id: '/upload', + path: '/upload', getParentRoute: () => rootRouteImport, } as any) const OwnerSlugRoute = OwnerSlugRouteImport.update({ @@ -236,9 +142,124 @@ const OwnerSlugRoute = OwnerSlugRouteImport.update({ path: '/$owner/$slug', getParentRoute: () => rootRouteImport, } as any) -const PluginsScopeNameRoute = PluginsScopeNameRouteImport.update({ - id: '/plugins/$scope/$name', - path: '/plugins/$scope/$name', +const AuthDocsRoute = AuthDocsRouteImport.update({ + id: '/auth/docs', + path: '/auth/docs', + getParentRoute: () => rootRouteImport, +} as any) +const CliAuthRoute = CliAuthRouteImport.update({ + id: '/cli/auth', + path: '/cli/auth', + getParentRoute: () => rootRouteImport, +} as any) +const CliDeviceRoute = CliDeviceRouteImport.update({ + id: '/cli/device', + path: '/cli/device', + getParentRoute: () => rootRouteImport, +} as any) +const CreatorsIndexRoute = CreatorsIndexRouteImport.update({ + id: '/creators/', + path: '/creators/', + getParentRoute: () => rootRouteImport, +} as any) +const OrgsHandleRoute = OrgsHandleRouteImport.update({ + id: '/orgs/$handle', + path: '/orgs/$handle', + getParentRoute: () => rootRouteImport, +} as any) +const PHandleRoute = PHandleRouteImport.update({ + id: '/p/$handle', + path: '/p/$handle', + getParentRoute: () => rootRouteImport, +} as any) +const PackagesIndexRoute = PackagesIndexRouteImport.update({ + id: '/packages/', + path: '/packages/', + getParentRoute: () => rootRouteImport, +} as any) +const PackagesNameRoute = PackagesNameRouteImport.update({ + id: '/packages/$name', + path: '/packages/$name', + getParentRoute: () => rootRouteImport, +} as any) +const PackagesNewRoute = PackagesNewRouteImport.update({ + id: '/packages/new', + path: '/packages/new', + getParentRoute: () => rootRouteImport, +} as any) +const PluginsIndexRoute = PluginsIndexRouteImport.update({ + id: '/plugins/', + path: '/plugins/', + getParentRoute: () => rootRouteImport, +} as any) +const PluginsNameRoute = PluginsNameRouteImport.update({ + id: '/plugins/$name', + path: '/plugins/$name', + getParentRoute: () => rootRouteImport, +} as any) +const PluginsNewRoute = PluginsNewRouteImport.update({ + id: '/plugins/new', + path: '/plugins/new', + getParentRoute: () => rootRouteImport, +} as any) +const PluginsPublishRoute = PluginsPublishRouteImport.update({ + id: '/plugins/publish', + path: '/plugins/publish', + getParentRoute: () => rootRouteImport, +} as any) +const PublishersIndexRoute = PublishersIndexRouteImport.update({ + id: '/publishers/', + path: '/publishers/', + getParentRoute: () => rootRouteImport, +} as any) +const SkillsIndexRoute = SkillsIndexRouteImport.update({ + id: '/skills/', + path: '/skills/', + getParentRoute: () => rootRouteImport, +} as any) +const SkillsPublishRoute = SkillsPublishRouteImport.update({ + id: '/skills/publish', + path: '/skills/publish', + getParentRoute: () => rootRouteImport, +} as any) +const UHandleRoute = UHandleRouteImport.update({ + id: '/u/$handle', + path: '/u/$handle', + getParentRoute: () => rootRouteImport, +} as any) +const UserHandleRoute = UserHandleRouteImport.update({ + id: '/user/$handle', + path: '/user/$handle', + getParentRoute: () => rootRouteImport, +} as any) +const UsersIndexRoute = UsersIndexRouteImport.update({ + id: '/users/', + path: '/users/', + getParentRoute: () => rootRouteImport, +} as any) +const OwnerSlugSecurityAuditRoute = OwnerSlugSecurityAuditRouteImport.update({ + id: '/security-audit', + path: '/security-audit', + getParentRoute: () => OwnerSlugRoute, +} as any) +const OwnerSlugSettingsRoute = OwnerSlugSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => OwnerSlugRoute, +} as any) +const OwnerPluginsSlugRoute = OwnerPluginsSlugRouteImport.update({ + id: '/$owner/plugins/$slug', + path: '/$owner/plugins/$slug', + getParentRoute: () => rootRouteImport, +} as any) +const OwnerSkillsSlugRoute = OwnerSkillsSlugRouteImport.update({ + id: '/$owner/skills/$slug', + path: '/$owner/skills/$slug', + getParentRoute: () => rootRouteImport, +} as any) +const PackagesScopeNameRoute = PackagesScopeNameRouteImport.update({ + id: '/packages/$scope/$name', + path: '/packages/$scope/$name', getParentRoute: () => rootRouteImport, } as any) const PluginsNameSecurityAuditRoute = @@ -247,53 +268,16 @@ const PluginsNameSecurityAuditRoute = path: '/security-audit', getParentRoute: () => PluginsNameRoute, } as any) -const PackagesScopeNameRoute = PackagesScopeNameRouteImport.update({ - id: '/packages/$scope/$name', - path: '/packages/$scope/$name', +const PluginsScopeNameRoute = PluginsScopeNameRouteImport.update({ + id: '/plugins/$scope/$name', + path: '/plugins/$scope/$name', getParentRoute: () => rootRouteImport, } as any) -const OwnerSkillsSlugRoute = OwnerSkillsSlugRouteImport.update({ - id: '/$owner/skills/$slug', - path: '/$owner/skills/$slug', - getParentRoute: () => rootRouteImport, -} as any) -const OwnerPluginsSlugRoute = OwnerPluginsSlugRouteImport.update({ - id: '/$owner/plugins/$slug', - path: '/$owner/plugins/$slug', - getParentRoute: () => rootRouteImport, -} as any) -const OwnerSlugSettingsRoute = OwnerSlugSettingsRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => OwnerSlugRoute, -} as any) -const OwnerSlugSecurityAuditRoute = OwnerSlugSecurityAuditRouteImport.update({ - id: '/security-audit', - path: '/security-audit', - getParentRoute: () => OwnerSlugRoute, -} as any) -const PluginsScopeNameSecurityAuditRoute = - PluginsScopeNameSecurityAuditRouteImport.update({ - id: '/security-audit', - path: '/security-audit', - getParentRoute: () => PluginsScopeNameRoute, - } as any) -const PluginsNameSecurityScannerRoute = - PluginsNameSecurityScannerRouteImport.update({ +const OwnerSlugSecurityScannerRoute = + OwnerSlugSecurityScannerRouteImport.update({ id: '/security/$scanner', path: '/security/$scanner', - getParentRoute: () => PluginsNameRoute, - } as any) -const OwnerSkillsSlugSettingsRoute = OwnerSkillsSlugSettingsRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => OwnerSkillsSlugRoute, -} as any) -const OwnerSkillsSlugSecurityAuditRoute = - OwnerSkillsSlugSecurityAuditRouteImport.update({ - id: '/security-audit', - path: '/security-audit', - getParentRoute: () => OwnerSkillsSlugRoute, + getParentRoute: () => OwnerSlugRoute, } as any) const OwnerPluginsSlugSecurityAuditRoute = OwnerPluginsSlugSecurityAuditRouteImport.update({ @@ -301,29 +285,51 @@ const OwnerPluginsSlugSecurityAuditRoute = path: '/security-audit', getParentRoute: () => OwnerPluginsSlugRoute, } as any) -const OwnerSlugSecurityScannerRoute = - OwnerSlugSecurityScannerRouteImport.update({ - id: '/security/$scanner', - path: '/security/$scanner', - getParentRoute: () => OwnerSlugRoute, +const OwnerSkillsSlugSecurityAuditRoute = + OwnerSkillsSlugSecurityAuditRouteImport.update({ + id: '/security-audit', + path: '/security-audit', + getParentRoute: () => OwnerSkillsSlugRoute, } as any) -const PluginsScopeNameSecurityScannerRoute = - PluginsScopeNameSecurityScannerRouteImport.update({ +const OwnerSkillsSlugSettingsRoute = OwnerSkillsSlugSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => OwnerSkillsSlugRoute, +} as any) +const PluginsNameSecurityScannerRoute = + PluginsNameSecurityScannerRouteImport.update({ id: '/security/$scanner', path: '/security/$scanner', + getParentRoute: () => PluginsNameRoute, + } as any) +const PluginsScopeNameSecurityAuditRoute = + PluginsScopeNameSecurityAuditRouteImport.update({ + id: '/security-audit', + path: '/security-audit', getParentRoute: () => PluginsScopeNameRoute, } as any) +const SkillsShOwnerRepoSlugRoute = SkillsShOwnerRepoSlugRouteImport.update({ + id: '/skills-sh/$owner/$repo/$slug', + path: '/skills-sh/$owner/$repo/$slug', + getParentRoute: () => rootRouteImport, +} as any) +const OwnerPluginsSlugSecurityScannerRoute = + OwnerPluginsSlugSecurityScannerRouteImport.update({ + id: '/security/$scanner', + path: '/security/$scanner', + getParentRoute: () => OwnerPluginsSlugRoute, + } as any) const OwnerSkillsSlugSecurityScannerRoute = OwnerSkillsSlugSecurityScannerRouteImport.update({ id: '/security/$scanner', path: '/security/$scanner', getParentRoute: () => OwnerSkillsSlugRoute, } as any) -const OwnerPluginsSlugSecurityScannerRoute = - OwnerPluginsSlugSecurityScannerRouteImport.update({ +const PluginsScopeNameSecurityScannerRoute = + PluginsScopeNameSecurityScannerRouteImport.update({ id: '/security/$scanner', path: '/security/$scanner', - getParentRoute: () => OwnerPluginsSlugRoute, + getParentRoute: () => PluginsScopeNameRoute, } as any) export interface FileRoutesByFullPath { @@ -375,6 +381,7 @@ export interface FileRoutesByFullPath { '/$owner/skills/$slug/settings': typeof OwnerSkillsSlugSettingsRoute '/plugins/$name/security/$scanner': typeof PluginsNameSecurityScannerRoute '/plugins/$scope/$name/security-audit': typeof PluginsScopeNameSecurityAuditRoute + '/skills-sh/$owner/$repo/$slug': typeof SkillsShOwnerRepoSlugRoute '/$owner/plugins/$slug/security/$scanner': typeof OwnerPluginsSlugSecurityScannerRoute '/$owner/skills/$slug/security/$scanner': typeof OwnerSkillsSlugSecurityScannerRoute '/plugins/$scope/$name/security/$scanner': typeof PluginsScopeNameSecurityScannerRoute @@ -428,6 +435,7 @@ export interface FileRoutesByTo { '/$owner/skills/$slug/settings': typeof OwnerSkillsSlugSettingsRoute '/plugins/$name/security/$scanner': typeof PluginsNameSecurityScannerRoute '/plugins/$scope/$name/security-audit': typeof PluginsScopeNameSecurityAuditRoute + '/skills-sh/$owner/$repo/$slug': typeof SkillsShOwnerRepoSlugRoute '/$owner/plugins/$slug/security/$scanner': typeof OwnerPluginsSlugSecurityScannerRoute '/$owner/skills/$slug/security/$scanner': typeof OwnerSkillsSlugSecurityScannerRoute '/plugins/$scope/$name/security/$scanner': typeof PluginsScopeNameSecurityScannerRoute @@ -482,6 +490,7 @@ export interface FileRoutesById { '/$owner/skills/$slug/settings': typeof OwnerSkillsSlugSettingsRoute '/plugins/$name/security/$scanner': typeof PluginsNameSecurityScannerRoute '/plugins/$scope/$name/security-audit': typeof PluginsScopeNameSecurityAuditRoute + '/skills-sh/$owner/$repo/$slug': typeof SkillsShOwnerRepoSlugRoute '/$owner/plugins/$slug/security/$scanner': typeof OwnerPluginsSlugSecurityScannerRoute '/$owner/skills/$slug/security/$scanner': typeof OwnerSkillsSlugSecurityScannerRoute '/plugins/$scope/$name/security/$scanner': typeof PluginsScopeNameSecurityScannerRoute @@ -537,6 +546,7 @@ export interface FileRouteTypes { | '/$owner/skills/$slug/settings' | '/plugins/$name/security/$scanner' | '/plugins/$scope/$name/security-audit' + | '/skills-sh/$owner/$repo/$slug' | '/$owner/plugins/$slug/security/$scanner' | '/$owner/skills/$slug/security/$scanner' | '/plugins/$scope/$name/security/$scanner' @@ -590,6 +600,7 @@ export interface FileRouteTypes { | '/$owner/skills/$slug/settings' | '/plugins/$name/security/$scanner' | '/plugins/$scope/$name/security-audit' + | '/skills-sh/$owner/$repo/$slug' | '/$owner/plugins/$slug/security/$scanner' | '/$owner/skills/$slug/security/$scanner' | '/plugins/$scope/$name/security/$scanner' @@ -643,6 +654,7 @@ export interface FileRouteTypes { | '/$owner/skills/$slug/settings' | '/plugins/$name/security/$scanner' | '/plugins/$scope/$name/security-audit' + | '/skills-sh/$owner/$repo/$slug' | '/$owner/plugins/$slug/security/$scanner' | '/$owner/skills/$slug/security/$scanner' | '/plugins/$scope/$name/security/$scanner' @@ -688,99 +700,16 @@ export interface RootRouteChildren { OwnerSkillsSlugRoute: typeof OwnerSkillsSlugRouteWithChildren PackagesScopeNameRoute: typeof PackagesScopeNameRoute PluginsScopeNameRoute: typeof PluginsScopeNameRouteWithChildren + SkillsShOwnerRepoSlugRoute: typeof SkillsShOwnerRepoSlugRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/upload': { - id: '/upload' - path: '/upload' - fullPath: '/upload' - preLoaderRoute: typeof UploadRouteImport - parentRoute: typeof rootRouteImport - } - '/stars': { - id: '/stars' - path: '/stars' - fullPath: '/stars' - preLoaderRoute: typeof StarsRouteImport - parentRoute: typeof rootRouteImport - } - '/settings': { - id: '/settings' - path: '/settings' - fullPath: '/settings' - preLoaderRoute: typeof SettingsRouteImport - parentRoute: typeof rootRouteImport - } - '/search': { - id: '/search' - path: '/search' - fullPath: '/search' - preLoaderRoute: typeof SearchRouteImport - parentRoute: typeof rootRouteImport - } - '/publish-skill': { - id: '/publish-skill' - path: '/publish-skill' - fullPath: '/publish-skill' - preLoaderRoute: typeof PublishSkillRouteImport - parentRoute: typeof rootRouteImport - } - '/publish-plugin': { - id: '/publish-plugin' - path: '/publish-plugin' - fullPath: '/publish-plugin' - preLoaderRoute: typeof PublishPluginRouteImport - parentRoute: typeof rootRouteImport - } - '/management': { - id: '/management' - path: '/management' - fullPath: '/management' - preLoaderRoute: typeof ManagementRouteImport - parentRoute: typeof rootRouteImport - } - '/import': { - id: '/import' - path: '/import' - fullPath: '/import' - preLoaderRoute: typeof ImportRouteImport - parentRoute: typeof rootRouteImport - } - '/dashboard': { - id: '/dashboard' - path: '/dashboard' - fullPath: '/dashboard' - preLoaderRoute: typeof DashboardRouteImport - parentRoute: typeof rootRouteImport - } - '/audits': { - id: '/audits' - path: '/audits' - fullPath: '/audits' - preLoaderRoute: typeof AuditsRouteImport - parentRoute: typeof rootRouteImport - } - '/admin': { - id: '/admin' - path: '/admin' - fullPath: '/admin' - preLoaderRoute: typeof AdminRouteImport - parentRoute: typeof rootRouteImport - } - '/add': { - id: '/add' - path: '/add' - fullPath: '/add' - preLoaderRoute: typeof AddRouteImport - parentRoute: typeof rootRouteImport - } - '/account-banned': { - id: '/account-banned' - path: '/account-banned' - fullPath: '/account-banned' - preLoaderRoute: typeof AccountBannedRouteImport + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } '/$slug': { @@ -790,144 +719,95 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SlugRouteImport parentRoute: typeof rootRouteImport } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport + '/account-banned': { + id: '/account-banned' + path: '/account-banned' + fullPath: '/account-banned' + preLoaderRoute: typeof AccountBannedRouteImport parentRoute: typeof rootRouteImport } - '/users/': { - id: '/users/' - path: '/users' - fullPath: '/users/' - preLoaderRoute: typeof UsersIndexRouteImport + '/add': { + id: '/add' + path: '/add' + fullPath: '/add' + preLoaderRoute: typeof AddRouteImport parentRoute: typeof rootRouteImport } - '/skills/': { - id: '/skills/' - path: '/skills' - fullPath: '/skills/' - preLoaderRoute: typeof SkillsIndexRouteImport + '/admin': { + id: '/admin' + path: '/admin' + fullPath: '/admin' + preLoaderRoute: typeof AdminRouteImport parentRoute: typeof rootRouteImport } - '/publishers/': { - id: '/publishers/' - path: '/publishers' - fullPath: '/publishers/' - preLoaderRoute: typeof PublishersIndexRouteImport + '/audits': { + id: '/audits' + path: '/audits' + fullPath: '/audits' + preLoaderRoute: typeof AuditsRouteImport parentRoute: typeof rootRouteImport } - '/plugins/': { - id: '/plugins/' - path: '/plugins' - fullPath: '/plugins/' - preLoaderRoute: typeof PluginsIndexRouteImport + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof DashboardRouteImport parentRoute: typeof rootRouteImport } - '/packages/': { - id: '/packages/' - path: '/packages' - fullPath: '/packages/' - preLoaderRoute: typeof PackagesIndexRouteImport + '/import': { + id: '/import' + path: '/import' + fullPath: '/import' + preLoaderRoute: typeof ImportRouteImport parentRoute: typeof rootRouteImport } - '/creators/': { - id: '/creators/' - path: '/creators' - fullPath: '/creators/' - preLoaderRoute: typeof CreatorsIndexRouteImport + '/management': { + id: '/management' + path: '/management' + fullPath: '/management' + preLoaderRoute: typeof ManagementRouteImport parentRoute: typeof rootRouteImport } - '/user/$handle': { - id: '/user/$handle' - path: '/user/$handle' - fullPath: '/user/$handle' - preLoaderRoute: typeof UserHandleRouteImport + '/publish-plugin': { + id: '/publish-plugin' + path: '/publish-plugin' + fullPath: '/publish-plugin' + preLoaderRoute: typeof PublishPluginRouteImport parentRoute: typeof rootRouteImport } - '/u/$handle': { - id: '/u/$handle' - path: '/u/$handle' - fullPath: '/u/$handle' - preLoaderRoute: typeof UHandleRouteImport + '/publish-skill': { + id: '/publish-skill' + path: '/publish-skill' + fullPath: '/publish-skill' + preLoaderRoute: typeof PublishSkillRouteImport parentRoute: typeof rootRouteImport } - '/skills/publish': { - id: '/skills/publish' - path: '/skills/publish' - fullPath: '/skills/publish' - preLoaderRoute: typeof SkillsPublishRouteImport + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport parentRoute: typeof rootRouteImport } - '/plugins/publish': { - id: '/plugins/publish' - path: '/plugins/publish' - fullPath: '/plugins/publish' - preLoaderRoute: typeof PluginsPublishRouteImport + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport parentRoute: typeof rootRouteImport } - '/plugins/new': { - id: '/plugins/new' - path: '/plugins/new' - fullPath: '/plugins/new' - preLoaderRoute: typeof PluginsNewRouteImport + '/stars': { + id: '/stars' + path: '/stars' + fullPath: '/stars' + preLoaderRoute: typeof StarsRouteImport parentRoute: typeof rootRouteImport } - '/plugins/$name': { - id: '/plugins/$name' - path: '/plugins/$name' - fullPath: '/plugins/$name' - preLoaderRoute: typeof PluginsNameRouteImport - parentRoute: typeof rootRouteImport - } - '/packages/new': { - id: '/packages/new' - path: '/packages/new' - fullPath: '/packages/new' - preLoaderRoute: typeof PackagesNewRouteImport - parentRoute: typeof rootRouteImport - } - '/packages/$name': { - id: '/packages/$name' - path: '/packages/$name' - fullPath: '/packages/$name' - preLoaderRoute: typeof PackagesNameRouteImport - parentRoute: typeof rootRouteImport - } - '/p/$handle': { - id: '/p/$handle' - path: '/p/$handle' - fullPath: '/p/$handle' - preLoaderRoute: typeof PHandleRouteImport - parentRoute: typeof rootRouteImport - } - '/orgs/$handle': { - id: '/orgs/$handle' - path: '/orgs/$handle' - fullPath: '/orgs/$handle' - preLoaderRoute: typeof OrgsHandleRouteImport - parentRoute: typeof rootRouteImport - } - '/cli/device': { - id: '/cli/device' - path: '/cli/device' - fullPath: '/cli/device' - preLoaderRoute: typeof CliDeviceRouteImport - parentRoute: typeof rootRouteImport - } - '/cli/auth': { - id: '/cli/auth' - path: '/cli/auth' - fullPath: '/cli/auth' - preLoaderRoute: typeof CliAuthRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/docs': { - id: '/auth/docs' - path: '/auth/docs' - fullPath: '/auth/docs' - preLoaderRoute: typeof AuthDocsRouteImport + '/upload': { + id: '/upload' + path: '/upload' + fullPath: '/upload' + preLoaderRoute: typeof UploadRouteImport parentRoute: typeof rootRouteImport } '/$owner/$slug': { @@ -937,25 +817,158 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OwnerSlugRouteImport parentRoute: typeof rootRouteImport } - '/plugins/$scope/$name': { - id: '/plugins/$scope/$name' - path: '/plugins/$scope/$name' - fullPath: '/plugins/$scope/$name' - preLoaderRoute: typeof PluginsScopeNameRouteImport + '/auth/docs': { + id: '/auth/docs' + path: '/auth/docs' + fullPath: '/auth/docs' + preLoaderRoute: typeof AuthDocsRouteImport parentRoute: typeof rootRouteImport } - '/plugins/$name/security-audit': { - id: '/plugins/$name/security-audit' - path: '/security-audit' - fullPath: '/plugins/$name/security-audit' - preLoaderRoute: typeof PluginsNameSecurityAuditRouteImport - parentRoute: typeof PluginsNameRoute + '/cli/auth': { + id: '/cli/auth' + path: '/cli/auth' + fullPath: '/cli/auth' + preLoaderRoute: typeof CliAuthRouteImport + parentRoute: typeof rootRouteImport } - '/packages/$scope/$name': { - id: '/packages/$scope/$name' - path: '/packages/$scope/$name' - fullPath: '/packages/$scope/$name' - preLoaderRoute: typeof PackagesScopeNameRouteImport + '/cli/device': { + id: '/cli/device' + path: '/cli/device' + fullPath: '/cli/device' + preLoaderRoute: typeof CliDeviceRouteImport + parentRoute: typeof rootRouteImport + } + '/creators/': { + id: '/creators/' + path: '/creators' + fullPath: '/creators/' + preLoaderRoute: typeof CreatorsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/orgs/$handle': { + id: '/orgs/$handle' + path: '/orgs/$handle' + fullPath: '/orgs/$handle' + preLoaderRoute: typeof OrgsHandleRouteImport + parentRoute: typeof rootRouteImport + } + '/p/$handle': { + id: '/p/$handle' + path: '/p/$handle' + fullPath: '/p/$handle' + preLoaderRoute: typeof PHandleRouteImport + parentRoute: typeof rootRouteImport + } + '/packages/': { + id: '/packages/' + path: '/packages' + fullPath: '/packages/' + preLoaderRoute: typeof PackagesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/packages/$name': { + id: '/packages/$name' + path: '/packages/$name' + fullPath: '/packages/$name' + preLoaderRoute: typeof PackagesNameRouteImport + parentRoute: typeof rootRouteImport + } + '/packages/new': { + id: '/packages/new' + path: '/packages/new' + fullPath: '/packages/new' + preLoaderRoute: typeof PackagesNewRouteImport + parentRoute: typeof rootRouteImport + } + '/plugins/': { + id: '/plugins/' + path: '/plugins' + fullPath: '/plugins/' + preLoaderRoute: typeof PluginsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/plugins/$name': { + id: '/plugins/$name' + path: '/plugins/$name' + fullPath: '/plugins/$name' + preLoaderRoute: typeof PluginsNameRouteImport + parentRoute: typeof rootRouteImport + } + '/plugins/new': { + id: '/plugins/new' + path: '/plugins/new' + fullPath: '/plugins/new' + preLoaderRoute: typeof PluginsNewRouteImport + parentRoute: typeof rootRouteImport + } + '/plugins/publish': { + id: '/plugins/publish' + path: '/plugins/publish' + fullPath: '/plugins/publish' + preLoaderRoute: typeof PluginsPublishRouteImport + parentRoute: typeof rootRouteImport + } + '/publishers/': { + id: '/publishers/' + path: '/publishers' + fullPath: '/publishers/' + preLoaderRoute: typeof PublishersIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/skills/': { + id: '/skills/' + path: '/skills' + fullPath: '/skills/' + preLoaderRoute: typeof SkillsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/skills/publish': { + id: '/skills/publish' + path: '/skills/publish' + fullPath: '/skills/publish' + preLoaderRoute: typeof SkillsPublishRouteImport + parentRoute: typeof rootRouteImport + } + '/u/$handle': { + id: '/u/$handle' + path: '/u/$handle' + fullPath: '/u/$handle' + preLoaderRoute: typeof UHandleRouteImport + parentRoute: typeof rootRouteImport + } + '/user/$handle': { + id: '/user/$handle' + path: '/user/$handle' + fullPath: '/user/$handle' + preLoaderRoute: typeof UserHandleRouteImport + parentRoute: typeof rootRouteImport + } + '/users/': { + id: '/users/' + path: '/users' + fullPath: '/users/' + preLoaderRoute: typeof UsersIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/$owner/$slug/security-audit': { + id: '/$owner/$slug/security-audit' + path: '/security-audit' + fullPath: '/$owner/$slug/security-audit' + preLoaderRoute: typeof OwnerSlugSecurityAuditRouteImport + parentRoute: typeof OwnerSlugRoute + } + '/$owner/$slug/settings': { + id: '/$owner/$slug/settings' + path: '/settings' + fullPath: '/$owner/$slug/settings' + preLoaderRoute: typeof OwnerSlugSettingsRouteImport + parentRoute: typeof OwnerSlugRoute + } + '/$owner/plugins/$slug': { + id: '/$owner/plugins/$slug' + path: '/$owner/plugins/$slug' + fullPath: '/$owner/plugins/$slug' + preLoaderRoute: typeof OwnerPluginsSlugRouteImport parentRoute: typeof rootRouteImport } '/$owner/skills/$slug': { @@ -965,61 +978,26 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OwnerSkillsSlugRouteImport parentRoute: typeof rootRouteImport } - '/$owner/plugins/$slug': { - id: '/$owner/plugins/$slug' - path: '/$owner/plugins/$slug' - fullPath: '/$owner/plugins/$slug' - preLoaderRoute: typeof OwnerPluginsSlugRouteImport + '/packages/$scope/$name': { + id: '/packages/$scope/$name' + path: '/packages/$scope/$name' + fullPath: '/packages/$scope/$name' + preLoaderRoute: typeof PackagesScopeNameRouteImport parentRoute: typeof rootRouteImport } - '/$owner/$slug/settings': { - id: '/$owner/$slug/settings' - path: '/settings' - fullPath: '/$owner/$slug/settings' - preLoaderRoute: typeof OwnerSlugSettingsRouteImport - parentRoute: typeof OwnerSlugRoute - } - '/$owner/$slug/security-audit': { - id: '/$owner/$slug/security-audit' + '/plugins/$name/security-audit': { + id: '/plugins/$name/security-audit' path: '/security-audit' - fullPath: '/$owner/$slug/security-audit' - preLoaderRoute: typeof OwnerSlugSecurityAuditRouteImport - parentRoute: typeof OwnerSlugRoute - } - '/plugins/$scope/$name/security-audit': { - id: '/plugins/$scope/$name/security-audit' - path: '/security-audit' - fullPath: '/plugins/$scope/$name/security-audit' - preLoaderRoute: typeof PluginsScopeNameSecurityAuditRouteImport - parentRoute: typeof PluginsScopeNameRoute - } - '/plugins/$name/security/$scanner': { - id: '/plugins/$name/security/$scanner' - path: '/security/$scanner' - fullPath: '/plugins/$name/security/$scanner' - preLoaderRoute: typeof PluginsNameSecurityScannerRouteImport + fullPath: '/plugins/$name/security-audit' + preLoaderRoute: typeof PluginsNameSecurityAuditRouteImport parentRoute: typeof PluginsNameRoute } - '/$owner/skills/$slug/settings': { - id: '/$owner/skills/$slug/settings' - path: '/settings' - fullPath: '/$owner/skills/$slug/settings' - preLoaderRoute: typeof OwnerSkillsSlugSettingsRouteImport - parentRoute: typeof OwnerSkillsSlugRoute - } - '/$owner/skills/$slug/security-audit': { - id: '/$owner/skills/$slug/security-audit' - path: '/security-audit' - fullPath: '/$owner/skills/$slug/security-audit' - preLoaderRoute: typeof OwnerSkillsSlugSecurityAuditRouteImport - parentRoute: typeof OwnerSkillsSlugRoute - } - '/$owner/plugins/$slug/security-audit': { - id: '/$owner/plugins/$slug/security-audit' - path: '/security-audit' - fullPath: '/$owner/plugins/$slug/security-audit' - preLoaderRoute: typeof OwnerPluginsSlugSecurityAuditRouteImport - parentRoute: typeof OwnerPluginsSlugRoute + '/plugins/$scope/$name': { + id: '/plugins/$scope/$name' + path: '/plugins/$scope/$name' + fullPath: '/plugins/$scope/$name' + preLoaderRoute: typeof PluginsScopeNameRouteImport + parentRoute: typeof rootRouteImport } '/$owner/$slug/security/$scanner': { id: '/$owner/$slug/security/$scanner' @@ -1028,13 +1006,55 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OwnerSlugSecurityScannerRouteImport parentRoute: typeof OwnerSlugRoute } - '/plugins/$scope/$name/security/$scanner': { - id: '/plugins/$scope/$name/security/$scanner' + '/$owner/plugins/$slug/security-audit': { + id: '/$owner/plugins/$slug/security-audit' + path: '/security-audit' + fullPath: '/$owner/plugins/$slug/security-audit' + preLoaderRoute: typeof OwnerPluginsSlugSecurityAuditRouteImport + parentRoute: typeof OwnerPluginsSlugRoute + } + '/$owner/skills/$slug/security-audit': { + id: '/$owner/skills/$slug/security-audit' + path: '/security-audit' + fullPath: '/$owner/skills/$slug/security-audit' + preLoaderRoute: typeof OwnerSkillsSlugSecurityAuditRouteImport + parentRoute: typeof OwnerSkillsSlugRoute + } + '/$owner/skills/$slug/settings': { + id: '/$owner/skills/$slug/settings' + path: '/settings' + fullPath: '/$owner/skills/$slug/settings' + preLoaderRoute: typeof OwnerSkillsSlugSettingsRouteImport + parentRoute: typeof OwnerSkillsSlugRoute + } + '/plugins/$name/security/$scanner': { + id: '/plugins/$name/security/$scanner' path: '/security/$scanner' - fullPath: '/plugins/$scope/$name/security/$scanner' - preLoaderRoute: typeof PluginsScopeNameSecurityScannerRouteImport + fullPath: '/plugins/$name/security/$scanner' + preLoaderRoute: typeof PluginsNameSecurityScannerRouteImport + parentRoute: typeof PluginsNameRoute + } + '/plugins/$scope/$name/security-audit': { + id: '/plugins/$scope/$name/security-audit' + path: '/security-audit' + fullPath: '/plugins/$scope/$name/security-audit' + preLoaderRoute: typeof PluginsScopeNameSecurityAuditRouteImport parentRoute: typeof PluginsScopeNameRoute } + '/skills-sh/$owner/$repo/$slug': { + id: '/skills-sh/$owner/$repo/$slug' + path: '/skills-sh/$owner/$repo/$slug' + fullPath: '/skills-sh/$owner/$repo/$slug' + preLoaderRoute: typeof SkillsShOwnerRepoSlugRouteImport + parentRoute: typeof rootRouteImport + } + '/$owner/plugins/$slug/security/$scanner': { + id: '/$owner/plugins/$slug/security/$scanner' + path: '/security/$scanner' + fullPath: '/$owner/plugins/$slug/security/$scanner' + preLoaderRoute: typeof OwnerPluginsSlugSecurityScannerRouteImport + parentRoute: typeof OwnerPluginsSlugRoute + } '/$owner/skills/$slug/security/$scanner': { id: '/$owner/skills/$slug/security/$scanner' path: '/security/$scanner' @@ -1042,12 +1062,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OwnerSkillsSlugSecurityScannerRouteImport parentRoute: typeof OwnerSkillsSlugRoute } - '/$owner/plugins/$slug/security/$scanner': { - id: '/$owner/plugins/$slug/security/$scanner' + '/plugins/$scope/$name/security/$scanner': { + id: '/plugins/$scope/$name/security/$scanner' path: '/security/$scanner' - fullPath: '/$owner/plugins/$slug/security/$scanner' - preLoaderRoute: typeof OwnerPluginsSlugSecurityScannerRouteImport - parentRoute: typeof OwnerPluginsSlugRoute + fullPath: '/plugins/$scope/$name/security/$scanner' + preLoaderRoute: typeof PluginsScopeNameSecurityScannerRouteImport + parentRoute: typeof PluginsScopeNameRoute } } } @@ -1164,6 +1184,7 @@ const rootRouteChildren: RootRouteChildren = { OwnerSkillsSlugRoute: OwnerSkillsSlugRouteWithChildren, PackagesScopeNameRoute: PackagesScopeNameRoute, PluginsScopeNameRoute: PluginsScopeNameRouteWithChildren, + SkillsShOwnerRepoSlugRoute: SkillsShOwnerRepoSlugRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/skills-sh/$owner/$repo/$slug.tsx b/src/routes/skills-sh/$owner/$repo/$slug.tsx new file mode 100644 index 00000000..a9efb7be --- /dev/null +++ b/src/routes/skills-sh/$owner/$repo/$slug.tsx @@ -0,0 +1,118 @@ +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { ExternalLink, ShieldAlert, ShieldCheck } from "lucide-react"; +import { api } from "../../../../../convex/_generated/api"; +import { InstallCopyButton } from "../../../../components/InstallCopyButton"; +import { Container } from "../../../../components/layout/Container"; +import { convexHttp } from "../../../../convex/client"; + +export const Route = createFileRoute("/skills-sh/$owner/$repo/$slug")({ + loader: async ({ params }) => { + const entry = await convexHttp.query(api.skillsShCatalog.getPublicEntry, params); + if (!entry) throw notFound(); + return entry; + }, + head: ({ loaderData }) => ({ + meta: [ + { title: `${loaderData?.displayName ?? "Skill"} - ClawHub` }, + { name: "description", content: loaderData?.summary ?? "ClawHub verified skill" }, + ], + }), + component: SkillsShCatalogEntryPage, +}); + +function SkillsShCatalogEntryPage() { + const entry = Route.useLoaderData(); + const installCommand = `openclaw skills install ${entry.ref}`; + const suspicious = entry.security.verdict === "suspicious"; + const VerdictIcon = suspicious ? ShieldAlert : ShieldCheck; + return ( +
+ +
+
+

+ {entry.ref} +

+

+ {entry.displayName} +

+

+ {entry.summary} +

+
+
+
+
+ +
+ +
+
Repository
+
+ {entry.repository} +
+
+
+
Path
+
+ {entry.githubPath} +
+
+
+
Commit
+
+ + {entry.githubCommit} + +
+
+
+ +
+

+ Install +

+
+ + {installCommand} + + +
+
+
+
+ ); +}