fix: distinguish read-only plugin options

This commit is contained in:
Gustavo Madeira Santana
2026-04-21 11:22:15 -04:00
parent 2d0aa18606
commit 06c23e36ef
2 changed files with 73 additions and 9 deletions

View File

@@ -366,6 +366,33 @@ describe("listReadOnlyChannelPluginsForConfig", () => {
expect(fs.existsSync(fullMarker)).toBe(false);
});
it("treats process env maps with option-like keys as env maps", () => {
const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin({
pluginId: "external-chat-plugin",
channelId: "external-chat",
});
const plugins = listReadOnlyChannelPluginsForConfig(
{
plugins: {
load: { paths: [pluginDir] },
allow: ["external-chat-plugin"],
},
} as never,
{
...process.env,
cache: "true",
env: "prod",
EXTERNAL_CHAT_TOKEN: "configured",
workspaceDir: "workspace-env-value",
} as NodeJS.ProcessEnv,
);
const plugin = plugins.find((entry) => entry.id === "external-chat");
expect(plugin?.meta.blurb).toBe("setup entry");
expect(fs.existsSync(setupMarker)).toBe(true);
expect(fs.existsSync(fullMarker)).toBe(false);
});
it("discovers trusted external channel plugins from the default agent workspace", () => {
const workspaceDir = makeTempDir();
const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "external-chat-plugin");

View File

@@ -27,22 +27,59 @@ type ReadOnlyChannelPluginResolution = {
missingConfiguredChannelIds: string[];
};
const READ_ONLY_CHANNEL_PLUGIN_OPTION_KEYS = new Set([
"env",
"workspaceDir",
"activationSourceConfig",
"includePersistedAuthState",
"cache",
]);
function hasOwnRecordKey(record: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(record, key);
}
function isRecordLike(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isReadOnlyChannelPluginOptions(
value: NodeJS.ProcessEnv | ReadOnlyChannelPluginOptions,
): value is ReadOnlyChannelPluginOptions {
const record = value as Record<string, unknown>;
if (hasOwnRecordKey(record, "env")) {
return record.env === undefined || isRecordLike(record.env);
}
if (hasOwnRecordKey(record, "activationSourceConfig")) {
return (
record.activationSourceConfig === undefined || isRecordLike(record.activationSourceConfig)
);
}
if (hasOwnRecordKey(record, "includePersistedAuthState")) {
return (
record.includePersistedAuthState === undefined ||
typeof record.includePersistedAuthState === "boolean"
);
}
if (hasOwnRecordKey(record, "cache")) {
return record.cache === undefined || typeof record.cache === "boolean";
}
if (hasOwnRecordKey(record, "workspaceDir")) {
return Object.keys(record).every((key) => READ_ONLY_CHANNEL_PLUGIN_OPTION_KEYS.has(key));
}
return false;
}
function resolveReadOnlyChannelPluginOptions(
envOrOptions?: NodeJS.ProcessEnv | ReadOnlyChannelPluginOptions,
): ReadOnlyChannelPluginOptions {
if (!envOrOptions) {
return {};
}
if (
"env" in envOrOptions ||
"workspaceDir" in envOrOptions ||
"activationSourceConfig" in envOrOptions ||
"includePersistedAuthState" in envOrOptions ||
"cache" in envOrOptions
) {
return envOrOptions as ReadOnlyChannelPluginOptions;
if (isReadOnlyChannelPluginOptions(envOrOptions)) {
return envOrOptions;
}
return { env: envOrOptions as NodeJS.ProcessEnv };
return { env: envOrOptions };
}
function addChannelPlugins(