refactor: harden generated-file guards and provider ids

This commit is contained in:
Peter Steinberger
2026-03-22 18:54:50 -07:00
parent 7d11f6cf69
commit 96d61aa50c
8 changed files with 110 additions and 8 deletions

View File

@@ -7,7 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Breaking/Plugins: bare `openclaw plugins install <package>` now prefers ClawHub before npm for npm-safe names, and only falls back to npm when ClawHub does not have that package or version.
- Tools/image generation: deprecate the bundled `openai-image-gen` skill, standardize on the native `image` and `image_generate` tools, clarify provider-specific image auth requirements in runtime output and docs, and harden provider-auth hint lookups against prototype-like provider ids. (#52552) Thanks @vincentkoc.
- Breaking/Tools/image generation: deprecate the bundled `openai-image-gen` skill, standardize on the native `image` and `image_generate` tools, clarify provider-specific image auth requirements in runtime output and docs, and harden provider-auth hint lookups against prototype-like provider ids. (#52552) Thanks @vincentkoc.
- ClawHub/install: add native `openclaw skills search|install|update` flows plus `openclaw plugins install clawhub:<package>` with tracked update metadata, gateway skill-install/update support for ClawHub-backed requests, and regression coverage/docs for the new source path.
- Models/Anthropic Vertex: add core `anthropic-vertex` provider support for Claude via Google Vertex AI, including GCP auth/discovery and main run-path routing. (#43356) Thanks @sallyom and @yossiovadia.
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.

View File

@@ -63,6 +63,7 @@ describe("msteams setup surface", () => {
).toBe(true);
hasConfiguredMSTeamsCredentials.mockReturnValue(false);
expect(msteamsSetupWizard.status.resolveStatusLines).toBeTypeOf("function");
expect(
msteamsSetupWizard.status.resolveStatusLines?.({
cfg: { channels: { msteams: {} } },

View File

@@ -583,7 +583,7 @@
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
"check": "pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm check:no-conflict-markers && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
"check:bundled-plugin-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
"check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check",

View File

@@ -7,6 +7,7 @@ import { formatGeneratedModule } from "./lib/format-generated-module.mjs";
const GENERATED_BY = "scripts/generate-base-config-schema.ts";
const DEFAULT_OUTPUT_PATH = "src/config/schema.base.generated.ts";
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
function readIfExists(filePath: string): string | null {
try {
@@ -17,9 +18,8 @@ function readIfExists(filePath: string): string | null {
}
function formatTypeScriptModule(source: string, outputPath: string): string {
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
return formatGeneratedModule(source, {
repoRoot,
repoRoot: REPO_ROOT,
outputPath,
errorLabel: "base config schema",
});
@@ -45,9 +45,7 @@ export function writeBaseConfigSchemaModule(params?: {
outputPath?: string;
check?: boolean;
}): { changed: boolean; wrote: boolean; outputPath: string } {
const repoRoot = path.resolve(
params?.repoRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."),
);
const repoRoot = path.resolve(params?.repoRoot ?? REPO_ROOT);
const outputPath = path.resolve(repoRoot, params?.outputPath ?? DEFAULT_OUTPUT_PATH);
const current = readIfExists(outputPath);
const generatedAt =

View File

@@ -49,4 +49,48 @@ describe("image-generation provider registry", () => {
expect(provider?.id).toBe("custom-image");
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("ignores prototype-like provider ids and aliases", () => {
const registry = createEmptyPluginRegistry();
registry.imageGenerationProviders.push(
{
pluginId: "blocked-image",
pluginName: "Blocked Image",
source: "test",
provider: {
id: "__proto__",
aliases: ["constructor", "prototype"],
capabilities: {
generate: {},
edit: { enabled: false },
},
generateImage: async () => ({
images: [{ buffer: Buffer.from("image"), mimeType: "image/png" }],
}),
},
},
{
pluginId: "safe-image",
pluginName: "Safe Image",
source: "test",
provider: {
id: "safe-image",
aliases: ["safe-alias", "constructor"],
capabilities: {
generate: {},
edit: { enabled: false },
},
generateImage: async () => ({
images: [{ buffer: Buffer.from("image"), mimeType: "image/png" }],
}),
},
},
);
setActivePluginRegistry(registry);
expect(listImageGenerationProviders().map((provider) => provider.id)).toEqual(["safe-image"]);
expect(getImageGenerationProvider("__proto__")).toBeUndefined();
expect(getImageGenerationProvider("constructor")).toBeUndefined();
expect(getImageGenerationProvider("safe-alias")?.id).toBe("safe-image");
});
});

View File

@@ -1,5 +1,6 @@
import { normalizeProviderId } from "../agents/model-selection.js";
import type { OpenClawConfig } from "../config/config.js";
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
import { loadOpenClawPlugins } from "../plugins/loader.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
import type { ImageGenerationProviderPlugin } from "../plugins/types.js";
@@ -8,7 +9,10 @@ const BUILTIN_IMAGE_GENERATION_PROVIDERS: readonly ImageGenerationProviderPlugin
function normalizeImageGenerationProviderId(id: string | undefined): string | undefined {
const normalized = normalizeProviderId(id ?? "");
return normalized || undefined;
if (!normalized || isBlockedObjectKey(normalized)) {
return undefined;
}
return normalized;
}
function resolvePluginImageGenerationProviders(

View File

@@ -1,3 +1,4 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -5,6 +6,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
findConflictMarkerLines,
findConflictMarkersInFiles,
listTrackedFiles,
} from "../../scripts/check-no-conflict-markers.mjs";
const tempDirs: string[] = [];
@@ -21,6 +23,13 @@ function makeTempDir(): string {
return dir;
}
function git(cwd: string, ...args: string[]): string {
return execFileSync("git", args, {
cwd,
encoding: "utf8",
}).trim();
}
describe("check-no-conflict-markers", () => {
it("finds git conflict markers at the start of lines", () => {
expect(
@@ -61,4 +70,34 @@ describe("check-no-conflict-markers", () => {
},
]);
});
it("finds conflict markers in tracked script files", () => {
const rootDir = makeTempDir();
git(rootDir, "init", "-q");
git(rootDir, "config", "user.email", "test@example.com");
git(rootDir, "config", "user.name", "Test User");
const scriptFile = path.join(rootDir, "scripts", "generate-bundled-plugin-metadata.mjs");
fs.mkdirSync(path.dirname(scriptFile), { recursive: true });
fs.writeFileSync(
scriptFile,
[
"<<<<<<< HEAD",
'const left = "left";',
"=======",
'const right = "right";',
">>>>>>> branch",
].join("\n"),
);
git(rootDir, "add", "scripts/generate-bundled-plugin-metadata.mjs");
const violations = findConflictMarkersInFiles(listTrackedFiles(rootDir));
expect(violations).toEqual([
{
filePath: scriptFile,
lines: [1, 3, 5],
},
]);
});
});

View File

@@ -99,4 +99,20 @@ describe("scripts/committer", () => {
expect(committedPaths(repo)).toEqual(testCase.expected);
}
});
it("commits changelog-only changes without pulling in unrelated dirty files", () => {
const repo = createRepo();
writeRepoFile(repo, "CHANGELOG.md", "initial\n");
writeRepoFile(repo, "unrelated.ts", "export const ok = true;\n");
git(repo, "add", "CHANGELOG.md", "unrelated.ts");
git(repo, "commit", "-qm", "seed extra files");
writeRepoFile(repo, "CHANGELOG.md", "breaking note\n");
writeRepoFile(repo, "unrelated.ts", "<<<<<<< HEAD\nleft\n=======\nright\n>>>>>>> branch\n");
commitWithHelper(repo, "docs(changelog): note breaking change", "CHANGELOG.md");
expect(committedPaths(repo)).toEqual(["CHANGELOG.md"]);
expect(git(repo, "status", "--short")).toContain("M unrelated.ts");
});
});