mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(providers): load trusted external thinking policies
This commit is contained in:
+2
-22
@@ -36,6 +36,7 @@ import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
|
||||
import { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { buildZaiModelDefinition, resolveZaiBaseUrl } from "./model-definitions.js";
|
||||
import { applyZaiConfig, applyZaiProviderConfig, resolveZaiModelId } from "./onboard.js";
|
||||
import { isGlm52ModelId, resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
|
||||
const PROVIDER_ID = "zai";
|
||||
const GLM5_TEMPLATE_MODEL_ID = "glm-4.7";
|
||||
@@ -128,10 +129,6 @@ function isDisabledThinkingLevel(thinkingLevel: ProviderWrapStreamFnContext["thi
|
||||
return thinkingLevel === "off";
|
||||
}
|
||||
|
||||
function isGlm52ModelId(modelId?: string | null): boolean {
|
||||
return normalizeLowercaseStringOrEmpty(modelId).startsWith("glm-5.2");
|
||||
}
|
||||
|
||||
function mapThinkingLevelToZaiReasoningEffort(
|
||||
thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"],
|
||||
): "high" | "max" | undefined {
|
||||
@@ -394,24 +391,7 @@ export default definePluginEntry({
|
||||
}),
|
||||
prepareExtraParams: (ctx) => defaultToolStreamExtraParams(ctx.extraParams),
|
||||
wrapStreamFn: (ctx) => wrapZaiStreamFn(ctx),
|
||||
resolveThinkingProfile: (ctx) =>
|
||||
isGlm52ModelId(ctx.modelId)
|
||||
? {
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "high", label: "high" },
|
||||
{ id: "max", label: "max" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
}
|
||||
: {
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "on" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
},
|
||||
resolveThinkingProfile,
|
||||
isModernModelRef: ({ modelId }) => {
|
||||
const lower = normalizeLowercaseStringOrEmpty(modelId);
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Z.AI tests cover its cold provider thinking policy.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
|
||||
describe("zai provider thinking policy", () => {
|
||||
it.each(["glm-5.2", "glm-5.2-flash"])("exposes full GLM 5.2 levels for %s", (modelId) => {
|
||||
expect(resolveThinkingProfile({ provider: "zai", modelId })).toEqual({
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "high", label: "high" },
|
||||
{ id: "max", label: "max" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["glm-5.1", "glm-4.7"])("keeps older GLM models binary for %s", (modelId) => {
|
||||
expect(resolveThinkingProfile({ provider: "zai", modelId })).toEqual({
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "on" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
// Z.AI public policy surface shared by cold selection and the provider runtime.
|
||||
import type {
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderThinkingProfile,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
export function isGlm52ModelId(modelId?: string | null): boolean {
|
||||
return normalizeLowercaseStringOrEmpty(modelId).startsWith("glm-5.2");
|
||||
}
|
||||
|
||||
export function resolveThinkingProfile(
|
||||
ctx: ProviderDefaultThinkingPolicyContext,
|
||||
): ProviderThinkingProfile {
|
||||
if (isGlm52ModelId(ctx.modelId)) {
|
||||
return {
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "high", label: "high" },
|
||||
{ id: "max", label: "max" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
};
|
||||
}
|
||||
return {
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "on" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,10 @@ import type {
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderThinkingProfile,
|
||||
} from "./provider-thinking.types.js";
|
||||
import { loadBundledPluginPublicArtifactModuleSync } from "./public-surface-loader.js";
|
||||
import {
|
||||
loadBundledPluginPublicArtifactModuleSync,
|
||||
loadPluginPublicArtifactModuleSync,
|
||||
} from "./public-surface-loader.js";
|
||||
|
||||
const PROVIDER_POLICY_ARTIFACT_CANDIDATES = ["provider-policy-api.js"] as const;
|
||||
const providerPolicySurfaceByPluginId = new Map<string, BundledProviderPolicySurface | null>();
|
||||
@@ -52,35 +55,64 @@ function hasProviderPolicyHook(
|
||||
);
|
||||
}
|
||||
|
||||
/** Loads policy hooks directly by canonical bundled plugin id. */
|
||||
export function resolveDirectBundledProviderPolicySurface(
|
||||
pluginId: string,
|
||||
): BundledProviderPolicySurface | null {
|
||||
const cacheKey = `${resolveBundledPluginsDir() ?? ""}\0${pluginId}`;
|
||||
const cached = providerPolicySurfaceByPluginId.get(cacheKey);
|
||||
function resolveCachedProviderPolicySurface(params: {
|
||||
cacheKey: string;
|
||||
loadModule: (artifactBasename: string) => Record<string, unknown>;
|
||||
missingSurfacePrefix: string;
|
||||
}): BundledProviderPolicySurface | null {
|
||||
const cached = providerPolicySurfaceByPluginId.get(params.cacheKey);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
for (const artifactBasename of PROVIDER_POLICY_ARTIFACT_CANDIDATES) {
|
||||
try {
|
||||
const mod = loadBundledPluginPublicArtifactModuleSync<Record<string, unknown>>({
|
||||
dirName: pluginId,
|
||||
artifactBasename,
|
||||
});
|
||||
const mod = params.loadModule(artifactBasename);
|
||||
if (hasProviderPolicyHook(mod)) {
|
||||
providerPolicySurfaceByPluginId.set(cacheKey, mod);
|
||||
providerPolicySurfaceByPluginId.set(params.cacheKey, mod);
|
||||
return mod;
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.startsWith("Unable to resolve bundled plugin public surface ")
|
||||
) {
|
||||
if (error instanceof Error && error.message.startsWith(params.missingSurfacePrefix)) {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
providerPolicySurfaceByPluginId.set(cacheKey, null);
|
||||
providerPolicySurfaceByPluginId.set(params.cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Loads policy hooks directly by canonical bundled plugin id. */
|
||||
export function resolveDirectBundledProviderPolicySurface(
|
||||
pluginId: string,
|
||||
): BundledProviderPolicySurface | null {
|
||||
return resolveCachedProviderPolicySurface({
|
||||
cacheKey: `${resolveBundledPluginsDir() ?? ""}\0${pluginId}`,
|
||||
loadModule: (artifactBasename) =>
|
||||
loadBundledPluginPublicArtifactModuleSync<Record<string, unknown>>({
|
||||
dirName: pluginId,
|
||||
artifactBasename,
|
||||
}),
|
||||
missingSurfacePrefix: "Unable to resolve bundled plugin public surface ",
|
||||
});
|
||||
}
|
||||
|
||||
/** Loads policy hooks from a host-verified official external plugin install. */
|
||||
export function resolveTrustedExternalProviderPolicySurface(params: {
|
||||
pluginId: string;
|
||||
pluginRoot: string;
|
||||
trustedOfficialInstall?: boolean;
|
||||
}): BundledProviderPolicySurface | null {
|
||||
if (params.trustedOfficialInstall !== true) {
|
||||
return null;
|
||||
}
|
||||
return resolveCachedProviderPolicySurface({
|
||||
cacheKey: `${params.pluginRoot}\0${params.pluginId}`,
|
||||
loadModule: (artifactBasename) =>
|
||||
loadPluginPublicArtifactModuleSync<Record<string, unknown>>({
|
||||
pluginRoot: params.pluginRoot,
|
||||
artifactBasename,
|
||||
}),
|
||||
missingSurfacePrefix: "Unable to resolve plugin public surface ",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,13 +5,33 @@ import path from "node:path";
|
||||
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ModelProviderConfig } from "../config/types.models.js";
|
||||
import { resolveBundledProviderPolicySurface } from "./provider-public-artifacts.js";
|
||||
import {
|
||||
resolveBundledProviderPolicySurface,
|
||||
resolveProviderPolicySurface,
|
||||
} from "./provider-public-artifacts.js";
|
||||
|
||||
function writeExternalPolicyFixture(): string {
|
||||
const pluginRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-provider-policy-external-"));
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "provider-policy-api.js"),
|
||||
[
|
||||
"export function resolveThinkingProfile({ modelId }) {",
|
||||
' return modelId === "full"',
|
||||
' ? { levels: [{ id: "off" }, { id: "high" }, { id: "max" }], defaultLevel: "off" }',
|
||||
' : { levels: [{ id: "off" }, { id: "low", label: "on" }], defaultLevel: "off" };',
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
describe("provider public artifacts", () => {
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
|
||||
afterEach(() => {
|
||||
function restoreBundledPluginEnv() {
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
@@ -22,6 +42,10 @@ describe("provider public artifacts", () => {
|
||||
} else {
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir;
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreBundledPluginEnv();
|
||||
vi.doUnmock("./bundled-dir.js");
|
||||
vi.doUnmock("./manifest-registry.js");
|
||||
vi.doUnmock("./public-surface-loader.js");
|
||||
@@ -98,6 +122,67 @@ describe("provider public artifacts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads trusted official external provider policy before runtime registration", () => {
|
||||
const bundledPluginsDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "openclaw-empty-bundled-plugins-"),
|
||||
);
|
||||
const pluginRoot = writeExternalPolicyFixture();
|
||||
|
||||
try {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1";
|
||||
const fixturePlugin = {
|
||||
id: "fixture-provider",
|
||||
origin: "external",
|
||||
trustedOfficialInstall: true,
|
||||
rootDir: pluginRoot,
|
||||
providers: ["fixture-provider"],
|
||||
cliBackends: [],
|
||||
} as const;
|
||||
const surface = resolveProviderPolicySurface("fixture-provider", {
|
||||
manifestRegistry: { plugins: [fixturePlugin as never] },
|
||||
});
|
||||
|
||||
expect(
|
||||
surface
|
||||
?.resolveThinkingProfile?.({ provider: "fixture-provider", modelId: "full" })
|
||||
?.levels.map((level) => level.id),
|
||||
).toEqual(["off", "high", "max"]);
|
||||
expect(
|
||||
surface
|
||||
?.resolveThinkingProfile?.({ provider: "fixture-provider", modelId: "legacy" })
|
||||
?.levels.map((level) => level.label),
|
||||
).toEqual([undefined, "on"]);
|
||||
} finally {
|
||||
restoreBundledPluginEnv();
|
||||
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||
fs.rmSync(bundledPluginsDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not load public policy code from untrusted external plugins", () => {
|
||||
const pluginRoot = writeExternalPolicyFixture();
|
||||
try {
|
||||
expect(
|
||||
resolveProviderPolicySurface("fixture-provider", {
|
||||
manifestRegistry: {
|
||||
plugins: [
|
||||
{
|
||||
id: "fixture-provider",
|
||||
origin: "external",
|
||||
rootDir: pluginRoot,
|
||||
providers: ["fixture-provider"],
|
||||
cliBackends: [],
|
||||
} as never,
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toBeNull();
|
||||
} finally {
|
||||
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves multi-provider policy artifacts by manifest-owned provider id", async () => {
|
||||
const bundledPluginsDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-provider-policy-"));
|
||||
const pluginDir = path.join(bundledPluginsDir, "openai");
|
||||
|
||||
@@ -4,6 +4,7 @@ import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
resolveDirectBundledProviderPolicySurface,
|
||||
resolveTrustedExternalProviderPolicySurface,
|
||||
type BundledProviderPolicySurface,
|
||||
} from "./provider-policy-surface.js";
|
||||
|
||||
@@ -78,3 +79,36 @@ export function resolveBundledProviderPolicySurface(
|
||||
}
|
||||
return resolveDirectBundledProviderPolicySurface(ownerPluginId);
|
||||
}
|
||||
|
||||
/** Resolves provider policy hooks from bundled or trusted official plugin artifacts. */
|
||||
export function resolveProviderPolicySurface(
|
||||
providerId: string,
|
||||
options: { manifestRegistry?: Pick<PluginManifestRegistry, "plugins"> } = {},
|
||||
): BundledProviderPolicySurface | null {
|
||||
const bundledSurface = resolveBundledProviderPolicySurface(providerId, options);
|
||||
if (bundledSurface) {
|
||||
return bundledSurface;
|
||||
}
|
||||
const normalizedProviderId = normalizeProviderId(providerId);
|
||||
if (!normalizedProviderId || !options.manifestRegistry) {
|
||||
return null;
|
||||
}
|
||||
for (const plugin of options.manifestRegistry.plugins.toSorted((left, right) =>
|
||||
left.id.localeCompare(right.id),
|
||||
)) {
|
||||
if (
|
||||
pluginOwnsProviderPolicyRef(plugin, normalizedProviderId) &&
|
||||
plugin.trustedOfficialInstall === true
|
||||
) {
|
||||
const surface = resolveTrustedExternalProviderPolicySurface({
|
||||
pluginId: plugin.id,
|
||||
pluginRoot: plugin.rootDir,
|
||||
trustedOfficialInstall: plugin.trustedOfficialInstall,
|
||||
});
|
||||
if (surface) {
|
||||
return surface;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Verifies cold thinking policy resolution for trusted external providers.
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const fixtureState = vi.hoisted(() => ({ pluginRoot: "" }));
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
const emptyBundledPluginsDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-empty-plugins-"));
|
||||
const externalPluginRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "openclaw-provider-policy-external-"),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(externalPluginRoot, "provider-policy-api.js"),
|
||||
[
|
||||
"export function resolveThinkingProfile({ modelId }) {",
|
||||
' return modelId === "full"',
|
||||
' ? { levels: [{ id: "off" }, { id: "high" }, { id: "max" }], defaultLevel: "off" }',
|
||||
' : { levels: [{ id: "off" }, { id: "low", label: "on" }], defaultLevel: "off" };',
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
fixtureState.pluginRoot = externalPluginRoot;
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = emptyBundledPluginsDir;
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1";
|
||||
|
||||
vi.mock("./current-plugin-metadata-snapshot.js", () => ({
|
||||
getCurrentPluginMetadataSnapshot: () => ({
|
||||
manifestRegistry: {
|
||||
plugins: [
|
||||
{
|
||||
id: "fixture-provider",
|
||||
origin: "external",
|
||||
trustedOfficialInstall: true,
|
||||
rootDir: fixtureState.pluginRoot,
|
||||
providers: ["fixture-provider"],
|
||||
cliBackends: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const { isThinkingLevelSupported, listThinkingLevels, resolveThinkingDefaultForModel } =
|
||||
await import("../auto-reply/thinking.js");
|
||||
|
||||
afterAll(() => {
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
|
||||
}
|
||||
if (originalTrustBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir;
|
||||
}
|
||||
fs.rmSync(emptyBundledPluginsDir, { recursive: true, force: true });
|
||||
fs.rmSync(externalPluginRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("trusted external provider thinking policy", () => {
|
||||
it("resolves a full profile without activating the provider runtime", () => {
|
||||
expect(listThinkingLevels("fixture-provider", "full")).toEqual(["off", "high", "max"]);
|
||||
expect(
|
||||
isThinkingLevelSupported({
|
||||
provider: "fixture-provider",
|
||||
model: "full",
|
||||
level: "max",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(resolveThinkingDefaultForModel({ provider: "fixture-provider", model: "full" })).toBe(
|
||||
"off",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the fixture legacy model on its binary profile", () => {
|
||||
expect(listThinkingLevels("fixture-provider", "legacy")).toEqual(["off", "low"]);
|
||||
expect(
|
||||
isThinkingLevelSupported({
|
||||
provider: "fixture-provider",
|
||||
model: "legacy",
|
||||
level: "high",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// Resolves provider thinking-level policy from plugin metadata.
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { resolveBundledProviderPolicySurface } from "./provider-public-artifacts.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||
import { resolveProviderPolicySurface } from "./provider-public-artifacts.js";
|
||||
import type {
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderThinkingProfile,
|
||||
@@ -57,6 +58,16 @@ function resolveActiveThinkingProvider(providerId: string): ThinkingProviderPlug
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveProviderPublicPolicySurface(providerId: string) {
|
||||
const metadataSnapshot = getCurrentPluginMetadataSnapshot({
|
||||
allowScopedSnapshot: true,
|
||||
allowWorkspaceScopedSnapshot: true,
|
||||
});
|
||||
return resolveProviderPolicySurface(providerId, {
|
||||
manifestRegistry: metadataSnapshot?.manifestRegistry,
|
||||
});
|
||||
}
|
||||
|
||||
type ThinkingHookParams<TContext> = {
|
||||
provider: string;
|
||||
context: TContext;
|
||||
@@ -86,7 +97,7 @@ export function resolveProviderThinkingProfile(
|
||||
if (activeProfile !== undefined) {
|
||||
return activeProfile;
|
||||
}
|
||||
return resolveBundledProviderPolicySurface(params.provider)?.resolveThinkingProfile?.(
|
||||
return resolveProviderPublicPolicySurface(params.provider)?.resolveThinkingProfile?.(
|
||||
params.context,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { resolveBundledPluginPublicSurfacePath } from "./public-surface-runtime.js";
|
||||
import {
|
||||
resolveBundledPluginPublicSurfacePath,
|
||||
resolvePluginRootPublicSurfacePath,
|
||||
} from "./public-surface-runtime.js";
|
||||
import { resolvePluginLoaderTryNative, resolveLoaderPackageRoot } from "./sdk-alias.js";
|
||||
|
||||
const OPENCLAW_PACKAGE_ROOT =
|
||||
@@ -114,6 +117,49 @@ function loadPublicSurfaceModule(modulePath: string): unknown {
|
||||
return getModuleLoader(modulePath)(modulePath);
|
||||
}
|
||||
|
||||
function loadValidatedPublicSurfaceModule(params: {
|
||||
modulePath: string;
|
||||
boundaryRoot: string;
|
||||
boundaryLabel: string;
|
||||
surfaceLabel: string;
|
||||
}): object {
|
||||
const cached = publicSurfaceModuleCache.get(params.modulePath);
|
||||
if (cached) {
|
||||
return cached as object;
|
||||
}
|
||||
|
||||
const opened = openRootFileSync({
|
||||
absolutePath: params.modulePath,
|
||||
rootPath: params.boundaryRoot,
|
||||
boundaryLabel: params.boundaryLabel,
|
||||
rejectHardlinks: false,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error(`Unable to open ${params.surfaceLabel}`, { cause: opened.error });
|
||||
}
|
||||
const validatedPath = opened.path;
|
||||
const validatedStat = opened.stat;
|
||||
fs.closeSync(opened.fd);
|
||||
|
||||
const currentStat = fs.statSync(validatedPath);
|
||||
if (!sameFileIdentity(validatedStat, currentStat)) {
|
||||
throw new Error(`${params.surfaceLabel} changed after validation`);
|
||||
}
|
||||
|
||||
const sentinel: Record<string, unknown> = {};
|
||||
publicSurfaceModuleCache.set(params.modulePath, sentinel);
|
||||
publicSurfaceModuleCache.set(validatedPath, sentinel);
|
||||
try {
|
||||
const loaded = loadPublicSurfaceModule(validatedPath) as object;
|
||||
Object.assign(sentinel, loaded);
|
||||
return sentinel;
|
||||
} catch (error) {
|
||||
publicSurfaceModuleCache.delete(params.modulePath);
|
||||
publicSurfaceModuleCache.delete(validatedPath);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Dynamic public artifact loaders use caller-supplied module surface types.
|
||||
export function loadBundledPluginPublicArtifactModuleSync<T extends object>(params: {
|
||||
dirName: string;
|
||||
@@ -125,47 +171,32 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
|
||||
`Unable to resolve bundled plugin public surface ${params.dirName}/${params.artifactBasename}`,
|
||||
);
|
||||
}
|
||||
const cached = publicSurfaceModuleCache.get(location.modulePath);
|
||||
if (cached) {
|
||||
return cached as T;
|
||||
}
|
||||
|
||||
const opened = openRootFileSync({
|
||||
absolutePath: location.modulePath,
|
||||
rootPath: location.boundaryRoot,
|
||||
return loadValidatedPublicSurfaceModule({
|
||||
modulePath: location.modulePath,
|
||||
boundaryRoot: location.boundaryRoot,
|
||||
boundaryLabel:
|
||||
location.boundaryRoot === OPENCLAW_PACKAGE_ROOT ? "OpenClaw package root" : "plugin root",
|
||||
rejectHardlinks: false,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
surfaceLabel: `bundled plugin public surface ${params.dirName}/${params.artifactBasename}`,
|
||||
}) as T;
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Dynamic public artifact loaders use caller-supplied module surface types.
|
||||
export function loadPluginPublicArtifactModuleSync<T extends object>(params: {
|
||||
pluginRoot: string;
|
||||
artifactBasename: string;
|
||||
}): T {
|
||||
const modulePath = resolvePluginRootPublicSurfacePath(params);
|
||||
if (!modulePath) {
|
||||
throw new Error(
|
||||
`Unable to open bundled plugin public surface ${params.dirName}/${params.artifactBasename}`,
|
||||
{ cause: opened.error },
|
||||
`Unable to resolve plugin public surface ${params.pluginRoot}/${params.artifactBasename}`,
|
||||
);
|
||||
}
|
||||
const validatedPath = opened.path;
|
||||
const validatedStat = opened.stat;
|
||||
fs.closeSync(opened.fd);
|
||||
|
||||
const currentStat = fs.statSync(validatedPath);
|
||||
if (!sameFileIdentity(validatedStat, currentStat)) {
|
||||
throw new Error(
|
||||
`Bundled plugin public surface changed after validation: ${params.dirName}/${params.artifactBasename}`,
|
||||
);
|
||||
}
|
||||
|
||||
const sentinel = {} as T;
|
||||
publicSurfaceModuleCache.set(location.modulePath, sentinel);
|
||||
publicSurfaceModuleCache.set(validatedPath, sentinel);
|
||||
try {
|
||||
const loaded = loadPublicSurfaceModule(validatedPath) as T;
|
||||
Object.assign(sentinel, loaded);
|
||||
return sentinel;
|
||||
} catch (error) {
|
||||
publicSurfaceModuleCache.delete(location.modulePath);
|
||||
publicSurfaceModuleCache.delete(validatedPath);
|
||||
throw error;
|
||||
}
|
||||
return loadValidatedPublicSurfaceModule({
|
||||
modulePath,
|
||||
boundaryRoot: path.resolve(params.pluginRoot),
|
||||
boundaryLabel: "plugin root",
|
||||
surfaceLabel: `plugin public surface ${params.artifactBasename}`,
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/** Loads the first resolvable bundled public artifact from an ordered candidate list. */
|
||||
|
||||
@@ -75,6 +75,31 @@ export function resolveBundledPluginSourcePublicSurfacePath(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolves a public surface artifact within one installed plugin root. */
|
||||
export function resolvePluginRootPublicSurfacePath(params: {
|
||||
pluginRoot: string;
|
||||
artifactBasename: string;
|
||||
}): string | null {
|
||||
const artifactBasename = normalizeBundledPluginArtifactSubpath(params.artifactBasename);
|
||||
const pluginRoot = path.resolve(params.pluginRoot);
|
||||
for (const candidate of [
|
||||
path.join(pluginRoot, artifactBasename),
|
||||
path.join(pluginRoot, "dist", artifactBasename),
|
||||
]) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
const sourceBaseName = artifactBasename.replace(/\.js$/u, "");
|
||||
for (const ext of PUBLIC_SURFACE_SOURCE_EXTENSIONS) {
|
||||
const candidate = path.join(pluginRoot, `${sourceBaseName}${ext}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePackageFallbackForBundledDir(params: {
|
||||
rootDir: string;
|
||||
bundledPluginsDir: string;
|
||||
|
||||
@@ -102,4 +102,33 @@ describe("plugin npm runtime build checks", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds top-level public policy surfaces as standalone package artifacts", async () => {
|
||||
const repoRoot = mkdtempSync(join(tmpdir(), "openclaw-plugin-runtime-check-"));
|
||||
tempDirs.push(repoRoot);
|
||||
writePackage(repoRoot, "provider-plugin", { publishToNpm: true }, ["./index.ts"]);
|
||||
const packageDir = join(repoRoot, "extensions", "provider-plugin");
|
||||
writeFileSync(join(packageDir, "index.ts"), "export default {};\n");
|
||||
writeFileSync(
|
||||
join(packageDir, "provider-policy-api.ts"),
|
||||
"export function resolveThinkingProfile() { return { levels: [] }; }\n",
|
||||
);
|
||||
|
||||
await expect(
|
||||
checkPluginNpmRuntimeBuilds({
|
||||
repoRoot,
|
||||
packageDirs: ["extensions/provider-plugin"],
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
pluginDir: "provider-plugin",
|
||||
status: "built",
|
||||
entryCount: 2,
|
||||
copiedStaticAssets: [],
|
||||
},
|
||||
]);
|
||||
expect(await import(join(packageDir, "dist", "provider-policy-api.js"))).toHaveProperty(
|
||||
"resolveThinkingProfile",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user