Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,41 @@ jobs:
- name: HTTP e2e
run: bun run ci:e2e-http

claws-openclaw-contract:
name: claws-openclaw-contract
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 25
env:
OPENCLAW_CONTRACT_SHA: 59fc573fb9e938a93b93522be6bf4d7bec0dbc6f

steps:
- uses: actions/checkout@v7.0.1

- uses: ./.github/actions/setup-bun

- name: Check out the pinned OpenClaw contract source
uses: actions/checkout@v7.0.1
with:
repository: openclaw/openclaw
ref: ${{ env.OPENCLAW_CONTRACT_SHA }}
path: .artifacts/openclaw-contract

- name: Set up OpenClaw Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.15.0

- name: Install pinned OpenClaw dependencies
working-directory: .artifacts/openclaw-contract
run: |
corepack enable
corepack pnpm install --frozen-lockfile

- name: Run ClawHub to OpenClaw contract proof
env:
OPENCLAW_CLAWS_CHECKOUT: ${{ github.workspace }}/.artifacts/openclaw-contract
run: bunx vitest run scripts/claws-feed-openclaw-e2e.test.ts --maxWorkers=1

static:
name: static
runs-on: ubuntu-latest
Expand Down
75 changes: 75 additions & 0 deletions convex/catalogFeed.runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/// <reference types="vite/client" />
/* @vitest-environment edge-runtime */
import { register as registerRateLimiter } from "@convex-dev/rate-limiter/test";
import { convexTest } from "convex-test";
import { afterEach, describe, expect, it, vi } from "vitest";
import { internal } from "./_generated/api";
import schema from "./schema";

const modules = import.meta.glob("./**/*.ts");
const digest = `sha256:${"a".repeat(64)}`;

describe("experimental Claw feed runtime", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it("stores and serves the exact publication only while the gate is enabled", async () => {
vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "1");
const t = convexTest(schema, modules);
registerRateLimiter(t);
const stored = await t.mutation(internal.catalogFeed.storeClawPublication, {
generatedAt: "2026-07-24T00:00:00.000Z",
expiresAt: "2026-07-25T00:00:00.000Z",
entries: [
{
type: "claw",
id: "@openclaw/runtime-proof",
title: "Runtime proof",
version: "1.0.0",
state: "available",
publisher: { id: "openclaw", trust: "official" },
clawManifestSummary: {
schemaVersion: 1,
agent: { id: "runtime-proof", name: "Runtime proof" },
workspace: { bootstrapFiles: ["SOUL.md"], fileCount: 1 },
packages: { skillCount: 0, pluginCount: 0 },
mcpServerCount: 0,
cronJobCount: 0,
},
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@openclaw/runtime-proof",
version: "1.0.0",
integrity: digest,
},
],
},
},
],
});
expect(stored).toMatchObject({
feedId: "clawhub-official-claws",
sequence: 1,
entryCount: 1,
});

const publication = await t.query(internal.catalogFeed.getLatestPublication, {
feedId: "clawhub-official-claws",
});
expect(publication?.payload).toContain('"id":"@openclaw/runtime-proof"');

const enabled = await t.fetch("/api/v1/feeds/claws");
expect(enabled.status).toBe(200);
expect(enabled.headers.get("cache-control")).toBe("no-store");
expect(enabled.headers.get("surrogate-control")).toBeNull();
expect(await enabled.text()).toBe(publication?.payload);

vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "0");
const disabled = await t.fetch("/api/v1/feeds/claws");
expect(disabled.status).toBe(404);
expect(disabled.headers.get("cache-control")).toBe("no-store");
});
});
119 changes: 116 additions & 3 deletions convex/catalogFeed.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { CATALOG_FEED_ID, CATALOG_SKILLS_FEED_ID } from "clawhub-schema";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { listOfficialEntries, listOfficialSkillEntries, publish } from "./catalogFeed";
import { CATALOG_FEED_ID, CATALOG_SKILLS_FEED_ID, EXPERIMENTAL_CLAW_FEED_ID } from "clawhub-schema";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
listOfficialClawEntries,
listOfficialEntries,
listOfficialSkillEntries,
publish,
} from "./catalogFeed";

vi.mock("./lib/publishers", () => ({
getOwnerPublisher: vi.fn().mockResolvedValue({ handle: "openclaw" }),
Expand All @@ -19,6 +24,9 @@ const listOfficialEntriesHandler = (
unknown[]
>
)._handler;
const listOfficialClawEntriesHandler = (
listOfficialClawEntries as unknown as WrappedHandler<Record<string, never>, unknown[]>
)._handler;
const listOfficialSkillEntriesHandler = (
listOfficialSkillEntries as unknown as WrappedHandler<
{ publisherId: string; cursor: string | null },
Expand Down Expand Up @@ -189,6 +197,10 @@ describe("catalog feed projection", () => {
vi.clearAllMocks();
});

afterEach(() => {
vi.unstubAllEnvs();
});

it("projects official releases into ClawHub install candidates", async () => {
const result = await listOfficialEntriesHandler(
makeCtx(
Expand Down Expand Up @@ -259,6 +271,55 @@ describe("catalog feed projection", () => {
]);
});

it("projects validated Claw releases with only their safe summary", async () => {
vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "1");
const clawManifestSummary = {
schemaVersion: 1,
agent: { id: "triage", name: "Triage" },
workspace: { bootstrapFiles: ["SOUL.md"], fileCount: 1 },
packages: { skillCount: 1, pluginCount: 0 },
mcpServerCount: 0,
cronJobCount: 1,
};
const result = await listOfficialClawEntriesHandler(
makeCtx([makePackage({ family: "claw" })], {
"packageReleases:1": makeRelease({
clawManifestSummary,
}),
}),
{},
);

expect(result).toEqual([
expect.objectContaining({
type: "claw",
id: "@openclaw/demo",
clawManifestSummary,
install: {
candidates: [
expect.objectContaining({
package: "@openclaw/demo",
version: "1.2.3",
integrity: "sha256:artifact-hash",
}),
],
},
}),
]);
});

it("excludes Claw releases without a validated manifest summary", async () => {
vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "1");
const result = await listOfficialClawEntriesHandler(
makeCtx([makePackage({ family: "claw" })], {
"packageReleases:1": makeRelease(),
}),
{},
);

expect(result).toEqual([]);
});

it("excludes non-official, blocked, deleted, and undigested releases", async () => {
const result = await listOfficialEntriesHandler(
makeCtx(
Expand Down Expand Up @@ -495,6 +556,58 @@ describe("catalog feed projection", () => {
]);
});

it("publishes Claws through the separate experimental mutation", async () => {
vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "1");
const clawEntry = {
type: "claw",
id: "@openclaw/triage",
title: "Triage",
version: "1.0.0",
state: "available",
publisher: { id: "openclaw", trust: "official" },
clawManifestSummary: {
schemaVersion: 1,
agent: { id: "triage" },
workspace: { bootstrapFiles: [], fileCount: 0 },
packages: { skillCount: 0, pluginCount: 0 },
mcpServerCount: 0,
cronJobCount: 0,
},
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@openclaw/triage",
version: "1.0.0",
integrity: "sha256:abc",
},
],
},
};
const runQuery = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
if ("family" in args) return [];
if ("cursor" in args) return { publishers: [], isDone: true, continueCursor: "" };
return [clawEntry];
});
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => ({
feedId: typeof args.feedId === "string" ? args.feedId : EXPERIMENTAL_CLAW_FEED_ID,
entryCount: Array.isArray(args.entries) ? args.entries.length : 0,
}));

const result = await publishHandler(
{ runQuery, runMutation },
{ expiresAt: "2026-07-20T00:00:00.000Z" },
);

expect(runMutation).toHaveBeenCalledTimes(3);
expect(runMutation).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({ entries: [clawEntry] }),
);
expect(runMutation.mock.calls.at(-1)?.[1]).not.toHaveProperty("feedId");
expect(result.at(-1)).toEqual({ feedId: EXPERIMENTAL_CLAW_FEED_ID, entryCount: 1 });
});

it("projects suspicious current GitHub-backed skills into public GitHub install candidates", async () => {
const result = (await listOfficialSkillEntriesHandler(
makeCtx(
Expand Down
Loading
Loading