From 85a3fde60834cc21dfeeb311a230ef9512ee340f Mon Sep 17 00:00:00 2001 From: Gio Della-Libera Date: Fri, 24 Jul 2026 16:14:12 -0700 Subject: [PATCH] feat(claws): add gated publication and profile validation (#3090) Gate Claw publication before storage access or mutation, validate bounded exact package/profile bytes and archive hierarchy, implement the managed CLAW.md body envelope, and prevent disabled-family list/search starvation. Validated at exact head a9f1bb419fecef3191fbf012b9e2483fc8092b3f with all repository CI, CodeQL, secret scanning, focused tests, real local Convex schema/function validation, and clean final autoreview. Co-authored-by: Gio Della-Libera --- bun.lock | 8 +- convex/_generated/api.d.ts | 4 + convex/httpApiV1.handlers.test.ts | 246 +++++ convex/httpApiV1/packagesV1.ts | 42 +- convex/lib/clawpack.test.ts | 90 +- convex/lib/clawpack.ts | 107 ++- convex/lib/experimentalClaws.test.ts | 16 + convex/lib/experimentalClaws.ts | 10 + convex/lib/packageRegistry.ts | 33 +- convex/lib/skillZip.test.ts | 26 + convex/lib/skillZip.ts | 13 + convex/packages.public.test.ts | 746 ++++++++++++++- convex/packages.ts | 645 +++++++++++-- convex/schema.ts | 10 +- docs/README.md | 9 +- docs/claws.md | 99 ++ package.json | 2 +- packages/clawhub/package.json | 5 +- packages/clawhub/src/clawpack.ts | 116 ++- packages/clawhub/src/cli.ts | 4 +- .../clawhub/src/cli/commands/packages.test.ts | 180 +++- packages/clawhub/src/cli/commands/packages.ts | 53 +- packages/clawhub/src/schema/clawPackage.ts | 622 ++++++++++++ packages/clawhub/src/schema/claws.ts | 904 ++++++++++++++++++ packages/clawhub/src/schema/packages.ts | 2 +- packages/schema/dist/clawPackage.d.ts | 36 + packages/schema/dist/clawPackage.js | 468 +++++++++ packages/schema/dist/clawPackage.js.map | 1 + packages/schema/dist/index.d.ts | 1 + packages/schema/dist/index.js | 1 + packages/schema/dist/index.js.map | 2 +- packages/schema/dist/packages.d.ts | 6 +- packages/schema/dist/packages.js | 3 +- packages/schema/dist/packages.js.map | 2 +- packages/schema/package.json | 3 +- packages/schema/src/clawPackage.test.ts | 630 ++++++++++++ packages/schema/src/clawPackage.ts | 622 ++++++++++++ packages/schema/src/claws.test.ts | 4 +- packages/schema/src/index.ts | 1 + packages/schema/src/packages.ts | 3 +- specs/claws.md | 41 +- 41 files changed, 5588 insertions(+), 228 deletions(-) create mode 100755 convex/lib/experimentalClaws.test.ts create mode 100755 convex/lib/experimentalClaws.ts create mode 100644 docs/claws.md create mode 100644 packages/clawhub/src/schema/clawPackage.ts create mode 100644 packages/clawhub/src/schema/claws.ts create mode 100644 packages/schema/dist/clawPackage.d.ts create mode 100644 packages/schema/dist/clawPackage.js create mode 100644 packages/schema/dist/clawPackage.js.map create mode 100644 packages/schema/src/clawPackage.test.ts create mode 100644 packages/schema/src/clawPackage.ts diff --git a/bun.lock b/bun.lock index d3373dae..0db26632 100644 --- a/bun.lock +++ b/bun.lock @@ -111,6 +111,7 @@ "@openclaw/plugin-inspector": "0.3.17", "arktype": "2.2.3", "commander": "15.0.0", + "croner": "10.0.1", "fflate": "0.8.3", "ignore": "7.0.6", "json5": "2.2.3", @@ -119,6 +120,8 @@ "p-retry": "8.0.0", "semver": "7.8.5", "undici": "7.28.0", + "unicode-case-folding": "1.1.1", + "yaml": "2.9.0", }, "devDependencies": { "@types/node": "26.1.1", @@ -158,6 +161,7 @@ "arktype": "2.2.3", "croner": "10.0.1", "unicode-case-folding": "1.1.1", + "yaml": "2.9.0", }, "devDependencies": { "@typescript/native": "npm:typescript@7.0.2", @@ -167,7 +171,7 @@ }, "overrides": { "ast-v8-to-istanbul": "1.0.4", - "brace-expansion": "5.0.7", + "brace-expansion": "5.0.8", "dompurify": "3.4.12", "esbuild": "0.28.1", "fast-uri": "3.1.4", @@ -1070,7 +1074,7 @@ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], + "brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="], "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index c2fdc7e2..579baad0 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -69,6 +69,7 @@ import type * as lib_emailRendering from "../lib/emailRendering.js"; import type * as lib_emails from "../lib/emails.js"; import type * as lib_embeddingVisibility from "../lib/embeddingVisibility.js"; import type * as lib_embeddings from "../lib/embeddings.js"; +import type * as lib_experimentalClaws from "../lib/experimentalClaws.js"; import type * as lib_githubAccount from "../lib/githubAccount.js"; import type * as lib_githubActionsOidc from "../lib/githubActionsOidc.js"; import type * as lib_githubAuth from "../lib/githubAuth.js"; @@ -107,6 +108,7 @@ import type * as lib_publisherAbuseScoring from "../lib/publisherAbuseScoring.js import type * as lib_publisherCatalogDisplay from "../lib/publisherCatalogDisplay.js"; import type * as lib_publisherStats from "../lib/publisherStats.js"; import type * as lib_publishers from "../lib/publishers.js"; +import type * as lib_rankingMetricsImportLock from "../lib/rankingMetricsImportLock.js"; import type * as lib_recommendationScore from "../lib/recommendationScore.js"; import type * as lib_reporting from "../lib/reporting.js"; import type * as lib_reservedHandles from "../lib/reservedHandles.js"; @@ -251,6 +253,7 @@ declare const fullApi: ApiFromModules<{ "lib/emails": typeof lib_emails; "lib/embeddingVisibility": typeof lib_embeddingVisibility; "lib/embeddings": typeof lib_embeddings; + "lib/experimentalClaws": typeof lib_experimentalClaws; "lib/githubAccount": typeof lib_githubAccount; "lib/githubActionsOidc": typeof lib_githubActionsOidc; "lib/githubAuth": typeof lib_githubAuth; @@ -289,6 +292,7 @@ declare const fullApi: ApiFromModules<{ "lib/publisherCatalogDisplay": typeof lib_publisherCatalogDisplay; "lib/publisherStats": typeof lib_publisherStats; "lib/publishers": typeof lib_publishers; + "lib/rankingMetricsImportLock": typeof lib_rankingMetricsImportLock; "lib/recommendationScore": typeof lib_recommendationScore; "lib/reporting": typeof lib_reporting; "lib/reservedHandles": typeof lib_reservedHandles; diff --git a/convex/httpApiV1.handlers.test.ts b/convex/httpApiV1.handlers.test.ts index 65848cfb..dffaa8dc 100644 --- a/convex/httpApiV1.handlers.test.ts +++ b/convex/httpApiV1.handlers.test.ts @@ -12191,6 +12191,7 @@ describe("httpApiV1 handlers", () => { artifactKind: "legacy-zip", integritySha256: "a".repeat(64), sha256hash: "b".repeat(64), + clawpackSize: 321, }, }; } @@ -12214,6 +12215,7 @@ describe("httpApiV1 handlers", () => { source: "clawhub", artifactKind: "legacy-zip", artifactSha256: "b".repeat(64), + size: 321, packageName: "demo-plugin", version: "1.0.0", downloadUrl: "https://example.com/api/v1/packages/demo-plugin/download?version=1.0.0", @@ -14444,6 +14446,126 @@ describe("httpApiV1 handlers", () => { expect(storageGet).toHaveBeenCalledWith("storage:1"); }); + it("serves the immutable stored legacy archive without reconstructing it", async () => { + const archive = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0xaa, 0xbb]); + const runMutation = vi.fn().mockResolvedValue(okRate()); + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + if ("name" in args) { + return { + package: { + _id: "packages:1", + name: "demo-plugin", + displayName: "Demo Plugin", + family: "code-plugin", + tags: {}, + latestReleaseId: "packageReleases:1", + channel: "community", + isOfficial: false, + createdAt: 1, + updatedAt: 1, + }, + latestRelease: null, + owner: null, + }; + } + if ("releaseId" in args) { + return { + _id: "packageReleases:1", + version: "1.0.0", + createdAt: 1, + changelog: "init", + artifactKind: "legacy-zip", + clawpackStorageId: "storage:archive", + files: [ + { + path: "package.json", + size: 2, + sha256: "a".repeat(64), + storageId: "storage:file-that-must-not-be-read", + }, + ], + }; + } + return null; + }); + const storageGet = vi.fn(async (id: string) => + id === "storage:archive" ? new Blob([archive], { type: "application/zip" }) : null, + ); + + const response = await __handlers.packagesGetRouterV1Handler( + makeCtx({ runQuery, runMutation, storage: { get: storageGet } }), + new Request("https://example.com/api/v1/packages/demo-plugin/download"), + ); + + expect(response.status).toBe(200); + expect(new Uint8Array(await response.arrayBuffer())).toEqual(archive); + expect(storageGet).toHaveBeenCalledTimes(1); + expect(storageGet).toHaveBeenCalledWith("storage:archive"); + }); + + it("reconstructs compatibility ZIP downloads instead of serving stored npm tarballs", async () => { + const storedTarball = gzipSync(new TextEncoder().encode("not a zip")); + const packageJson = new TextEncoder().encode('{"name":"demo-plugin"}'); + const runMutation = vi.fn().mockResolvedValue(okRate()); + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + if ("name" in args) { + return { + package: { + _id: "packages:1", + name: "demo-plugin", + displayName: "Demo Plugin", + family: "code-plugin", + tags: {}, + latestReleaseId: "packageReleases:1", + channel: "community", + isOfficial: false, + createdAt: 1, + updatedAt: 1, + }, + latestRelease: null, + owner: null, + }; + } + if ("releaseId" in args) { + return { + _id: "packageReleases:1", + version: "1.0.0", + createdAt: 1, + changelog: "init", + artifactKind: "npm-pack", + clawpackStorageId: "storage:tarball", + files: [ + { + path: "package.json", + size: packageJson.byteLength, + sha256: "a".repeat(64), + storageId: "storage:package-json", + }, + ], + }; + } + return null; + }); + const storageGet = vi.fn(async (id: string) => { + if (id === "storage:tarball") return new Blob([storedTarball], { type: "application/gzip" }); + if (id === "storage:package-json") return new Blob([packageJson]); + return null; + }); + + const response = await __handlers.packagesGetRouterV1Handler( + makeCtx({ runQuery, runMutation, storage: { get: storageGet } }), + new Request("https://example.com/api/v1/packages/demo-plugin/download"), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("application/zip"); + expect( + strFromU8(unzipSync(new Uint8Array(await response.arrayBuffer()))["package/package.json"]!), + ).toBe('{"name":"demo-plugin"}'); + expect(storageGet).not.toHaveBeenCalledWith("storage:tarball"); + expect(storageGet).toHaveBeenCalledWith("storage:package-json"); + }); + it("allows package downloads when verification is clean even without cached vtAnalysis", async () => { const runMutation = vi.fn().mockResolvedValue(okRate()); const runQuery = vi.fn(async (_query: unknown, args: Record) => { @@ -14750,6 +14872,65 @@ describe("httpApiV1 handlers", () => { expect(runAction).not.toHaveBeenCalled(); }); + it.each(["loose", "direct-tgz", "staged-tgz"] as const)( + "disabled Claw publication rejects %s before multipart storage or ticket mutation", + async (mode) => { + vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "0"); + vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never); + vi.mocked(requirePackagePublishAuth).mockResolvedValue({ + kind: "user", + userId: "users:1", + user: { _id: "users:1", handle: "p" }, + } as never); + const runMutation = vi.fn().mockResolvedValue(okRate()); + const runAction = vi.fn(); + const storageGet = vi.fn(); + const storageStore = vi.fn(); + const form = packagePublishForm(packagePublishMetadata({ family: "claw" })); + if (mode === "loose") { + form.append("files", new File(["manifest"], "CLAW.md", { type: "text/markdown" })); + } else if (mode === "direct-tgz") { + const pack = npmPackFixture({ + "package/package.json": JSON.stringify({ name: "demo-claw", version: "1.0.0" }), + "package/CLAW.md": "manifest", + }); + form.append( + "clawpack", + new File([bytesToArrayBuffer(pack)], "demo-claw-1.0.0.tgz", { + type: "application/octet-stream", + }), + ); + } else { + form.set("clawpack", "storage:clawpack"); + form.set("clawpackUploadTicket", "packagePublishUploadTickets:1"); + } + + const response = await __handlers.publishPackageV1Handler( + makeCtx({ + runAction, + runMutation, + storage: { get: storageGet, store: storageStore }, + }), + new Request("https://example.com/api/v1/packages", { + method: "POST", + headers: { Authorization: "Bearer clh_test" }, + body: form, + }), + ); + + expect(response.status).toBe(400); + expect(await response.text()).toBe("Experimental Claw publication is disabled"); + expect(storageGet).not.toHaveBeenCalled(); + expect(storageStore).not.toHaveBeenCalled(); + expect(runAction).not.toHaveBeenCalled(); + expect( + runMutation.mock.calls.some(([, args]) => + Boolean(args && typeof args === "object" && "uploadTicket" in args), + ), + ).toBe(false); + }, + ); + it("package publish rejects browser session auth when token auth is not an API token", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:session" as never); vi.mocked(requirePackagePublishAuth).mockRejectedValue(new Error("Unauthorized")); @@ -14921,6 +15102,71 @@ describe("httpApiV1 handlers", () => { expect(payload?.files?.map((file) => file.path)).toContain("dist/index.js"); }); + it("multipart Claw publish accepts an npm pack without a plugin manifest", async () => { + vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "1"); + vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never); + vi.mocked(requirePackagePublishAuth).mockResolvedValue({ + kind: "user", + userId: "users:1", + user: { _id: "users:1", handle: "p" }, + } as never); + const runMutation = vi.fn().mockResolvedValue(okRate()); + const runAction = vi + .fn() + .mockResolvedValue({ ok: true, packageId: "pkg:claw", releaseId: "rel:claw" }); + const storageStore = vi.fn(async () => `storage:${storageStore.mock.calls.length}`); + const pack = npmPackFixture({ + "package/package.json": JSON.stringify({ + name: "demo-claw", + version: "1.0.0", + openclaw: { claw: "CLAW.md" }, + }), + "package/CLAW.md": + "---\nschemaVersion: 1\nagent:\n id: demo-claw\n---\nYou are a focused demo agent.\n", + }); + const form = new FormData(); + form.set( + "payload", + JSON.stringify({ + name: "demo-claw", + family: "claw", + version: "1.0.0", + changelog: "init", + }), + ); + form.append( + "clawpack", + new File([bytesToArrayBuffer(pack)], "demo-claw-1.0.0.tgz", { + type: "application/octet-stream", + }), + ); + + const response = await __handlers.publishPackageV1Handler( + makeCtx({ runAction, runMutation, storage: { store: storageStore } }), + new Request("https://example.com/api/v1/packages", { + method: "POST", + headers: { Authorization: "Bearer clh_test" }, + body: form, + }), + ); + + expect(response.status, await response.clone().text()).toBe(200); + expect(storageStore).toHaveBeenCalledTimes(3); + expect(runAction).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + payload: expect.objectContaining({ + family: "claw", + artifact: expect.objectContaining({ kind: "npm-pack", npmFileCount: 2 }), + files: [ + expect.objectContaining({ path: "package.json" }), + expect.objectContaining({ path: "CLAW.md" }), + ], + }), + }), + ); + }); + it("staged ClawPack publish derives artifact metadata from stored bytes", async () => { vi.mocked(getOptionalApiTokenUserId).mockResolvedValue("users:1" as never); vi.mocked(requirePackagePublishAuth).mockResolvedValue({ diff --git a/convex/httpApiV1/packagesV1.ts b/convex/httpApiV1/packagesV1.ts index a5eaff84..7291d9f3 100644 --- a/convex/httpApiV1/packagesV1.ts +++ b/convex/httpApiV1/packagesV1.ts @@ -34,6 +34,7 @@ import { buildDownloadMetricArgs, getDownloadIdentity } from "../downloadMetrics import { getOptionalActiveAuthUserIdFromAction } from "../lib/access"; import { getOptionalApiTokenUserId, requireApiTokenUser } from "../lib/apiTokenAuth"; import { parseClawPack, sha256Base64, sha256Hex } from "../lib/clawpack"; +import { experimentalClawsEnabled } from "../lib/experimentalClaws"; import { fetchGitHubRepositoryIdentity, verifyGitHubActionsTrustedPublishJwt, @@ -650,6 +651,7 @@ function toReleaseArtifact(release: ReleaseLike, packageName?: string) { return { kind: "legacy-zip" as const, ...(sha256 ? { sha256 } : {}), + ...(release.clawpackSize !== undefined ? { size: release.clawpackSize } : {}), format: "zip", source: "clawhub" as const, artifactKind: "legacy-zip" as const, @@ -1461,6 +1463,9 @@ async function parseMultipartPackagePublish( parsedPayload, "Package publish payload", ); + if (metadata.family === "claw" && !experimentalClawsEnabled()) { + throw new Error("Experimental Claw publication is disabled"); + } const tarballPart = getTarballPart(form); const fileParts = getFileParts( @@ -4137,16 +4142,29 @@ export async function packagesGetRouterV1Handler(ctx: ActionCtx, request: Reques if (!release) return text("Version not found", 404, rate.headers); const securityBlock = getReleaseSecurityBlock(release); if (securityBlock) return text(securityBlock.message, securityBlock.status, rate.headers); - const entries: Array<{ path: string; bytes: Uint8Array }> = []; - for (const file of release.files) { - const blob = await ctx.storage.get(file.storageId); - if (!blob) return text(`Missing stored file: ${file.path}`, 500, rate.headers); - entries.push({ - path: file.path, - bytes: new Uint8Array(await blob.arrayBuffer()), - }); + const storedZip = + release.artifactKind === "legacy-zip" && release.clawpackStorageId + ? await ctx.storage.get(release.clawpackStorageId) + : null; + if (release.artifactKind === "legacy-zip" && release.clawpackStorageId && !storedZip) { + return text("Missing stored package artifact", 500, rate.headers); + } + let zip: Uint8Array; + if (storedZip) { + zip = new Uint8Array(await storedZip.arrayBuffer()); + } else { + // Historical legacy releases and npm packs need a compatibility ZIP projection. + const entries: Array<{ path: string; bytes: Uint8Array }> = []; + for (const file of release.files) { + const blob = await ctx.storage.get(file.storageId); + if (!blob) return text(`Missing stored file: ${file.path}`, 500, rate.headers); + entries.push({ + path: file.path, + bytes: new Uint8Array(await blob.arrayBuffer()), + }); + } + zip = buildDeterministicPackageZip(entries); } - const zip = buildDeterministicPackageZip(entries); const [zipSha256, zipSha256Base64] = await Promise.all([sha256Hex(zip), sha256Base64(zip)]); try { const identity = getDownloadIdentity(request, viewerUserId ? String(viewerUserId) : null); @@ -4164,7 +4182,11 @@ export async function packagesGetRouterV1Handler(ctx: ActionCtx, request: Reques } catch { // Best-effort metric path; never fail package downloads. } - return new Response(new Blob([zip], { type: "application/zip" }), { + const zipBody = zip.buffer.slice( + zip.byteOffset, + zip.byteOffset + zip.byteLength, + ) as ArrayBuffer; + return new Response(new Blob([zipBody], { type: "application/zip" }), { status: 200, headers: mergeHeaders( rate.headers, diff --git a/convex/lib/clawpack.test.ts b/convex/lib/clawpack.test.ts index 33580d56..f003a6cd 100644 --- a/convex/lib/clawpack.test.ts +++ b/convex/lib/clawpack.test.ts @@ -13,8 +13,8 @@ function writeString(target: Uint8Array, offset: number, width: number, value: s target.set(encoded.subarray(0, width), offset); } -function tarFile(path: string, content: string) { - const bytes = new TextEncoder().encode(content); +function tarFile(path: string, content: string | Uint8Array) { + const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content; const header = new Uint8Array(BLOCK_SIZE); writeString(header, 0, 100, path); writeString(header, 100, 8, octal(0o644, 8)); @@ -37,7 +37,7 @@ function tarFile(path: string, content: string) { return [header, body]; } -function npmPackFixtureEntries(files: Array<[string, string]>) { +function npmPackFixtureEntries(files: Array<[string, string | Uint8Array]>) { const parts: Uint8Array[] = []; for (const [path, content] of files) { parts.push(...tarFile(path, content)); @@ -53,7 +53,7 @@ function npmPackFixtureEntries(files: Array<[string, string]>) { return gzipSync(tar); } -function npmPackFixture(files: Record) { +function npmPackFixture(files: Record) { return npmPackFixtureEntries(Object.entries(files)); } @@ -81,14 +81,62 @@ describe("clawpack", () => { ]); }); - it("rejects plugin tarballs without openclaw.plugin.json", async () => { + it("accepts Claw tarballs without a plugin manifest", async () => { const pack = npmPackFixture({ - "package/package.json": JSON.stringify({ name: "demo", version: "1.0.0" }), + "package/package.json": JSON.stringify({ + name: "demo", + version: "1.0.0", + openclaw: { claw: "CLAW.md" }, + }), + "package/CLAW.md": "---\nschemaVersion: 1\nagent: { id: demo }\n---\n", }); - await expect(parseClawPack(pack)).rejects.toThrow( - "ClawPack must contain package/openclaw.plugin.json", - ); + const parsed = await parseClawPack(pack); + expect(parsed.pluginManifest).toBeUndefined(); + expect(parsed.entries.map((entry) => entry.path)).toContain("CLAW.md"); + }); + + it("preserves and rejects padded tar path identity", async () => { + const pack = npmPackFixture({ + "package/package.json": JSON.stringify({ name: "demo", version: "1.0.0" }), + "package/CLAW.md ": "padded", + }); + + await expect(parseClawPack(pack)).rejects.toThrow("unsafe tar path"); + }); + + it("rejects empty tar path segments", async () => { + const pack = npmPackFixture({ + "package/package.json": JSON.stringify({ name: "demo", version: "1.0.0" }), + "package/workspace//SOUL.md": "unsafe", + }); + + await expect(parseClawPack(pack)).rejects.toThrow("unsafe tar path"); + }); + + it.each(["\u007f", "\u0085"])("rejects control character U+%s in tar paths", async (control) => { + const pack = npmPackFixture({ + "package/package.json": JSON.stringify({ name: "demo", version: "1.0.0" }), + [`package/workspace/a${control}.md`]: "unsafe", + }); + + await expect(parseClawPack(pack)).rejects.toThrow("unsafe tar path"); + }); + + it("rejects oversized package metadata before JSON parsing", async () => { + const pack = npmPackFixture({ + "package/package.json": new Uint8Array(256 * 1024 + 1).fill(0x20), + }); + + await expect(parseClawPack(pack)).rejects.toThrow("package.json exceeds 256KB limit"); + }); + + it("rejects invalid UTF-8 package metadata", async () => { + const pack = npmPackFixture({ + "package/package.json": new Uint8Array([0xc3, 0x28]), + }); + + await expect(parseClawPack(pack)).rejects.toThrow("package.json is invalid JSON"); }); it("rejects archives that are not rooted under package/", async () => { @@ -111,6 +159,30 @@ describe("clawpack", () => { ); }); + it.each([ + [ + "ancestor first", + [ + ["package/a", "file"], + ["package/a/b", "child"], + ], + ], + [ + "descendant first", + [ + ["package/a/b", "child"], + ["package/a", "file"], + ], + ], + ] as const)("rejects tar file/ancestor collisions with %s", async (_label, entries) => { + const pack = npmPackFixtureEntries([ + ["package/package.json", JSON.stringify({ name: "demo", version: "1.0.0" })], + ...entries.map(([path, content]) => [path, content] as [string, string]), + ]); + + await expect(parseClawPack(pack)).rejects.toThrow("file/ancestor path collision"); + }); + it("uses npm-style tarball names", () => { expect(npmTarballName("demo", "1.0.0")).toBe("demo-1.0.0.tgz"); expect(npmTarballName("@scope/demo", "1.0.0")).toBe("scope-demo-1.0.0.tgz"); diff --git a/convex/lib/clawpack.ts b/convex/lib/clawpack.ts index d606b0ac..66364a82 100644 --- a/convex/lib/clawpack.ts +++ b/convex/lib/clawpack.ts @@ -1,4 +1,5 @@ -import { gunzipSync } from "fflate"; +import { findClawPackagePathHierarchyCollision } from "clawhub-schema"; +import { Gunzip } from "fflate"; type ClawPackEntry = { path: string; @@ -16,23 +17,29 @@ type ParsedClawPack = { fileCount: number; entries: ClawPackEntry[]; packageJson: Record; - pluginManifest: Record; + pluginManifest?: Record; }; const TAR_BLOCK_SIZE = 512; +const MAX_UNPACKED_BYTES = 50 * 1024 * 1024; +const MAX_TAR_BYTES = 64 * 1024 * 1024; +const MAX_FILE_BYTES = MAX_UNPACKED_BYTES; +const MAX_PACKAGE_JSON_BYTES = 256 * 1024; +const MAX_FILE_COUNT = 10_000; +const GZIP_INPUT_CHUNK_BYTES = 1_024; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function textFromBytes(bytes: Uint8Array) { - return new TextDecoder().decode(bytes); + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); } function readTarString(block: Uint8Array, offset: number, length: number) { const slice = block.subarray(offset, offset + length); const end = slice.indexOf(0); - return textFromBytes(end === -1 ? slice : slice.subarray(0, end)).trim(); + return textFromBytes(end === -1 ? slice : slice.subarray(0, end)); } function readTarSize(block: Uint8Array) { @@ -44,15 +51,49 @@ function readTarSize(block: Uint8Array) { } function normalizeTarPath(path: string) { - const normalized = path.replaceAll("\\", "/").replace(/^\.\/+/, ""); - if (!normalized || normalized.startsWith("/") || normalized.includes("\0")) return null; - const segments = normalized.split("/").filter(Boolean); - if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) { + if ( + !path || + path !== path.trim() || + path.includes("\\") || + path.startsWith("/") || + /\p{Cc}/u.test(path) + ) { + return null; + } + const segments = path.split("/"); + if ( + segments.length === 0 || + segments.some( + (segment, index) => + segment === "." || segment === ".." || (segment === "" && index !== segments.length - 1), + ) + ) { return null; } return segments.join("/"); } +function gunzipBounded(bytes: Uint8Array): Uint8Array { + const chunks: Uint8Array[] = []; + let total = 0; + const gunzip = new Gunzip((chunk) => { + total += chunk.byteLength; + if (total > MAX_TAR_BYTES) throw new Error("ClawPack unpacked archive exceeds 64MB limit"); + chunks.push(Uint8Array.from(chunk)); + }); + for (let offset = 0; offset < bytes.byteLength; offset += GZIP_INPUT_CHUNK_BYTES) { + const end = Math.min(offset + GZIP_INPUT_CHUNK_BYTES, bytes.byteLength); + gunzip.push(bytes.subarray(offset, end), end === bytes.byteLength); + } + const result = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.byteLength; + } + return result; +} + function tarEntryPayload(bytes: Uint8Array, offset: number, size: number) { return bytes.subarray(offset, offset + size); } @@ -80,6 +121,7 @@ function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { if (!path) throw new Error("ClawPack contains an unsafe tar path"); const size = readTarSize(header); + if (size > MAX_FILE_BYTES) throw new Error("ClawPack file exceeds 50MB limit"); const payloadOffset = offset + TAR_BLOCK_SIZE; const payloadEnd = payloadOffset + size; if (payloadEnd > bytes.byteLength) throw new Error("ClawPack tar entry is truncated"); @@ -90,14 +132,13 @@ function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { throw new Error("ClawPack entries must be rooted under package/"); } const relPath = path.slice("package/".length); - if (!relPath || relPath.endsWith("/")) { - offset = nextTarOffset(payloadOffset, size); - continue; - } + if (!relPath || relPath.endsWith("/")) + throw new Error("ClawPack contains an unsafe tar path"); if (paths.has(relPath)) { throw new Error(`ClawPack contains duplicate path: ${relPath}`); } paths.add(relPath); + if (entries.length >= MAX_FILE_COUNT) throw new Error("ClawPack exceeds 10000 file limit"); entries.push({ path: relPath, bytes: Uint8Array.from(tarEntryPayload(bytes, payloadOffset, size)), @@ -110,6 +151,16 @@ function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { } if (entries.length === 0) throw new Error("ClawPack contains no files"); + const hierarchyCollision = findClawPackagePathHierarchyCollision( + entries.map((entry) => entry.path), + ); + if (hierarchyCollision) { + throw new Error( + `ClawPack contains file/ancestor path collision: ${hierarchyCollision.ancestor} and ${hierarchyCollision.descendant}`, + ); + } + const unpackedSize = entries.reduce((sum, entry) => sum + entry.bytes.byteLength, 0); + if (unpackedSize > MAX_UNPACKED_BYTES) throw new Error("ClawPack package exceeds 50MB limit"); return entries; } @@ -152,18 +203,19 @@ export async function parseClawPack(bytes: Uint8Array): Promise let tarBytes: Uint8Array; try { - tarBytes = gunzipSync(bytes); - } catch { - throw new Error("ClawPack must be a gzip-compressed npm pack tarball"); + tarBytes = gunzipBounded(bytes); + } catch (error) { + if (error instanceof Error && error.message.startsWith("ClawPack ")) throw error; + throw new Error("ClawPack must be a gzip-compressed npm pack tarball", { cause: error }); } const entries = parseTarEntries(tarBytes); const packageJsonEntry = entries.find((entry) => entry.path === "package.json"); if (!packageJsonEntry) throw new Error("ClawPack must contain package/package.json"); - const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json"); - if (!pluginManifestEntry) { - throw new Error("ClawPack must contain package/openclaw.plugin.json"); + if (packageJsonEntry.bytes.byteLength > MAX_PACKAGE_JSON_BYTES) { + throw new Error("ClawPack package.json exceeds 256KB limit"); } + const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json"); let packageJson: unknown; try { @@ -178,14 +230,15 @@ export async function parseClawPack(bytes: Uint8Array): Promise if (!packageName) throw new Error("ClawPack package.json must declare a name"); if (!packageVersion) throw new Error("ClawPack package.json must declare a version"); - let pluginManifest: unknown; - try { - pluginManifest = JSON.parse(textFromBytes(pluginManifestEntry.bytes)); - } catch { - throw new Error("ClawPack openclaw.plugin.json is invalid JSON"); - } - if (!isRecord(pluginManifest)) { - throw new Error("ClawPack openclaw.plugin.json must be an object"); + let pluginManifest: Record | undefined; + if (pluginManifestEntry) { + try { + const parsed = JSON.parse(textFromBytes(pluginManifestEntry.bytes)) as unknown; + if (!isRecord(parsed)) throw new Error(); + pluginManifest = parsed; + } catch { + throw new Error("ClawPack openclaw.plugin.json is invalid JSON object"); + } } return { @@ -199,6 +252,6 @@ export async function parseClawPack(bytes: Uint8Array): Promise fileCount: entries.length, entries, packageJson, - pluginManifest, + ...(pluginManifest ? { pluginManifest } : {}), }; } diff --git a/convex/lib/experimentalClaws.test.ts b/convex/lib/experimentalClaws.test.ts new file mode 100755 index 00000000..10b15c0f --- /dev/null +++ b/convex/lib/experimentalClaws.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { experimentalClawsEnabled, isClawFamilyPubliclyVisible } from "./experimentalClaws"; + +describe("experimental Claw visibility", () => { + it("requires the exact deployment gate value", () => { + expect(experimentalClawsEnabled({})).toBe(false); + expect(experimentalClawsEnabled({ CLAWHUB_EXPERIMENTAL_CLAWS: "true" })).toBe(false); + expect(experimentalClawsEnabled({ CLAWHUB_EXPERIMENTAL_CLAWS: "1" })).toBe(true); + }); + + it("keeps existing package families visible while gating Claws", () => { + expect(isClawFamilyPubliclyVisible("code-plugin", {})).toBe(true); + expect(isClawFamilyPubliclyVisible("claw", {})).toBe(false); + expect(isClawFamilyPubliclyVisible("claw", { CLAWHUB_EXPERIMENTAL_CLAWS: "1" })).toBe(true); + }); +}); diff --git a/convex/lib/experimentalClaws.ts b/convex/lib/experimentalClaws.ts new file mode 100755 index 00000000..5dbc7531 --- /dev/null +++ b/convex/lib/experimentalClaws.ts @@ -0,0 +1,10 @@ +export function experimentalClawsEnabled(env: Record = process.env) { + return env.CLAWHUB_EXPERIMENTAL_CLAWS === "1"; +} + +export function isClawFamilyPubliclyVisible( + family: string, + env: Record = process.env, +) { + return family !== "claw" || experimentalClawsEnabled(env); +} diff --git a/convex/lib/packageRegistry.ts b/convex/lib/packageRegistry.ts index 8d4e8b5c..b47a7156 100644 --- a/convex/lib/packageRegistry.ts +++ b/convex/lib/packageRegistry.ts @@ -355,13 +355,17 @@ export function normalizePublishFiles(files: PublishFile[]) { } export function assertPackageVersion( - family: "code-plugin" | "bundle-plugin" | "skill", + family: "code-plugin" | "bundle-plugin" | "skill" | "claw", version: string, ) { const trimmed = version.trim(); if (!trimmed) throw new ConvexError("Version required"); - if (family === "code-plugin" && !semver.valid(trimmed)) { - throw new ConvexError("Code plugin versions must be valid semver"); + if ((family === "code-plugin" || family === "claw") && !semver.valid(trimmed)) { + throw new ConvexError( + family === "claw" + ? "Claw versions must be valid semver" + : "Code plugin versions must be valid semver", + ); } return trimmed; } @@ -369,22 +373,39 @@ export function assertPackageVersion( export async function readStorageText( ctx: Pick, storageId: string, + options: { maxBytes?: number; label?: string; strictUtf8?: boolean } = {}, ): Promise { const blob = await ctx.storage.get(storageId as never); if (!blob) throw new ConvexError("Uploaded file no longer exists"); - return await blob.text(); + if (options.maxBytes !== undefined && blob.size > options.maxBytes) { + throw new ConvexError(`${options.label ?? "Uploaded text file"} exceeds byte limit`); + } + if (!options.strictUtf8) return await blob.text(); + try { + return new TextDecoder("utf-8", { fatal: true }).decode(await blob.arrayBuffer()); + } catch { + throw new ConvexError(`${options.label ?? "Uploaded text file"} must be valid UTF-8`); + } } export async function readOptionalTextFile( ctx: Pick, files: PublishFile[], pathMatch: (path: string) => boolean, + options: { + exactPath?: boolean; + maxBytes?: number; + label?: string; + strictUtf8?: boolean; + } = {}, ) { - const file = files.find((entry) => pathMatch(entry.path.toLowerCase())); + const file = files.find((entry) => + pathMatch(options.exactPath ? entry.path : entry.path.toLowerCase()), + ); if (!file) return null; return { file, - text: await readStorageText(ctx, file.storageId), + text: await readStorageText(ctx, file.storageId, options), }; } diff --git a/convex/lib/skillZip.test.ts b/convex/lib/skillZip.test.ts index f529887b..5c1d19b0 100644 --- a/convex/lib/skillZip.test.ts +++ b/convex/lib/skillZip.test.ts @@ -156,5 +156,31 @@ describe("skillZip", () => { ]); expect(unzipped["_meta.json"]).toBeUndefined(); }); + + it("rejects file/ancestor collisions before creating the legacy ZIP", () => { + expect(() => + buildDeterministicPackageZip([ + { path: "workspace", bytes: new TextEncoder().encode("file") }, + { path: "workspace/SOUL.md", bytes: new TextEncoder().encode("child") }, + ]), + ).toThrow("file/ancestor path collision"); + }); + + it("rejects Unicode-folded hierarchy collisions before creating the legacy ZIP", () => { + expect(() => + buildDeterministicPackageZip([ + { path: "Straße", bytes: new TextEncoder().encode("file") }, + { path: "STRASSE/child", bytes: new TextEncoder().encode("child") }, + ]), + ).toThrow("file/ancestor path collision"); + }); + + it("rejects unsafe package paths before creating the legacy ZIP", () => { + expect(() => + buildDeterministicPackageZip([ + { path: "workspace/a\u0085.md", bytes: new TextEncoder().encode("unsafe") }, + ]), + ).toThrow("unsafe package path"); + }); }); }); diff --git a/convex/lib/skillZip.ts b/convex/lib/skillZip.ts index 87093689..386f80b5 100644 --- a/convex/lib/skillZip.ts +++ b/convex/lib/skillZip.ts @@ -1,3 +1,4 @@ +import { findClawPackagePathHierarchyCollision, isSafeClawPackagePath } from "clawhub-schema"; import { zipSync } from "fflate"; type ZipEntry = { @@ -68,6 +69,18 @@ export function buildDeterministicZip(entries: ZipEntry[], meta?: SkillZipMeta) } export function buildDeterministicPackageZip(entries: ZipEntry[]) { + const unsafeEntry = entries.find((entry) => !isSafeClawPackagePath(entry.path)); + if (unsafeEntry) { + throw new Error(`Package contains unsafe package path: ${unsafeEntry.path}`); + } + const hierarchyCollision = findClawPackagePathHierarchyCollision( + entries.map((entry) => entry.path), + ); + if (hierarchyCollision) { + throw new Error( + `Package contains file/ancestor path collision: ${hierarchyCollision.ancestor} and ${hierarchyCollision.descendant}`, + ); + } const sorted = [...entries].sort((a, b) => a.path.localeCompare(b.path)); const zipData: ZipInput = {}; diff --git a/convex/packages.public.test.ts b/convex/packages.public.test.ts index 514d54e9..6afbb9c1 100644 --- a/convex/packages.public.test.ts +++ b/convex/packages.public.test.ts @@ -1,7 +1,7 @@ /* @vitest-environment node */ import { getAuthUserId } from "@convex-dev/auth/server"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { sha256Hex } from "./lib/clawpack"; import { MAX_PUBLISH_FILE_BYTES } from "./lib/publishLimits"; import { @@ -408,6 +408,7 @@ function makePackageManifestStorage() { get: vi.fn(async (id: string) => id === "storage:manifest" ? new Blob([JSON.stringify({ id: "demo.plugin" })]) : null, ), + store: vi.fn(async () => "storage:legacy-zip"), }; } @@ -975,6 +976,10 @@ const repairPackageIdentityInternalHandler = ( > )._handler; +beforeEach(() => { + process.env.CLAWHUB_EXPERIMENTAL_CLAWS = "1"; +}); + afterEach(() => { vi.mocked(getAuthUserId).mockReset(); vi.mocked(getAuthUserId).mockResolvedValue(null); @@ -1184,6 +1189,16 @@ function makeDigestCtx(options: { isDone: boolean; continueCursor: string; }>; + topicPagesByFamily?: Partial< + Record< + "skill" | "code-plugin" | "bundle-plugin", + Array<{ + page: Array>; + isDone: boolean; + continueCursor: string; + }> + > + >; categoryPages?: Array<{ page: Array>; isDone: boolean; @@ -1213,6 +1228,20 @@ function makeDigestCtx(options: { > >(); const rowsByTable = new Map>>(); + const familyPagesByTable = new Map< + string, + Map< + string, + Map< + string | null, + { + page: Array>; + isDone: boolean; + continueCursor: string; + } + > + > + >(); const indexNames: string[] = []; const indexFilters: Array<{ indexName: string; @@ -1252,6 +1281,36 @@ function makeDigestCtx(options: { setPages("packageSearchDigest", options.pages ?? []); setPages("packageCapabilitySearchDigest", options.capabilityPages ?? []); setPages("packageTopicSearchDigest", options.topicPages ?? []); + if (options.topicPagesByFamily) { + const pagesByFamily = new Map< + string, + Map< + string | null, + { + page: Array>; + isDone: boolean; + continueCursor: string; + } + > + >(); + for (const [family, pages] of Object.entries(options.topicPagesByFamily)) { + const pagesByCursor = new Map< + string | null, + { + page: Array>; + isDone: boolean; + continueCursor: string; + } + >(); + let cursor: string | null = null; + for (const page of pages ?? []) { + pagesByCursor.set(cursor, page); + cursor = page.continueCursor || null; + } + pagesByFamily.set(family, pagesByCursor); + } + familyPagesByTable.set("packageTopicSearchDigest", pagesByFamily); + } setPages("packagePluginCategorySearchDigest", options.categoryPages ?? []); if (options.categoryRows) { rowsByTable.set("packagePluginCategorySearchDigest", options.categoryRows); @@ -1260,10 +1319,14 @@ function makeDigestCtx(options: { const paginate = vi.fn(); const take = vi.fn(); - const paginateForTable = (table: string) => + const paginateForTable = (table: string, family?: string) => vi.fn(async (args: { cursor: string | null }) => { paginate(args); return ( + familyPagesByTable + .get(table) + ?.get(family ?? "") + ?.get(args.cursor ?? null) ?? pageByTable.get(table)?.get(args.cursor ?? null) ?? { page: [], isDone: true, @@ -1272,11 +1335,12 @@ function makeDigestCtx(options: { ); }); const paginateByTable = new Map>(); - const getPaginate = (table: string) => { - const existing = paginateByTable.get(table); + const getPaginate = (table: string, family?: string) => { + const key = `${table}:${family ?? ""}`; + const existing = paginateByTable.get(key); if (existing) return existing; - const next = paginateForTable(table); - paginateByTable.set(table, next); + const next = paginateForTable(table, family); + paginateByTable.set(key, next); return next; }; const takeForTable = (table: string) => @@ -1548,6 +1612,51 @@ function makeDigestCtx(options: { take: vi.fn().mockResolvedValue(matches), }; } + if (indexName.includes("_family_")) { + indexNames.push(indexName); + let family = ""; + let lowerField = ""; + let lowerBound = ""; + let upperBound = ""; + const queryBuilder = { + eq: (field: string, value: string | undefined) => { + if (field === "family") family = value ?? ""; + return queryBuilder; + }, + gte: (field: string, value: string) => { + lowerField = field; + lowerBound = value; + return queryBuilder; + }, + lt: (_field: string, value: string) => { + upperBound = value; + return queryBuilder; + }, + }; + builder?.(queryBuilder); + const takeFamilyRows = async (limit: number) => { + take(limit); + return (rowsByTable.get(table) ?? []) + .filter((row) => row.family === family) + .filter((row) => { + if (!lowerField) return true; + const value = readTestField(row, lowerField); + return ( + typeof value === "string" && + value >= lowerBound && + (!upperBound || value < upperBound) + ); + }) + .slice(0, limit); + }; + return { + take: vi.fn(takeFamilyRows), + order: vi.fn(() => ({ + paginate: getPaginate(table), + take: vi.fn(takeFamilyRows), + })), + }; + } return withIndex(table, indexName); }, withSearchIndex: ( @@ -1565,12 +1674,11 @@ function makeDigestCtx(options: { let searchField = ""; let query = ""; const queryBuilder = { + eq: () => queryBuilder, search: (field: string, value: string) => { searchField = field; query = value; - return { - eq: () => queryBuilder, - }; + return queryBuilder; }, }; builder?.(queryBuilder); @@ -1595,6 +1703,52 @@ function makeDigestCtx(options: { lt: (field: string, value: string) => unknown; }) => unknown, ) => { + if (indexName.includes("_family_")) { + indexNames.push(indexName); + let family = ""; + let exactTopic = ""; + let exactCategory = ""; + let lowerTopic = ""; + let upperTopic = ""; + const queryBuilder = { + eq: (field: string, value: unknown) => { + if (field === "family" && typeof value === "string") family = value; + if (field === "topic" && typeof value === "string") exactTopic = value; + if (field === "pluginCategory" && typeof value === "string") { + exactCategory = value; + } + return queryBuilder; + }, + gte: (field: string, value: string) => { + if (field === "topic") lowerTopic = value; + return queryBuilder; + }, + lt: (field: string, value: string) => { + if (field === "topic") upperTopic = value; + return queryBuilder; + }, + }; + builder?.(queryBuilder); + const takeFamilyRows = async (limit: number) => { + take(limit); + return (rowsByTable.get(table) ?? []) + .filter((row) => row.family === family) + .filter((row) => !exactTopic || row.topic === exactTopic) + .filter((row) => !exactCategory || row.pluginCategory === exactCategory) + .filter((row) => { + if (!lowerTopic && !upperTopic) return true; + const topic = typeof row.topic === "string" ? row.topic : ""; + return topic >= lowerTopic && topic < upperTopic; + }) + .slice(0, limit); + }; + return { + order: vi.fn(() => ({ + paginate: getPaginate(table, family), + take: vi.fn(takeFamilyRows), + })), + }; + } if (table !== "packageTopicSearchDigest" || indexName !== "by_active_topic_updated") { return withIndex(table, indexName); } @@ -1671,7 +1825,10 @@ function makePluginExportIndexKey(row: Record) { return [row.softDeletedAt, row.family, row.updatedAt, row._creationTime, row._id] as unknown[]; } -function makePluginExportCtx(digests: Array>) { +function makePluginExportCtx( + digests: Array>, + options?: { recommendationScoresMissing?: () => boolean }, +) { const packagesById = new Map( digests.map((digest) => [ String(digest.packageId), @@ -1697,6 +1854,15 @@ function makePluginExportCtx(digests: Array>) { db: { get: vi.fn(async (id: string) => packagesById.get(id) ?? null), query: vi.fn((table: string) => { + if (table === "packages") { + return { + withIndex: vi.fn(() => ({ + first: vi.fn(async () => + options?.recommendationScoresMissing?.() ? { _id: "packages:missing-score" } : null, + ), + })), + }; + } if (table !== "packageSearchDigest") throw new Error(`Unexpected table ${table}`); return { withIndex: vi.fn( @@ -1757,7 +1923,8 @@ function makePluginExportCtx(digests: Array>) { .sort((a, b) => { const updatedDiff = Number(a.updatedAt) - Number(b.updatedAt); if (updatedDiff !== 0) return order === "desc" ? -updatedDiff : updatedDiff; - return String(a._id).localeCompare(String(b._id)); + const idDiff = String(a._id).localeCompare(String(b._id)); + return order === "desc" ? -idDiff : idDiff; }); return { async *[Symbol.asyncIterator]() { @@ -4211,7 +4378,7 @@ describe("packages public queries", () => { expect(result.map((entry) => entry.package.name)).toEqual(["needle-plugin"]); expect(paginate).not.toHaveBeenCalled(); - expect(take).toHaveBeenCalledTimes(3); + expect(take.mock.calls.length).toBeLessThanOrEqual(8); expect(take).toHaveBeenCalledWith(20); expect(take).toHaveBeenCalledWith(50); }); @@ -5418,6 +5585,159 @@ describe("packages public queries", () => { expect(paginate).toHaveBeenCalledTimes(2); }); + it("ranks bounded candidates from every stable family before applying the search limit", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const { ctx } = makeDigestCtx({ + pages: [ + { + page: [ + makeDigest("skill-helper", { + family: "skill", + summary: "Calendar integration", + }), + makeDigest("official-plugin", { + family: "code-plugin", + isOfficial: true, + summary: "Calendar integration", + }), + ], + isDone: true, + continueCursor: "", + }, + ], + }); + + try { + const result = await searchPublicHandler(ctx, { query: "calendar", limit: 1 }); + expect(result.map((entry) => entry.package.name)).toEqual(["official-plugin"]); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("gives every stable family a fair combined-filter scan budget", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const noisePage = (family: "skill" | "code-plugin" | "bundle-plugin", page: number) => + Array.from({ length: 50 }, (_, index) => + makeDigest(`${family}-noise-${page}-${index}`, { + family, + topic: "calendar", + topics: ["calendar"], + pluginCategoryTags: ["channels"], + }), + ); + const { ctx } = makeDigestCtx({ + topicPagesByFamily: { + skill: [ + { page: noisePage("skill", 1), isDone: false, continueCursor: "skill:2" }, + { page: noisePage("skill", 2), isDone: false, continueCursor: "skill:3" }, + { page: noisePage("skill", 3), isDone: true, continueCursor: "" }, + ], + "code-plugin": [ + { + page: noisePage("code-plugin", 1), + isDone: false, + continueCursor: "code:2", + }, + { + page: noisePage("code-plugin", 2), + isDone: false, + continueCursor: "code:3", + }, + { page: noisePage("code-plugin", 3), isDone: true, continueCursor: "" }, + ], + "bundle-plugin": [ + { + page: noisePage("bundle-plugin", 1), + isDone: false, + continueCursor: "bundle:2", + }, + { + page: noisePage("bundle-plugin", 2), + isDone: false, + continueCursor: "bundle:3", + }, + { + page: [ + makeDigest("calendar-bundle-api", { + family: "bundle-plugin", + topic: "calendar", + topics: ["calendar"], + pluginCategoryTags: ["tools"], + }), + ], + isDone: true, + continueCursor: "", + }, + ], + }, + }); + + try { + const result = await searchPublicHandler(ctx, { + query: "calendar", + topic: "calendar", + category: "tools", + limit: 1, + }); + expect(result.map((entry) => entry.package.name)).toEqual(["calendar-bundle-api"]); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("scans each stable family past the first combined-filter window while Claws are disabled", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const { ctx, paginate } = makeDigestCtx({ + topicPages: [ + { + page: Array.from({ length: 50 }, (_, index) => + makeDigest(`calendar-skill-noise-${index}`, { + family: "skill", + topic: "calendar", + topics: ["calendar"], + pluginCategoryTags: ["channels"], + }), + ), + isDone: false, + continueCursor: "later", + }, + { + page: [ + makeDigest("calendar-skill-api", { + family: "skill", + topic: "calendar", + topics: ["calendar"], + pluginCategoryTags: ["tools"], + }), + ], + isDone: true, + continueCursor: "", + }, + ], + }); + + try { + const result = await searchPublicHandler(ctx, { + query: "calendar", + topic: "calendar", + category: "tools", + limit: 1, + }); + + expect(result.map((entry) => entry.package.name)).toEqual(["calendar-skill-api"]); + expect(paginate).toHaveBeenCalled(); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + it("bounds sparse combined-filter search scans", async () => { const topicPages = Array.from({ length: 7 }, (_page, pageIndex) => ({ page: Array.from({ length: 50 }, (_digest, digestIndex) => @@ -8863,6 +9183,182 @@ describe("packages public queries", () => { ).rejects.toThrow("Skill packages must use the skills publish flow"); }); + it("rejects Claw publication before mutation when the experimental gate is disabled", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + try { + await expect( + publishPackageForUserInternalHandler({} as never, { + actorUserId: "users:owner", + payload: { + name: "demo-claw", + family: "claw", + version: "1.0.0", + changelog: "init", + files: [], + }, + }), + ).rejects.toThrow("Experimental Claw publication is disabled"); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("rechecks the Claw gate at release insertion for staged publications", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const dbGet = vi.fn(); + try { + await expect( + insertReleaseInternalHandler( + { + db: { + system: {}, + get: dbGet, + insert: vi.fn(), + patch: vi.fn(), + replace: vi.fn(), + delete: vi.fn(), + query: vi.fn(), + normalizeId: vi.fn(), + }, + } as never, + { + family: "claw", + } as never, + ), + ).rejects.toThrow("Experimental Claw publication is disabled"); + expect(dbGet).not.toHaveBeenCalled(); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("publishes a profile-bearing Claw through the existing release pipeline when enabled", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + process.env.CLAWHUB_EXPERIMENTAL_CLAWS = "1"; + const longClawDescription = "x".repeat(1_100); + const storedFiles = new Map([ + [ + "storage:package", + JSON.stringify({ + name: "demo-claw", + version: "1.0.0", + openclaw: { claw: "manifests/CLAW.md" }, + }), + ], + [ + "storage:claw", + `---\nschemaVersion: 1\nagent:\n id: demo-claw\n name: Demo Claw\n description: ${longClawDescription}\nmetadata:\n openclaw.config: profiles/openclaw.yml\n---\n# Demo Claw\n`, + ], + ["storage:profile", "schemaVersion: 1\nagent:\n tools:\n profile: coding\n"], + ]); + const runMutation = vi.fn(async (_ref: unknown, args: Record) => { + if (args.minimumRole === "publisher") { + return { publisherId: "publishers:owner", linkedUserId: "users:owner" }; + } + if (args.family === "claw") { + return { ok: true, packageId: "packages:claw", releaseId: "releases:claw-1" }; + } + return null; + }); + const ctx = { + runQuery: vi + .fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + _id: "users:owner", + role: "user", + githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000, + }) + .mockResolvedValueOnce({ + _id: "users:owner", + role: "user", + githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000, + }) + .mockResolvedValueOnce({ + _id: "publishers:owner", + kind: "user", + handle: "owner", + linkedUserId: "users:owner", + }), + runMutation, + scheduler: { runAfter: vi.fn() }, + storage: { + get: vi.fn(async (storageId: string) => { + const content = storedFiles.get(storageId); + return content === undefined ? null : new Blob([content]); + }), + store: vi.fn(async () => "storage:legacy-zip"), + }, + }; + + try { + await expect( + publishPackageForUserInternalHandler(ctx as never, { + actorUserId: "users:owner", + payload: { + name: "demo-claw", + displayName: "Demo Claw", + family: "claw", + version: "1.0.0", + changelog: "init", + files: [ + { path: "package.json", size: 1, storageId: "storage:package", sha256: "package" }, + { + path: "manifests/CLAW.md", + size: 1, + storageId: "storage:claw", + sha256: "claw", + }, + { + path: "profiles/openclaw.yml", + size: 1, + storageId: "storage:profile", + sha256: "profile", + }, + ], + }, + }), + ).resolves.toEqual({ + ok: true, + packageId: "packages:claw", + releaseId: "releases:claw-1", + publicationStatus: "published", + }); + + expect(runMutation).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + family: "claw", + summary: expect.any(String), + artifactKind: "legacy-zip", + clawpackStorageId: "storage:legacy-zip", + clawpackSha256: expect.stringMatching(/^[a-f0-9]{64}$/), + clawpackSize: expect.any(Number), + clawManifestSummary: expect.objectContaining({ + agent: expect.objectContaining({ + id: "demo-claw", + description: "x".repeat(1_024), + }), + workspace: expect.objectContaining({ bootstrapFiles: ["SOUL.md"] }), + }), + pluginManifestSummary: undefined, + }), + ); + const publishMutationArgs = runMutation.mock.calls.find( + ([, args]) => args.family === "claw", + )?.[1]; + expect(publishMutationArgs?.summary).toBe("x".repeat(1_024)); + expect(publishMutationArgs).not.toHaveProperty("extractedClawManifest"); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + it("keeps raw package publishes behind the per-file size limit", async () => { const ctx = { runQuery: vi @@ -8958,6 +9454,7 @@ describe("packages public queries", () => { const content = files.get(storageId); return content ? new Blob([content]) : null; }), + store: vi.fn(async () => "storage:legacy-zip"), }, }; @@ -9971,6 +10468,7 @@ describe("packages public queries", () => { const content = storedFiles.get(storageId); return content ? new Blob([content]) : null; }), + store: vi.fn(async () => "storage:legacy-zip"), }, }; @@ -10090,6 +10588,7 @@ describe("packages public queries", () => { const content = storedFiles.get(storageId); return content ? new Blob([content]) : null; }), + store: vi.fn(async () => "storage:legacy-zip"), }, }; @@ -10228,6 +10727,7 @@ describe("packages public queries", () => { const content = storedFiles.get(storageId); return content ? new Blob([content]) : null; }), + store: vi.fn(async () => "storage:legacy-zip"), }, runAction: vi.fn(async () => ({ status: "pass", @@ -10380,6 +10880,7 @@ describe("packages public queries", () => { const content = files.get(storageId); return content ? new Blob([content]) : null; }), + store: vi.fn(async () => "storage:legacy-zip"), }, }; @@ -10504,6 +11005,7 @@ describe("packages public queries", () => { const content = files.get(storageId); return content ? new Blob([content]) : null; }), + store: vi.fn(async () => "storage:legacy-zip"), }, }; @@ -16361,4 +16863,224 @@ describe("restorePackageInternal", () => { ).rejects.toThrow("Forbidden"); expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything()); }); + + it("fails closed on Claw reads and never exposes the extracted manifest", async () => { + vi.mocked(getAuthUserId).mockResolvedValue(null); + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const clawManifestSummary = { + schemaVersion: 1, + agent: { id: "demo-claw" }, + workspace: { bootstrapFiles: ["SOUL.md"], fileCount: 0 }, + packages: { skillCount: 0, pluginCount: 0 }, + mcpServerCount: 0, + cronJobCount: 0, + }; + const { ctx } = makePackageCtx({ + pkg: makePackageDoc({ family: "claw" }), + latestRelease: makeReleaseDoc({ + clawManifestSummary, + extractedClawManifest: { + schemaVersion: 1, + agent: { id: "demo-claw" }, + workspace: { bootstrapFiles: { "SOUL.md": { source: "workspace/SOUL.md" } } }, + }, + }), + }); + + try { + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + await expect(getByNameHandler(ctx, { name: "demo-plugin" })).resolves.toBeNull(); + await expect( + getVersionByNameHandler(ctx, { name: "demo-plugin", version: "1.0.0" }), + ).resolves.toBeNull(); + + process.env.CLAWHUB_EXPERIMENTAL_CLAWS = "1"; + const detail = await getByNameHandler(ctx, { name: "demo-plugin" }); + expect(detail?.latestRelease).toMatchObject({ clawManifestSummary }); + expect(detail?.latestRelease).not.toHaveProperty("extractedClawManifest"); + + const version = await getVersionByNameHandler(ctx, { + name: "demo-plugin", + version: "1.0.0", + }); + expect(version?.version).toMatchObject({ clawManifestSummary }); + expect(version?.version).not.toHaveProperty("extractedClawManifest"); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("omits stored Claws from unfiltered public lists while disabled", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const ctx = makePluginExportCtx([ + makeDigest("demo-claw", { family: "claw", updatedAt: 2, _creationTime: 2 }), + makeDigest("demo-plugin", { updatedAt: 1, _creationTime: 1 }), + ]); + + try { + const result = await listPublicPageHandler(ctx, { + paginationOpts: { cursor: null, numItems: 25 }, + }); + expect(result.page.map((entry) => entry.name)).toEqual(["demo-plugin"]); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("does not let disabled Claws starve unfiltered public list pages", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const hiddenClaws = Array.from({ length: 50 }, (_, index) => + makeDigest(`demo-claw-${index}`, { + family: "claw", + updatedAt: 100 - index, + _creationTime: 100 - index, + }), + ); + const ctx = makePluginExportCtx([ + ...hiddenClaws, + makeDigest("visible-plugin", { updatedAt: 1, _creationTime: 1 }), + ]); + + try { + const result = await listPublicPageHandler(ctx, { + paginationOpts: { cursor: null, numItems: 1 }, + }); + expect(result.page.map((entry) => entry.name)).toEqual(["visible-plugin"]); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("paginates stable families beyond the per-query read window while Claws are disabled", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const digests = Array.from({ length: 250 }, (_, index) => + makeDigest(`stable-${index.toString().padStart(3, "0")}`, { + updatedAt: 250 - index, + _creationTime: 250 - index, + }), + ); + const ctx = makePluginExportCtx(digests); + + try { + const first = await listPublicPageHandler(ctx, { + paginationOpts: { cursor: null, numItems: 200 }, + }); + const second = await listPublicPageHandler(ctx, { + paginationOpts: { cursor: first.continueCursor, numItems: 200 }, + }); + expect(first.page).toHaveLength(200); + expect(first.isDone).toBe(false); + expect(second.page).toHaveLength(50); + expect(second.isDone).toBe(true); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("preserves each stable family's index order across tied public list pages", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const ctx = makePluginExportCtx([ + makeDigest("zulu-plugin", { + _id: "packageSearchDigest:zulu", + packageId: "packages:zulu", + updatedAt: 1, + _creationTime: 1, + }), + makeDigest("alpha-plugin", { + _id: "packageSearchDigest:alpha", + packageId: "packages:alpha", + updatedAt: 1, + _creationTime: 1, + }), + ]); + + try { + const first = await listPublicPageHandler(ctx, { + paginationOpts: { cursor: null, numItems: 1 }, + }); + const second = await listPublicPageHandler(ctx, { + paginationOpts: { cursor: first.continueCursor, numItems: 1 }, + }); + expect([...first.page, ...second.page].map((entry) => entry.name)).toEqual([ + "zulu-plugin", + "alpha-plugin", + ]); + expect(second.isDone).toBe(true); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("keeps the cursor's effective sort when recommendation backfill completes between pages", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + let recommendationScoresMissing = true; + const ctx = makePluginExportCtx( + [ + makeDigest("newer-plugin", { updatedAt: 2, _creationTime: 2 }), + makeDigest("older-plugin", { updatedAt: 1, _creationTime: 1 }), + ], + { recommendationScoresMissing: () => recommendationScoresMissing }, + ); + + try { + const first = await listPublicPageHandler(ctx, { + sort: "recommended", + paginationOpts: { cursor: null, numItems: 1 }, + }); + recommendationScoresMissing = false; + const second = await listPublicPageHandler(ctx, { + sort: "recommended", + paginationOpts: { cursor: first.continueCursor, numItems: 1 }, + }); + expect(first.page.map((entry) => entry.name)).toEqual(["newer-plugin"]); + expect(second.page.map((entry) => entry.name)).toEqual(["older-plugin"]); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); + + it("does not let disabled Claws starve unfiltered public search", async () => { + const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + const hiddenClaws = Array.from({ length: 50 }, (_, index) => + makeDigest(`matching-claw-${index}`, { + family: "claw", + displayName: `Matching Claw ${index}`, + updatedAt: 100 - index, + }), + ); + const { ctx } = makeDigestCtx({ + pages: [ + { + page: hiddenClaws, + isDone: false, + continueCursor: "after-claws", + }, + { + page: [makeDigest("matching-plugin", { displayName: "Matching Plugin" })], + isDone: true, + continueCursor: "", + }, + ], + }); + + try { + const result = await searchPublicHandler(ctx, { query: "matching", limit: 1 }); + expect(result.map((entry) => entry.package.name)).toEqual(["matching-plugin"]); + } finally { + if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS; + else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous; + } + }); }); diff --git a/convex/packages.ts b/convex/packages.ts index fb6c6096..f11ba313 100644 --- a/convex/packages.ts +++ b/convex/packages.ts @@ -1,5 +1,6 @@ import { ServerPackagePublishRequestSchema, + validateClawPackageContents, derivePluginCategoryTags, getCatalogTopicSlugs, getPackageScopeOwnerMismatch, @@ -60,6 +61,7 @@ import { getActivityTrendRangeForEndDay, } from "./lib/downloadTrend"; import { buildPackageInspectorFindingsEmail } from "./lib/emails"; +import { experimentalClawsEnabled, isClawFamilyPubliclyVisible } from "./lib/experimentalClaws"; import { requireGitHubAccountAge } from "./lib/githubAccount"; import { normalizeGitHubRepository } from "./lib/githubActionsOidc"; import { readGlobalPublicPluginsCount } from "./lib/globalStats"; @@ -135,6 +137,7 @@ const MAX_PLUGIN_EXPORT_LIST_LIMIT = 250; const MAX_SEARCH_PAGE_SIZE = 200; const MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES = 20; const MAX_DIRECT_PACKAGE_FULL_TEXT_CANDIDATES = 40; +const STABLE_PACKAGE_FAMILIES = ["skill", "code-plugin", "bundle-plugin"] as const; const MAX_PACKAGE_VERSION_DELETE_LOOKUP_CANDIDATES = 4; const MAX_POINTERLESS_RELEASE_SURVIVOR_SCAN = 100; // Release rows can approach 1 MiB, and trigger-wrapped patches materialize full documents. @@ -916,6 +919,7 @@ type PackageDigestLike = Pick< | "pluginCategoryTags" | "verificationTier" | "stats" + | "recommendedScore" | "scanStatus" | "softDeletedAt" > & { pluginCategory?: string }; @@ -940,6 +944,13 @@ type PublicPackageListPage = { isDone: boolean; continueCursor: string; }; +const STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX = "pkgstable:"; +type StablePackageFamily = (typeof STABLE_PACKAGE_FAMILIES)[number]; +type StablePackageDiscoverySourceState = { key: IndexKey | null; done: boolean }; +type StablePackageDiscoveryCursorState = { + sort: "updated" | "downloads" | "recommended" | "installs"; + sources: Record; +}; const OFFICIAL_FIRST_PACKAGE_CATEGORY_CURSOR_PREFIX = "pkgofficialfirst:"; async function runQueryRef( @@ -1210,8 +1221,13 @@ function toPublicPackage( } function toPublicPackageRelease(release: Doc<"packageReleases">) { - const { capabilities: _capabilities, ...publicRelease } = release as Doc<"packageReleases"> & { + const { + capabilities: _capabilities, + extractedClawManifest: _extractedClawManifest, + ...publicRelease + } = release as Doc<"packageReleases"> & { capabilities?: unknown; + extractedClawManifest?: unknown; }; const sourcePath = release.verification?.sourcePath ?? getReleaseSourcePath(release); if (!release.verification || !sourcePath) return publicRelease; @@ -1319,6 +1335,7 @@ function digestMatchesFilters( excludedScanStatuses?: PackageListScanStatus[]; }, ) { + if (!isClawFamilyPubliclyVisible(digest.family)) return false; if (digest.scanStatus && args.excludedScanStatuses?.includes(digest.scanStatus)) return false; if (args.category) { if (digest.pluginCategory) { @@ -1365,6 +1382,7 @@ function packageMatchesListFilters( excludedScanStatuses?: PackageListScanStatus[]; }, ) { + if (!isClawFamilyPubliclyVisible(pkg.family)) return false; if (pkg.scanStatus && args.excludedScanStatuses?.includes(pkg.scanStatus)) return false; if (args.family && pkg.family !== args.family) return false; if (args.channel && pkg.channel !== args.channel) return false; @@ -1875,6 +1893,7 @@ function maybeNormalizePackageQuery(value: string) { async function resolveDirectPackageSearchDigests( ctx: DbReaderCtx, queryText: string, + family?: PackageFamily, ): Promise { const normalizedQuery = maybeNormalizePackageQuery(queryText); const topicQuery = normalizeCatalogTopic(queryText); @@ -1894,49 +1913,85 @@ async function resolveDirectPackageSearchDigests( categoryDigests, ] = await Promise.all([ normalizedQuery - ? ctx.db - .query("packageSearchDigest") - .withIndex("by_active_normalized_name", (q) => - q - .eq("softDeletedAt", undefined) - .gte("normalizedName", normalizedQuery) - .lt("normalizedName", prefixUpperBound(normalizedQuery)), - ) - .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) + ? family + ? ctx.db + .query("packageSearchDigest") + .withIndex("by_active_family_normalized_name", (q) => + q + .eq("softDeletedAt", undefined) + .eq("family", family) + .gte("normalizedName", normalizedQuery) + .lt("normalizedName", prefixUpperBound(normalizedQuery)), + ) + .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) + : ctx.db + .query("packageSearchDigest") + .withIndex("by_active_normalized_name", (q) => + q + .eq("softDeletedAt", undefined) + .gte("normalizedName", normalizedQuery) + .lt("normalizedName", prefixUpperBound(normalizedQuery)), + ) + .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) : Promise.resolve([]), runtimePrefix - ? ctx.db - .query("packageSearchDigest") - .withIndex("by_active_runtime_id", (q) => - q - .eq("softDeletedAt", undefined) - .gte("runtimeId", runtimePrefix) - .lt("runtimeId", prefixUpperBound(runtimePrefix)), - ) - .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) + ? family + ? ctx.db + .query("packageSearchDigest") + .withIndex("by_active_family_runtime_id", (q) => + q + .eq("softDeletedAt", undefined) + .eq("family", family) + .gte("runtimeId", runtimePrefix) + .lt("runtimeId", prefixUpperBound(runtimePrefix)), + ) + .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) + : ctx.db + .query("packageSearchDigest") + .withIndex("by_active_runtime_id", (q) => + q + .eq("softDeletedAt", undefined) + .gte("runtimeId", runtimePrefix) + .lt("runtimeId", prefixUpperBound(runtimePrefix)), + ) + .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) : Promise.resolve([]), ctx.db .query("packageSearchDigest") - .withSearchIndex("search_by_display_name", (q) => - q.search("displayName", queryText).eq("softDeletedAt", undefined), - ) + .withSearchIndex("search_by_display_name", (q) => { + const search = q.search("displayName", queryText).eq("softDeletedAt", undefined); + return family ? search.eq("family", family) : search; + }) .take(MAX_DIRECT_PACKAGE_FULL_TEXT_CANDIDATES), ownerHandlePrefix - ? ctx.db - .query("packageSearchDigest") - .withIndex("by_active_owner_handle", (q) => - q - .eq("softDeletedAt", undefined) - .gte("ownerHandle", ownerHandlePrefix) - .lt("ownerHandle", prefixUpperBound(ownerHandlePrefix)), - ) - .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) + ? family + ? ctx.db + .query("packageSearchDigest") + .withIndex("by_active_family_owner_handle", (q) => + q + .eq("softDeletedAt", undefined) + .eq("family", family) + .gte("ownerHandle", ownerHandlePrefix) + .lt("ownerHandle", prefixUpperBound(ownerHandlePrefix)), + ) + .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) + : ctx.db + .query("packageSearchDigest") + .withIndex("by_active_owner_handle", (q) => + q + .eq("softDeletedAt", undefined) + .gte("ownerHandle", ownerHandlePrefix) + .lt("ownerHandle", prefixUpperBound(ownerHandlePrefix)), + ) + .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) : Promise.resolve([]), topicQuery ? ctx.db .query("packageTopicSearchDigest") - .withIndex("by_active_topic_updated", (q) => - q.eq("softDeletedAt", undefined).eq("topic", topicQuery), + .withIndex(family ? "by_active_family_topic_updated" : "by_active_topic_updated", (q) => + family + ? q.eq("softDeletedAt", undefined).eq("family", family).eq("topic", topicQuery) + : q.eq("softDeletedAt", undefined).eq("topic", topicQuery), ) .order("desc") .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) @@ -1944,8 +1999,15 @@ async function resolveDirectPackageSearchDigests( categoryQuery ? ctx.db .query("packagePluginCategorySearchDigest") - .withIndex("by_active_category_updated", (q) => - q.eq("softDeletedAt", undefined).eq("pluginCategory", categoryQuery), + .withIndex( + family ? "by_active_family_category_updated" : "by_active_category_updated", + (q) => + family + ? q + .eq("softDeletedAt", undefined) + .eq("family", family) + .eq("pluginCategory", categoryQuery) + : q.eq("softDeletedAt", undefined).eq("pluginCategory", categoryQuery), ) .order("desc") .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES) @@ -1955,11 +2017,17 @@ async function resolveDirectPackageSearchDigests( topicQuery && exactTopicDigests.length < MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES ? await ctx.db .query("packageTopicSearchDigest") - .withIndex("by_active_topic_updated", (q) => - q - .eq("softDeletedAt", undefined) - .gte("topic", topicQuery) - .lt("topic", prefixUpperBound(topicQuery)), + .withIndex(family ? "by_active_family_topic_updated" : "by_active_topic_updated", (q) => + family + ? q + .eq("softDeletedAt", undefined) + .eq("family", family) + .gte("topic", topicQuery) + .lt("topic", prefixUpperBound(topicQuery)) + : q + .eq("softDeletedAt", undefined) + .gte("topic", topicQuery) + .lt("topic", prefixUpperBound(topicQuery)), ) .order("desc") .take(MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES - exactTopicDigests.length) @@ -2634,6 +2702,7 @@ async function getReadablePackageByName( const normalizedName = normalizePackageName(name); const pkg = await getPackageByNormalizedName(ctx, normalizedName); if (!pkg || pkg.softDeletedAt) return null; + if (!isClawFamilyPubliclyVisible(pkg.family)) return null; if (pkg.channel === "private" || isPackageBlockedFromPublic(pkg.scanStatus)) { const canAccessOwner = await viewerCanAccessPackageOwner(ctx, pkg, viewerUserId); if (pkg.channel === "private" && !canAccessOwner) return null; @@ -2654,6 +2723,7 @@ async function getPackageReadableForPublicTrust( ) { const pkg = await getPackageByNormalizedName(ctx, normalizePackageName(name)); if (!pkg || pkg.softDeletedAt) return null; + if (!isClawFamilyPubliclyVisible(pkg.family)) return null; if (pkg.channel === "private" && !(await viewerCanAccessPackageOwner(ctx, pkg, viewerUserId))) { return null; } @@ -3256,7 +3326,7 @@ export const listAuditPage = query({ const page = []; const membershipCache = new Map>(); for (const pkg of result.page) { - if (pkg.family === "skill") continue; + if (pkg.family !== "code-plugin" && pkg.family !== "bundle-plugin") continue; if (!(await canViewerReadPackage(ctx, pkg, undefined, membershipCache))) continue; const owner = toPublicPublisher( @@ -3376,6 +3446,91 @@ function decodePackageIndexKeyValue(val: unknown): Value | undefined { return val as Value; } +function emptyStablePackageDiscoveryCursor( + sort: StablePackageDiscoveryCursorState["sort"], +): StablePackageDiscoveryCursorState { + return { + sort, + sources: { + skill: { key: null, done: false }, + "code-plugin": { key: null, done: false }, + "bundle-plugin": { key: null, done: false }, + }, + }; +} + +function encodeStablePackageDiscoveryCursor(state: StablePackageDiscoveryCursorState) { + const sources = Object.fromEntries( + STABLE_PACKAGE_FAMILIES.map((family) => [ + family, + { + key: state.sources[family].key?.map(encodePackageIndexKeyValue) ?? null, + done: state.sources[family].done, + }, + ]), + ); + return `${STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX}${JSON.stringify({ + v: 1, + sort: state.sort, + sources, + })}`; +} + +function readStablePackageDiscoveryCursorSort( + raw: string | null | undefined, +): "updated" | "recommended" | null { + if (!raw?.startsWith(STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX)) return null; + try { + const parsed = JSON.parse(raw.slice(STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX.length)) as { + v?: unknown; + sort?: unknown; + }; + return parsed.v === 1 && (parsed.sort === "updated" || parsed.sort === "recommended") + ? parsed.sort + : null; + } catch { + return null; + } +} + +function decodeStablePackageDiscoveryCursor( + raw: string | null | undefined, + sort: StablePackageDiscoveryCursorState["sort"], +): StablePackageDiscoveryCursorState { + const fallback = emptyStablePackageDiscoveryCursor(sort); + if (!raw?.startsWith(STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX)) return fallback; + try { + const parsed = JSON.parse(raw.slice(STABLE_PACKAGE_DISCOVERY_CURSOR_PREFIX.length)) as { + v?: unknown; + sort?: unknown; + sources?: unknown; + }; + if ( + parsed.v !== 1 || + parsed.sort !== sort || + !parsed.sources || + typeof parsed.sources !== "object" + ) { + return fallback; + } + const sources = parsed.sources as Record; + const decoded = emptyStablePackageDiscoveryCursor(sort); + for (const family of STABLE_PACKAGE_FAMILIES) { + const value = sources[family]; + if (!value || typeof value !== "object") return fallback; + const source = value as { key?: unknown; done?: unknown }; + if (source.key !== null && !Array.isArray(source.key)) return fallback; + decoded.sources[family] = { + key: Array.isArray(source.key) ? source.key.map(decodePackageIndexKeyValue) : null, + done: source.done === true, + }; + } + return decoded; + } catch { + return fallback; + } +} + function encodePackageIndexCursor(indexName: string, key: IndexKey): string { return JSON.stringify({ v: 1, @@ -3754,6 +3909,146 @@ export const countPublicPlugins = query({ }, }); +function compareStablePackageDiscoveryCandidates( + a: PackageDigestLike, + b: PackageDigestLike, + sort: "updated" | "downloads" | "recommended" | "installs", +) { + const metric = (candidate: PackageDigestLike) => { + if (sort === "downloads") return candidate.stats?.downloads ?? 0; + if (sort === "installs") return candidate.stats?.installs ?? 0; + if (sort === "recommended") return candidate.recommendedScore ?? 0; + return candidate.updatedAt; + }; + const metricDiff = metric(b) - metric(a); + if (metricDiff !== 0) return metricDiff; + const updatedDiff = b.updatedAt - a.updatedAt; + if (updatedDiff !== 0) return updatedDiff; + const familyDiff = a.family.localeCompare(b.family); + if (familyDiff !== 0) return familyDiff; + return a.name.localeCompare(b.name); +} + +async function listStablePackageDiscoveryPage( + ctx: DbReaderCtx, + args: { + channel?: PackageChannel; + isOfficial?: boolean; + category?: PluginCategorySlug; + topic?: string; + excludedScanStatuses?: PackageListScanStatus[]; + sort?: "updated" | "downloads" | "recommended" | "installs"; + viewerUserId?: Id<"users">; + paginationOpts: { cursor: string | null; numItems: number }; + }, +): Promise { + const targetCount = Math.max( + 1, + Math.min(args.paginationOpts.numItems, MAX_PUBLIC_LIST_PAGE_SIZE), + ); + const requestedSort = args.sort ?? "updated"; + const cursorSort = + requestedSort === "recommended" + ? readStablePackageDiscoveryCursorSort(args.paginationOpts.cursor) + : null; + let sort = cursorSort ?? requestedSort; + if (!cursorSort && requestedSort === "recommended") { + const recommendationScoresMissing = ( + await Promise.all( + STABLE_PACKAGE_FAMILIES.map( + async (family) => await hasMissingPackageRecommendedScore(ctx, family), + ), + ) + ).some(Boolean); + if (recommendationScoresMissing) sort = "updated"; + } + const cursor = decodeStablePackageDiscoveryCursor(args.paginationOpts.cursor, sort); + const scanLimit = Math.min(MAX_PUBLIC_LIST_PAGE_SIZE, Math.max(targetCount * 5, 50)); + const loadFamily = async (family: StablePackageFamily) => { + const state = cursor.sources[family]; + if (state.done) { + return { family, rows: [] as PackageDigestLike[], keys: [] as IndexKey[], hasMore: false }; + } + const eqPrefix: IndexKey = [undefined, family]; + if (sort === "updated") { + const result = await getPage(ctx, { + table: "packageSearchDigest", + index: "by_active_family_updated", + startIndexKey: state.key ?? eqPrefix, + startInclusive: state.key === null, + endIndexKey: eqPrefix, + endInclusive: true, + order: "desc", + absoluteMaxRows: scanLimit, + schema, + }); + return { + family, + rows: result.page as PackageDigestLike[], + keys: result.indexKeys, + hasMore: result.hasMore, + }; + } + const index = + sort === "downloads" + ? "by_active_family_downloads" + : sort === "installs" + ? "by_active_family_installs" + : "by_active_family_recommended_score"; + const result = await getPage(ctx, { + table: "packages", + index, + startIndexKey: state.key ?? eqPrefix, + startInclusive: state.key === null, + endIndexKey: eqPrefix, + endInclusive: true, + order: "desc", + absoluteMaxRows: scanLimit, + schema, + }); + return { + family, + rows: result.page.map(extractPackageDigestFields), + keys: result.indexKeys, + hasMore: result.hasMore, + }; + }; + const familyPages = await Promise.all( + STABLE_PACKAGE_FAMILIES.map(async (family) => await loadFamily(family)), + ); + const membershipCache = new Map>(); + const page: PublicPackageListItem[] = []; + const consumedByFamily = new Map(); + while (page.length < targetCount) { + // Merge only the current head from each source so every persisted source + // cursor advances through a contiguous prefix of that source's index order. + const candidate = familyPages + .flatMap((source) => { + const index = consumedByFamily.get(source.family) ?? 0; + const row = source.rows[index]; + return row ? [{ family: source.family, row, key: source.keys[index] }] : []; + }) + .sort((a, b) => compareStablePackageDiscoveryCandidates(a.row, b.row, sort))[0]; + if (!candidate) break; + const digest = candidate.row; + consumedByFamily.set(candidate.family, (consumedByFamily.get(candidate.family) ?? 0) + 1); + cursor.sources[candidate.family].key = candidate.key; + if (!digestMatchesSearchFilters(digest, args)) continue; + if (!(await canViewerReadPackage(ctx, digest, args.viewerUserId, membershipCache))) continue; + page.push(await toPublicPackageListItem(ctx, digest)); + } + for (const source of familyPages) { + const consumed = consumedByFamily.get(source.family) ?? 0; + cursor.sources[source.family].done = consumed === source.rows.length && !source.hasMore; + } + const hasMore = STABLE_PACKAGE_FAMILIES.some((family) => !cursor.sources[family].done); + return { + page, + isDone: !hasMore, + continueCursor: hasMore ? encodeStablePackageDiscoveryCursor(cursor) : "", + }; +} + async function listPackagePageImpl( ctx: DbReaderCtx, args: { @@ -3849,6 +4144,19 @@ async function listPackagePageImpl( return { page, isDone: true, continueCursor: "" }; } + if (!args.family && !experimentalClawsEnabled()) { + return await listStablePackageDiscoveryPage(ctx, { + channel: args.channel, + isOfficial: args.isOfficial, + category, + topic, + excludedScanStatuses: args.excludedScanStatuses, + sort: args.sort, + viewerUserId: args.viewerUserId, + paginationOpts: args.paginationOpts, + }); + } + const collected: PublicPackageListItem[] = []; const family = args.family; const channel = args.channel; @@ -4311,6 +4619,7 @@ async function searchPackagesImpl( topic, }); const entries = highlightedEntries + .filter(({ digest }) => isClawFamilyPubliclyVisible(digest.family)) .filter( ({ digest }) => !digest.scanStatus || !args.excludedScanStatuses?.includes(digest.scanStatus), @@ -4340,30 +4649,42 @@ async function searchPackagesImpl( return results; } - const buildSearchDigestQuery = () => + const searchFamilies = + !args.family && !experimentalClawsEnabled() + ? ([...STABLE_PACKAGE_FAMILIES] as PackageFamily[]) + : [args.family]; + const buildSearchDigestQuery = (family: PackageFamily | undefined) => topic ? buildPackageTopicDigestQuery(ctx, { topic, - family: args.family, + family, channel: args.channel, isOfficial: args.isOfficial, }) : category ? buildPackagePluginCategoryDigestQuery(ctx, { category, - family: args.family, + family, channel: args.channel, isOfficial: args.isOfficial, }) : buildPackageDigestQuery(ctx, { - family: args.family, + family, channel: args.channel, isOfficial: args.isOfficial, }); const matches: Array = []; const seen = new Set(); const directDigests = - category && !topic ? [] : await resolveDirectPackageSearchDigests(ctx, queryText); + category && !topic + ? [] + : ( + await Promise.all( + searchFamilies.map( + async (family) => await resolveDirectPackageSearchDigests(ctx, queryText, family), + ), + ) + ).flat(); for (const digest of directDigests) { if (!(await canViewPackage(digest))) continue; if (!digestMatchesSearchFilters(digest, { ...args, topic })) continue; @@ -4396,37 +4717,60 @@ async function searchPackagesImpl( ...match, package: await toPublicPackageListItem(ctx, digest), }); - if (authoritativeMatchCount() >= targetCount) break; } }; if (topic && category) { - let cursor: string | null = null; - let isDone = false; - let scanPages = 0; + const scanStates = searchFamilies.map((family) => ({ + family, + cursor: null as string | null, + isDone: false, + pagesScanned: 0, + })); let remainingScanBudget = MAX_PUBLIC_LIST_FILTER_SCAN_DOCUMENTS; while ( authoritativeMatchCount() < targetCount && - !isDone && - scanPages < MAX_PUBLIC_LIST_FILTER_SCAN_PAGES && + scanStates.some( + (state) => !state.isDone && state.pagesScanned < MAX_PUBLIC_LIST_FILTER_SCAN_PAGES, + ) && remainingScanBudget > 0 ) { - const pageSize = Math.min(scanLimit, remainingScanBudget); - const page: { - page: PackageDigestLike[]; - isDone: boolean; - continueCursor: string; - } = await buildSearchDigestQuery().order("desc").paginate({ cursor, numItems: pageSize }); - scanPages += 1; - remainingScanBudget -= pageSize; - await collectDigestMatches(page.page); - cursor = page.continueCursor; - isDone = page.isDone; + // Finish each round before checking the match quota so the fixed family + // order cannot decide global relevance or consume another family's cap. + for (const state of scanStates) { + if ( + state.isDone || + state.pagesScanned >= MAX_PUBLIC_LIST_FILTER_SCAN_PAGES || + remainingScanBudget <= 0 + ) { + continue; + } + const pageSize = Math.min(scanLimit, remainingScanBudget); + const page: { + page: PackageDigestLike[]; + isDone: boolean; + continueCursor: string; + } = await buildSearchDigestQuery(state.family) + .order("desc") + .paginate({ cursor: state.cursor, numItems: pageSize }); + state.pagesScanned += 1; + remainingScanBudget -= pageSize; + await collectDigestMatches(page.page); + state.cursor = page.continueCursor; + state.isDone = page.isDone; + } } } else { - const digests: PackageDigestLike[] = await buildSearchDigestQuery() - .order("desc") - .take(scanLimit); + const digests = ( + await Promise.all( + searchFamilies.map( + async (family) => + (await buildSearchDigestQuery(family) + .order("desc") + .take(scanLimit)) as PackageDigestLike[], + ), + ) + ).flat(); await collectDigestMatches(digests); } } @@ -7581,11 +7925,17 @@ async function publishPackageImpl( rawPayload, "Package publish payload", ); + if (payload.family === "claw" && !experimentalClawsEnabled()) { + throw new ConvexError("Experimental Claw publication is disabled"); + } if (payload.family === "skill") { throw new ConvexError("Skill packages must use the skills publish flow"); } const family = payload.family; const name = normalizePackageName(payload.name); + if (payload.family === "claw" && payload.name !== name) { + throw new ConvexError(`Claw package name must use canonical form ${name}`); + } const version = assertPackageVersion(family, payload.version); const existingPackage = await runQueryRef | null>( ctx, @@ -7713,7 +8063,8 @@ async function publishPackageImpl( if (totalBytes > MAX_PUBLISH_TOTAL_BYTES) { throw new ConvexError(getPublishTotalSizeError("package")); } - const legacyZipSha256 = await sha256Hex(buildDeterministicPackageZip(legacyZipEntries)); + const legacyZipBytes = buildDeterministicPackageZip(legacyZipEntries); + const legacyZipSha256 = await sha256Hex(legacyZipBytes); if (family === "code-plugin" && (!effectiveSource?.repo || !effectiveSource?.commit)) { throw new ConvexError("Code plugins require source repo and commit metadata"); @@ -7723,6 +8074,14 @@ async function publishPackageImpl( ctx, files, (path) => path === "package.json", + family === "claw" + ? { + exactPath: true, + maxBytes: 256 * 1024, + label: "Claw package.json", + strictUtf8: true, + } + : undefined, ); const pluginManifestEntry = await readOptionalTextFile( ctx, @@ -7746,6 +8105,23 @@ async function publishPackageImpl( ); const packageJson = maybeParseJson(packageJsonEntry?.text); + const declaredClawManifestPath = + family === "claw" && + packageJson && + typeof packageJson.openclaw === "object" && + packageJson.openclaw !== null && + !Array.isArray(packageJson.openclaw) && + typeof (packageJson.openclaw as Record).claw === "string" + ? ((packageJson.openclaw as Record).claw as string) + : undefined; + const clawManifestEntry = declaredClawManifestPath + ? await readOptionalTextFile(ctx, files, (path) => path === declaredClawManifestPath, { + exactPath: true, + maxBytes: 1024 * 1024, + label: "Claw manifest", + strictUtf8: true, + }) + : null; const pluginManifest = maybeParseJson(pluginManifestEntry?.text); const bundleManifest = maybeParseJson(bundleManifestEntry?.text); const storedPackageJson = toConvexSafeJsonValue(packageJson, { @@ -7757,11 +8133,55 @@ async function publishPackageImpl( const storedBundleManifest = toConvexSafeJsonValue(bundleManifest, { maxDepth: MAX_STORED_PACKAGE_METADATA_DEPTH, }); - if (packageJson) ensurePluginNameMatchesPackage(name, packageJson); - if (!pluginManifest) { + if (family !== "claw" && packageJson) ensurePluginNameMatchesPackage(name, packageJson); + if (family !== "claw" && !pluginManifest) { throw new ConvexError("openclaw.plugin.json is required for plugin packages"); } - const icon = normalizePluginManifestIcon(pluginManifest); + const clawValidationFiles = files.map((file) => ({ + path: file.path, + ...(clawManifestEntry?.file.path === file.path ? { text: clawManifestEntry.text } : {}), + })); + let clawPackage = + family === "claw" + ? validateClawPackageContents({ + packageName: name, + version, + packageJson, + files: clawValidationFiles, + }) + : null; + if (clawPackage && !clawPackage.ok) { + const profilePath = clawPackage.issues.find( + (entry) => entry.code === "missing_openclaw_profile", + )?.path; + if (profilePath) { + const profileEntry = await readOptionalTextFile(ctx, files, (path) => path === profilePath, { + exactPath: true, + maxBytes: 256 * 1024, + label: "OpenClaw profile", + strictUtf8: true, + }); + if (profileEntry) { + clawPackage = validateClawPackageContents({ + packageName: name, + version, + packageJson, + files: clawValidationFiles.map((file) => + file.path === profileEntry.file.path ? { ...file, text: profileEntry.text } : file, + ), + }); + } + } + } + if (clawPackage && !clawPackage.ok) { + throw new ConvexError( + `Invalid Claw package: ${clawPackage.issues + .map((entry) => `${entry.path}: ${entry.message}`) + .join(" ")}`, + ); + } + const validatedClaw = clawPackage?.ok ? clawPackage.value : undefined; + const icon = family === "claw" ? undefined : normalizePluginManifestIcon(pluginManifest); if (family === "code-plugin") { const validation = validateOpenClawExternalCodePluginPackageContents( packageJson, @@ -7785,7 +8205,11 @@ async function publishPackageImpl( ? extractBundlePluginArtifacts({ packageName: name, packageJson, - pluginManifest, + pluginManifest: + pluginManifest ?? + (() => { + throw new ConvexError("openclaw.plugin.json is required for plugin packages"); + })(), bundleManifest, bundleMetadata: payload.bundle || detectedBundleFormat @@ -7807,22 +8231,32 @@ async function publishPackageImpl( (() => { throw new ConvexError("package.json is required for code plugins"); })(), - pluginManifest, + pluginManifest: + pluginManifest ?? + (() => { + throw new ConvexError("openclaw.plugin.json is required for plugin packages"); + })(), source: effectiveSource, }) : null; - const summary = summarizePackageForSearch({ - packageName: name, - packageJson, - readmeText: readmeEntry?.text ?? null, - }); + const summary = + validatedClaw?.summary.agent.description || + validatedClaw?.summary.agent.name || + summarizePackageForSearch({ + packageName: name, + packageJson, + readmeText: readmeEntry?.text ?? null, + }); let categories: string[]; let normalizedTopics: string[]; try { const declaredCategories = payload.categories ?? normalizeStoredPluginCategoryOverride(existingPackage?.categories); - categories = resolvePluginCategories({ declared: declaredCategories }); + categories = + family === "claw" + ? (declaredCategories ?? []) + : resolvePluginCategories({ declared: declaredCategories }); normalizedTopics = normalizeCatalogTopics(payload.topics ?? existingPackage?.topics); } catch (error) { throw new ConvexError(error instanceof Error ? error.message : "Invalid catalog metadata"); @@ -7836,6 +8270,7 @@ async function publishPackageImpl( packageJson, pluginManifest, bundleManifest, + clawManifest: validatedClaw?.manifest, source: effectiveSource, }, files, @@ -7870,12 +8305,34 @@ async function publishPackageImpl( const integritySha256 = await hashSkillFiles( files.map((file) => ({ path: file.path, sha256: file.sha256 })), ); - const pluginManifestSummary = derivePluginManifestSummary({ - pluginManifest, - ...(bundleManifest ? { skillManifest: bundleManifest } : {}), - compatibility: codeArtifacts?.compatibility ?? bundleArtifacts?.compatibility, - files: await withSkillMarkdownTextsForManifestSummary(ctx, files), - }); + const pluginManifestSummary = + family === "claw" + ? undefined + : derivePluginManifestSummary({ + pluginManifest: + pluginManifest ?? + (() => { + throw new ConvexError("openclaw.plugin.json is required for plugin packages"); + })(), + ...(bundleManifest ? { skillManifest: bundleManifest } : {}), + compatibility: codeArtifacts?.compatibility ?? bundleArtifacts?.compatibility, + files: await withSkillMarkdownTextsForManifestSummary(ctx, files), + }); + + const legacyZipStorageId = + payload.artifact?.kind === "npm-pack" + ? undefined + : await ctx.storage.store( + new Blob( + [ + legacyZipBytes.buffer.slice( + legacyZipBytes.byteOffset, + legacyZipBytes.byteOffset + legacyZipBytes.byteLength, + ) as ArrayBuffer, + ], + { type: "application/zip" }, + ), + ); const packageInsertArgs = { actorUserId, @@ -7902,9 +8359,10 @@ async function publishPackageImpl( integritySha256, sha256hash: legacyZipSha256, artifactKind: payload.artifact?.kind ?? "legacy-zip", - clawpackStorageId: payload.artifact?.storageId as Id<"_storage"> | undefined, - clawpackSha256: payload.artifact?.sha256, - clawpackSize: payload.artifact?.size, + clawpackStorageId: + (payload.artifact?.storageId as Id<"_storage"> | undefined) ?? legacyZipStorageId, + clawpackSha256: payload.artifact?.sha256 ?? legacyZipSha256, + clawpackSize: payload.artifact?.size ?? legacyZipBytes.byteLength, clawpackFormat: payload.artifact?.format, npmIntegrity: payload.artifact?.npmIntegrity, npmShasum: payload.artifact?.npmShasum, @@ -7918,6 +8376,7 @@ async function publishPackageImpl( extractedPluginManifest: storedPluginManifest, normalizedBundleManifest: family === "bundle-plugin" ? storedBundleManifest : undefined, pluginManifestSummary, + clawManifestSummary: validatedClaw?.summary, source: effectiveSource, }; @@ -9807,7 +10266,12 @@ export const insertReleaseInternal = internalMutation({ ), name: v.string(), displayName: v.string(), - family: v.union(v.literal("skill"), v.literal("code-plugin"), v.literal("bundle-plugin")), + family: v.union( + v.literal("skill"), + v.literal("code-plugin"), + v.literal("bundle-plugin"), + v.literal("claw"), + ), version: v.string(), publicationStatus: v.optional(v.union(v.literal("pending"), v.literal("published"))), changelog: v.string(), @@ -9851,9 +10315,13 @@ export const insertReleaseInternal = internalMutation({ extractedPluginManifest: v.optional(v.any()), normalizedBundleManifest: v.optional(v.any()), pluginManifestSummary: v.optional(v.any()), + clawManifestSummary: v.optional(v.any()), source: v.optional(v.any()), }, handler: async (ctx, args) => { + if (args.family === "claw" && !experimentalClawsEnabled()) { + throw new ConvexError("Experimental Claw publication is disabled"); + } const now = Date.now(); const publicationStatus = args.publicationStatus ?? "published"; const pendingPublication = publicationStatus === "pending"; @@ -10086,6 +10554,7 @@ export const insertReleaseInternal = internalMutation({ extractedPluginManifest: args.extractedPluginManifest, normalizedBundleManifest: args.normalizedBundleManifest, pluginManifestSummary: args.pluginManifestSummary, + clawManifestSummary: args.clawManifestSummary, compatibility: args.compatibility, runtimeId: args.runtimeId, sourceRepo: args.sourceRepo, diff --git a/convex/schema.ts b/convex/schema.ts index 6184579c..f1e52e11 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -2167,12 +2167,20 @@ const packageSearchDigest = defineTable({ "updatedAt", ]) .index("by_active_normalized_name", ["softDeletedAt", "normalizedName", "updatedAt"]) + .index("by_active_family_normalized_name", [ + "softDeletedAt", + "family", + "normalizedName", + "updatedAt", + ]) .index("by_active_runtime_id", ["softDeletedAt", "runtimeId", "updatedAt"]) + .index("by_active_family_runtime_id", ["softDeletedAt", "family", "runtimeId", "updatedAt"]) .index("by_active_owner_handle", ["softDeletedAt", "ownerHandle", "updatedAt"]) + .index("by_active_family_owner_handle", ["softDeletedAt", "family", "ownerHandle", "updatedAt"]) .index("by_active_name", ["softDeletedAt", "displayName"]) .searchIndex("search_by_display_name", { searchField: "displayName", - filterFields: ["softDeletedAt"], + filterFields: ["softDeletedAt", "family"], }); const packageTopicSearchDigest = defineTable({ diff --git a/docs/README.md b/docs/README.md index 57d5d4a6..b7f79628 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,10 +28,11 @@ Reading order: 4. `docs/publishing.md`: owner-scoped skill/plugin publishing flow. 5. `docs/cli.md`: ClawHub CLI reference. 6. `docs/skill-format.md`: skill bundle metadata and package shape. -7. `docs/auth.md`: GitHub OAuth, API tokens, and CLI login. -8. `docs/telemetry.md`: install telemetry and how to opt out. -9. `docs/namespace-claims.md`: org, brand, owner-handle, package-scope, skill-slug, and namespace ownership disputes. -10. `docs/troubleshooting.md`: user-facing CLI, install, publish, update, and API fixes. +7. `docs/claws.md`: experimental Claw package authoring and publication. +8. `docs/auth.md`: GitHub OAuth, API tokens, and CLI login. +9. `docs/telemetry.md`: install telemetry and how to opt out. +10. `docs/namespace-claims.md`: org, brand, owner-handle, package-scope, skill-slug, and namespace ownership disputes. +11. `docs/troubleshooting.md`: user-facing CLI, install, publish, update, and API fixes. Policy, API, and trust docs: diff --git a/docs/claws.md b/docs/claws.md new file mode 100644 index 00000000..c146da0f --- /dev/null +++ b/docs/claws.md @@ -0,0 +1,99 @@ +--- +summary: "How to author, validate, and publish experimental Claw packages." +read_when: + - Authoring a Claw package + - Publishing a Claw to an experimental ClawHub deployment +--- + +# Experimental Claw packages + +A Claw is a versioned package that describes one complete OpenClaw agent and +the reusable resources it needs. ClawHub stores and scans the package; +OpenClaw owns local preview, consent, apply, update, and removal. + +Claw publication is experimental. The ClawHub deployment must set +`CLAWHUB_EXPERIMENTAL_CLAWS=1`; otherwise the server rejects publication. + +## Package shape + +A publishable Claw is a normal package directory with a `package.json` that +points to its manifest: + +```json +{ + "name": "@acme/github-triage", + "version": "1.0.0", + "openclaw": { + "claw": "CLAW.md" + } +} +``` + +`CLAW.md` starts with the grouped Claw manifest as YAML frontmatter. Its +non-empty Markdown body becomes the managed `SOUL.md` for the new agent, so do +not also declare an explicit `SOUL.md` bootstrap or supporting-file destination. + +```markdown +--- +schemaVersion: 1 +agent: + id: github-triage + name: GitHub Triage + description: Reviews incoming issues. +workspace: + files: + - source: workspace/reference.md + path: reference.md +packages: + - kind: skill + source: clawhub + ref: "@acme/triage" + version: 1.2.0 +mcpServers: {} +cronJobs: [] +--- + +# GitHub Triage + +Reviews and classifies incoming GitHub issues. +``` + +JSON manifests remain compatible. Set `openclaw.claw` to a package-relative +JSON file such as `openclaw.claw.json`. JSON has no implicit body file and may +declare an explicit `SOUL.md` source. + +Every `workspace.*.source` must name a file in the same package. Package names +and versions must match `package.json`, dependency versions must be exact, and +MCP environment values must remain unresolved `${ENV_VAR}` references. + +## Validate and publish + +Preview the package without uploading it: + +```bash +clawhub package publish . --family claw --dry-run +``` + +When the target ClawHub deployment has experimental Claws enabled, publish +through the existing authenticated package flow: + +```bash +clawhub package publish . --family claw +``` + +The CLI detects `family: claw` when `package.json` contains `openclaw.claw`, so +`--family claw` is optional for a well-formed package. + +Publication rejects: + +- a missing, invalid, or escaping `openclaw.claw` path; +- package identity or version mismatches; +- malformed `CLAW.md` frontmatter or manifest fields; +- an empty `CLAW.md` body or simultaneous explicit `SOUL.md` destination; +- missing workspace source files or portable path collisions; +- floating skill/plugin versions and resolved MCP credentials. + +Accepted packages continue through ClawHub's existing ownership, moderation, +static scanning, release, and artifact storage pipeline. The stored release +retains the exact artifact plus a non-sensitive summary for later search and +detail surfaces; it does not duplicate the full manifest into Convex storage. diff --git a/package.json b/package.json index b2c17dc0..72a353c6 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ }, "overrides": { "ast-v8-to-istanbul": "1.0.4", - "brace-expansion": "5.0.7", + "brace-expansion": "5.0.8", "dompurify": "3.4.12", "esbuild": "0.28.1", "fast-uri": "3.1.4", diff --git a/packages/clawhub/package.json b/packages/clawhub/package.json index 31c6b4f9..b2f3ce5b 100644 --- a/packages/clawhub/package.json +++ b/packages/clawhub/package.json @@ -41,6 +41,7 @@ "@openclaw/plugin-inspector": "0.3.17", "arktype": "2.2.3", "commander": "15.0.0", + "croner": "10.0.1", "fflate": "0.8.3", "ignore": "7.0.6", "json5": "2.2.3", @@ -48,7 +49,9 @@ "ora": "9.4.1", "p-retry": "8.0.0", "semver": "7.8.5", - "undici": "7.28.0" + "undici": "7.28.0", + "unicode-case-folding": "1.1.1", + "yaml": "2.9.0" }, "devDependencies": { "@types/node": "26.1.1", diff --git a/packages/clawhub/src/clawpack.ts b/packages/clawhub/src/clawpack.ts index 7a78a33a..4038bd03 100644 --- a/packages/clawhub/src/clawpack.ts +++ b/packages/clawhub/src/clawpack.ts @@ -1,4 +1,5 @@ -import { gunzipSync } from "fflate"; +import { Gunzip } from "fflate"; +import { findClawPackagePathHierarchyCollision } from "./schema/clawPackage.js"; type ClawPackEntry = { path: string; @@ -10,23 +11,29 @@ type ParsedClawPack = { packageVersion: string; entries: ClawPackEntry[]; packageJson: Record; - pluginManifest: Record; + pluginManifest?: Record; }; const TAR_BLOCK_SIZE = 512; +const MAX_UNPACKED_BYTES = 50 * 1024 * 1024; +const MAX_TAR_BYTES = 64 * 1024 * 1024; +const MAX_FILE_BYTES = MAX_UNPACKED_BYTES; +const MAX_PACKAGE_JSON_BYTES = 256 * 1024; +const MAX_FILE_COUNT = 10_000; +const GZIP_INPUT_CHUNK_BYTES = 1_024; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function textFromBytes(bytes: Uint8Array) { - return new TextDecoder().decode(bytes); + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); } function readTarString(block: Uint8Array, offset: number, length: number) { const slice = block.subarray(offset, offset + length); const end = slice.indexOf(0); - return textFromBytes(end === -1 ? slice : slice.subarray(0, end)).trim(); + return textFromBytes(end === -1 ? slice : slice.subarray(0, end)); } function readTarSize(block: Uint8Array) { @@ -38,15 +45,49 @@ function readTarSize(block: Uint8Array) { } function normalizeTarPath(path: string) { - const normalized = path.replaceAll("\\", "/").replace(/^\.\/+/, ""); - if (!normalized || normalized.startsWith("/") || normalized.includes("\0")) return null; - const segments = normalized.split("/").filter(Boolean); - if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) { + if ( + !path || + path !== path.trim() || + path.includes("\\") || + path.startsWith("/") || + /\p{Cc}/u.test(path) + ) { + return null; + } + const segments = path.split("/"); + if ( + segments.length === 0 || + segments.some( + (segment, index) => + segment === "." || segment === ".." || (segment === "" && index !== segments.length - 1), + ) + ) { return null; } return segments.join("/"); } +function gunzipBounded(bytes: Uint8Array): Uint8Array { + const chunks: Uint8Array[] = []; + let total = 0; + const gunzip = new Gunzip((chunk) => { + total += chunk.byteLength; + if (total > MAX_TAR_BYTES) throw new Error("ClawPack unpacked archive exceeds 64MB limit"); + chunks.push(Uint8Array.from(chunk)); + }); + for (let offset = 0; offset < bytes.byteLength; offset += GZIP_INPUT_CHUNK_BYTES) { + const end = Math.min(offset + GZIP_INPUT_CHUNK_BYTES, bytes.byteLength); + gunzip.push(bytes.subarray(offset, end), end === bytes.byteLength); + } + const result = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.byteLength; + } + return result; +} + function isZeroBlock(block: Uint8Array) { return block.every((byte) => byte === 0); } @@ -57,6 +98,7 @@ function nextTarOffset(offset: number, size: number) { function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { const entries: ClawPackEntry[] = []; + const paths = new Set(); let offset = 0; while (offset + TAR_BLOCK_SIZE <= bytes.byteLength) { @@ -69,6 +111,7 @@ function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { if (!path) throw new Error("ClawPack contains an unsafe tar path"); const size = readTarSize(header); + if (size > MAX_FILE_BYTES) throw new Error("ClawPack file exceeds 50MB limit"); const payloadOffset = offset + TAR_BLOCK_SIZE; const payloadEnd = payloadOffset + size; if (payloadEnd > bytes.byteLength) throw new Error("ClawPack tar entry is truncated"); @@ -79,12 +122,17 @@ function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { throw new Error("ClawPack entries must be rooted under package/"); } const relPath = path.slice("package/".length); - if (relPath) { - entries.push({ - path: relPath, - bytes: Uint8Array.from(bytes.subarray(payloadOffset, payloadEnd)), - }); + if (!relPath || relPath.endsWith("/")) + throw new Error("ClawPack contains an unsafe tar path"); + if (paths.has(relPath)) { + throw new Error(`ClawPack contains duplicate path: ${relPath}`); } + paths.add(relPath); + if (entries.length >= MAX_FILE_COUNT) throw new Error("ClawPack exceeds 10000 file limit"); + entries.push({ + path: relPath, + bytes: Uint8Array.from(bytes.subarray(payloadOffset, payloadEnd)), + }); } else if (typeflag !== "5") { throw new Error("ClawPack may only contain regular files and directories"); } @@ -93,24 +141,35 @@ function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { } if (entries.length === 0) throw new Error("ClawPack contains no files"); + const hierarchyCollision = findClawPackagePathHierarchyCollision( + entries.map((entry) => entry.path), + ); + if (hierarchyCollision) { + throw new Error( + `ClawPack contains file/ancestor path collision: ${hierarchyCollision.ancestor} and ${hierarchyCollision.descendant}`, + ); + } + const unpackedSize = entries.reduce((sum, entry) => sum + entry.bytes.byteLength, 0); + if (unpackedSize > MAX_UNPACKED_BYTES) throw new Error("ClawPack package exceeds 50MB limit"); return entries; } export function parseClawPack(bytes: Uint8Array): ParsedClawPack { let tarBytes: Uint8Array; try { - tarBytes = gunzipSync(bytes); - } catch { - throw new Error("ClawPack must be a gzip-compressed npm pack tarball"); + tarBytes = gunzipBounded(bytes); + } catch (error) { + if (error instanceof Error && error.message.startsWith("ClawPack ")) throw error; + throw new Error("ClawPack must be a gzip-compressed npm pack tarball", { cause: error }); } const entries = parseTarEntries(tarBytes); const packageJsonEntry = entries.find((entry) => entry.path === "package.json"); if (!packageJsonEntry) throw new Error("ClawPack must contain package/package.json"); - const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json"); - if (!pluginManifestEntry) { - throw new Error("ClawPack must contain package/openclaw.plugin.json"); + if (packageJsonEntry.bytes.byteLength > MAX_PACKAGE_JSON_BYTES) { + throw new Error("ClawPack package.json exceeds 256KB limit"); } + const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json"); let packageJson: unknown; try { @@ -125,14 +184,15 @@ export function parseClawPack(bytes: Uint8Array): ParsedClawPack { if (!packageName) throw new Error("ClawPack package.json must declare a name"); if (!packageVersion) throw new Error("ClawPack package.json must declare a version"); - let pluginManifest: unknown; - try { - pluginManifest = JSON.parse(textFromBytes(pluginManifestEntry.bytes)); - } catch { - throw new Error("ClawPack openclaw.plugin.json is invalid JSON"); - } - if (!isRecord(pluginManifest)) { - throw new Error("ClawPack openclaw.plugin.json must be an object"); + let pluginManifest: Record | undefined; + if (pluginManifestEntry) { + try { + const parsed = JSON.parse(textFromBytes(pluginManifestEntry.bytes)) as unknown; + if (!isRecord(parsed)) throw new Error(); + pluginManifest = parsed; + } catch { + throw new Error("ClawPack openclaw.plugin.json is invalid JSON object"); + } } return { @@ -140,6 +200,6 @@ export function parseClawPack(bytes: Uint8Array): ParsedClawPack { packageVersion, entries, packageJson, - pluginManifest, + ...(pluginManifest ? { pluginManifest } : {}), }; } diff --git a/packages/clawhub/src/cli.ts b/packages/clawhub/src/cli.ts index acdee824..bdbf232f 100644 --- a/packages/clawhub/src/cli.ts +++ b/packages/clawhub/src/cli.ts @@ -542,7 +542,7 @@ const packageCmd = registerCommandGroup(program, ["package"]).description( registerCommand(packageCmd, ["package", "explore"]) .description("Browse published packages and plugins") .argument("[query...]", "Optional search query") - .option("--family ", "skill|code-plugin|bundle-plugin") + .option("--family ", "skill|code-plugin|bundle-plugin|claw") .option("--official", "Only official packages") .option("--executes-code", "Only packages that execute code") .option("--target ", "Filter by host target, e.g. darwin-arm64") @@ -721,7 +721,7 @@ registerCommand(packageCmd, ["package", "pack"]) registerCommand(packageCmd, ["package", "publish"]) .description("Publish a code plugin or bundle plugin from a folder or GitHub source") .argument("", "Package folder path, GitHub repo (owner/repo[@ref]), or URL") - .option("--family ", "code-plugin|bundle-plugin") + .option("--family ", "code-plugin|bundle-plugin|claw") .option("--name ", "Package name") .option("--display-name ", "Display name") .option("--owner ", "Publish under this owner/publisher handle") diff --git a/packages/clawhub/src/cli/commands/packages.test.ts b/packages/clawhub/src/cli/commands/packages.test.ts index 5d964416..bc7129a7 100644 --- a/packages/clawhub/src/cli/commands/packages.test.ts +++ b/packages/clawhub/src/cli/commands/packages.test.ts @@ -227,6 +227,16 @@ afterEach(() => { }); describe("package commands", () => { + it("rejects npm pack file/ancestor collisions before extraction", () => { + const bytes = npmPackFixture({ + "package/package.json": JSON.stringify({ name: "demo", version: "1.0.0" }), + "package/a": "file", + "package/a/b": "child", + }); + + expect(() => parseClawPack(bytes)).toThrow("file/ancestor path collision"); + }); + it("validates a local plugin package with bundled Plugin Inspector offline", async () => { const workdir = await makeTmpWorkdir(); try { @@ -1946,7 +1956,7 @@ describe("package commands", () => { } }); - it("rejects a ClawPack tarball without openclaw.plugin.json", async () => { + it("rejects a code plugin ClawPack tarball without openclaw.plugin.json", async () => { const workdir = await makeTmpWorkdir(); try { const packName = "demo-plugin-1.0.0.tgz"; @@ -1964,16 +1974,54 @@ describe("package commands", () => { await expect( cmdPublishPackage(makeOpts(workdir), packName, { + family: "code-plugin", sourceRepo: "openclaw/demo-plugin", sourceCommit: "abc123", }), - ).rejects.toThrow("ClawPack must contain package/openclaw.plugin.json"); + ).rejects.toThrow("openclaw.plugin.json required"); expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); } finally { await rm(workdir, { recursive: true, force: true }); } }); + it("publishes a Claw tarball without a plugin manifest", async () => { + const workdir = await makeTmpWorkdir(); + try { + const packName = "demo-claw-1.0.0.tgz"; + await writeFile( + join(workdir, packName), + npmPackFixture({ + "package/package.json": JSON.stringify({ + name: "demo-claw", + version: "1.0.0", + openclaw: { claw: "CLAW.md" }, + }), + "package/CLAW.md": "---\nschemaVersion: 1\nagent:\n id: demo-claw\n---\n# Demo Claw\n", + }), + ); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_claw", + releaseId: "rel_claw", + }); + + await cmdPublishPackage(makeOpts(workdir), packName); + + expect(getPublishPayload()).toMatchObject({ + name: "demo-claw", + family: "claw", + version: "1.0.0", + }); + const uploaded = getPublishForm().get("clawpack"); + expect(uploaded).toBeInstanceOf(File); + const parsed = parseClawPack(new Uint8Array(await (uploaded as File).arrayBuffer())); + expect(parsed.entries.map((entry) => entry.path)).toContain("CLAW.md"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + it("mints a short-lived publish token from GitHub Actions OIDC in CI", async () => { const workdir = await makeTmpWorkdir(); try { @@ -2527,6 +2575,134 @@ describe("package commands", () => { } }); + it("detects, validates, and publishes a Claw package", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "github-triage"); + await mkdir(join(folder, "profiles"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ + name: "@acme/github-triage", + displayName: "GitHub Triage", + version: "1.0.0", + openclaw: { claw: "CLAW.md" }, + }), + "utf8", + ); + await writeFile( + join(folder, "CLAW.md"), + [ + "---", + "schemaVersion: 1", + "agent:", + " id: github-triage", + " name: GitHub Triage", + "metadata:", + " openclaw.config: profiles/openclaw.yml", + "---", + "# GitHub Triage", + ].join("\n"), + "utf8", + ); + await writeFile( + join(folder, "profiles", "openclaw.yml"), + [ + "schemaVersion: 1", + "agent:", + " tools:", + " profile: coding", + " alsoAllow: [cron]", + " fs:", + " workspaceOnly: true", + " memory:", + " search:", + " enabled: true", + " rememberAcrossConversations: true", + " sources: [memory, sessions]", + ].join("\n"), + "utf8", + ); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_claw", + releaseId: "rel_claw", + }); + + await cmdPublishPackage(makeOpts(workdir), "github-triage"); + + expect(getPublishPayload()).toMatchObject({ + name: "@acme/github-triage", + family: "claw", + version: "1.0.0", + }); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects Claw manifests with malformed UTF-8 before upload", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "invalid-utf8-claw"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ + name: "invalid-utf8-claw", + displayName: "Invalid UTF-8 Claw", + version: "1.0.0", + openclaw: { claw: "CLAW.md" }, + }), + "utf8", + ); + await writeFile( + join(folder, "CLAW.md"), + Uint8Array.from([ + ...new TextEncoder().encode( + "---\nschemaVersion: 1\nagent:\n id: invalid-utf8-claw\n---\n# Invalid UTF-8 Claw\n", + ), + 0xff, + ]), + ); + await expect(cmdPublishPackage(makeOpts(workdir), "invalid-utf8-claw")).rejects.toThrow( + "CLAW.md: The declared Claw manifest is missing or is not UTF-8 text.", + ); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects Claws with missing workspace sources before upload", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "broken-claw"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ + name: "broken-claw", + version: "1.0.0", + openclaw: { claw: "CLAW.md" }, + }), + "utf8", + ); + await writeFile( + join(folder, "CLAW.md"), + "---\nschemaVersion: 1\nagent:\n id: broken-claw\nworkspace:\n files:\n - source: missing.md\n path: missing.md\n---\n# Broken Claw\n", + "utf8", + ); + + await expect(cmdPublishPackage(makeOpts(workdir), "broken-claw")).rejects.toThrow( + "missing.md: Declared workspace source is missing", + ); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + it("respects package ignore rules and built-in ignored directories", async () => { const workdir = await makeTmpWorkdir(); try { diff --git a/packages/clawhub/src/cli/commands/packages.ts b/packages/clawhub/src/cli/commands/packages.ts index 3faba946..0e501f46 100644 --- a/packages/clawhub/src/cli/commands/packages.ts +++ b/packages/clawhub/src/cli/commands/packages.ts @@ -46,6 +46,7 @@ import { type PackageFamily, type PackageTrustedPublisher, type PackageVerificationSummary, + decodeUtf8Text, validateOpenClawExternalCodePluginPackageContents, validateOpenClawExternalCodePluginPackageJson, } from "../../schema/index.js"; @@ -143,8 +144,10 @@ type PackageExploreOptions = { json?: boolean; }; +type PublishablePackageFamily = "code-plugin" | "bundle-plugin" | "claw"; + type PackagePublishOptions = { - family?: "code-plugin" | "bundle-plugin"; + family?: PublishablePackageFamily; name?: string; displayName?: string; owner?: string; @@ -261,7 +264,7 @@ type PackagePublishPayload = { name: string; displayName: string; ownerHandle?: string; - family: "code-plugin" | "bundle-plugin"; + family: PublishablePackageFamily; version: string; changelog: string; manualOverrideReason?: string; @@ -288,7 +291,7 @@ type PackagePublishPlan = { source: string; name: string; displayName: string; - family: "code-plugin" | "bundle-plugin"; + family: PublishablePackageFamily; version: string; commit?: string; files: number; @@ -2184,9 +2187,23 @@ function hasLooseBundleMarker(fileSet: Set) { function detectPackageFamily( fileSet: Set, - explicit?: "code-plugin" | "bundle-plugin", -): "code-plugin" | "bundle-plugin" { + packageJson: unknown, + explicit?: PublishablePackageFamily, +): PublishablePackageFamily { if (explicit) return explicit; + const packageRecord = + packageJson && typeof packageJson === "object" && !Array.isArray(packageJson) + ? (packageJson as Record) + : undefined; + const openclaw = + packageRecord?.openclaw && + typeof packageRecord.openclaw === "object" && + !Array.isArray(packageRecord.openclaw) + ? (packageRecord.openclaw as Record) + : undefined; + if (typeof openclaw?.claw === "string") { + return "claw"; + } if (hasRealBundleManifest(fileSet)) return "bundle-plugin"; if (fileSet.has("openclaw.plugin.json")) return "code-plugin"; if (hasLooseBundleMarker(fileSet)) return "bundle-plugin"; @@ -2318,7 +2335,7 @@ async function preparePackagePublishPlan( (parsedClawpack ? null : await readJsonFile(join(folder, "openclaw.plugin.json"))); const bundleManifestInfo = await readBundleManifestInfo(filesOnDisk, folder, parsedClawpack); const bundleManifest = bundleManifestInfo.manifest; - const family = detectPackageFamily(fileSet, options.family); + const family = detectPackageFamily(fileSet, packageJson, options.family); const name = options.name?.trim() || parsedClawpack?.packageName || @@ -2345,9 +2362,11 @@ async function preparePackagePublishPlan( if (!name) fail("--name required"); if (!displayName) fail("--display-name required"); if (!version) fail("--version required"); - if (!fileSet.has("openclaw.plugin.json")) fail("openclaw.plugin.json required"); - if (family === "code-plugin" && !semver.valid(version)) { - fail("--version must be valid semver for code plugins"); + if (family !== "claw" && !fileSet.has("openclaw.plugin.json")) { + fail("openclaw.plugin.json required"); + } + if ((family === "code-plugin" || family === "claw") && !semver.valid(version)) { + fail(`--version must be valid semver for ${family === "claw" ? "Claws" : "code plugins"}`); } if (family === "code-plugin") { if (!fileSet.has("package.json")) fail("package.json required"); @@ -2358,6 +2377,22 @@ async function preparePackagePublishPlan( } } + if (family === "claw") { + const { validateClawPackageContents } = await import("../../schema/clawPackage.js"); + const validation = validateClawPackageContents({ + packageName: name, + version, + packageJson, + files: filesOnDisk.map((file) => ({ + path: file.relPath, + text: decodeUtf8Text(file.bytes) ?? undefined, + })), + }); + if (!validation.ok) { + fail(validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join(" ")); + } + } + if (family === "code-plugin" && !clawpackOnDisk) { const packDestination = await mkdtemp(join(tmpdir(), "clawhub-clawpack-")); let packed: PackedClawPack; diff --git a/packages/clawhub/src/schema/clawPackage.ts b/packages/clawhub/src/schema/clawPackage.ts new file mode 100644 index 00000000..fafe5ded --- /dev/null +++ b/packages/clawhub/src/schema/clawPackage.ts @@ -0,0 +1,622 @@ +import { ArkErrors, type } from "arktype"; +import { caseFold } from "unicode-case-folding"; +import { isScalar, parseDocument, visit } from "yaml"; +import { + summarizeClawManifest, + validateClawManifest, + type ClawManifest, + type ClawManifestSummary, +} from "./claws.js"; + +export type ClawPackageTextFile = { path: string; text?: string }; +export type ClawPackageValidationIssue = { + code: string; + path: string; + message: string; +}; +export type ValidatedClawPackage = { + manifestPath: string; + manifest: ClawManifest; + implicitWorkspaceFile?: { path: "SOUL.md"; text: string }; + summary: ClawManifestSummary; +}; + +const EXACT_VERSION_PATTERN = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; +const WINDOWS_INVALID_PATH_CHARS = /[<>:"|?*]/; +const WINDOWS_RESERVED_PATH_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i; +const UNICODE_CONTROL_CHARACTER = /\p{Cc}/u; +const MAX_CLAW_MANIFEST_BYTES = 1024 * 1024; +const MAX_OPENCLAW_PROFILE_BYTES = 256 * 1024; +const OPENCLAW_TOOL_PROFILE_IDS = new Set(["minimal", "coding", "messaging", "full"]); +const StrictStringArraySchema = type("string[]"); +const OpenClawProfileSchema = type({ + "+": "reject", + schemaVersion: "1", + agent: { + "+": "reject", + groupChat: type({ + "+": "reject", + mentionPatterns: StrictStringArraySchema.optional(), + }).optional(), + sandbox: type({ + "+": "reject", + mode: '"off"|"non-main"|"all"?', + scope: '"session"|"agent"|"shared"?', + workspaceAccess: '"none"|"ro"|"rw"?', + }).optional(), + tools: type({ + "+": "reject", + profile: "string?", + allow: StrictStringArraySchema.optional(), + alsoAllow: StrictStringArraySchema.optional(), + deny: StrictStringArraySchema.optional(), + fs: type({ + "+": "reject", + workspaceOnly: "true?", + }).optional(), + }).optional(), + memory: type({ + "+": "reject", + search: type({ + "+": "reject", + enabled: "boolean?", + rememberAcrossConversations: "boolean?", + sources: type("('memory' | 'sessions')[]").optional(), + }).optional(), + }).optional(), + heartbeat: type({ + "+": "reject", + every: "string?", + activeHours: type({ + "+": "reject", + start: "string?", + end: "string?", + timezone: "string?", + }).optional(), + lightContext: "boolean?", + isolatedSession: "boolean?", + timeoutSeconds: "number?", + }).optional(), + humanDelay: type({ + "+": "reject", + mode: '"off"|"natural"|"custom"?', + minMs: "number?", + maxMs: "number?", + }).optional(), + }, +}); + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +export function isSafeClawPackagePath(value: string): boolean { + const normalized = value.replaceAll("\\", "/"); + if ( + !normalized || + normalized !== value || + normalized !== normalized.trim() || + normalized.startsWith("/") || + /^[A-Za-z]:\//.test(normalized) + ) { + return false; + } + return normalized + .split("/") + .every( + (segment) => + segment !== "" && + segment !== "." && + segment !== ".." && + !WINDOWS_INVALID_PATH_CHARS.test(segment) && + !UNICODE_CONTROL_CHARACTER.test(segment) && + !segment.endsWith(".") && + !segment.endsWith(" ") && + !WINDOWS_RESERVED_PATH_SEGMENT.test(segment), + ); +} + +function portablePathKey(value: string): string { + return caseFold(value.replaceAll("\\", "/").normalize("NFC")); +} + +export function findClawPackagePathHierarchyCollision( + paths: readonly string[], +): { ancestor: string; descendant: string } | null { + const pathByKey = new Map(paths.map((path) => [portablePathKey(path), path])); + for (const descendant of paths) { + const segments = portablePathKey(descendant).split("/"); + for (let length = 1; length < segments.length; length += 1) { + const ancestor = pathByKey.get(segments.slice(0, length).join("/")); + if (ancestor !== undefined) { + return { ancestor, descendant }; + } + } + } + return null; +} + +function issue(code: string, path: string, message: string): ClawPackageValidationIssue { + return { code, path, message }; +} + +function parseJsonCompatibleYaml(raw: string, path: string) { + const document = parseDocument(raw.startsWith("\uFEFF") ? raw.slice(1) : raw, { + prettyErrors: false, + uniqueKeys: true, + }); + if (document.errors.length > 0) { + return { + issues: document.errors.map((error) => + issue("invalid_openclaw_profile", path, error.message), + ), + }; + } + let unsupportedFeature: string | undefined; + visit(document, { + Alias() { + unsupportedFeature ??= "aliases"; + }, + Node(_key, node) { + if (node.anchor) { + unsupportedFeature ??= "anchors"; + } else if (node.tag) { + unsupportedFeature ??= "explicit tags"; + } + }, + Pair(_key, pair) { + if (!isScalar(pair.key) || typeof pair.key.value !== "string") { + unsupportedFeature ??= "non-string mapping keys"; + } else if (pair.key.value === "<<") { + unsupportedFeature ??= "merge keys"; + } + }, + }); + if (unsupportedFeature) { + return { + issues: [ + issue( + "unsupported_openclaw_profile_yaml_feature", + path, + `${path} uses ${unsupportedFeature}; OpenClaw profile YAML must map directly to JSON data.`, + ), + ], + }; + } + try { + return { value: document.toJSON() }; + } catch (error) { + return { + issues: [ + issue( + "invalid_openclaw_profile", + path, + error instanceof Error ? error.message : "Could not parse OpenClaw profile.", + ), + ], + }; + } +} + +function isStrictNonEmpty(value: string): boolean { + return value.length > 0 && value === value.trim(); +} + +function isValidDuration(value: string): boolean { + if (!isStrictNonEmpty(value)) return false; + const multipliers: Record = { + ms: 1, + s: 1_000, + m: 60_000, + h: 3_600_000, + d: 86_400_000, + }; + const normalized = value.toLowerCase(); + const single = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(normalized); + if (single) { + return Number.isSafeInteger(Math.round(Number(single[1]) * multipliers[single[2] ?? "m"])); + } + let totalMs = 0; + let consumed = 0; + for (const match of normalized.matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h|d)/g)) { + if (match.index !== consumed) return false; + totalMs += Number(match[1]) * multipliers[match[2]]; + consumed += match[0].length; + } + return ( + consumed === normalized.length && consumed > 0 && Number.isSafeInteger(Math.round(totalMs)) + ); +} + +function validateOpenClawProfile( + value: unknown, + profilePath: string, +): ClawPackageValidationIssue[] { + const parsed = OpenClawProfileSchema(value); + if (parsed instanceof ArkErrors) { + return Array.from(parsed, (error) => + issue( + "invalid_openclaw_profile", + `${profilePath}${error.path.length > 0 ? `.${error.path.join(".")}` : ""}`, + error.description ?? "Invalid value.", + ), + ); + } + const issues: ClawPackageValidationIssue[] = []; + const add = (path: string, message: string) => + issues.push(issue("invalid_openclaw_profile", `${profilePath}.${path}`, message)); + const requireNonEmpty = (path: string, values: string[] | undefined) => { + if (values !== undefined && values.length === 0) add(path, "Must contain at least one value."); + for (const [index, entry] of (values ?? []).entries()) { + if (!isStrictNonEmpty(entry)) { + add(`${path}.${index}`, "Must be non-empty without leading or trailing whitespace."); + } + } + }; + + requireNonEmpty("agent.groupChat.mentionPatterns", parsed.agent.groupChat?.mentionPatterns); + if (parsed.agent.tools?.profile !== undefined && !isStrictNonEmpty(parsed.agent.tools.profile)) { + add("agent.tools.profile", "Must be non-empty without leading or trailing whitespace."); + } else if ( + parsed.agent.tools?.profile !== undefined && + !OPENCLAW_TOOL_PROFILE_IDS.has(parsed.agent.tools.profile) + ) { + add("agent.tools.profile", "Must name a registered OpenClaw built-in profile."); + } + requireNonEmpty("agent.tools.allow", parsed.agent.tools?.allow); + requireNonEmpty("agent.tools.alsoAllow", parsed.agent.tools?.alsoAllow); + requireNonEmpty("agent.tools.deny", parsed.agent.tools?.deny); + if (parsed.agent.tools?.allow && parsed.agent.tools.alsoAllow) { + add("agent.tools.alsoAllow", "Must not be combined with tools.allow."); + } + if (parsed.agent.memory?.search?.sources?.length === 0) { + add("agent.memory.search.sources", "Must contain at least one source."); + } + if ( + parsed.agent.memory?.search?.sources?.includes("sessions") && + parsed.agent.memory.search.rememberAcrossConversations !== true + ) { + add( + "agent.memory.search.rememberAcrossConversations", + "Must be true when memory.search.sources includes sessions.", + ); + } + const heartbeat = parsed.agent.heartbeat; + if (heartbeat?.every !== undefined && !isValidDuration(heartbeat.every)) { + add("agent.heartbeat.every", "Must be a valid duration."); + } + const activeHours = heartbeat?.activeHours; + if (activeHours?.start !== undefined && !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(activeHours.start)) { + add("agent.heartbeat.activeHours.start", "Must be a valid 24-hour start time."); + } + if ( + activeHours?.end !== undefined && + !/^(?:(?:[01]\d|2[0-3]):[0-5]\d|24:00)$/.test(activeHours.end) + ) { + add("agent.heartbeat.activeHours.end", "Must be a valid 24-hour end time."); + } + if (activeHours?.timezone !== undefined) { + try { + new Intl.DateTimeFormat("en-US", { timeZone: activeHours.timezone }).format(); + } catch { + add("agent.heartbeat.activeHours.timezone", "Must be a valid IANA timezone."); + } + } + if ( + heartbeat?.timeoutSeconds !== undefined && + (!Number.isInteger(heartbeat.timeoutSeconds) || heartbeat.timeoutSeconds <= 0) + ) { + add("agent.heartbeat.timeoutSeconds", "Must be a positive integer."); + } + for (const field of ["minMs", "maxMs"] as const) { + const delay = parsed.agent.humanDelay?.[field]; + if (delay !== undefined && (!Number.isInteger(delay) || delay < 0)) { + add(`agent.humanDelay.${field}`, "Must be a nonnegative integer."); + } + } + return issues; +} + +function parseManifestDocument(raw: string, manifestPath: string) { + const filename = manifestPath.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase(); + if (filename === "claw.md") { + const markdown = raw.startsWith("\uFEFF") ? raw.slice(1) : raw; + const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) { + return { + issues: [ + issue( + "missing_claw_frontmatter", + manifestPath, + `${manifestPath} must start with YAML frontmatter delimited by --- lines.`, + ), + ], + }; + } + const document = parseDocument(match[1], { prettyErrors: false, uniqueKeys: true }); + if (document.errors.length > 0) { + return { + issues: document.errors.map((error) => + issue("invalid_claw_frontmatter", manifestPath, error.message), + ), + }; + } + let unsupportedFeature: string | undefined; + visit(document, { + Alias() { + unsupportedFeature ??= "aliases"; + }, + Node(_key, node) { + if (node.anchor) { + unsupportedFeature ??= "anchors"; + } else if (node.tag) { + unsupportedFeature ??= "explicit tags"; + } + }, + Pair(_key, pair) { + if (!isScalar(pair.key) || typeof pair.key.value !== "string") { + unsupportedFeature ??= "non-string mapping keys"; + } else if (pair.key.value === "<<") { + unsupportedFeature ??= "merge keys"; + } + }, + }); + if (unsupportedFeature) { + return { + issues: [ + issue( + "unsupported_claw_yaml_feature", + manifestPath, + `${manifestPath} uses ${unsupportedFeature}; CLAW.md frontmatter must map directly to JSON data.`, + ), + ], + }; + } + try { + return { + value: document.toJSON(), + markdownBody: markdown.slice(match[0].length), + }; + } catch (error) { + return { + issues: [ + issue( + "invalid_claw_frontmatter", + manifestPath, + error instanceof Error ? error.message : "Could not parse Claw frontmatter.", + ), + ], + }; + } + } + try { + return { value: JSON.parse(raw) as unknown }; + } catch (error) { + return { + issues: [ + issue( + "invalid_claw_json", + manifestPath, + error instanceof Error ? error.message : "Could not parse Claw JSON.", + ), + ], + }; + } +} + +export function validateClawPackageContents(input: { + packageName: string; + version: string; + packageJson: unknown; + files: readonly ClawPackageTextFile[]; +}): + | { ok: true; value: ValidatedClawPackage } + | { ok: false; issues: ClawPackageValidationIssue[] } { + const issues: ClawPackageValidationIssue[] = []; + if (!isRecord(input.packageJson)) { + return { + ok: false, + issues: [ + issue("missing_package_json", "package.json", "Claw packages require package.json."), + ], + }; + } + const declaredName = typeof input.packageJson.name === "string" ? input.packageJson.name : ""; + const declaredVersion = + typeof input.packageJson.version === "string" ? input.packageJson.version : ""; + const openclaw = isRecord(input.packageJson.openclaw) ? input.packageJson.openclaw : undefined; + const manifestPath = typeof openclaw?.claw === "string" ? openclaw.claw : ""; + if (declaredName !== input.packageName) { + issues.push( + issue("package_name_mismatch", "package.json.name", `Must match ${input.packageName}.`), + ); + } + if (!EXACT_VERSION_PATTERN.test(declaredVersion) || declaredVersion !== input.version) { + issues.push( + issue( + "package_version_mismatch", + "package.json.version", + `Must be exact semver ${input.version}.`, + ), + ); + } + if (!manifestPath || !isSafeClawPackagePath(manifestPath)) { + issues.push( + issue( + "invalid_claw_manifest_path", + "package.json.openclaw.claw", + "Must name a safe package-relative Claw manifest path.", + ), + ); + } + if (issues.length > 0) return { ok: false, issues }; + + const fileByPath = new Map(); + const portablePaths = new Set(); + for (const file of input.files) { + if (!isSafeClawPackagePath(file.path)) { + issues.push( + issue( + "invalid_package_path", + file.path, + "Package files must use safe canonical package-relative paths.", + ), + ); + continue; + } + const key = portablePathKey(file.path); + if (portablePaths.has(key)) { + issues.push( + issue( + "duplicate_portable_path", + file.path, + "Package paths must be unique across supported filesystems.", + ), + ); + } else { + portablePaths.add(key); + fileByPath.set(file.path, file); + } + } + const hierarchyCollision = findClawPackagePathHierarchyCollision([...fileByPath.keys()]); + if (hierarchyCollision) { + issues.push( + issue( + "portable_path_hierarchy_collision", + hierarchyCollision.descendant, + `Package file ${hierarchyCollision.ancestor} cannot also be an ancestor of ${hierarchyCollision.descendant}.`, + ), + ); + } + const manifestFile = fileByPath.get(manifestPath); + if (!manifestFile || manifestFile.text === undefined) { + issues.push( + issue( + "missing_claw_manifest", + manifestPath, + "The declared Claw manifest is missing or is not UTF-8 text.", + ), + ); + return { ok: false, issues }; + } + if (new TextEncoder().encode(manifestFile.text).byteLength > MAX_CLAW_MANIFEST_BYTES) { + return { + ok: false, + issues: [ + issue( + "claw_manifest_too_large", + manifestPath, + `The Claw manifest exceeds ${MAX_CLAW_MANIFEST_BYTES} bytes.`, + ), + ], + }; + } + const parsed = parseManifestDocument(manifestFile.text, manifestPath); + if (parsed.issues) return { ok: false, issues: parsed.issues }; + if (parsed.markdownBody !== undefined && parsed.markdownBody.trim().length === 0) { + return { + ok: false, + issues: [ + issue("empty_claw_body", manifestPath, "CLAW.md must contain a non-empty Markdown body."), + ], + }; + } + const validated = validateClawManifest(parsed.value); + if (!validated.ok) { + return { + ok: false, + issues: validated.issues.map((entry) => + issue("invalid_claw_manifest", entry.path, entry.message), + ), + }; + } + if ( + parsed.markdownBody !== undefined && + (validated.manifest.workspace?.bootstrapFiles?.["SOUL.md"] !== undefined || + (validated.manifest.workspace?.files ?? []).some( + (entry) => portablePathKey(entry.path) === portablePathKey("SOUL.md"), + )) + ) { + return { + ok: false, + issues: [ + issue( + "duplicate_claw_body_destination", + "SOUL.md", + "The CLAW.md body is the managed SOUL.md source and cannot be combined with an explicit SOUL.md declaration.", + ), + ], + }; + } + + const openClawProfilePath = validated.manifest.metadata?.["openclaw.config"]; + if (openClawProfilePath !== undefined) { + const profileFile = fileByPath.get(openClawProfilePath); + if (!profileFile || profileFile.text === undefined) { + issues.push( + issue( + "missing_openclaw_profile", + openClawProfilePath, + "The declared OpenClaw profile is missing or is not UTF-8 text.", + ), + ); + } else if (new TextEncoder().encode(profileFile.text).byteLength > MAX_OPENCLAW_PROFILE_BYTES) { + issues.push( + issue( + "openclaw_profile_too_large", + openClawProfilePath, + `The OpenClaw profile exceeds ${MAX_OPENCLAW_PROFILE_BYTES} bytes.`, + ), + ); + } else { + const profile = parseJsonCompatibleYaml(profileFile.text, openClawProfilePath); + if (profile.issues) { + issues.push(...profile.issues); + } else { + issues.push(...validateOpenClawProfile(profile.value, openClawProfilePath)); + } + } + } + + const sources = [ + ...Object.values(validated.manifest.workspace?.bootstrapFiles ?? {}).map( + (entry) => entry.source, + ), + ...(validated.manifest.workspace?.files ?? []).map((entry) => entry.source), + ]; + for (const source of sources) { + if (!fileByPath.has(source)) { + issues.push( + issue( + "missing_workspace_source", + source, + "Declared workspace source is missing from the package.", + ), + ); + } + } + if (issues.length > 0) return { ok: false, issues }; + const summary = summarizeClawManifest(validated.manifest); + const implicitWorkspaceFile = + parsed.markdownBody === undefined + ? undefined + : { path: "SOUL.md" as const, text: parsed.markdownBody }; + return { + ok: true, + value: { + manifestPath, + manifest: validated.manifest, + ...(implicitWorkspaceFile ? { implicitWorkspaceFile } : {}), + summary: implicitWorkspaceFile + ? { + ...summary, + workspace: { + ...summary.workspace, + bootstrapFiles: [...summary.workspace.bootstrapFiles, "SOUL.md"], + }, + } + : summary, + }, + }; +} diff --git a/packages/clawhub/src/schema/claws.ts b/packages/clawhub/src/schema/claws.ts new file mode 100644 index 00000000..268ae296 --- /dev/null +++ b/packages/clawhub/src/schema/claws.ts @@ -0,0 +1,904 @@ +import { ArkErrors, type BaseType, type inferred, type } from "arktype"; +import { Cron } from "croner"; +import { caseFold } from "unicode-case-folding"; + +export const CLAW_SCHEMA_VERSION = 1 as const; +export const CLAW_SUMMARY_AGENT_NAME_MAX_CHARS = 128; +export const CLAW_SUMMARY_AGENT_DESCRIPTION_MAX_CHARS = 1_024; +export const CLAW_BOOTSTRAP_FILE_NAMES = [ + "AGENTS.md", + "SOUL.md", + "IDENTITY.md", + "TOOLS.md", + "HEARTBEAT.md", +] as const; + +const StringArraySchema = type("string[]"); +const WorkspaceSourceSchema = type({ "+": "reject", source: "string" }); +const ToolFilterSchema = type({ + "+": "reject", + include: StringArraySchema.optional(), + exclude: StringArraySchema.optional(), +}); +const StdioMcpServerSchema = type({ + "+": "reject", + command: "string", + transport: '"stdio"?', + args: StringArraySchema.optional(), + env: type({ "[string]": "string" }).optional(), + toolFilter: ToolFilterSchema.optional(), + timeout: "number?", + connectTimeout: "number?", +}); +const RemoteMcpServerSchema = type({ + "+": "reject", + url: "string", + transport: '"sse"|"streamable-http"', + auth: '"oauth"?', + toolFilter: ToolFilterSchema.optional(), + timeout: "number?", + connectTimeout: "number?", +}); + +export const ClawManifestSchema = type({ + "+": "reject", + schemaVersion: "1", + agent: { + "+": "reject", + id: "string", + name: "string?", + description: "string?", + identity: type({ + "+": "reject", + name: "string?", + theme: "string?", + emoji: "string?", + avatar: "string?", + }).optional(), + }, + metadata: type({ "[string]": "string" }).optional(), + workspace: type({ + "+": "reject", + bootstrapFiles: type({ + "+": "reject", + "AGENTS.md": WorkspaceSourceSchema.optional(), + "SOUL.md": WorkspaceSourceSchema.optional(), + "IDENTITY.md": WorkspaceSourceSchema.optional(), + "TOOLS.md": WorkspaceSourceSchema.optional(), + "HEARTBEAT.md": WorkspaceSourceSchema.optional(), + }).optional(), + files: type({ "+": "reject", source: "string", path: "string" }).array().optional(), + }).optional(), + packages: type({ + "+": "reject", + kind: '"skill"|"plugin"', + source: '"clawhub"', + ref: "string", + version: "string", + }) + .array() + .optional(), + mcpServers: type({ + "[string]": StdioMcpServerSchema.or(RemoteMcpServerSchema), + }).optional(), + cronJobs: type({ + "+": "reject", + id: "string", + name: "string?", + schedule: { "+": "reject", cron: "string", timezone: "string" }, + session: '"main"|"isolated"', + message: "string", + delivery: type({ + "+": "reject", + mode: '"none"|"announce"', + channel: '"last"?', + }).optional(), + }) + .array() + .optional(), +}); +export type ClawManifest = (typeof ClawManifestSchema)[inferred]; + +export type ClawManifestSummary = { + schemaVersion: 1; + agent: { id: string; name?: string; description?: string }; + workspace: { bootstrapFiles: string[]; fileCount: number }; + packages: { skillCount: number; pluginCount: number }; + mcpServerCount: number; + cronJobCount: number; +}; + +export type ClawManifestSummarySchemaAdapter = { + literalOne: TValue; + string: TValue; + number: TValue; + stringArray: TValue; + boundedString: (maxCharacters: number) => TValue; + optional: (schema: TValue) => TOptional; + object: (fields: Record) => TValue; +}; + +/** Builds the v1 summary structure for both the public schema and durable storage validators. */ +export function createClawManifestSummarySchema( + adapter: ClawManifestSummarySchemaAdapter, +): TValue { + return adapter.object({ + schemaVersion: adapter.literalOne, + agent: adapter.object({ + id: adapter.string, + name: adapter.optional(adapter.boundedString(CLAW_SUMMARY_AGENT_NAME_MAX_CHARS)), + description: adapter.optional( + adapter.boundedString(CLAW_SUMMARY_AGENT_DESCRIPTION_MAX_CHARS), + ), + }), + workspace: adapter.object({ + bootstrapFiles: adapter.stringArray, + fileCount: adapter.number, + }), + packages: adapter.object({ + skillCount: adapter.number, + pluginCount: adapter.number, + }), + mcpServerCount: adapter.number, + cronJobCount: adapter.number, + }); +} + +export const ClawManifestSummarySchema = createClawManifestSummarySchema( + { + literalOne: type("1"), + string: type("string"), + number: type("number"), + stringArray: type("string[]"), + boundedString: (maxCharacters) => + type("string").narrow((value) => Array.from(value).length <= maxCharacters), + optional: (schema) => schema.optional(), + object: (fields) => type({ "+": "reject", ...fields }), + }, +) as BaseType; + +export const CLAW_MANIFEST_VALIDATION_PHASE = "schema" as const; +export const CLAW_MANIFEST_VALIDATION_CODES = { + invalidManifestShape: "claw_v1_invalid_manifest_shape", + invalidAgentId: "claw_v1_invalid_agent_id", + nonCanonicalString: "claw_v1_non_canonical_string", + emptyList: "claw_v1_empty_list", + invalidProfilePath: "claw_v1_invalid_profile_path", + unsafePath: "claw_v1_unsafe_path", + duplicateWorkspaceDestination: "claw_v1_duplicate_workspace_destination", + invalidAvatar: "claw_v1_invalid_avatar", + undeclaredAvatar: "claw_v1_undeclared_avatar", + invalidPackageReference: "claw_v1_invalid_package_reference", + invalidPackageVersion: "claw_v1_invalid_package_version", + duplicatePackage: "claw_v1_duplicate_package", + invalidMcpServerId: "claw_v1_invalid_mcp_server_id", + invalidMcpUrl: "claw_v1_invalid_mcp_url", + mcpUrlCredentials: "claw_v1_mcp_url_credentials", + unpinnedMcpPackage: "claw_v1_unpinned_mcp_package", + invalidToolFilter: "claw_v1_invalid_tool_filter", + duplicateToolFilter: "claw_v1_duplicate_tool_filter", + invalidEnvironmentKey: "claw_v1_invalid_environment_key", + blockedEnvironmentKey: "claw_v1_blocked_environment_key", + invalidEnvironmentReference: "claw_v1_invalid_environment_reference", + invalidTimeout: "claw_v1_invalid_timeout", + invalidCronJobId: "claw_v1_invalid_cron_job_id", + invalidCronSchedule: "claw_v1_invalid_cron_schedule", + invalidCronDelivery: "claw_v1_invalid_cron_delivery", + duplicateCronJob: "claw_v1_duplicate_cron_job", +} as const; +export type ClawManifestValidationCode = + (typeof CLAW_MANIFEST_VALIDATION_CODES)[keyof typeof CLAW_MANIFEST_VALIDATION_CODES]; +export type ClawManifestValidationIssue = { + code: ClawManifestValidationCode; + phase: typeof CLAW_MANIFEST_VALIDATION_PHASE; + path: string; + message: string; +}; + +const AGENT_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/; +const EXACT_VERSION_PATTERN = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; +const ENV_REFERENCE_PATTERN = /^\$\{[A-Z_][A-Z0-9_]*\}$/; +const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/; +const WINDOWS_INVALID_PATH_CHARS = /[<>:"|?*]/; +const WINDOWS_RESERVED_PATH_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i; +const UNICODE_CONTROL_CHARACTER = /\p{Cc}/u; +const AVATAR_MAX_BYTES = 2 * 1024 * 1024; +const AVATAR_MAX_DATA_URL_CHARS = 2_796_230; +const AVATAR_DATA_URL_PATTERN = /^data:image\/[^,]*,/i; +const AVATAR_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp|svg)$/i; +const BASE64_PAYLOAD_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; +const PORTABLE_ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; +// Versioned compatibility artifact for the OpenClaw policy applied by the v1 consumer. +export const OPENCLAW_CLAW_HOST_ENV_POLICY_V1 = { + contractVersion: 1, + source: { + repository: "openclaw/openclaw", + commit: "f50a918af102b950721c1b380f7eb8af4e0389d1", + path: "src/infra/host-env-security-policy.json", + sha256: "4639dcfa157d4fa4c2fb84f8c5347ffe2252eb4ddb3d818afd7f87d68173b92b", + }, + blockedKeys: [ + "NODE_OPTIONS", + "NODE_PATH", + "NODE_REDIRECT_WARNINGS", + "NODE_REPL_EXTERNAL_MODULE", + "NODE_REPL_HISTORY", + "NODE_V8_COVERAGE", + "PYTHONHOME", + "PYTHONPATH", + "PERL5LIB", + "PERL5OPT", + "RUBYLIB", + "RUBYOPT", + "BASHOPTS", + "BASH_ENV", + "ENV", + "KSH_ENV", + "BROWSER", + "GIT_ALLOW_PROTOCOL", + "GIT_EDITOR", + "GIT_EXTERNAL_DIFF", + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_COMMON_DIR", + "GIT_EXEC_PATH", + "GIT_INDEX_FILE", + "GIT_OBJECT_DIRECTORY", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_NAMESPACE", + "GIT_PROTOCOL_FROM_USER", + "GIT_SEQUENCE_EDITOR", + "GIT_TEMPLATE_DIR", + "GIT_SSL_NO_VERIFY", + "GIT_SSL_CAINFO", + "GIT_SSL_CAPATH", + "CC", + "CPP", + "CXX", + "CARGO_BUILD_RUSTC", + "CARGO_BUILD_RUSTC_WRAPPER", + "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", + "CARGO_BUILD_RUSTDOC", + "RUSTC", + "RUSTC_WRAPPER", + "RUSTC_WORKSPACE_WRAPPER", + "RUSTDOC", + "CMAKE_C_COMPILER", + "CMAKE_CXX_COMPILER", + "SHELL", + "SHELLOPTS", + "PS4", + "GCONV_PATH", + "IFS", + "SSLKEYLOGFILE", + "JAVA_OPTS", + "JAVA_TOOL_OPTIONS", + "_JAVA_OPTIONS", + "JDK_JAVA_OPTIONS", + "PYTHONBREAKPOINT", + "DOTNET_STARTUP_HOOKS", + "DOTNET_ADDITIONAL_DEPS", + "FPATH", + "GLIBC_TUNABLES", + "MAVEN_OPTS", + "MAKE", + "MAKEFLAGS", + "MFLAGS", + "SBT_OPTS", + "GRADLE_OPTS", + "ANT_OPTS", + "HGRCPATH", + "HGEDITOR", + "HGMERGE", + "EXINIT", + "VIMINIT", + "MYVIMRC", + "GVIMINIT", + "LUA_INIT", + "LUA_INIT_5_1", + "LUA_INIT_5_2", + "LUA_INIT_5_3", + "LUA_INIT_5_4", + "EMACSLOADPATH", + "RUBYSHELL", + "GIT_HOOK_PATH", + "SVN_EDITOR", + "SVN_SSH", + "BZR_EDITOR", + "BZR_SSH", + "BZR_PLUGIN_PATH", + "SUDO_ASKPASS", + "JULIA_EDITOR", + "CONFIG_SITE", + "CONFIG_SHELL", + "CMAKE_TOOLCHAIN_FILE", + "CATALINA_OPTS", + "CORECLR_PROFILER", + "HELM_PLUGINS", + "PACKER_PLUGIN_PATH", + "VAGRANT_VAGRANTFILE", + "ERL_AFLAGS", + "ERL_FLAGS", + "ERL_ZFLAGS", + "ELIXIR_ERL_OPTIONS", + "R_ENVIRON", + "R_PROFILE", + "R_ENVIRON_USER", + "R_PROFILE_USER", + "TCLLIBPATH", + "HOSTALIASES", + ], + blockedPrefixes: ["DYLD_", "LD_", "BASH_FUNC_"], +} as const; + +const BLOCKED_PROCESS_ENV_KEYS = new Set(OPENCLAW_CLAW_HOST_ENV_POLICY_V1.blockedKeys); +const BLOCKED_PROCESS_ENV_PREFIXES = OPENCLAW_CLAW_HOST_ENV_POLICY_V1.blockedPrefixes; + +function isStrictNonEmpty(value: string): boolean { + return value.length > 0 && value === value.trim(); +} + +function isBlockedProcessEnvKey(value: string): boolean { + const upper = value.toUpperCase(); + return ( + BLOCKED_PROCESS_ENV_KEYS.has(upper) || + BLOCKED_PROCESS_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix)) + ); +} + +function isSafePackagePath(value: string): boolean { + const normalized = value.replaceAll("\\", "/"); + if ( + !isStrictNonEmpty(normalized) || + normalized.startsWith("/") || + /^[A-Za-z]:\//.test(normalized) + ) { + return false; + } + return normalized + .split("/") + .every( + (segment) => + segment !== "" && + segment !== "." && + segment !== ".." && + !WINDOWS_INVALID_PATH_CHARS.test(segment) && + !UNICODE_CONTROL_CHARACTER.test(segment) && + !segment.endsWith(".") && + !segment.endsWith(" ") && + !WINDOWS_RESERVED_PATH_SEGMENT.test(segment), + ); +} + +function portablePathKey(value: string): string { + return caseFold(value.replaceAll("\\", "/").normalize("NFC")); +} + +function isPortableAvatar(value: string): boolean { + if (AVATAR_DATA_URL_PATTERN.test(value)) { + if (value.length > AVATAR_MAX_DATA_URL_CHARS) return false; + const comma = value.indexOf(","); + if (comma < 0) return false; + const metadata = value.slice(0, comma); + const payload = value.slice(comma + 1); + try { + const base64 = /;base64(?:;|$)/i.test(metadata); + if (payload.length === 0 || (base64 && !BASE64_PAYLOAD_PATTERN.test(payload))) return false; + const byteLength = base64 + ? (payload.length / 4) * 3 - (payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0) + : new TextEncoder().encode(decodeURIComponent(payload)).byteLength; + return byteLength > 0 && byteLength <= AVATAR_MAX_BYTES; + } catch { + return false; + } + } + return isSafePackagePath(value) && AVATAR_EXTENSION_PATTERN.test(value); +} + +function conflictsWithWorkspaceTarget(targets: Set, candidate: string): boolean { + for (const target of targets) { + if ( + target === candidate || + target.startsWith(`${candidate}/`) || + candidate.startsWith(`${target}/`) + ) { + return true; + } + } + return false; +} + +function packageManagerArtifacts(command: string, args: string[]): string[] | undefined { + const executable = command + .split(/[\\/]/) + .at(-1) + ?.replace(/\.(?:cmd|exe)$/i, "") + .toLowerCase(); + let start = 0; + let scansOptionsAfterPositional = false; + if (executable === "npm") { + if (args[0] !== "exec" && args[0] !== "x") return undefined; + start = 1; + scansOptionsAfterPositional = true; + } else if (executable === "bun") { + if (args[0] !== "x") return undefined; + start = 1; + } else if (executable === "pnpm" || executable === "yarn") { + if (args[0] !== "dlx") return undefined; + start = 1; + } else if (executable !== "npx" && executable !== "pnpx" && executable !== "bunx") { + return undefined; + } + const selected: string[] = []; + let positional: string | undefined; + for (let index = start; index < args.length; index += 1) { + const value = args[index]; + if (!value) continue; + if (value === "--") { + positional ??= args[index + 1] ?? ""; + break; + } + if (value === "-p" || value === "--package") { + selected.push(args[index + 1] ?? ""); + index += 1; + continue; + } + if (value.startsWith("--package=")) { + selected.push(value.slice("--package=".length)); + continue; + } + if (value === "-y" || value === "--yes") continue; + if (value.startsWith("-")) return [""]; + if (positional === undefined && !value.startsWith("-")) { + positional = value; + if (!scansOptionsAfterPositional) break; + } + } + return selected.length > 0 ? selected : [positional ?? ""]; +} + +function truncateSummaryText(value: string | undefined, maxChars: number): string | undefined { + if (!value) return undefined; + return Array.from(value).slice(0, maxChars).join(""); +} + +function isPackageManagerArtifactPinned(command: string, args: string[]): boolean | undefined { + const artifacts = packageManagerArtifacts(command, args); + if (artifacts === undefined) return undefined; + return artifacts.every((artifact) => { + const separator = artifact.lastIndexOf("@"); + const scopedSlash = artifact.startsWith("@") ? artifact.indexOf("/") : -1; + return ( + separator > 0 && + separator > scopedSlash && + EXACT_VERSION_PATTERN.test(artifact.slice(separator + 1)) + ); + }); +} + +function validationIssue( + code: ClawManifestValidationCode, + path: string, + message: string, +): ClawManifestValidationIssue { + return { code, phase: CLAW_MANIFEST_VALIDATION_PHASE, path, message }; +} + +function arkIssues(errors: ArkErrors): ClawManifestValidationIssue[] { + return Array.from(errors, (error) => + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidManifestShape, + error.path.length > 0 ? `$.${error.path.join(".")}` : "$", + error.description ?? "Manifest must match the strict Claw v1 shape.", + ), + ); +} + +function pushNonEmpty( + issues: ClawManifestValidationIssue[], + path: string, + value: string | undefined, +): void { + if (value !== undefined && !isStrictNonEmpty(value)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.nonCanonicalString, + path, + "Must be non-empty without leading or trailing whitespace.", + ), + ); + } +} + +function pushNonEmptyArray( + issues: ClawManifestValidationIssue[], + path: string, + values: string[] | undefined, + requireEntry: boolean, +): void { + if (requireEntry && values !== undefined && values.length === 0) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.emptyList, + path, + "Must contain at least one value.", + ), + ); + } + for (const [index, value] of (values ?? []).entries()) { + pushNonEmpty(issues, `${path}.${index}`, value); + } +} + +export function validateClawManifest( + value: unknown, +): { ok: true; manifest: ClawManifest } | { ok: false; issues: ClawManifestValidationIssue[] } { + const parsed = ClawManifestSchema(value); + if (parsed instanceof ArkErrors) return { ok: false, issues: arkIssues(parsed) }; + + const issues: ClawManifestValidationIssue[] = []; + if (!AGENT_ID_PATTERN.test(parsed.agent.id)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidAgentId, + "$.agent.id", + "Invalid portable agent id.", + ), + ); + } + pushNonEmpty(issues, "$.agent.name", parsed.agent.name); + pushNonEmpty(issues, "$.agent.description", parsed.agent.description); + for (const field of ["name", "theme", "emoji", "avatar"] as const) { + pushNonEmpty(issues, `$.agent.identity.${field}`, parsed.agent.identity?.[field]); + } + for (const key of Object.keys(parsed.metadata ?? {})) { + pushNonEmpty(issues, `$.metadata.${key}`, key); + } + const openClawProfilePath = parsed.metadata?.["openclaw.config"]; + if ( + openClawProfilePath !== undefined && + (openClawProfilePath.includes("\\") || + !isSafePackagePath(openClawProfilePath) || + !/\.ya?ml$/i.test(openClawProfilePath)) + ) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidProfilePath, + "$.metadata.openclaw.config", + "Must reference a forward-slash package-relative .yml or .yaml file.", + ), + ); + } + const workspaceTargets = new Set(); + for (const name of CLAW_BOOTSTRAP_FILE_NAMES) { + const source = parsed.workspace?.bootstrapFiles?.[name]?.source; + if (source === undefined) continue; + if (!isSafePackagePath(source)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.unsafePath, + `$.workspace.bootstrapFiles.${name}.source`, + "Must be a safe package-relative path.", + ), + ); + } + workspaceTargets.add(portablePathKey(name)); + } + for (const [index, file] of (parsed.workspace?.files ?? []).entries()) { + if (!isSafePackagePath(file.source)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.unsafePath, + `$.workspace.files.${index}.source`, + "Must be a safe package-relative path.", + ), + ); + } + if (!isSafePackagePath(file.path)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.unsafePath, + `$.workspace.files.${index}.path`, + "Must be a safe package-relative path.", + ), + ); + } + const destinationKey = portablePathKey(file.path); + if (conflictsWithWorkspaceTarget(workspaceTargets, destinationKey)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.duplicateWorkspaceDestination, + `$.workspace.files.${index}.path`, + "Workspace destination is declared more than once.", + ), + ); + } + workspaceTargets.add(destinationKey); + } + const avatar = parsed.agent.identity?.avatar; + if (avatar !== undefined) { + if (!isPortableAvatar(avatar)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidAvatar, + "$.agent.identity.avatar", + "Must be a bounded image data URL or managed workspace-relative image path.", + ), + ); + } else if ( + !AVATAR_DATA_URL_PATTERN.test(avatar) && + !workspaceTargets.has(portablePathKey(avatar)) + ) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.undeclaredAvatar, + "$.agent.identity.avatar", + "Workspace-relative avatar must match a workspace.files destination.", + ), + ); + } + } + + const packageKeys = new Set(); + for (const [index, pkg] of (parsed.packages ?? []).entries()) { + pushNonEmpty(issues, `$.packages.${index}.ref`, pkg.ref); + if (!PACKAGE_NAME_PATTERN.test(pkg.ref)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidPackageReference, + `$.packages.${index}.ref`, + "Must be a lowercase ClawHub package name.", + ), + ); + } + if (!EXACT_VERSION_PATTERN.test(pkg.version)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidPackageVersion, + `$.packages.${index}.version`, + "Must be an exact semantic version.", + ), + ); + } + const key = `${pkg.kind}:${pkg.source}:${pkg.ref.toLowerCase()}`; + if (packageKeys.has(key)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.duplicatePackage, + `$.packages.${index}`, + "Package is declared more than once.", + ), + ); + } + packageKeys.add(key); + } + + for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) { + if (!AGENT_ID_PATTERN.test(name)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidMcpServerId, + `$.mcpServers.${name}`, + "Invalid MCP server name.", + ), + ); + } + if ("url" in server) { + pushNonEmpty(issues, `$.mcpServers.${name}.url`, server.url); + try { + const url = new URL(server.url); + const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname); + if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) { + throw new Error("protocol"); + } + if (url.username || url.password || url.hash) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.mcpUrlCredentials, + `$.mcpServers.${name}.url`, + "Must not contain embedded credentials or fragments.", + ), + ); + } + } catch { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidMcpUrl, + `$.mcpServers.${name}.url`, + "Must use HTTPS, except HTTP on an exact loopback host.", + ), + ); + } + } + if ("command" in server) { + pushNonEmpty(issues, `$.mcpServers.${name}.command`, server.command); + pushNonEmptyArray(issues, `$.mcpServers.${name}.args`, server.args, false); + if (isPackageManagerArtifactPinned(server.command, server.args ?? []) === false) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.unpinnedMcpPackage, + `$.mcpServers.${name}.args`, + "Package-manager MCP commands must select one exact immutable package version.", + ), + ); + } + } + pushNonEmptyArray( + issues, + `$.mcpServers.${name}.toolFilter.include`, + server.toolFilter?.include, + true, + ); + pushNonEmptyArray( + issues, + `$.mcpServers.${name}.toolFilter.exclude`, + server.toolFilter?.exclude, + true, + ); + for (const field of ["include", "exclude"] as const) { + const seen = new Set(); + for (const [index, entry] of (server.toolFilter?.[field] ?? []).entries()) { + if (entry.includes("?") || entry.includes("[") || entry.includes("]")) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidToolFilter, + `$.mcpServers.${name}.toolFilter.${field}.${index}`, + "Tool filters support only exact names and * wildcards.", + ), + ); + } + if (seen.has(entry)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.duplicateToolFilter, + `$.mcpServers.${name}.toolFilter.${field}.${index}`, + "Tool filter entries must be unique.", + ), + ); + } + seen.add(entry); + } + } + if ("env" in server) { + for (const [key, envValue] of Object.entries(server.env ?? {})) { + pushNonEmpty(issues, `$.mcpServers.${name}.env`, key); + if (!PORTABLE_ENV_KEY_PATTERN.test(key)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidEnvironmentKey, + `$.mcpServers.${name}.env.${key}`, + "Invalid portable environment key.", + ), + ); + } + if (isBlockedProcessEnvKey(key)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.blockedEnvironmentKey, + `$.mcpServers.${name}.env.${key}`, + "Environment key is blocked by the spawned-process safety policy.", + ), + ); + } + if (!ENV_REFERENCE_PATTERN.test(envValue)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidEnvironmentReference, + `$.mcpServers.${name}.env.${key}`, + "Must be an unresolved ${ENV_VAR} reference.", + ), + ); + } + } + } + if (server.timeout !== undefined && (!Number.isFinite(server.timeout) || server.timeout <= 0)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidTimeout, + `$.mcpServers.${name}.timeout`, + "Must be positive.", + ), + ); + } + if ( + server.connectTimeout !== undefined && + (!Number.isFinite(server.connectTimeout) || server.connectTimeout <= 0) + ) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidTimeout, + `$.mcpServers.${name}.connectTimeout`, + "Must be positive.", + ), + ); + } + } + + const cronIds = new Set(); + for (const [index, job] of (parsed.cronJobs ?? []).entries()) { + if (!AGENT_ID_PATTERN.test(job.id)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidCronJobId, + `$.cronJobs.${index}.id`, + "Invalid cron job id.", + ), + ); + } + pushNonEmpty(issues, `$.cronJobs.${index}.name`, job.name); + pushNonEmpty(issues, `$.cronJobs.${index}.schedule.cron`, job.schedule.cron); + pushNonEmpty(issues, `$.cronJobs.${index}.schedule.timezone`, job.schedule.timezone); + pushNonEmpty(issues, `$.cronJobs.${index}.message`, job.message); + try { + if (job.schedule.cron.trim().split(/\s+/).length !== 5) throw new Error("fields"); + const cron = new Cron(job.schedule.cron.trim(), { + timezone: job.schedule.timezone.trim(), + catch: false, + paused: true, + }); + if (!cron.nextRun(new Date())) throw new Error("No future run"); + } catch { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidCronSchedule, + `$.cronJobs.${index}.schedule`, + "Must contain a valid cron expression and timezone.", + ), + ); + } + if ( + (job.delivery?.mode === "none" && job.delivery.channel !== undefined) || + (job.delivery?.mode === "announce" && job.delivery.channel !== "last") + ) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.invalidCronDelivery, + `$.cronJobs.${index}.delivery`, + 'Must be { mode: "none" } or { mode: "announce", channel: "last" }.', + ), + ); + } + if (cronIds.has(job.id)) { + issues.push( + validationIssue( + CLAW_MANIFEST_VALIDATION_CODES.duplicateCronJob, + `$.cronJobs.${index}.id`, + "Cron job id is declared more than once.", + ), + ); + } + cronIds.add(job.id); + } + + return issues.length > 0 ? { ok: false, issues } : { ok: true, manifest: parsed }; +} + +export function summarizeClawManifest(manifest: ClawManifest): ClawManifestSummary { + const packages = manifest.packages ?? []; + const agentName = truncateSummaryText(manifest.agent.name, CLAW_SUMMARY_AGENT_NAME_MAX_CHARS); + const agentDescription = truncateSummaryText( + manifest.agent.description, + CLAW_SUMMARY_AGENT_DESCRIPTION_MAX_CHARS, + ); + return { + schemaVersion: CLAW_SCHEMA_VERSION, + agent: { + id: manifest.agent.id, + ...(agentName ? { name: agentName } : {}), + ...(agentDescription ? { description: agentDescription } : {}), + }, + workspace: { + bootstrapFiles: CLAW_BOOTSTRAP_FILE_NAMES.filter( + (name) => manifest.workspace?.bootstrapFiles?.[name] !== undefined, + ), + fileCount: manifest.workspace?.files?.length ?? 0, + }, + packages: { + skillCount: packages.filter((pkg) => pkg.kind === "skill").length, + pluginCount: packages.filter((pkg) => pkg.kind === "plugin").length, + }, + mcpServerCount: Object.keys(manifest.mcpServers ?? {}).length, + cronJobCount: manifest.cronJobs?.length ?? 0, + }; +} diff --git a/packages/clawhub/src/schema/packages.ts b/packages/clawhub/src/schema/packages.ts index 8cd71f9c..fb8eba35 100644 --- a/packages/clawhub/src/schema/packages.ts +++ b/packages/clawhub/src/schema/packages.ts @@ -1,7 +1,7 @@ import { type inferred, type } from "arktype"; import { CliPublishFileSchema, PublishSourceSchema } from "./schemas.js"; -export const PackageFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"'); +export const PackageFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"|"claw"'); export type PackageFamily = (typeof PackageFamilySchema)[inferred]; export const PackageChannelSchema = type('"official"|"community"|"private"'); diff --git a/packages/schema/dist/clawPackage.d.ts b/packages/schema/dist/clawPackage.d.ts new file mode 100644 index 00000000..cc486899 --- /dev/null +++ b/packages/schema/dist/clawPackage.d.ts @@ -0,0 +1,36 @@ +import { type ClawManifest, type ClawManifestSummary } from "./claws.js"; +export type ClawPackageTextFile = { + path: string; + text?: string; +}; +export type ClawPackageValidationIssue = { + code: string; + path: string; + message: string; +}; +export type ValidatedClawPackage = { + manifestPath: string; + manifest: ClawManifest; + implicitWorkspaceFile?: { + path: "SOUL.md"; + text: string; + }; + summary: ClawManifestSummary; +}; +export declare function isSafeClawPackagePath(value: string): boolean; +export declare function findClawPackagePathHierarchyCollision(paths: readonly string[]): { + ancestor: string; + descendant: string; +} | null; +export declare function validateClawPackageContents(input: { + packageName: string; + version: string; + packageJson: unknown; + files: readonly ClawPackageTextFile[]; +}): { + ok: true; + value: ValidatedClawPackage; +} | { + ok: false; + issues: ClawPackageValidationIssue[]; +}; diff --git a/packages/schema/dist/clawPackage.js b/packages/schema/dist/clawPackage.js new file mode 100644 index 00000000..8980f457 --- /dev/null +++ b/packages/schema/dist/clawPackage.js @@ -0,0 +1,468 @@ +import { ArkErrors, type } from "arktype"; +import { caseFold } from "unicode-case-folding"; +import { isScalar, parseDocument, visit } from "yaml"; +import { summarizeClawManifest, validateClawManifest, } from "./claws.js"; +const EXACT_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; +const WINDOWS_INVALID_PATH_CHARS = /[<>:"|?*]/; +const WINDOWS_RESERVED_PATH_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i; +const UNICODE_CONTROL_CHARACTER = /\p{Cc}/u; +const MAX_CLAW_MANIFEST_BYTES = 1024 * 1024; +const MAX_OPENCLAW_PROFILE_BYTES = 256 * 1024; +const OPENCLAW_TOOL_PROFILE_IDS = new Set(["minimal", "coding", "messaging", "full"]); +const StrictStringArraySchema = type("string[]"); +const OpenClawProfileSchema = type({ + "+": "reject", + schemaVersion: "1", + agent: { + "+": "reject", + groupChat: type({ + "+": "reject", + mentionPatterns: StrictStringArraySchema.optional(), + }).optional(), + sandbox: type({ + "+": "reject", + mode: '"off"|"non-main"|"all"?', + scope: '"session"|"agent"|"shared"?', + workspaceAccess: '"none"|"ro"|"rw"?', + }).optional(), + tools: type({ + "+": "reject", + profile: "string?", + allow: StrictStringArraySchema.optional(), + alsoAllow: StrictStringArraySchema.optional(), + deny: StrictStringArraySchema.optional(), + fs: type({ + "+": "reject", + workspaceOnly: "true?", + }).optional(), + }).optional(), + memory: type({ + "+": "reject", + search: type({ + "+": "reject", + enabled: "boolean?", + rememberAcrossConversations: "boolean?", + sources: type("('memory' | 'sessions')[]").optional(), + }).optional(), + }).optional(), + heartbeat: type({ + "+": "reject", + every: "string?", + activeHours: type({ + "+": "reject", + start: "string?", + end: "string?", + timezone: "string?", + }).optional(), + lightContext: "boolean?", + isolatedSession: "boolean?", + timeoutSeconds: "number?", + }).optional(), + humanDelay: type({ + "+": "reject", + mode: '"off"|"natural"|"custom"?', + minMs: "number?", + maxMs: "number?", + }).optional(), + }, +}); +function isRecord(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} +export function isSafeClawPackagePath(value) { + const normalized = value.replaceAll("\\", "/"); + if (!normalized || + normalized !== value || + normalized !== normalized.trim() || + normalized.startsWith("/") || + /^[A-Za-z]:\//.test(normalized)) { + return false; + } + return normalized + .split("/") + .every((segment) => segment !== "" && + segment !== "." && + segment !== ".." && + !WINDOWS_INVALID_PATH_CHARS.test(segment) && + !UNICODE_CONTROL_CHARACTER.test(segment) && + !segment.endsWith(".") && + !segment.endsWith(" ") && + !WINDOWS_RESERVED_PATH_SEGMENT.test(segment)); +} +function portablePathKey(value) { + return caseFold(value.replaceAll("\\", "/").normalize("NFC")); +} +export function findClawPackagePathHierarchyCollision(paths) { + const pathByKey = new Map(paths.map((path) => [portablePathKey(path), path])); + for (const descendant of paths) { + const segments = portablePathKey(descendant).split("/"); + for (let length = 1; length < segments.length; length += 1) { + const ancestor = pathByKey.get(segments.slice(0, length).join("/")); + if (ancestor !== undefined) { + return { ancestor, descendant }; + } + } + } + return null; +} +function issue(code, path, message) { + return { code, path, message }; +} +function parseJsonCompatibleYaml(raw, path) { + const document = parseDocument(raw.startsWith("\uFEFF") ? raw.slice(1) : raw, { + prettyErrors: false, + uniqueKeys: true, + }); + if (document.errors.length > 0) { + return { + issues: document.errors.map((error) => issue("invalid_openclaw_profile", path, error.message)), + }; + } + let unsupportedFeature; + visit(document, { + Alias() { + unsupportedFeature ??= "aliases"; + }, + Node(_key, node) { + if (node.anchor) { + unsupportedFeature ??= "anchors"; + } + else if (node.tag) { + unsupportedFeature ??= "explicit tags"; + } + }, + Pair(_key, pair) { + if (!isScalar(pair.key) || typeof pair.key.value !== "string") { + unsupportedFeature ??= "non-string mapping keys"; + } + else if (pair.key.value === "<<") { + unsupportedFeature ??= "merge keys"; + } + }, + }); + if (unsupportedFeature) { + return { + issues: [ + issue("unsupported_openclaw_profile_yaml_feature", path, `${path} uses ${unsupportedFeature}; OpenClaw profile YAML must map directly to JSON data.`), + ], + }; + } + try { + return { value: document.toJSON() }; + } + catch (error) { + return { + issues: [ + issue("invalid_openclaw_profile", path, error instanceof Error ? error.message : "Could not parse OpenClaw profile."), + ], + }; + } +} +function isStrictNonEmpty(value) { + return value.length > 0 && value === value.trim(); +} +function isValidDuration(value) { + if (!isStrictNonEmpty(value)) + return false; + const multipliers = { + ms: 1, + s: 1_000, + m: 60_000, + h: 3_600_000, + d: 86_400_000, + }; + const normalized = value.toLowerCase(); + const single = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(normalized); + if (single) { + return Number.isSafeInteger(Math.round(Number(single[1]) * multipliers[single[2] ?? "m"])); + } + let totalMs = 0; + let consumed = 0; + for (const match of normalized.matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h|d)/g)) { + if (match.index !== consumed) + return false; + totalMs += Number(match[1]) * multipliers[match[2]]; + consumed += match[0].length; + } + return (consumed === normalized.length && consumed > 0 && Number.isSafeInteger(Math.round(totalMs))); +} +function validateOpenClawProfile(value, profilePath) { + const parsed = OpenClawProfileSchema(value); + if (parsed instanceof ArkErrors) { + return Array.from(parsed, (error) => issue("invalid_openclaw_profile", `${profilePath}${error.path.length > 0 ? `.${error.path.join(".")}` : ""}`, error.description ?? "Invalid value.")); + } + const issues = []; + const add = (path, message) => issues.push(issue("invalid_openclaw_profile", `${profilePath}.${path}`, message)); + const requireNonEmpty = (path, values) => { + if (values !== undefined && values.length === 0) + add(path, "Must contain at least one value."); + for (const [index, entry] of (values ?? []).entries()) { + if (!isStrictNonEmpty(entry)) { + add(`${path}.${index}`, "Must be non-empty without leading or trailing whitespace."); + } + } + }; + requireNonEmpty("agent.groupChat.mentionPatterns", parsed.agent.groupChat?.mentionPatterns); + if (parsed.agent.tools?.profile !== undefined && !isStrictNonEmpty(parsed.agent.tools.profile)) { + add("agent.tools.profile", "Must be non-empty without leading or trailing whitespace."); + } + else if (parsed.agent.tools?.profile !== undefined && + !OPENCLAW_TOOL_PROFILE_IDS.has(parsed.agent.tools.profile)) { + add("agent.tools.profile", "Must name a registered OpenClaw built-in profile."); + } + requireNonEmpty("agent.tools.allow", parsed.agent.tools?.allow); + requireNonEmpty("agent.tools.alsoAllow", parsed.agent.tools?.alsoAllow); + requireNonEmpty("agent.tools.deny", parsed.agent.tools?.deny); + if (parsed.agent.tools?.allow && parsed.agent.tools.alsoAllow) { + add("agent.tools.alsoAllow", "Must not be combined with tools.allow."); + } + if (parsed.agent.memory?.search?.sources?.length === 0) { + add("agent.memory.search.sources", "Must contain at least one source."); + } + if (parsed.agent.memory?.search?.sources?.includes("sessions") && + parsed.agent.memory.search.rememberAcrossConversations !== true) { + add("agent.memory.search.rememberAcrossConversations", "Must be true when memory.search.sources includes sessions."); + } + const heartbeat = parsed.agent.heartbeat; + if (heartbeat?.every !== undefined && !isValidDuration(heartbeat.every)) { + add("agent.heartbeat.every", "Must be a valid duration."); + } + const activeHours = heartbeat?.activeHours; + if (activeHours?.start !== undefined && !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(activeHours.start)) { + add("agent.heartbeat.activeHours.start", "Must be a valid 24-hour start time."); + } + if (activeHours?.end !== undefined && + !/^(?:(?:[01]\d|2[0-3]):[0-5]\d|24:00)$/.test(activeHours.end)) { + add("agent.heartbeat.activeHours.end", "Must be a valid 24-hour end time."); + } + if (activeHours?.timezone !== undefined) { + try { + new Intl.DateTimeFormat("en-US", { timeZone: activeHours.timezone }).format(); + } + catch { + add("agent.heartbeat.activeHours.timezone", "Must be a valid IANA timezone."); + } + } + if (heartbeat?.timeoutSeconds !== undefined && + (!Number.isInteger(heartbeat.timeoutSeconds) || heartbeat.timeoutSeconds <= 0)) { + add("agent.heartbeat.timeoutSeconds", "Must be a positive integer."); + } + for (const field of ["minMs", "maxMs"]) { + const delay = parsed.agent.humanDelay?.[field]; + if (delay !== undefined && (!Number.isInteger(delay) || delay < 0)) { + add(`agent.humanDelay.${field}`, "Must be a nonnegative integer."); + } + } + return issues; +} +function parseManifestDocument(raw, manifestPath) { + const filename = manifestPath.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase(); + if (filename === "claw.md") { + const markdown = raw.startsWith("\uFEFF") ? raw.slice(1) : raw; + const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) { + return { + issues: [ + issue("missing_claw_frontmatter", manifestPath, `${manifestPath} must start with YAML frontmatter delimited by --- lines.`), + ], + }; + } + const document = parseDocument(match[1], { prettyErrors: false, uniqueKeys: true }); + if (document.errors.length > 0) { + return { + issues: document.errors.map((error) => issue("invalid_claw_frontmatter", manifestPath, error.message)), + }; + } + let unsupportedFeature; + visit(document, { + Alias() { + unsupportedFeature ??= "aliases"; + }, + Node(_key, node) { + if (node.anchor) { + unsupportedFeature ??= "anchors"; + } + else if (node.tag) { + unsupportedFeature ??= "explicit tags"; + } + }, + Pair(_key, pair) { + if (!isScalar(pair.key) || typeof pair.key.value !== "string") { + unsupportedFeature ??= "non-string mapping keys"; + } + else if (pair.key.value === "<<") { + unsupportedFeature ??= "merge keys"; + } + }, + }); + if (unsupportedFeature) { + return { + issues: [ + issue("unsupported_claw_yaml_feature", manifestPath, `${manifestPath} uses ${unsupportedFeature}; CLAW.md frontmatter must map directly to JSON data.`), + ], + }; + } + try { + return { + value: document.toJSON(), + markdownBody: markdown.slice(match[0].length), + }; + } + catch (error) { + return { + issues: [ + issue("invalid_claw_frontmatter", manifestPath, error instanceof Error ? error.message : "Could not parse Claw frontmatter."), + ], + }; + } + } + try { + return { value: JSON.parse(raw) }; + } + catch (error) { + return { + issues: [ + issue("invalid_claw_json", manifestPath, error instanceof Error ? error.message : "Could not parse Claw JSON."), + ], + }; + } +} +export function validateClawPackageContents(input) { + const issues = []; + if (!isRecord(input.packageJson)) { + return { + ok: false, + issues: [ + issue("missing_package_json", "package.json", "Claw packages require package.json."), + ], + }; + } + const declaredName = typeof input.packageJson.name === "string" ? input.packageJson.name : ""; + const declaredVersion = typeof input.packageJson.version === "string" ? input.packageJson.version : ""; + const openclaw = isRecord(input.packageJson.openclaw) ? input.packageJson.openclaw : undefined; + const manifestPath = typeof openclaw?.claw === "string" ? openclaw.claw : ""; + if (declaredName !== input.packageName) { + issues.push(issue("package_name_mismatch", "package.json.name", `Must match ${input.packageName}.`)); + } + if (!EXACT_VERSION_PATTERN.test(declaredVersion) || declaredVersion !== input.version) { + issues.push(issue("package_version_mismatch", "package.json.version", `Must be exact semver ${input.version}.`)); + } + if (!manifestPath || !isSafeClawPackagePath(manifestPath)) { + issues.push(issue("invalid_claw_manifest_path", "package.json.openclaw.claw", "Must name a safe package-relative Claw manifest path.")); + } + if (issues.length > 0) + return { ok: false, issues }; + const fileByPath = new Map(); + const portablePaths = new Set(); + for (const file of input.files) { + if (!isSafeClawPackagePath(file.path)) { + issues.push(issue("invalid_package_path", file.path, "Package files must use safe canonical package-relative paths.")); + continue; + } + const key = portablePathKey(file.path); + if (portablePaths.has(key)) { + issues.push(issue("duplicate_portable_path", file.path, "Package paths must be unique across supported filesystems.")); + } + else { + portablePaths.add(key); + fileByPath.set(file.path, file); + } + } + const hierarchyCollision = findClawPackagePathHierarchyCollision([...fileByPath.keys()]); + if (hierarchyCollision) { + issues.push(issue("portable_path_hierarchy_collision", hierarchyCollision.descendant, `Package file ${hierarchyCollision.ancestor} cannot also be an ancestor of ${hierarchyCollision.descendant}.`)); + } + const manifestFile = fileByPath.get(manifestPath); + if (!manifestFile || manifestFile.text === undefined) { + issues.push(issue("missing_claw_manifest", manifestPath, "The declared Claw manifest is missing or is not UTF-8 text.")); + return { ok: false, issues }; + } + if (new TextEncoder().encode(manifestFile.text).byteLength > MAX_CLAW_MANIFEST_BYTES) { + return { + ok: false, + issues: [ + issue("claw_manifest_too_large", manifestPath, `The Claw manifest exceeds ${MAX_CLAW_MANIFEST_BYTES} bytes.`), + ], + }; + } + const parsed = parseManifestDocument(manifestFile.text, manifestPath); + if (parsed.issues) + return { ok: false, issues: parsed.issues }; + if (parsed.markdownBody !== undefined && parsed.markdownBody.trim().length === 0) { + return { + ok: false, + issues: [ + issue("empty_claw_body", manifestPath, "CLAW.md must contain a non-empty Markdown body."), + ], + }; + } + const validated = validateClawManifest(parsed.value); + if (!validated.ok) { + return { + ok: false, + issues: validated.issues.map((entry) => issue("invalid_claw_manifest", entry.path, entry.message)), + }; + } + if (parsed.markdownBody !== undefined && + (validated.manifest.workspace?.bootstrapFiles?.["SOUL.md"] !== undefined || + (validated.manifest.workspace?.files ?? []).some((entry) => portablePathKey(entry.path) === portablePathKey("SOUL.md")))) { + return { + ok: false, + issues: [ + issue("duplicate_claw_body_destination", "SOUL.md", "The CLAW.md body is the managed SOUL.md source and cannot be combined with an explicit SOUL.md declaration."), + ], + }; + } + const openClawProfilePath = validated.manifest.metadata?.["openclaw.config"]; + if (openClawProfilePath !== undefined) { + const profileFile = fileByPath.get(openClawProfilePath); + if (!profileFile || profileFile.text === undefined) { + issues.push(issue("missing_openclaw_profile", openClawProfilePath, "The declared OpenClaw profile is missing or is not UTF-8 text.")); + } + else if (new TextEncoder().encode(profileFile.text).byteLength > MAX_OPENCLAW_PROFILE_BYTES) { + issues.push(issue("openclaw_profile_too_large", openClawProfilePath, `The OpenClaw profile exceeds ${MAX_OPENCLAW_PROFILE_BYTES} bytes.`)); + } + else { + const profile = parseJsonCompatibleYaml(profileFile.text, openClawProfilePath); + if (profile.issues) { + issues.push(...profile.issues); + } + else { + issues.push(...validateOpenClawProfile(profile.value, openClawProfilePath)); + } + } + } + const sources = [ + ...Object.values(validated.manifest.workspace?.bootstrapFiles ?? {}).map((entry) => entry.source), + ...(validated.manifest.workspace?.files ?? []).map((entry) => entry.source), + ]; + for (const source of sources) { + if (!fileByPath.has(source)) { + issues.push(issue("missing_workspace_source", source, "Declared workspace source is missing from the package.")); + } + } + if (issues.length > 0) + return { ok: false, issues }; + const summary = summarizeClawManifest(validated.manifest); + const implicitWorkspaceFile = parsed.markdownBody === undefined + ? undefined + : { path: "SOUL.md", text: parsed.markdownBody }; + return { + ok: true, + value: { + manifestPath, + manifest: validated.manifest, + ...(implicitWorkspaceFile ? { implicitWorkspaceFile } : {}), + summary: implicitWorkspaceFile + ? { + ...summary, + workspace: { + ...summary.workspace, + bootstrapFiles: [...summary.workspace.bootstrapFiles, "SOUL.md"], + }, + } + : summary, + }, + }; +} +//# sourceMappingURL=clawPackage.js.map \ No newline at end of file diff --git a/packages/schema/dist/clawPackage.js.map b/packages/schema/dist/clawPackage.js.map new file mode 100644 index 00000000..5422aa32 --- /dev/null +++ b/packages/schema/dist/clawPackage.js.map @@ -0,0 +1 @@ +{"version":3,"file":"clawPackage.js","sourceRoot":"","sources":["../src/clawPackage.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/dist/index.d.ts b/packages/schema/dist/index.d.ts index d03a08a7..8d58b85d 100644 --- a/packages/schema/dist/index.d.ts +++ b/packages/schema/dist/index.d.ts @@ -1,6 +1,7 @@ export type { ArkValidator } from "./ark.js"; export { formatArkErrors, parseArk } from "./ark.js"; export * from "./claws.js"; +export * from "./clawPackage.js"; export * from "./catalogFeed.js"; export * from "./catalogMetadata.js"; export * from "./docsLinks.js"; diff --git a/packages/schema/dist/index.js b/packages/schema/dist/index.js index 2ded7f40..6633f43e 100644 --- a/packages/schema/dist/index.js +++ b/packages/schema/dist/index.js @@ -1,5 +1,6 @@ export { formatArkErrors, parseArk } from "./ark.js"; export * from "./claws.js"; +export * from "./clawPackage.js"; export * from "./catalogFeed.js"; export * from "./catalogMetadata.js"; export * from "./docsLinks.js"; diff --git a/packages/schema/dist/index.js.map b/packages/schema/dist/index.js.map index b5e68b2e..bb84e201 100644 --- a/packages/schema/dist/index.js.map +++ b/packages/schema/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACrD,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACzD,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACrD,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACzD,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"} \ No newline at end of file diff --git a/packages/schema/dist/packages.d.ts b/packages/schema/dist/packages.d.ts index f8acebaf..27415770 100644 --- a/packages/schema/dist/packages.d.ts +++ b/packages/schema/dist/packages.d.ts @@ -10,7 +10,7 @@ export declare function getPackageScopeOwnerMismatch(name: string, ownerHandle: } | null; export declare const PackageFamilySchema: import("arktype/internal/variants/string.ts").StringType<"bundle-plugin" | "claw" | "code-plugin" | "skill", {}>; export type PackageFamily = (typeof PackageFamilySchema)[inferred]; -export declare const PackagePublishFamilySchema: import("arktype/internal/variants/string.ts").StringType<"bundle-plugin" | "code-plugin" | "skill", {}>; +export declare const PackagePublishFamilySchema: import("arktype/internal/variants/string.ts").StringType<"bundle-plugin" | "claw" | "code-plugin" | "skill", {}>; export type PackagePublishFamily = (typeof PackagePublishFamilySchema)[inferred]; export declare const PackageChannelSchema: import("arktype/internal/variants/string.ts").StringType<"community" | "official" | "private", {}>; export type PackageChannel = (typeof PackageChannelSchema)[inferred]; @@ -265,7 +265,7 @@ export declare const PackagePublishMetadataSchema: import("arktype/internal/vari name: string; displayName?: string | undefined; ownerHandle?: string | undefined; - family: "bundle-plugin" | "code-plugin" | "skill"; + family: "bundle-plugin" | "claw" | "code-plugin" | "skill"; version: string; changelog: string; manualOverrideReason?: string | undefined; @@ -293,7 +293,7 @@ export declare const ServerPackagePublishRequestSchema: import("arktype/internal name: string; displayName?: string | undefined; ownerHandle?: string | undefined; - family: "bundle-plugin" | "code-plugin" | "skill"; + family: "bundle-plugin" | "claw" | "code-plugin" | "skill"; version: string; changelog: string; manualOverrideReason?: string | undefined; diff --git a/packages/schema/dist/packages.js b/packages/schema/dist/packages.js index b3f1a5ca..00a0aeb5 100644 --- a/packages/schema/dist/packages.js +++ b/packages/schema/dist/packages.js @@ -23,8 +23,7 @@ export function getPackageScopeOwnerMismatch(name, ownerHandle) { }; } export const PackageFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"|"claw"'); -// Publication stays narrower until a family has a complete validation path. -export const PackagePublishFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"'); +export const PackagePublishFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"|"claw"'); export const PackageChannelSchema = type('"official"|"community"|"private"'); export const PackageVerificationTierSchema = type('"structural"|"source-linked"|"provenance-verified"|"rebuild-verified"'); export const PackageVerificationScopeSchema = type('"artifact-only"|"dependency-graph-aware"'); diff --git a/packages/schema/dist/packages.js.map b/packages/schema/dist/packages.js.map index fc72befb..6fca5eaa 100644 --- a/packages/schema/dist/packages.js.map +++ b/packages/schema/dist/packages.js.map @@ -1 +1 @@ -{"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/package.json b/packages/schema/package.json index c86b85ba..8c9d5847 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -32,7 +32,8 @@ "dependencies": { "arktype": "2.2.3", "croner": "10.0.1", - "unicode-case-folding": "1.1.1" + "unicode-case-folding": "1.1.1", + "yaml": "2.9.0" }, "devDependencies": { "@typescript/native": "npm:typescript@7.0.2", diff --git a/packages/schema/src/clawPackage.test.ts b/packages/schema/src/clawPackage.test.ts new file mode 100644 index 00000000..9c9e0e97 --- /dev/null +++ b/packages/schema/src/clawPackage.test.ts @@ -0,0 +1,630 @@ +import { describe, expect, it } from "vitest"; +import { + findClawPackagePathHierarchyCollision, + validateClawPackageContents, +} from "./clawPackage.js"; + +const manifest = { + schemaVersion: 1, + agent: { id: "github-triage", name: "GitHub Triage", description: "Reviews issues." }, + workspace: { + bootstrapFiles: {}, + files: [{ source: "workspace/reference.md", path: "reference.md" }], + }, + packages: [{ kind: "skill", source: "clawhub", ref: "@acme/triage", version: "1.2.0" }], + mcpServers: {}, + cronJobs: [], +}; +const openClawProfile = [ + "schemaVersion: 1", + "agent:", + " groupChat:", + " mentionPatterns: ['@triage']", + " sandbox: { mode: non-main, scope: agent, workspaceAccess: rw }", + " tools:", + " profile: coding", + " alsoAllow: [cron]", + " deny: [gateway]", + " fs: { workspaceOnly: true }", + " memory:", + " search:", + " enabled: true", + " rememberAcrossConversations: true", + " sources: [memory, sessions]", + " heartbeat:", + " every: 30m", + " activeHours: { start: '09:00', end: '24:00', timezone: America/Los_Angeles }", + " lightContext: true", + " isolatedSession: false", + " timeoutSeconds: 30", + " humanDelay: { mode: custom, minMs: 0, maxMs: 2000 }", +].join("\n"); + +function packageJson(claw = "CLAW.md") { + return { + name: "@acme/github-triage", + version: "1.0.0", + openclaw: { claw }, + }; +} + +function files(manifestText = `---\n${JSON.stringify(manifest)}\n---\n# GitHub Triage\n`) { + return [ + { path: "package.json", text: JSON.stringify(packageJson()) }, + { path: "CLAW.md", text: manifestText }, + { path: "workspace/SOUL.md", text: "Be precise.\n" }, + { path: "workspace/reference.md", text: "Reference\n" }, + ]; +} + +describe("validateClawPackageContents", () => { + it("finds hierarchy collisions even when another path sorts between them", () => { + expect(findClawPackagePathHierarchyCollision(["a", "a-0", "a/child"])).toEqual({ + ancestor: "a", + descendant: "a/child", + }); + }); + + it("finds hierarchy collisions after full Unicode case folding", () => { + expect(findClawPackagePathHierarchyCollision(["Straße", "STRASSE/child"])).toEqual({ + ancestor: "Straße", + descendant: "STRASSE/child", + }); + }); + + it("validates CLAW.md and derives its safe summary", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files(), + }); + + expect(result).toEqual({ + ok: true, + value: expect.objectContaining({ + manifestPath: "CLAW.md", + implicitWorkspaceFile: { + path: "SOUL.md", + text: "# GitHub Triage\n", + }, + summary: expect.objectContaining({ + agent: { + id: "github-triage", + name: "GitHub Triage", + description: "Reviews issues.", + }, + packages: { skillCount: 1, pluginCount: 0 }, + workspace: { bootstrapFiles: ["SOUL.md"], fileCount: 1 }, + }), + }), + }); + }); + + it("accepts one UTF-8 BOM before CLAW.md frontmatter", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files(`\uFEFF---\n${JSON.stringify(manifest)}\n---\n# GitHub Triage\n`), + }); + + expect(result.ok).toBe(true); + }); + + it.each(["", "\n \t\n"])("rejects an empty CLAW.md body", (body) => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files(`---\n${JSON.stringify(manifest)}\n---\n${body}`), + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code: "empty_claw_body", path: "CLAW.md" })], + }); + }); + + it("rejects a CLAW.md envelope larger than 1 MiB", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files(`---\n${JSON.stringify(manifest)}\n---\n${"x".repeat(1024 * 1024)}`), + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code: "claw_manifest_too_large", path: "CLAW.md" })], + }); + }); + + it.each([ + [ + "bootstrap source", + { + ...manifest, + workspace: { + ...manifest.workspace, + bootstrapFiles: { "SOUL.md": { source: "workspace/SOUL.md" } }, + }, + }, + ], + [ + "case-folded supporting-file destination", + { + ...manifest, + workspace: { + ...manifest.workspace, + files: [{ source: "workspace/reference.md", path: "soul.md" }], + }, + }, + ], + ])("rejects an explicit SOUL.md %s beside the CLAW.md body", (_label, bodyManifest) => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files(`---\n${JSON.stringify(bodyManifest)}\n---\n# Prompt\n`), + }); + + expect(result).toEqual({ + ok: false, + issues: [ + expect.objectContaining({ code: "duplicate_claw_body_destination", path: "SOUL.md" }), + ], + }); + }); + + it("validates a package-local OpenClaw profile without returning it", () => { + const profiledManifest = { + ...manifest, + metadata: { "openclaw.config": "profiles/openclaw.yml" }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [ + ...files(`---\n${JSON.stringify(profiledManifest)}\n---\n# Prompt\n`), + { path: "profiles/openclaw.yml", text: openClawProfile }, + ], + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value).not.toHaveProperty("profile"); + expect(result.value.manifest.agent).toEqual(profiledManifest.agent); + } + }); + + it("rejects an unregistered OpenClaw tool profile", () => { + const profiledManifest = { + ...manifest, + metadata: { "openclaw.config": "profiles/openclaw.yml" }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [ + ...files(`---\n${JSON.stringify(profiledManifest)}\n---\n# Prompt\n`), + { + path: "profiles/openclaw.yml", + text: "schemaVersion: 1\nagent:\n tools:\n profile: future-profile", + }, + ], + }); + + expect(result).toEqual({ + ok: false, + issues: [ + expect.objectContaining({ + code: "invalid_openclaw_profile", + path: "profiles/openclaw.yml.agent.tools.profile", + }), + ], + }); + }); + + it("requires the exact UTF-8 profile target", () => { + const profiledManifest = { + ...manifest, + metadata: { "openclaw.config": "profiles/openclaw.yml" }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [ + ...files(`---\n${JSON.stringify(profiledManifest)}\n---\n# Prompt\n`), + { path: "profiles/OPENCLAW.yml", text: openClawProfile }, + ], + }); + + expect(result).toEqual({ + ok: false, + issues: [ + expect.objectContaining({ + code: "missing_openclaw_profile", + path: "profiles/openclaw.yml", + }), + ], + }); + }); + + it("enforces the shared OpenClaw host environment policy through package validation", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files( + `---\n${JSON.stringify({ + ...manifest, + mcpServers: { + unsafe: { command: "server", env: { CPP: "${CPP}" } }, + }, + })}\n---\n# Prompt\n`, + ), + }); + + expect(result).toEqual({ + ok: false, + issues: [ + expect.objectContaining({ + code: "invalid_claw_manifest", + path: "$.mcpServers.unsafe.env.CPP", + }), + ], + }); + }); + + it.each([ + ["malformed YAML", "schemaVersion: [", "invalid_openclaw_profile"], + ["duplicate keys", "schemaVersion: 1\nschemaVersion: 1\nagent: {}", "invalid_openclaw_profile"], + [ + "aliases", + "schemaVersion: 1\nagent: &agent {}\ncopy: *agent", + "unsupported_openclaw_profile_yaml_feature", + ], + ["anchors", "schemaVersion: 1\nagent: &agent {}", "unsupported_openclaw_profile_yaml_feature"], + [ + "merge keys", + "schemaVersion: 1\nagent:\n <<: { tools: {} }", + "unsupported_openclaw_profile_yaml_feature", + ], + [ + "explicit tags", + "schemaVersion: 1\nagent: !!map {}", + "unsupported_openclaw_profile_yaml_feature", + ], + [ + "non-string mapping keys", + "schemaVersion: 1\nagent:\n ? [tools]\n : {}", + "unsupported_openclaw_profile_yaml_feature", + ], + ])("rejects OpenClaw profile %s", (_label, text, code) => { + const profiledManifest = { + ...manifest, + metadata: { "openclaw.config": "profiles/openclaw.yml" }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [ + ...files(`---\n${JSON.stringify(profiledManifest)}\n---\n# Prompt\n`), + { path: "profiles/openclaw.yml", text }, + ], + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code })], + }); + }); + + it("rejects an oversized OpenClaw profile", () => { + const profiledManifest = { + ...manifest, + metadata: { "openclaw.config": "profiles/openclaw.yml" }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [ + ...files(`---\n${JSON.stringify(profiledManifest)}\n---\n# Prompt\n`), + { path: "profiles/openclaw.yml", text: `#${"x".repeat(256 * 1024)}` }, + ], + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code: "openclaw_profile_too_large" })], + }); + }); + + it.each([ + ["unknown version", "schemaVersion: 2\nagent: {}"], + ["unknown field", "schemaVersion: 1\nagent:\n model: gpt-5"], + [ + "unsupported heartbeat field", + "schemaVersion: 1\nagent:\n heartbeat:\n skipWhenBusy: true", + ], + [ + "workspaceOnly false", + "schemaVersion: 1\nagent:\n tools:\n fs:\n workspaceOnly: false", + ], + [ + "allow conflict", + "schemaVersion: 1\nagent:\n tools:\n allow: [read]\n alsoAllow: [write]", + ], + [ + "sessions without consent", + "schemaVersion: 1\nagent:\n memory:\n search:\n sources: [sessions]", + ], + ])("rejects OpenClaw profile with %s", (_label, text) => { + const profiledManifest = { + ...manifest, + metadata: { "openclaw.config": "profiles/openclaw.yml" }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [ + ...files(`---\n${JSON.stringify(profiledManifest)}\n---\n# Prompt\n`), + { path: "profiles/openclaw.yml", text }, + ], + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code: "invalid_openclaw_profile" })], + }); + }); + + it.each([ + ["anchor", "agent: &agent { id: github-triage }"], + ["alias", "agent: { id: &id github-triage, name: *id }"], + ["merge key", "agent: { <<: { id: github-triage } }"], + ["explicit tag", "agent: { id: !!str github-triage }"], + ["non-string mapping key", "agent: { id: github-triage, true: nope }"], + ])("rejects CLAW.md YAML %s", (_label, declaration) => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files( + [ + "---", + "schemaVersion: 1", + declaration, + "workspace: {}", + "packages: []", + "mcpServers: {}", + "cronJobs: []", + "---", + ].join("\n"), + ), + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code: "unsupported_claw_yaml_feature" })], + }); + }); + + it("does not treat a suffix-only filename as CLAW.md", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson("NOTCLAW.md"), + files: [ + ...files().filter((file) => file.path !== "CLAW.md"), + { path: "NOTCLAW.md", text: `---\n${JSON.stringify(manifest)}\n---\n` }, + ], + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code: "invalid_claw_json" })], + }); + }); + + it("accepts the JSON compatibility manifest", () => { + const jsonManifest = { + ...manifest, + workspace: { + ...manifest.workspace, + bootstrapFiles: { "SOUL.md": { source: "workspace/SOUL.md" } }, + }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson("openclaw.claw.json"), + files: [ + ...files().filter((file) => file.path !== "CLAW.md"), + { path: "openclaw.claw.json", text: JSON.stringify(jsonManifest) }, + ], + }); + + expect(result.ok).toBe(true); + if (result.ok) expect(result.value).not.toHaveProperty("implicitWorkspaceFile"); + }); + + it("requires package identity to match the publication", () => { + const result = validateClawPackageContents({ + packageName: "@acme/other", + version: "2.0.0", + packageJson: packageJson(), + files: files(), + }); + + expect(result).toEqual({ + ok: false, + issues: expect.arrayContaining([ + expect.objectContaining({ code: "package_name_mismatch" }), + expect.objectContaining({ code: "package_version_mismatch" }), + ]), + }); + }); + + it.each([ + ["name", { ...packageJson(), name: " @acme/github-triage" }], + ["version", { ...packageJson(), version: "1.0.0 " }], + ["manifest path", packageJson(" CLAW.md")], + ])("does not trim padded package %s into validity", (_label, metadata) => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: metadata, + files: files(), + }); + + expect(result.ok).toBe(false); + }); + + it("rejects missing declared workspace sources", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files().filter((file) => file.path !== "workspace/reference.md"), + }); + + expect(result).toEqual({ + ok: false, + issues: [ + expect.objectContaining({ + code: "missing_workspace_source", + path: "workspace/reference.md", + }), + ], + }); + }); + + it("rejects duplicate paths that collide on portable filesystems", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [...files(), { path: "claw.md", text: "duplicate" }], + }); + + expect(result).toEqual({ + ok: false, + issues: expect.arrayContaining([ + expect.objectContaining({ code: "duplicate_portable_path" }), + ]), + }); + }); + + it.each([ + ["ancestor first", [{ path: "workspace", text: "file" }, ...files()]], + ["descendant first", [...files(), { path: "workspace", text: "file" }]], + ["case-folded ancestor", [...files(), { path: "WORKSPACE", text: "file" }]], + [ + "Unicode-normalized ancestor", + [ + ...files(), + { path: "cafe\u0301/reference.md", text: "child" }, + { path: "caf\u00e9", text: "file" }, + ], + ], + ])("rejects package file/ancestor collisions with %s", (_label, packageFiles) => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: packageFiles, + }); + + expect(result).toEqual({ + ok: false, + issues: expect.arrayContaining([ + expect.objectContaining({ code: "portable_path_hierarchy_collision" }), + ]), + }); + }); + + it.each(["CON", "workspace/trailing.", "workspace\\backslash.md"])( + "rejects unsafe unreferenced package path %s", + (path) => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [...files(), { path, text: "unused" }], + }); + + expect(result).toEqual({ + ok: false, + issues: expect.arrayContaining([expect.objectContaining({ code: "invalid_package_path" })]), + }); + }, + ); + + it.each([ + ["DEL", "\u007f"], + ["C1", "\u0085"], + ])("rejects %s control characters in package paths", (_label, controlCharacter) => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: [...files(), { path: `workspace/a${controlCharacter}.md`, text: "unused" }], + }); + + expect(result).toEqual({ + ok: false, + issues: expect.arrayContaining([expect.objectContaining({ code: "invalid_package_path" })]), + }); + }); + + it("requires exact path spelling for manifests and workspace sources", () => { + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files().map((file) => + file.path === "CLAW.md" + ? { ...file, path: "claw.md" } + : file.path === "workspace/reference.md" + ? { ...file, path: "workspace/REFERENCE.md" } + : file, + ), + }); + + expect(result).toEqual({ + ok: false, + issues: [expect.objectContaining({ code: "missing_claw_manifest" })], + }); + }); + + it("rejects resolved credentials through the shared manifest validator", () => { + const unsafe = { + ...manifest, + mcpServers: { github: { command: "npx", env: { GITHUB_TOKEN: "secret" } } }, + }; + const result = validateClawPackageContents({ + packageName: "@acme/github-triage", + version: "1.0.0", + packageJson: packageJson(), + files: files(`---\n${JSON.stringify(unsafe)}\n---\n# Prompt\n`), + }); + + expect(result).toEqual({ + ok: false, + issues: expect.arrayContaining([ + expect.objectContaining({ + code: "invalid_claw_manifest", + path: "$.mcpServers.github.env.GITHUB_TOKEN", + }), + ]), + }); + }); +}); diff --git a/packages/schema/src/clawPackage.ts b/packages/schema/src/clawPackage.ts new file mode 100644 index 00000000..fafe5ded --- /dev/null +++ b/packages/schema/src/clawPackage.ts @@ -0,0 +1,622 @@ +import { ArkErrors, type } from "arktype"; +import { caseFold } from "unicode-case-folding"; +import { isScalar, parseDocument, visit } from "yaml"; +import { + summarizeClawManifest, + validateClawManifest, + type ClawManifest, + type ClawManifestSummary, +} from "./claws.js"; + +export type ClawPackageTextFile = { path: string; text?: string }; +export type ClawPackageValidationIssue = { + code: string; + path: string; + message: string; +}; +export type ValidatedClawPackage = { + manifestPath: string; + manifest: ClawManifest; + implicitWorkspaceFile?: { path: "SOUL.md"; text: string }; + summary: ClawManifestSummary; +}; + +const EXACT_VERSION_PATTERN = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; +const WINDOWS_INVALID_PATH_CHARS = /[<>:"|?*]/; +const WINDOWS_RESERVED_PATH_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i; +const UNICODE_CONTROL_CHARACTER = /\p{Cc}/u; +const MAX_CLAW_MANIFEST_BYTES = 1024 * 1024; +const MAX_OPENCLAW_PROFILE_BYTES = 256 * 1024; +const OPENCLAW_TOOL_PROFILE_IDS = new Set(["minimal", "coding", "messaging", "full"]); +const StrictStringArraySchema = type("string[]"); +const OpenClawProfileSchema = type({ + "+": "reject", + schemaVersion: "1", + agent: { + "+": "reject", + groupChat: type({ + "+": "reject", + mentionPatterns: StrictStringArraySchema.optional(), + }).optional(), + sandbox: type({ + "+": "reject", + mode: '"off"|"non-main"|"all"?', + scope: '"session"|"agent"|"shared"?', + workspaceAccess: '"none"|"ro"|"rw"?', + }).optional(), + tools: type({ + "+": "reject", + profile: "string?", + allow: StrictStringArraySchema.optional(), + alsoAllow: StrictStringArraySchema.optional(), + deny: StrictStringArraySchema.optional(), + fs: type({ + "+": "reject", + workspaceOnly: "true?", + }).optional(), + }).optional(), + memory: type({ + "+": "reject", + search: type({ + "+": "reject", + enabled: "boolean?", + rememberAcrossConversations: "boolean?", + sources: type("('memory' | 'sessions')[]").optional(), + }).optional(), + }).optional(), + heartbeat: type({ + "+": "reject", + every: "string?", + activeHours: type({ + "+": "reject", + start: "string?", + end: "string?", + timezone: "string?", + }).optional(), + lightContext: "boolean?", + isolatedSession: "boolean?", + timeoutSeconds: "number?", + }).optional(), + humanDelay: type({ + "+": "reject", + mode: '"off"|"natural"|"custom"?', + minMs: "number?", + maxMs: "number?", + }).optional(), + }, +}); + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +export function isSafeClawPackagePath(value: string): boolean { + const normalized = value.replaceAll("\\", "/"); + if ( + !normalized || + normalized !== value || + normalized !== normalized.trim() || + normalized.startsWith("/") || + /^[A-Za-z]:\//.test(normalized) + ) { + return false; + } + return normalized + .split("/") + .every( + (segment) => + segment !== "" && + segment !== "." && + segment !== ".." && + !WINDOWS_INVALID_PATH_CHARS.test(segment) && + !UNICODE_CONTROL_CHARACTER.test(segment) && + !segment.endsWith(".") && + !segment.endsWith(" ") && + !WINDOWS_RESERVED_PATH_SEGMENT.test(segment), + ); +} + +function portablePathKey(value: string): string { + return caseFold(value.replaceAll("\\", "/").normalize("NFC")); +} + +export function findClawPackagePathHierarchyCollision( + paths: readonly string[], +): { ancestor: string; descendant: string } | null { + const pathByKey = new Map(paths.map((path) => [portablePathKey(path), path])); + for (const descendant of paths) { + const segments = portablePathKey(descendant).split("/"); + for (let length = 1; length < segments.length; length += 1) { + const ancestor = pathByKey.get(segments.slice(0, length).join("/")); + if (ancestor !== undefined) { + return { ancestor, descendant }; + } + } + } + return null; +} + +function issue(code: string, path: string, message: string): ClawPackageValidationIssue { + return { code, path, message }; +} + +function parseJsonCompatibleYaml(raw: string, path: string) { + const document = parseDocument(raw.startsWith("\uFEFF") ? raw.slice(1) : raw, { + prettyErrors: false, + uniqueKeys: true, + }); + if (document.errors.length > 0) { + return { + issues: document.errors.map((error) => + issue("invalid_openclaw_profile", path, error.message), + ), + }; + } + let unsupportedFeature: string | undefined; + visit(document, { + Alias() { + unsupportedFeature ??= "aliases"; + }, + Node(_key, node) { + if (node.anchor) { + unsupportedFeature ??= "anchors"; + } else if (node.tag) { + unsupportedFeature ??= "explicit tags"; + } + }, + Pair(_key, pair) { + if (!isScalar(pair.key) || typeof pair.key.value !== "string") { + unsupportedFeature ??= "non-string mapping keys"; + } else if (pair.key.value === "<<") { + unsupportedFeature ??= "merge keys"; + } + }, + }); + if (unsupportedFeature) { + return { + issues: [ + issue( + "unsupported_openclaw_profile_yaml_feature", + path, + `${path} uses ${unsupportedFeature}; OpenClaw profile YAML must map directly to JSON data.`, + ), + ], + }; + } + try { + return { value: document.toJSON() }; + } catch (error) { + return { + issues: [ + issue( + "invalid_openclaw_profile", + path, + error instanceof Error ? error.message : "Could not parse OpenClaw profile.", + ), + ], + }; + } +} + +function isStrictNonEmpty(value: string): boolean { + return value.length > 0 && value === value.trim(); +} + +function isValidDuration(value: string): boolean { + if (!isStrictNonEmpty(value)) return false; + const multipliers: Record = { + ms: 1, + s: 1_000, + m: 60_000, + h: 3_600_000, + d: 86_400_000, + }; + const normalized = value.toLowerCase(); + const single = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(normalized); + if (single) { + return Number.isSafeInteger(Math.round(Number(single[1]) * multipliers[single[2] ?? "m"])); + } + let totalMs = 0; + let consumed = 0; + for (const match of normalized.matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h|d)/g)) { + if (match.index !== consumed) return false; + totalMs += Number(match[1]) * multipliers[match[2]]; + consumed += match[0].length; + } + return ( + consumed === normalized.length && consumed > 0 && Number.isSafeInteger(Math.round(totalMs)) + ); +} + +function validateOpenClawProfile( + value: unknown, + profilePath: string, +): ClawPackageValidationIssue[] { + const parsed = OpenClawProfileSchema(value); + if (parsed instanceof ArkErrors) { + return Array.from(parsed, (error) => + issue( + "invalid_openclaw_profile", + `${profilePath}${error.path.length > 0 ? `.${error.path.join(".")}` : ""}`, + error.description ?? "Invalid value.", + ), + ); + } + const issues: ClawPackageValidationIssue[] = []; + const add = (path: string, message: string) => + issues.push(issue("invalid_openclaw_profile", `${profilePath}.${path}`, message)); + const requireNonEmpty = (path: string, values: string[] | undefined) => { + if (values !== undefined && values.length === 0) add(path, "Must contain at least one value."); + for (const [index, entry] of (values ?? []).entries()) { + if (!isStrictNonEmpty(entry)) { + add(`${path}.${index}`, "Must be non-empty without leading or trailing whitespace."); + } + } + }; + + requireNonEmpty("agent.groupChat.mentionPatterns", parsed.agent.groupChat?.mentionPatterns); + if (parsed.agent.tools?.profile !== undefined && !isStrictNonEmpty(parsed.agent.tools.profile)) { + add("agent.tools.profile", "Must be non-empty without leading or trailing whitespace."); + } else if ( + parsed.agent.tools?.profile !== undefined && + !OPENCLAW_TOOL_PROFILE_IDS.has(parsed.agent.tools.profile) + ) { + add("agent.tools.profile", "Must name a registered OpenClaw built-in profile."); + } + requireNonEmpty("agent.tools.allow", parsed.agent.tools?.allow); + requireNonEmpty("agent.tools.alsoAllow", parsed.agent.tools?.alsoAllow); + requireNonEmpty("agent.tools.deny", parsed.agent.tools?.deny); + if (parsed.agent.tools?.allow && parsed.agent.tools.alsoAllow) { + add("agent.tools.alsoAllow", "Must not be combined with tools.allow."); + } + if (parsed.agent.memory?.search?.sources?.length === 0) { + add("agent.memory.search.sources", "Must contain at least one source."); + } + if ( + parsed.agent.memory?.search?.sources?.includes("sessions") && + parsed.agent.memory.search.rememberAcrossConversations !== true + ) { + add( + "agent.memory.search.rememberAcrossConversations", + "Must be true when memory.search.sources includes sessions.", + ); + } + const heartbeat = parsed.agent.heartbeat; + if (heartbeat?.every !== undefined && !isValidDuration(heartbeat.every)) { + add("agent.heartbeat.every", "Must be a valid duration."); + } + const activeHours = heartbeat?.activeHours; + if (activeHours?.start !== undefined && !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(activeHours.start)) { + add("agent.heartbeat.activeHours.start", "Must be a valid 24-hour start time."); + } + if ( + activeHours?.end !== undefined && + !/^(?:(?:[01]\d|2[0-3]):[0-5]\d|24:00)$/.test(activeHours.end) + ) { + add("agent.heartbeat.activeHours.end", "Must be a valid 24-hour end time."); + } + if (activeHours?.timezone !== undefined) { + try { + new Intl.DateTimeFormat("en-US", { timeZone: activeHours.timezone }).format(); + } catch { + add("agent.heartbeat.activeHours.timezone", "Must be a valid IANA timezone."); + } + } + if ( + heartbeat?.timeoutSeconds !== undefined && + (!Number.isInteger(heartbeat.timeoutSeconds) || heartbeat.timeoutSeconds <= 0) + ) { + add("agent.heartbeat.timeoutSeconds", "Must be a positive integer."); + } + for (const field of ["minMs", "maxMs"] as const) { + const delay = parsed.agent.humanDelay?.[field]; + if (delay !== undefined && (!Number.isInteger(delay) || delay < 0)) { + add(`agent.humanDelay.${field}`, "Must be a nonnegative integer."); + } + } + return issues; +} + +function parseManifestDocument(raw: string, manifestPath: string) { + const filename = manifestPath.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase(); + if (filename === "claw.md") { + const markdown = raw.startsWith("\uFEFF") ? raw.slice(1) : raw; + const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) { + return { + issues: [ + issue( + "missing_claw_frontmatter", + manifestPath, + `${manifestPath} must start with YAML frontmatter delimited by --- lines.`, + ), + ], + }; + } + const document = parseDocument(match[1], { prettyErrors: false, uniqueKeys: true }); + if (document.errors.length > 0) { + return { + issues: document.errors.map((error) => + issue("invalid_claw_frontmatter", manifestPath, error.message), + ), + }; + } + let unsupportedFeature: string | undefined; + visit(document, { + Alias() { + unsupportedFeature ??= "aliases"; + }, + Node(_key, node) { + if (node.anchor) { + unsupportedFeature ??= "anchors"; + } else if (node.tag) { + unsupportedFeature ??= "explicit tags"; + } + }, + Pair(_key, pair) { + if (!isScalar(pair.key) || typeof pair.key.value !== "string") { + unsupportedFeature ??= "non-string mapping keys"; + } else if (pair.key.value === "<<") { + unsupportedFeature ??= "merge keys"; + } + }, + }); + if (unsupportedFeature) { + return { + issues: [ + issue( + "unsupported_claw_yaml_feature", + manifestPath, + `${manifestPath} uses ${unsupportedFeature}; CLAW.md frontmatter must map directly to JSON data.`, + ), + ], + }; + } + try { + return { + value: document.toJSON(), + markdownBody: markdown.slice(match[0].length), + }; + } catch (error) { + return { + issues: [ + issue( + "invalid_claw_frontmatter", + manifestPath, + error instanceof Error ? error.message : "Could not parse Claw frontmatter.", + ), + ], + }; + } + } + try { + return { value: JSON.parse(raw) as unknown }; + } catch (error) { + return { + issues: [ + issue( + "invalid_claw_json", + manifestPath, + error instanceof Error ? error.message : "Could not parse Claw JSON.", + ), + ], + }; + } +} + +export function validateClawPackageContents(input: { + packageName: string; + version: string; + packageJson: unknown; + files: readonly ClawPackageTextFile[]; +}): + | { ok: true; value: ValidatedClawPackage } + | { ok: false; issues: ClawPackageValidationIssue[] } { + const issues: ClawPackageValidationIssue[] = []; + if (!isRecord(input.packageJson)) { + return { + ok: false, + issues: [ + issue("missing_package_json", "package.json", "Claw packages require package.json."), + ], + }; + } + const declaredName = typeof input.packageJson.name === "string" ? input.packageJson.name : ""; + const declaredVersion = + typeof input.packageJson.version === "string" ? input.packageJson.version : ""; + const openclaw = isRecord(input.packageJson.openclaw) ? input.packageJson.openclaw : undefined; + const manifestPath = typeof openclaw?.claw === "string" ? openclaw.claw : ""; + if (declaredName !== input.packageName) { + issues.push( + issue("package_name_mismatch", "package.json.name", `Must match ${input.packageName}.`), + ); + } + if (!EXACT_VERSION_PATTERN.test(declaredVersion) || declaredVersion !== input.version) { + issues.push( + issue( + "package_version_mismatch", + "package.json.version", + `Must be exact semver ${input.version}.`, + ), + ); + } + if (!manifestPath || !isSafeClawPackagePath(manifestPath)) { + issues.push( + issue( + "invalid_claw_manifest_path", + "package.json.openclaw.claw", + "Must name a safe package-relative Claw manifest path.", + ), + ); + } + if (issues.length > 0) return { ok: false, issues }; + + const fileByPath = new Map(); + const portablePaths = new Set(); + for (const file of input.files) { + if (!isSafeClawPackagePath(file.path)) { + issues.push( + issue( + "invalid_package_path", + file.path, + "Package files must use safe canonical package-relative paths.", + ), + ); + continue; + } + const key = portablePathKey(file.path); + if (portablePaths.has(key)) { + issues.push( + issue( + "duplicate_portable_path", + file.path, + "Package paths must be unique across supported filesystems.", + ), + ); + } else { + portablePaths.add(key); + fileByPath.set(file.path, file); + } + } + const hierarchyCollision = findClawPackagePathHierarchyCollision([...fileByPath.keys()]); + if (hierarchyCollision) { + issues.push( + issue( + "portable_path_hierarchy_collision", + hierarchyCollision.descendant, + `Package file ${hierarchyCollision.ancestor} cannot also be an ancestor of ${hierarchyCollision.descendant}.`, + ), + ); + } + const manifestFile = fileByPath.get(manifestPath); + if (!manifestFile || manifestFile.text === undefined) { + issues.push( + issue( + "missing_claw_manifest", + manifestPath, + "The declared Claw manifest is missing or is not UTF-8 text.", + ), + ); + return { ok: false, issues }; + } + if (new TextEncoder().encode(manifestFile.text).byteLength > MAX_CLAW_MANIFEST_BYTES) { + return { + ok: false, + issues: [ + issue( + "claw_manifest_too_large", + manifestPath, + `The Claw manifest exceeds ${MAX_CLAW_MANIFEST_BYTES} bytes.`, + ), + ], + }; + } + const parsed = parseManifestDocument(manifestFile.text, manifestPath); + if (parsed.issues) return { ok: false, issues: parsed.issues }; + if (parsed.markdownBody !== undefined && parsed.markdownBody.trim().length === 0) { + return { + ok: false, + issues: [ + issue("empty_claw_body", manifestPath, "CLAW.md must contain a non-empty Markdown body."), + ], + }; + } + const validated = validateClawManifest(parsed.value); + if (!validated.ok) { + return { + ok: false, + issues: validated.issues.map((entry) => + issue("invalid_claw_manifest", entry.path, entry.message), + ), + }; + } + if ( + parsed.markdownBody !== undefined && + (validated.manifest.workspace?.bootstrapFiles?.["SOUL.md"] !== undefined || + (validated.manifest.workspace?.files ?? []).some( + (entry) => portablePathKey(entry.path) === portablePathKey("SOUL.md"), + )) + ) { + return { + ok: false, + issues: [ + issue( + "duplicate_claw_body_destination", + "SOUL.md", + "The CLAW.md body is the managed SOUL.md source and cannot be combined with an explicit SOUL.md declaration.", + ), + ], + }; + } + + const openClawProfilePath = validated.manifest.metadata?.["openclaw.config"]; + if (openClawProfilePath !== undefined) { + const profileFile = fileByPath.get(openClawProfilePath); + if (!profileFile || profileFile.text === undefined) { + issues.push( + issue( + "missing_openclaw_profile", + openClawProfilePath, + "The declared OpenClaw profile is missing or is not UTF-8 text.", + ), + ); + } else if (new TextEncoder().encode(profileFile.text).byteLength > MAX_OPENCLAW_PROFILE_BYTES) { + issues.push( + issue( + "openclaw_profile_too_large", + openClawProfilePath, + `The OpenClaw profile exceeds ${MAX_OPENCLAW_PROFILE_BYTES} bytes.`, + ), + ); + } else { + const profile = parseJsonCompatibleYaml(profileFile.text, openClawProfilePath); + if (profile.issues) { + issues.push(...profile.issues); + } else { + issues.push(...validateOpenClawProfile(profile.value, openClawProfilePath)); + } + } + } + + const sources = [ + ...Object.values(validated.manifest.workspace?.bootstrapFiles ?? {}).map( + (entry) => entry.source, + ), + ...(validated.manifest.workspace?.files ?? []).map((entry) => entry.source), + ]; + for (const source of sources) { + if (!fileByPath.has(source)) { + issues.push( + issue( + "missing_workspace_source", + source, + "Declared workspace source is missing from the package.", + ), + ); + } + } + if (issues.length > 0) return { ok: false, issues }; + const summary = summarizeClawManifest(validated.manifest); + const implicitWorkspaceFile = + parsed.markdownBody === undefined + ? undefined + : { path: "SOUL.md" as const, text: parsed.markdownBody }; + return { + ok: true, + value: { + manifestPath, + manifest: validated.manifest, + ...(implicitWorkspaceFile ? { implicitWorkspaceFile } : {}), + summary: implicitWorkspaceFile + ? { + ...summary, + workspace: { + ...summary.workspace, + bootstrapFiles: [...summary.workspace.bootstrapFiles, "SOUL.md"], + }, + } + : summary, + }, + }; +} diff --git a/packages/schema/src/claws.test.ts b/packages/schema/src/claws.test.ts index e591ae02..23c5969c 100644 --- a/packages/schema/src/claws.test.ts +++ b/packages/schema/src/claws.test.ts @@ -50,7 +50,7 @@ const fixture = { } as const; describe("Claw manifest contract", () => { - it("allows Claws in storage contracts without opening generic publication", () => { + it("allows Claws in storage and publication metadata contracts", () => { expect(PackageFamilySchema("claw")).toBe("claw"); expect( PackagePublishMetadataSchema({ @@ -59,7 +59,7 @@ describe("Claw manifest contract", () => { version: "1.0.0", changelog: "Initial release", }), - ).toBeInstanceOf(ArkErrors); + ).not.toBeInstanceOf(ArkErrors); }); it("accepts the grouped OpenClaw v1 fixture and derives a safe summary", () => { diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index d03a08a7..8d58b85d 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,6 +1,7 @@ export type { ArkValidator } from "./ark.js"; export { formatArkErrors, parseArk } from "./ark.js"; export * from "./claws.js"; +export * from "./clawPackage.js"; export * from "./catalogFeed.js"; export * from "./catalogMetadata.js"; export * from "./docsLinks.js"; diff --git a/packages/schema/src/packages.ts b/packages/schema/src/packages.ts index 13e838df..b4ddf1f3 100644 --- a/packages/schema/src/packages.ts +++ b/packages/schema/src/packages.ts @@ -29,8 +29,7 @@ export function getPackageScopeOwnerMismatch(name: string, ownerHandle: string | export const PackageFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"|"claw"'); export type PackageFamily = (typeof PackageFamilySchema)[inferred]; -// Publication stays narrower until a family has a complete validation path. -export const PackagePublishFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"'); +export const PackagePublishFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"|"claw"'); export type PackagePublishFamily = (typeof PackagePublishFamilySchema)[inferred]; export const PackageChannelSchema = type('"official"|"community"|"private"'); diff --git a/specs/claws.md b/specs/claws.md index ea8634f1..0ea88c8d 100644 --- a/specs/claws.md +++ b/specs/claws.md @@ -8,9 +8,12 @@ schema. ClawHub owns publication, ownership, discovery, package detail APIs, and hosted feed export. OpenClaw remains authoritative for local planning, consent, mutation, provenance, update, and removal. -The YAML frontmatter is the portable manifest. The `CLAW.md` body remains -documentation-only in the current contract and may be empty; it does not imply -or replace any managed workspace file. +The YAML frontmatter is the portable manifest. A `CLAW.md` package envelope +must have a non-empty body; its exact body text is the implicit managed +`SOUL.md` workspace file. The manifest must not also declare an explicit +`SOUL.md` destination. The exact `CLAW.md` bytes, including the body, remain in +the immutable artifact digest and provenance input. Grouped JSON has no body, +creates no implicit file, and may declare `SOUL.md` explicitly. The portable agent object carries only identity and purpose. Harness-specific settings live in package-local profiles addressed through opaque string @@ -18,10 +21,12 @@ metadata. OpenClaw recognizes `metadata.openclaw.config`; export conventionally uses `profiles/openclaw.yml`, but the pointer is normative and authors may use another safe package-relative YAML path. -That profile exists only inside the Claw package. It is validated during -publication and application, participates in package integrity, and is never -copied into ordinary OpenClaw configuration. Other harnesses may ignore -OpenClaw's namespaced key or define their own profile-pointer contract. +That profile exists only inside the Claw package. ClawHub requires the pointer +to resolve to an exact, bounded UTF-8 YAML package file and validates the +profile's strict v1 OpenClaw policy during publication. OpenClaw validates it +again during application, includes it in package integrity, and never copies it +into ordinary OpenClaw configuration. Other harnesses may ignore OpenClaw's +namespaced key or define their own profile-pointer contract. ## Experimental contract @@ -36,16 +41,15 @@ OpenClaw's namespaced key or define their own profile-pointer contract. ## Staged implementation -1. Add the shared grouped manifest contract, safe summary, and storage model. -2. Add feature-gated authenticated publication, validation, and authoring docs. +1. Add the shared grouped manifest contract, safe summary, and storage model + ([PR #3089](https://github.com/openclaw/clawhub/pull/3089)). +2. Add feature-gated authenticated publication, package-content validation, + CLI authoring support, and authoring docs + ([PR #3090](https://github.com/openclaw/clawhub/pull/3090)). 3. Add feature-gated search, detail, and API surfaces. 4. Add hosted feed export and a published-package end-to-end proof through OpenClaw `claws add --dry-run`. -The publish schema intentionally remains narrower than the storage family in -the first slice. This prevents existing generic package endpoints from -accepting Claws before their dedicated validation and gate are in place. - The shared validator follows the RFC's strict v1 contract: strings are not trimmed into validity, MCP package selectors must resolve exact versions, process environment keys follow OpenClaw's host-wide safety policy, and tool @@ -68,3 +72,14 @@ field structure is built from `createClawManifestSummarySchema` in both the public ArkType contract and the Convex storage validator. Convex cannot express the summary text-length caps, so publication must validate or derive summaries through the shared schema before storage. + +Claws use the existing package publication pipeline. `package.json` declares +the package identity, version, and package-relative `openclaw.claw` manifest +path. Publication parses `CLAW.md` YAML frontmatter or the JSON compatibility +form and validates the grouped manifest, referenced workspace files, and any +declared package-local OpenClaw profile. The release retains the exact artifact +and a bounded derived summary rather than duplicating the full manifest or +OpenClaw profile into Convex storage. The server rejects +`family: claw` before mutation when the experimental gate is disabled; the gate +does not bypass ownership, moderation, scanning, or release invariants when +enabled.