mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(plugins): avoid false SecretRef setup errors (#155618)
Validate managed plugin setup against the authored config paired with the runtime snapshot. Capture that source before hosted catalog I/O so a concurrent runtime publication cannot substitute another generation. Preserve malformed and missing authored config errors and explicit candidate edits. Cover active and captured runtime snapshots across publication changes.
This commit is contained in:
parent
88f558d453
commit
b05632e131
2 changed files with 104 additions and 2 deletions
|
|
@ -3,6 +3,12 @@ import path from "node:path";
|
|||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
} from "../config/runtime-snapshot.js";
|
||||
import { captureRuntimeConfig } from "../config/runtime-source-projection.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { joinClawHubPluginCatalog } from "./catalog-discovery.js";
|
||||
import {
|
||||
emptyMetadataSnapshot,
|
||||
|
|
@ -64,7 +70,10 @@ function mockHostedOfficialCatalog(entries: unknown[]) {
|
|||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
describe("managed plugin catalog", () => {
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
clearRuntimeConfigSnapshot();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearManagedPluginCatalogCache();
|
||||
|
|
@ -157,6 +166,95 @@ describe("managed plugin catalog", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("authored credential validation", () => {
|
||||
const secretRef = { source: "store", provider: "default", id: "TEST_PLUGIN_KEY" };
|
||||
const configured = (config?: Record<string, unknown>, enabled = true): OpenClawConfig => ({
|
||||
plugins: { entries: { "ref-plugin": { enabled, ...(config ? { config } : {}) } } },
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.metadata.mockReturnValue(
|
||||
metadataSnapshot({
|
||||
enabled: true,
|
||||
id: "ref-plugin",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
required: ["apiKey"],
|
||||
properties: {
|
||||
apiKey: {
|
||||
type: "object",
|
||||
required: ["source", "provider", "id"],
|
||||
properties: {
|
||||
source: { const: "store" },
|
||||
provider: { type: "string" },
|
||||
id: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["active", "captured"])(
|
||||
"validates the %s runtime's authored refs across a catalog await",
|
||||
async (mode) => {
|
||||
const source = configured({ apiKey: secretRef });
|
||||
const runtime = configured({ apiKey: "synthetic-resolved-value" });
|
||||
setRuntimeConfigSnapshot(runtime, source);
|
||||
const config = mode === "captured" ? captureRuntimeConfig(runtime) : runtime;
|
||||
// Captured requests can already predate the current publication on entry.
|
||||
if (mode === "captured") {
|
||||
setRuntimeConfigSnapshot(configured(), configured());
|
||||
}
|
||||
mocks.officialCatalog.mockImplementationOnce(async () => {
|
||||
setRuntimeConfigSnapshot(configured(), configured({ apiKey: 42 }));
|
||||
return { source: "hosted", entries: [] };
|
||||
});
|
||||
|
||||
const catalog = await listManagedPlugins({ config, env: {} });
|
||||
|
||||
expect(catalog.plugins[0]).toMatchObject({ id: "ref-plugin", state: "enabled" });
|
||||
expect(catalog.plugins[0]).not.toHaveProperty("error");
|
||||
expect(runtime.plugins?.entries?.["ref-plugin"]?.config?.apiKey).toBe(
|
||||
"synthetic-resolved-value",
|
||||
);
|
||||
expect(source.plugins?.entries?.["ref-plugin"]?.config?.apiKey).toEqual(secretRef);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["invalid authored config", { apiKey: "synthetic-invalid-plaintext" }, "error"],
|
||||
["missing authored config", undefined, "needs-setup"],
|
||||
] as const)("preserves %s", async (_name, authoredConfig, state) => {
|
||||
const source = configured(authoredConfig, false);
|
||||
const runtime = configured({ apiKey: secretRef }, false);
|
||||
setRuntimeConfigSnapshot(runtime, source);
|
||||
|
||||
const catalog = await listManagedPlugins({ config: runtime, env: {} });
|
||||
|
||||
expect(catalog.plugins[0]).toMatchObject({ state });
|
||||
if (state === "error") {
|
||||
expect(catalog.plugins[0]?.error).toBe("apiKey: must be object");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not replace an explicit candidate with the active source", async () => {
|
||||
setRuntimeConfigSnapshot(
|
||||
configured({ apiKey: "synthetic-resolved-value" }),
|
||||
configured({ apiKey: secretRef }),
|
||||
);
|
||||
const candidate = configured({ apiKey: "synthetic-invalid-candidate" });
|
||||
|
||||
const catalog = await listManagedPlugins({ config: candidate, env: {} });
|
||||
|
||||
expect(catalog.plugins[0]).toMatchObject({
|
||||
state: "error",
|
||||
error: "apiKey: must be object",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("does not transfer bundled endorsement to a package identity impostor", async () => {
|
||||
mocks.metadata.mockReturnValue(emptyMetadataSnapshot());
|
||||
mockHostedOfficialCatalog([
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { resolveIsConfigReadOnly } from "../config/paths.js";
|
|||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveClawHubBaseUrl } from "../infra/clawhub-client.js";
|
||||
import { fetchClawHubPluginVersionCategories } from "../infra/clawhub-plugin-catalog.js";
|
||||
import { resolvePluginActivationSourceConfig } from "./activation-source-config.js";
|
||||
import { resolvePendingPluginCapabilityReview } from "./capability-consent.js";
|
||||
import {
|
||||
buildPluginCapabilitySummary,
|
||||
|
|
@ -237,6 +238,9 @@ export const listManagedPlugins = withManagedPluginCache(
|
|||
officialCatalog?: OfficialCatalogResult;
|
||||
metadata?: PluginMetadataSnapshot;
|
||||
}): Promise<ManagedPluginCatalog> => {
|
||||
// Manifest schemas describe authored SecretRefs, not resolved runtime strings.
|
||||
// Retain the paired source before hosted catalog I/O can publish another generation.
|
||||
const sourceConfig = resolvePluginActivationSourceConfig({ config: params.config });
|
||||
const env = params.env ?? process.env;
|
||||
const workspace = resolvePluginControlPlaneWorkspace({ config: params.config, env });
|
||||
const metadata = params.metadata ?? resolveManagedPluginMetadata(params.config, env);
|
||||
|
|
@ -302,7 +306,7 @@ export const listManagedPlugins = withManagedPluginCache(
|
|||
? { ...localCatalog, ...officialCatalogMetadata }
|
||||
: localCatalog;
|
||||
const setup = resolvePluginConfigEnablement({
|
||||
config: params.config,
|
||||
config: sourceConfig,
|
||||
pluginId: record.pluginId,
|
||||
manifest,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue