feat(plugins): derive setup auth choices

* feat(plugins): derive setup auth choices

* fix(plugins): sanitize derived provider auth choices

* fix(plugins): clean up extension gate regressions
This commit is contained in:
Vincent Koc
2026-04-24 16:57:39 -07:00
committed by GitHub
parent fb80405693
commit e625651de8
5 changed files with 269 additions and 7 deletions

View File

@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
- Plugins/setup: report descriptor/runtime drift when setup-api registrations disagree with `setup.providers` or `setup.cliBackends`, without rejecting legacy setup plugins. Thanks @vincentkoc.
- Plugin hooks: expose first-class run, message, sender, session, and trace correlation fields on message hook contexts and run lifecycle events. Thanks @vincentkoc.
- Plugins/setup: include `setup.providers[].envVars` in generic provider auth/env lookups and warn non-bundled plugins that still rely on deprecated `providerAuthEnvVars` compatibility metadata. Thanks @vincentkoc.
- Plugins/setup: derive generic provider setup choices from descriptor-safe `setup.providers[].authMethods` before falling back to setup runtime. Thanks @vincentkoc.
- Plugins/setup: surface manifest provider auth choices directly in provider setup flow before falling back to setup runtime or install-catalog choices. Thanks @vincentkoc.
- Plugins/setup: warn when descriptor-only setup plugins still ship ignored setup runtime entries, keeping `setup.requiresRuntime: false` semantics explicit without breaking existing metadata. Thanks @vincentkoc.
- Plugins/channels: use manifest `channelConfigs` for read-only external channel discovery when no setup entry is available or setup descriptors declare runtime unnecessary. Thanks @vincentkoc.

View File

@@ -335,6 +335,11 @@ adapter during the deprecation window, but non-bundled plugins that still use it
receive a manifest diagnostic. New plugins should put setup/status env metadata
on `setup.providers[].envVars`.
OpenClaw can also derive simple setup choices from `setup.providers[].authMethods`
when no setup entry is available, or when `setup.requiresRuntime: false`
declares setup runtime unnecessary. Explicit `providerAuthChoices` entries stay
preferred for custom labels, CLI flags, onboarding scope, and assistant metadata.
Set `requiresRuntime: false` only when those descriptors are sufficient for the
setup surface. OpenClaw treats explicit `false` as a descriptor-only contract
and will not execute `setup-api` or `openclaw.setupEntry` for setup lookup. If

View File

@@ -287,6 +287,9 @@ vi.mock("@slack/bolt", () => {
command() {
/* no-op */
}
use() {
/* no-op */
}
start = vi.fn().mockResolvedValue(undefined);
stop = vi.fn().mockResolvedValue(undefined);
}

View File

@@ -230,6 +230,177 @@ describe("provider auth choice manifest helpers", () => {
]);
});
it("derives generic auth choices from descriptor-safe setup provider auth methods", () => {
setManifestPlugins([
{
id: "demo-provider",
name: "Demo Provider",
origin: "global",
setup: {
providers: [
{
id: "demo-provider",
authMethods: ["api-key", "oauth"],
},
],
requiresRuntime: false,
},
},
]);
expect(resolveManifestProviderAuthChoices()).toEqual([
{
pluginId: "demo-provider",
providerId: "demo-provider",
methodId: "api-key",
choiceId: "demo-provider-api-key",
choiceLabel: "Demo Provider API key",
groupId: "demo-provider",
groupLabel: "Demo Provider",
},
{
pluginId: "demo-provider",
providerId: "demo-provider",
methodId: "oauth",
choiceId: "demo-provider-oauth",
choiceLabel: "Demo Provider OAuth",
groupId: "demo-provider",
groupLabel: "Demo Provider",
},
]);
});
it("sanitizes setup provider auth descriptors before deriving prompt labels", () => {
setManifestPlugins([
{
id: "evil-provider",
origin: "workspace",
setup: {
providers: [
{
id: "evil\u001b[31m-provider",
authMethods: ["jwt\u001b[2K", "oidc"],
},
],
requiresRuntime: false,
},
},
]);
expect(resolveManifestProviderAuthChoices()).toEqual([
{
pluginId: "evil-provider",
providerId: "evil-provider",
methodId: "jwt",
choiceId: "evil-provider-jwt",
choiceLabel: "Evil Provider JWT",
groupId: "evil-provider",
groupLabel: "Evil Provider",
},
{
pluginId: "evil-provider",
providerId: "evil-provider",
methodId: "oidc",
choiceId: "evil-provider-oidc",
choiceLabel: "Evil Provider OIDC",
groupId: "evil-provider",
groupLabel: "Evil Provider",
},
]);
});
it("uses setup provider auth methods when no setup entry exists", () => {
setManifestPlugins([
{
id: "no-runtime-provider",
origin: "global",
setup: {
providers: [
{
id: "no-runtime-provider",
authMethods: ["api-key"],
},
],
},
},
]);
expect(resolveManifestProviderAuthChoice("no-runtime-provider-api-key")).toEqual({
pluginId: "no-runtime-provider",
providerId: "no-runtime-provider",
methodId: "api-key",
choiceId: "no-runtime-provider-api-key",
choiceLabel: "No Runtime Provider API key",
groupId: "no-runtime-provider",
groupLabel: "No Runtime Provider",
});
});
it("keeps setup-entry providers on explicit manifest or runtime auth choices", () => {
setManifestPlugins([
{
id: "runtime-provider",
origin: "global",
setupSource: "/plugins/runtime-provider/setup-entry.cjs",
setup: {
providers: [
{
id: "runtime-provider",
authMethods: ["api-key"],
},
],
},
},
]);
expect(resolveManifestProviderAuthChoices()).toEqual([]);
});
it("does not duplicate explicit provider auth choices with setup auth methods", () => {
setManifestPlugins([
{
id: "explicit-provider",
origin: "global",
providerAuthChoices: [
{
provider: "explicit-provider",
method: "api-key",
choiceId: "explicit-api-key",
choiceLabel: "Explicit API key",
},
],
setup: {
providers: [
{
id: "explicit-provider",
authMethods: ["api-key", "oauth"],
},
],
requiresRuntime: false,
},
},
]);
expect(resolveManifestProviderAuthChoices()).toEqual([
{
pluginId: "explicit-provider",
providerId: "explicit-provider",
methodId: "api-key",
choiceId: "explicit-api-key",
choiceLabel: "Explicit API key",
},
{
pluginId: "explicit-provider",
providerId: "explicit-provider",
methodId: "oauth",
choiceId: "explicit-provider-oauth",
choiceLabel: "Explicit Provider OAuth",
groupId: "explicit-provider",
groupLabel: "Explicit Provider",
},
]);
});
it("prefers bundled auth-choice handlers when choice IDs collide across origins", () => {
setManifestPlugins([
{

View File

@@ -1,5 +1,6 @@
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
import type { PluginOrigin } from "./plugin-origin.types.js";
@@ -53,6 +54,15 @@ const PROVIDER_AUTH_CHOICE_ORIGIN_PRIORITY: Readonly<Record<PluginOrigin, number
global: 2,
workspace: 3,
};
const DESCRIPTOR_LABEL_ACRONYMS: ReadonlyMap<string, string> = new Map([
["api", "API"],
["jwt", "JWT"],
["oauth", "OAuth"],
["oidc", "OIDC"],
["pkce", "PKCE"],
["saml", "SAML"],
["sso", "SSO"],
] as const);
function resolveProviderAuthChoiceOriginPriority(origin: PluginOrigin | undefined): number {
if (!origin) {
@@ -91,6 +101,73 @@ function toProviderAuthChoiceCandidate(params: {
};
}
function formatDescriptorLabel(value: string): string {
return sanitizeForLog(value)
.trim()
.split(/[-_\s]+/gu)
.filter(Boolean)
.map((part) => {
const lower = part.toLowerCase();
const acronym = DESCRIPTOR_LABEL_ACRONYMS.get(lower);
if (acronym) {
return acronym;
}
return `${lower.slice(0, 1).toUpperCase()}${lower.slice(1)}`;
})
.join(" ");
}
function normalizeManifestAuthDescriptorId(value: string): string {
return sanitizeForLog(value).trim();
}
function toSetupProviderAuthChoiceCandidate(params: {
plugin: PluginManifestRecord;
providerId: string;
methodId: string;
}): ProviderAuthChoiceCandidate {
const providerLabel = formatDescriptorLabel(params.providerId);
const methodLabel = formatDescriptorLabel(params.methodId);
const choiceLabel =
params.methodId === "api-key" ? `${providerLabel} API key` : `${providerLabel} ${methodLabel}`;
return {
pluginId: params.plugin.id,
origin: params.plugin.origin,
providerId: params.providerId,
methodId: params.methodId,
choiceId: `${params.providerId}-${params.methodId}`,
choiceLabel,
groupId: params.providerId,
groupLabel: providerLabel,
};
}
function listSetupProviderAuthChoiceCandidates(plugin: PluginManifestRecord) {
if (plugin.setup?.requiresRuntime !== false && plugin.setupSource) {
return [];
}
const explicitProviderMethods = new Set(
(plugin.providerAuthChoices ?? []).map((choice) => `${choice.provider}::${choice.method}`),
);
return (plugin.setup?.providers ?? []).flatMap((provider) => {
const providerId = normalizeManifestAuthDescriptorId(provider.id);
if (!providerId) {
return [];
}
return (provider.authMethods ?? [])
.map(normalizeManifestAuthDescriptorId)
.filter(Boolean)
.filter((methodId) => !explicitProviderMethods.has(`${providerId}::${methodId}`))
.map((methodId) =>
toSetupProviderAuthChoiceCandidate({
plugin,
providerId,
methodId,
}),
);
});
}
function stripChoiceOrigin(choice: ProviderAuthChoiceCandidate): ProviderAuthChoiceMetadata {
const { origin: _origin, ...metadata } = choice;
return metadata;
@@ -121,13 +198,18 @@ function resolveManifestProviderAuthChoiceCandidates(params?: {
) {
return [];
}
return (plugin.providerAuthChoices ?? []).map((choice) =>
toProviderAuthChoiceCandidate({
pluginId: plugin.id,
origin: plugin.origin,
choice,
}),
);
const choices: ProviderAuthChoiceCandidate[] = [];
for (const choice of plugin.providerAuthChoices ?? []) {
choices.push(
toProviderAuthChoiceCandidate({
pluginId: plugin.id,
origin: plugin.origin,
choice,
}),
);
}
choices.push(...listSetupProviderAuthChoiceCandidates(plugin));
return choices;
});
}