From c2dd9130f6157ab8b561e261d1ee185daad46f0f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 1 Oct 2026 12:55:41 +0800 Subject: [PATCH] fix(telemetry): refresh public vocabulary from released metadata --- .github/workflows/deploy.yml | 7 + CHANGELOG.md | 1 + README.md | 14 +- data/public-vocabulary.json | 314 ++++++++++++++++++++++++++++++++ scripts/public-vocabulary.mjs | 23 ++- src/public-vocabulary.ts | 14 ++ test/public-vocabulary.test.mjs | 65 ++++++- 7 files changed, 431 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ca7f173..81a39f0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,6 +20,13 @@ jobs: node-version: 24 - run: npm ci - run: npm run check + - name: Check vocabulary covers the latest OpenClaw release + env: + GH_TOKEN: ${{ github.token }} + run: | + release_tag="$(gh api repos/openclaw/openclaw/releases/latest --jq .tag_name)" + release_sha="$(gh api "repos/openclaw/openclaw/commits/$release_tag" --jq .sha)" + npm run vocabulary:check -- --release-revision "$release_sha" - run: npx wrangler deploy --dry-run deploy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 907c574..adb5959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Preserve all reported public channel, provider, and plugin names instead of truncating each list to 32 before validation; keep upload and Analytics Engine byte limits covered by regression tests. +- Refresh retained public names from OpenClaw 2026.9.7, including GitHub, QuickJS Code Mode, and Session Share; require a reviewed snapshot for the latest published release in the existing CI check. - Accept strictly validated, identifier-free update outcomes in a separate, explicitly configured dataset; add a side-effect-free capability check while keeping production collection unbound. Thanks @roboclaw-bot, @fuller-stack-dev, and @vincentkoc. - Refresh Wrangler, Cloudflare Worker types, and Vitest with the matching workerd runtime and npm lockfile. - Discard malformed UTF-8 feature-statistics uploads instead of repairing and recording them, while preserving update responses. diff --git a/README.md b/README.md index a78f9d6..4e728b3 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,10 @@ npm run dev # local worker at http://localhost:8787 npm run deploy # requires Cloudflare credentials for the OpenClaw account ``` -Pull requests run the typecheck, tests, and a Wrangler dry-run build using the committed lockfile. +Pull requests run the typecheck, tests, a public-vocabulary release check, and a Wrangler dry-run build +using the committed lockfile. The release check resolves the latest published OpenClaw release tag +to its commit and fails if that commit has no reviewed vocabulary snapshot. GitHub lookup failures +fail the check; they do not report the vocabulary as fresh. Deploys run from GitHub Actions on pushes to `main` (see [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml)), using the `CLOUDFLARE_API_TOKEN` repository secret. @@ -254,19 +257,24 @@ OpenClaw Git repository containing the candidate commit and its history: npm run vocabulary:check -- --source --revision npm run vocabulary:update -- --source --revision npm run vocabulary:check -- --source +npm run vocabulary:check -- --release-revision npm run check ``` The first command fails when the candidate is not recorded. Review the generated diff, commit both metadata and generated source, and deploy through the normal PR workflow. Do not edit the generated names by hand. Plain `npm run vocabulary:check` runs offline in CI and detects metadata/output drift; -`--source` also reproduces every snapshot from immutable Git objects. It never changes the source -checkout, runs an install, or uses its uncommitted files. +`--source` also reproduces every snapshot from immutable Git objects. `--release-revision` additionally +requires a snapshot for the independently resolved release commit. This check is read-only and does +not automatically admit new names. Neither check changes the source checkout, runs an install, or +uses its uncommitted files. The generator calls upstream `listBundledPluginPackArtifacts` with the default packaging environment, then reads the selected plugin manifests, public provider overlays, and three official catalogs. Packaging exclusions remain owned by OpenClaw. A changed upstream metadata contract fails generation and needs review rather than silently falling back to a partial vocabulary. +The generator supports the provider-metadata location and shared packaging policy used by release +`v2026.9.7` (`c074824a27c96d3983043f9eeb33823cd1772d8c`), while preserving replay of older snapshots. The initial snapshot includes all catalog revisions on the public main history since commit `844e781ca40952c98ee997b016e3cc5d2f12f9f3`, before name allowlisting began in August 2026. diff --git a/data/public-vocabulary.json b/data/public-vocabulary.json index a71ce0b..2e45000 100644 --- a/data/public-vocabulary.json +++ b/data/public-vocabulary.json @@ -285,6 +285,320 @@ "zalouser", "zoom-meetings" ] + }, + { + "repository": "openclaw/openclaw", + "revision": "c074824a27c96d3983043f9eeb33823cd1772d8c", + "catalogHistoryStart": "844e781ca40952c98ee997b016e3cc5d2f12f9f3", + "catalogRevisions": [ + "0229a108fee749327b2cc60c4d228299dfe2f889", + "0a4bb73bdfa59ebad95e0498ea5801930dec099c", + "1605dbd3efef1c10da95c4a9bbb51a19a9d07865", + "1f2e99832b4239aa63a4007e211f80f390c1d9bd", + "2bbd9403b7c2c4fe01565401d535a3dd2bc38d03", + "3a5cb3847c77a0e021fdc90da8cdce0005e99d46", + "4b5f39fd6d4e093f3bdad3739059d180498a12dc", + "4d31905e39652fab7d6a323fa30581025a49648f", + "56fb8872ed77595bdb50ec3e62968d8966a5a69b", + "58fcf69cadd0be59ecee41b21b655f195ccda7b2", + "5acfb83d9fe84d5741343d85d5e3d99243bafd39", + "64bf824613261007a3f452974d63f83d6db7f512", + "74ad6ba4f2e9f9f462e66f6a6663c1dcd8d7928f", + "7a6a7945cecb4eb1a0eae8d0274cb8ee9462cf1e", + "7f0547293863a6ee80f28c15808505bd5e9d12b9", + "7f41da0c7e42ca8818615272d62977f677a1e482", + "80ae248de1c050d2508f378245b2f1add18f47ed", + "844e781ca40952c98ee997b016e3cc5d2f12f9f3", + "88c56ec432a8e9149987db99dbbec25874aec266", + "89f7971fc8e9c48f8b849bdff51533d39051d00e", + "8f82ec8ad2c6f76106d98b4fe4690150e7af1bf6", + "914530a9577b92d723effae0afa231196ef17d78", + "9b2831afd5f7f1540130872b7be9b61b568c4ba0", + "9b77c06bbde9bd9cf3d280ef1d515d8a824a1582", + "a26af89910e21c51d53b9f1720e31a231f20417e", + "a94a7b2768dec4799314c1c2058c26f231d282f7", + "a980c88c8876fbf5187731f5031b8ee7e8103353", + "b076a15e510374bbc73750d0d58bcc4e4025648a", + "b4653c7debb79d1e6e17fcb89299a4e8817cbf2a", + "b4d088531754d71af3d3446e2c37e7aafd0eddc4", + "bb8c04d53778d6c7ccf438a6d7a876c6ddaa979d", + "c074824a27c96d3983043f9eeb33823cd1772d8c", + "c432db8665317d377d9e1763253dcbcacac62dd2", + "c677baebc93f4de4ed323580fda0f85895512a3d", + "d7b0e07f4ca00f9646958a2bba63b600f11bc77a", + "e1a9f086e4e6d49508fdae9209129c28e426188e", + "e1d457ede625bddec7b02b7b0939e597a0730c64", + "e545da46f6d895c13dd7c3a2ee28e048e524fa92", + "f9358275abe0c8d76367f14c7b98b4bf6786bee6", + "f99d9620017f20a2fa781650e871592ce1d0f138", + "fd141ddb2777528f28abb647aa71412cf8c9d45d" + ], + "bundledPlugins": [ + "a2a", + "active-memory", + "admin-http-rpc", + "agentsapi", + "alibaba", + "anthropic", + "apple-fm", + "azure-speech", + "beam", + "bonjour", + "browser", + "canvas", + "clawrouter", + "code-mode-quickjs", + "copilot-proxy", + "crabbox", + "cua-computer", + "deepgram", + "device-pair", + "document-extract", + "elevenlabs", + "fal", + "file-transfer", + "geolocation", + "github", + "github-copilot", + "google", + "huggingface", + "imap", + "kie", + "linux-node", + "litellm", + "llm-task", + "lmstudio", + "logbook", + "memory-core", + "memory-wiki", + "microsoft", + "microsoft-foundry", + "migrate-claude", + "migrate-hermes", + "minimax", + "nvidia", + "oc-path", + "ollama", + "onepassword", + "openai", + "opencode-go", + "openrouter", + "policy", + "reef", + "runway", + "senseaudio", + "session-share", + "sglang", + "talk-voice", + "telegram", + "together", + "tts-local-cli", + "vault", + "vllm", + "web-readability", + "workboard", + "xai" + ], + "names": [ + "a2a", + "acpx", + "active-memory", + "admin-http-rpc", + "agentmail", + "agentsapi", + "alibaba", + "amazon-bedrock", + "amazon-bedrock-mantle", + "anthropic", + "anthropic-vertex", + "apple-fm", + "arcee", + "azure-openai-responses", + "azure-speech", + "bailian-token-plan", + "baseten", + "beam", + "bonjour", + "brave", + "browser", + "buzz", + "byteplus", + "byteplus-plan", + "canvas", + "cerebras", + "chutes", + "claude-cli", + "clawrouter", + "clickclack", + "cloudflare-ai-gateway", + "code-mode-quickjs", + "codex", + "cohere", + "comfy", + "copilot", + "copilot-proxy", + "crabbox", + "cua-computer", + "dashscope", + "daytona", + "deepgram", + "deepinfra", + "deepseek", + "device-pair", + "diagnostics-otel", + "diagnostics-prometheus", + "diffs", + "diffs-language-pack", + "discord", + "document-extract", + "duckduckgo", + "elevenlabs", + "exa", + "facetime", + "fal", + "featherless", + "feishu", + "file-transfer", + "firecrawl", + "fireworks", + "fish-audio-speech", + "geolocation", + "github", + "github-copilot", + "gmi", + "gmi-cloud", + "gmicloud", + "google", + "google-antigravity", + "google-gemini-cli", + "google-meet", + "google-vertex", + "googlechat", + "gradium", + "groq", + "huggingface", + "imap", + "imessage", + "inworld", + "irc", + "kie", + "kilocode", + "kimi", + "kimi-coding", + "line", + "linux-node", + "litellm", + "llama-cpp", + "llm-task", + "lmstudio", + "lobster", + "logbook", + "longcat", + "matrix", + "mattermost", + "memory-core", + "memory-lancedb", + "memory-wiki", + "meta", + "microsoft", + "microsoft-foundry", + "migrate-claude", + "migrate-hermes", + "minimax", + "minimax-portal", + "mistral", + "modelstudio", + "moonshot", + "moonshot-ai", + "moonshotai", + "msteams", + "mxc", + "nextcloud-talk", + "nostr", + "novita", + "novita-ai", + "novitaai", + "nvidia", + "oc-path", + "ollama", + "ollama-cloud", + "onepassword", + "onnx", + "openai", + "openclaw-plugin-yuanbao", + "openclaw-qqbot", + "openclaw-weixin", + "openclaw-zaloclawbot", + "opencode", + "opencode-go", + "openrouter", + "openshell", + "parallel", + "perplexity", + "pixverse", + "policy", + "qianfan", + "qqbot", + "qwen", + "qwen-token-plan", + "qwencloud", + "radius", + "raft", + "reef", + "runway", + "searxng", + "senseaudio", + "session-share", + "sglang", + "signal", + "slack", + "slack-huddles", + "sms", + "stepfun", + "stepfun-plan", + "synology-chat", + "synthetic", + "talk-voice", + "tavily", + "team-reports", + "teams-meetings", + "telegram", + "telnyx", + "tencent", + "tencent-tokenhub", + "tencent-tokenplan", + "tlon", + "together", + "tokenjuice", + "tts-local-cli", + "twitch", + "typesafe", + "vault", + "venice", + "vercel-ai-gateway", + "vllm", + "voice-call", + "volcengine", + "volcengine-plan", + "voyage", + "vydra", + "web-readability", + "wecom", + "wecom-openclaw-plugin", + "whatsapp", + "workboard", + "x-ai", + "xai", + "xiaomi", + "xiaomi-token-plan", + "yuanbao", + "z-ai", + "z.ai", + "zai", + "zalo", + "zalouser", + "zoom-meetings" + ] } ] } diff --git a/scripts/public-vocabulary.mjs b/scripts/public-vocabulary.mjs index 844898a..64d16f0 100644 --- a/scripts/public-vocabulary.mjs +++ b/scripts/public-vocabulary.mjs @@ -61,17 +61,23 @@ export async function buildSnapshot(source, revision, catalogHistoryStart) { collectCatalog(JSON.parse(git(source, "show", `${catalogRevision}:${path}`)), names); } } + const providerMetadata = "src/config/model-provider-overlay-ids.ts"; + // Retained snapshots predate the metadata's move out of model-provider-config. + const providerPath = git(source, "ls-tree", "--name-only", revision, "--", providerMetadata).trim() || + "src/config/model-provider-config.ts"; for (const name of readProviderOverlays( - git(source, "show", `${revision}:src/config/model-provider-config.ts`), + git(source, "show", `${revision}:${providerPath}`), )) addName(names, name); const directory = await mkdtemp(join(tmpdir(), "openclaw-public-vocabulary-")); try { // The packaging helper reads metadata and top-level source filenames. // Export those exact public files, without a checkout, dependencies, or index. + const packagingPolicy = "src/shared/non-packaged-plugin-dirs.ts"; const paths = git(source, "ls-tree", "-r", "--name-only", revision, "--", - "package.json", "scripts/lib", "extensions").trim().split("\n").filter( + "package.json", "scripts/lib", "extensions", packagingPolicy).trim().split("\n").filter( (path) => path === "package.json" || path.startsWith("scripts/lib/") || + path === packagingPolicy || /^extensions\/[^/]+\/[^/]+$/u.test(path), ); const archive = execFileSync("git", ["-C", source, "archive", revision, "--", ...paths], { @@ -134,6 +140,14 @@ export function renderVocabulary(metadata) { ].join("\n"); } +/** CI supplies the current release's resolved commit, independently of our recorded snapshots. */ +export function assertReleaseCoverage(metadata, revision) { + if (!SHA.test(revision)) throw new Error("Use a full immutable released commit SHA"); + if (!metadata.snapshots.some((snapshot) => snapshot.revision === revision)) { + throw new Error(`Released OpenClaw revision ${revision} has no reviewed vocabulary snapshot; refresh public metadata`); + } +} + async function main() { const { values } = parseArgs({ options: { @@ -141,9 +155,14 @@ async function main() { source: { type: "string" }, revision: { type: "string" }, "history-start": { type: "string" }, + "release-revision": { type: "string" }, }, }); const metadata = JSON.parse(await readFile(METADATA, "utf8")); + if (values["release-revision"]) { + if (!values.check) throw new Error("--release-revision requires --check"); + assertReleaseCoverage(metadata, values["release-revision"]); + } if (values.source) { if (values.revision) { const historyStart = values["history-start"] ?? metadata.snapshots[0]?.catalogHistoryStart; diff --git a/src/public-vocabulary.ts b/src/public-vocabulary.ts index 9bfa0a9..f56b169 100644 --- a/src/public-vocabulary.ts +++ b/src/public-vocabulary.ts @@ -5,11 +5,14 @@ export const PUBLIC_NAMES = [ "acpx", "active-memory", "admin-http-rpc", + "agentmail", + "agentsapi", "alibaba", "amazon-bedrock", "amazon-bedrock-mantle", "anthropic", "anthropic-vertex", + "apple-fm", "arcee", "azure-openai-responses", "azure-speech", @@ -31,6 +34,7 @@ export const PUBLIC_NAMES = [ "cli", "clickclack", "cloudflare-ai-gateway", + "code-mode-quickjs", "codex", "cohere", "comfy", @@ -53,6 +57,7 @@ export const PUBLIC_NAMES = [ "duckduckgo", "elevenlabs", "exa", + "facetime", "fal", "featherless", "feishu", @@ -62,6 +67,7 @@ export const PUBLIC_NAMES = [ "fish-audio-speech", "gemini", "geolocation", + "github", "github-copilot", "gmi", "gmi-cloud", @@ -79,6 +85,7 @@ export const PUBLIC_NAMES = [ "imessage", "inworld", "irc", + "kie", "kilocode", "kimi", "kimi-coding", @@ -120,6 +127,7 @@ export const PUBLIC_NAMES = [ "ollama", "ollama-cloud", "onepassword", + "onnx", "openai", "openclaw-plugin-yuanbao", "openclaw-qqbot", @@ -138,14 +146,17 @@ export const PUBLIC_NAMES = [ "qwen", "qwen-token-plan", "qwencloud", + "radius", "raft", "reef", "runway", "searxng", "senseaudio", + "session-share", "sglang", "signal", "slack", + "slack-huddles", "sms", "stepfun", "stepfun-plan", @@ -153,8 +164,10 @@ export const PUBLIC_NAMES = [ "synthetic", "talk-voice", "tavily", + "team-reports", "teams-meetings", "telegram", + "telnyx", "tencent", "tencent-tokenhub", "tencent-tokenplan", @@ -163,6 +176,7 @@ export const PUBLIC_NAMES = [ "tokenjuice", "tts-local-cli", "twitch", + "typesafe", "vault", "venice", "vercel-ai-gateway", diff --git a/test/public-vocabulary.test.mjs b/test/public-vocabulary.test.mjs index 85b3c95..8b8624d 100644 --- a/test/public-vocabulary.test.mjs +++ b/test/public-vocabulary.test.mjs @@ -1,9 +1,9 @@ import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, mkdir, rename, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { renderVocabulary } from "../scripts/public-vocabulary.mjs"; +import { assertReleaseCoverage, buildSnapshot, renderVocabulary } from "../scripts/public-vocabulary.mjs"; import { readProviderOverlays } from "../scripts/lib/public-provider-overlays.mjs"; const oldRevision = "1".repeat(40); @@ -42,6 +42,67 @@ describe("public provider metadata", () => { }); describe("public vocabulary generation", () => { + it("replays historical metadata and the released split provider and packaging owners", async () => { + const directory = await mkdtemp(join(tmpdir(), "vocabulary-release-test-")); + const git = (...args) => execFileSync("git", ["-C", directory, ...args], { + encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], + }); + const commit = () => { + git("add", "."); + git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", "commit", "-qm", "fixture"); + return git("rev-parse", "HEAD").trim(); + }; + try { + for (const path of ["scripts/lib", "src/config", "src/shared", "extensions/public-plugin"]) { + await mkdir(join(directory, path), { recursive: true }); + } + for (const kind of ["plugin", "channel", "provider"]) { + await writeFile(join(directory, `scripts/lib/official-external-${kind}-catalog.json`), + JSON.stringify({ entries: [] })); + } + await writeFile(join(directory, "package.json"), '{"type":"module"}'); + await writeFile(join(directory, "src/config/model-provider-config.ts"), + 'const BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS = new Set(["openai"]);'); + await writeFile(join(directory, "extensions/public-plugin/openclaw.plugin.json"), + JSON.stringify({ id: "public-plugin", channels: ["public-channel"] })); + const helper = join(directory, "scripts/lib/bundled-plugin-build-entries.mjs"); + await writeFile(helper, `export function listBundledPluginPackArtifacts() { + return ["dist/extensions/public-plugin/openclaw.plugin.json"]; + }`); + git("init", "--quiet"); + const historical = commit(); + const oldSnapshot = await buildSnapshot(directory, historical, historical); + await rename(join(directory, "src/config/model-provider-config.ts"), + join(directory, "src/config/model-provider-overlay-ids.ts")); + await writeFile(join(directory, "src/shared/non-packaged-plugin-dirs.ts"), + 'export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS: ReadonlySet = new Set(["private-plugin"]);'); + await writeFile(helper, `import { NON_PACKAGED_BUNDLED_PLUGIN_DIRS } from "../../src/shared/non-packaged-plugin-dirs.ts"; + export function listBundledPluginPackArtifacts() { + return ["public-plugin", "private-plugin"].filter(id => !NON_PACKAGED_BUNDLED_PLUGIN_DIRS.has(id)) + .map(id => "dist/extensions/" + id + "/openclaw.plugin.json"); + }`); + const released = commit(); + // Dirty working files must never influence an immutable release snapshot. + await writeFile(join(directory, "src/config/model-provider-overlay-ids.ts"), "invalid source"); + const current = await buildSnapshot(directory, released, historical); + expect(current.names).toEqual(["openai", "public-channel", "public-plugin"]); + expect(current.bundledPlugins).toEqual(["public-plugin"]); + expect(await buildSnapshot(directory, historical, historical)).toEqual(oldSnapshot); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }, 10_000); + + it("rejects a newer release even when the recorded vocabulary is internally consistent", () => { + const metadata = { schemaVersion: 1, legacyAliases: aliases, + snapshots: [{ revision: oldRevision, names: ["openai"] }] }; + expect(renderVocabulary(metadata)).toContain('"openai"'); + expect(() => assertReleaseCoverage(metadata, newRevision)).toThrow("no reviewed vocabulary snapshot"); + expect(() => assertReleaseCoverage(metadata, "main")).toThrow("immutable released commit SHA"); + expect(() => assertReleaseCoverage(metadata, oldRevision)).not.toThrow(); + }); + it("rejects malformed upstream provider declarations without unbounded parsing", async () => { const directory = await mkdtemp(join(tmpdir(), "vocabulary-source-test-")); try {