Skip to content

Commit e625651

Browse files
authored
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
1 parent fb80405 commit e625651

5 files changed

Lines changed: 269 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
4343
- Plugins/setup: report descriptor/runtime drift when setup-api registrations disagree with `setup.providers` or `setup.cliBackends`, without rejecting legacy setup plugins. Thanks @vincentkoc.
4444
- Plugin hooks: expose first-class run, message, sender, session, and trace correlation fields on message hook contexts and run lifecycle events. Thanks @vincentkoc.
4545
- 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.
46+
- Plugins/setup: derive generic provider setup choices from descriptor-safe `setup.providers[].authMethods` before falling back to setup runtime. Thanks @vincentkoc.
4647
- Plugins/setup: surface manifest provider auth choices directly in provider setup flow before falling back to setup runtime or install-catalog choices. Thanks @vincentkoc.
4748
- 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.
4849
- 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.

docs/plugins/manifest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ adapter during the deprecation window, but non-bundled plugins that still use it
335335
receive a manifest diagnostic. New plugins should put setup/status env metadata
336336
on `setup.providers[].envVars`.
337337

338+
OpenClaw can also derive simple setup choices from `setup.providers[].authMethods`
339+
when no setup entry is available, or when `setup.requiresRuntime: false`
340+
declares setup runtime unnecessary. Explicit `providerAuthChoices` entries stay
341+
preferred for custom labels, CLI flags, onboarding scope, and assistant metadata.
342+
338343
Set `requiresRuntime: false` only when those descriptors are sufficient for the
339344
setup surface. OpenClaw treats explicit `false` as a descriptor-only contract
340345
and will not execute `setup-api` or `openclaw.setupEntry` for setup lookup. If

extensions/slack/src/monitor.test-helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ vi.mock("@slack/bolt", () => {
287287
command() {
288288
/* no-op */
289289
}
290+
use() {
291+
/* no-op */
292+
}
290293
start = vi.fn().mockResolvedValue(undefined);
291294
stop = vi.fn().mockResolvedValue(undefined);
292295
}

src/plugins/provider-auth-choices.test.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,177 @@ describe("provider auth choice manifest helpers", () => {
230230
]);
231231
});
232232

233+
it("derives generic auth choices from descriptor-safe setup provider auth methods", () => {
234+
setManifestPlugins([
235+
{
236+
id: "demo-provider",
237+
name: "Demo Provider",
238+
origin: "global",
239+
setup: {
240+
providers: [
241+
{
242+
id: "demo-provider",
243+
authMethods: ["api-key", "oauth"],
244+
},
245+
],
246+
requiresRuntime: false,
247+
},
248+
},
249+
]);
250+
251+
expect(resolveManifestProviderAuthChoices()).toEqual([
252+
{
253+
pluginId: "demo-provider",
254+
providerId: "demo-provider",
255+
methodId: "api-key",
256+
choiceId: "demo-provider-api-key",
257+
choiceLabel: "Demo Provider API key",
258+
groupId: "demo-provider",
259+
groupLabel: "Demo Provider",
260+
},
261+
{
262+
pluginId: "demo-provider",
263+
providerId: "demo-provider",
264+
methodId: "oauth",
265+
choiceId: "demo-provider-oauth",
266+
choiceLabel: "Demo Provider OAuth",
267+
groupId: "demo-provider",
268+
groupLabel: "Demo Provider",
269+
},
270+
]);
271+
});
272+
273+
it("sanitizes setup provider auth descriptors before deriving prompt labels", () => {
274+
setManifestPlugins([
275+
{
276+
id: "evil-provider",
277+
origin: "workspace",
278+
setup: {
279+
providers: [
280+
{
281+
id: "evil\u001b[31m-provider",
282+
authMethods: ["jwt\u001b[2K", "oidc"],
283+
},
284+
],
285+
requiresRuntime: false,
286+
},
287+
},
288+
]);
289+
290+
expect(resolveManifestProviderAuthChoices()).toEqual([
291+
{
292+
pluginId: "evil-provider",
293+
providerId: "evil-provider",
294+
methodId: "jwt",
295+
choiceId: "evil-provider-jwt",
296+
choiceLabel: "Evil Provider JWT",
297+
groupId: "evil-provider",
298+
groupLabel: "Evil Provider",
299+
},
300+
{
301+
pluginId: "evil-provider",
302+
providerId: "evil-provider",
303+
methodId: "oidc",
304+
choiceId: "evil-provider-oidc",
305+
choiceLabel: "Evil Provider OIDC",
306+
groupId: "evil-provider",
307+
groupLabel: "Evil Provider",
308+
},
309+
]);
310+
});
311+
312+
it("uses setup provider auth methods when no setup entry exists", () => {
313+
setManifestPlugins([
314+
{
315+
id: "no-runtime-provider",
316+
origin: "global",
317+
setup: {
318+
providers: [
319+
{
320+
id: "no-runtime-provider",
321+
authMethods: ["api-key"],
322+
},
323+
],
324+
},
325+
},
326+
]);
327+
328+
expect(resolveManifestProviderAuthChoice("no-runtime-provider-api-key")).toEqual({
329+
pluginId: "no-runtime-provider",
330+
providerId: "no-runtime-provider",
331+
methodId: "api-key",
332+
choiceId: "no-runtime-provider-api-key",
333+
choiceLabel: "No Runtime Provider API key",
334+
groupId: "no-runtime-provider",
335+
groupLabel: "No Runtime Provider",
336+
});
337+
});
338+
339+
it("keeps setup-entry providers on explicit manifest or runtime auth choices", () => {
340+
setManifestPlugins([
341+
{
342+
id: "runtime-provider",
343+
origin: "global",
344+
setupSource: "/plugins/runtime-provider/setup-entry.cjs",
345+
setup: {
346+
providers: [
347+
{
348+
id: "runtime-provider",
349+
authMethods: ["api-key"],
350+
},
351+
],
352+
},
353+
},
354+
]);
355+
356+
expect(resolveManifestProviderAuthChoices()).toEqual([]);
357+
});
358+
359+
it("does not duplicate explicit provider auth choices with setup auth methods", () => {
360+
setManifestPlugins([
361+
{
362+
id: "explicit-provider",
363+
origin: "global",
364+
providerAuthChoices: [
365+
{
366+
provider: "explicit-provider",
367+
method: "api-key",
368+
choiceId: "explicit-api-key",
369+
choiceLabel: "Explicit API key",
370+
},
371+
],
372+
setup: {
373+
providers: [
374+
{
375+
id: "explicit-provider",
376+
authMethods: ["api-key", "oauth"],
377+
},
378+
],
379+
requiresRuntime: false,
380+
},
381+
},
382+
]);
383+
384+
expect(resolveManifestProviderAuthChoices()).toEqual([
385+
{
386+
pluginId: "explicit-provider",
387+
providerId: "explicit-provider",
388+
methodId: "api-key",
389+
choiceId: "explicit-api-key",
390+
choiceLabel: "Explicit API key",
391+
},
392+
{
393+
pluginId: "explicit-provider",
394+
providerId: "explicit-provider",
395+
methodId: "oauth",
396+
choiceId: "explicit-provider-oauth",
397+
choiceLabel: "Explicit Provider OAuth",
398+
groupId: "explicit-provider",
399+
groupLabel: "Explicit Provider",
400+
},
401+
]);
402+
});
403+
233404
it("prefers bundled auth-choice handlers when choice IDs collide across origins", () => {
234405
setManifestPlugins([
235406
{

src/plugins/provider-auth-choices.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
3+
import { sanitizeForLog } from "../terminal/ansi.js";
34
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
45
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
56
import type { PluginOrigin } from "./plugin-origin.types.js";
@@ -53,6 +54,15 @@ const PROVIDER_AUTH_CHOICE_ORIGIN_PRIORITY: Readonly<Record<PluginOrigin, number
5354
global: 2,
5455
workspace: 3,
5556
};
57+
const DESCRIPTOR_LABEL_ACRONYMS: ReadonlyMap<string, string> = new Map([
58+
["api", "API"],
59+
["jwt", "JWT"],
60+
["oauth", "OAuth"],
61+
["oidc", "OIDC"],
62+
["pkce", "PKCE"],
63+
["saml", "SAML"],
64+
["sso", "SSO"],
65+
] as const);
5666

5767
function resolveProviderAuthChoiceOriginPriority(origin: PluginOrigin | undefined): number {
5868
if (!origin) {
@@ -91,6 +101,73 @@ function toProviderAuthChoiceCandidate(params: {
91101
};
92102
}
93103

104+
function formatDescriptorLabel(value: string): string {
105+
return sanitizeForLog(value)
106+
.trim()
107+
.split(/[-_\s]+/gu)
108+
.filter(Boolean)
109+
.map((part) => {
110+
const lower = part.toLowerCase();
111+
const acronym = DESCRIPTOR_LABEL_ACRONYMS.get(lower);
112+
if (acronym) {
113+
return acronym;
114+
}
115+
return `${lower.slice(0, 1).toUpperCase()}${lower.slice(1)}`;
116+
})
117+
.join(" ");
118+
}
119+
120+
function normalizeManifestAuthDescriptorId(value: string): string {
121+
return sanitizeForLog(value).trim();
122+
}
123+
124+
function toSetupProviderAuthChoiceCandidate(params: {
125+
plugin: PluginManifestRecord;
126+
providerId: string;
127+
methodId: string;
128+
}): ProviderAuthChoiceCandidate {
129+
const providerLabel = formatDescriptorLabel(params.providerId);
130+
const methodLabel = formatDescriptorLabel(params.methodId);
131+
const choiceLabel =
132+
params.methodId === "api-key" ? `${providerLabel} API key` : `${providerLabel} ${methodLabel}`;
133+
return {
134+
pluginId: params.plugin.id,
135+
origin: params.plugin.origin,
136+
providerId: params.providerId,
137+
methodId: params.methodId,
138+
choiceId: `${params.providerId}-${params.methodId}`,
139+
choiceLabel,
140+
groupId: params.providerId,
141+
groupLabel: providerLabel,
142+
};
143+
}
144+
145+
function listSetupProviderAuthChoiceCandidates(plugin: PluginManifestRecord) {
146+
if (plugin.setup?.requiresRuntime !== false && plugin.setupSource) {
147+
return [];
148+
}
149+
const explicitProviderMethods = new Set(
150+
(plugin.providerAuthChoices ?? []).map((choice) => `${choice.provider}::${choice.method}`),
151+
);
152+
return (plugin.setup?.providers ?? []).flatMap((provider) => {
153+
const providerId = normalizeManifestAuthDescriptorId(provider.id);
154+
if (!providerId) {
155+
return [];
156+
}
157+
return (provider.authMethods ?? [])
158+
.map(normalizeManifestAuthDescriptorId)
159+
.filter(Boolean)
160+
.filter((methodId) => !explicitProviderMethods.has(`${providerId}::${methodId}`))
161+
.map((methodId) =>
162+
toSetupProviderAuthChoiceCandidate({
163+
plugin,
164+
providerId,
165+
methodId,
166+
}),
167+
);
168+
});
169+
}
170+
94171
function stripChoiceOrigin(choice: ProviderAuthChoiceCandidate): ProviderAuthChoiceMetadata {
95172
const { origin: _origin, ...metadata } = choice;
96173
return metadata;
@@ -121,13 +198,18 @@ function resolveManifestProviderAuthChoiceCandidates(params?: {
121198
) {
122199
return [];
123200
}
124-
return (plugin.providerAuthChoices ?? []).map((choice) =>
125-
toProviderAuthChoiceCandidate({
126-
pluginId: plugin.id,
127-
origin: plugin.origin,
128-
choice,
129-
}),
130-
);
201+
const choices: ProviderAuthChoiceCandidate[] = [];
202+
for (const choice of plugin.providerAuthChoices ?? []) {
203+
choices.push(
204+
toProviderAuthChoiceCandidate({
205+
pluginId: plugin.id,
206+
origin: plugin.origin,
207+
choice,
208+
}),
209+
);
210+
}
211+
choices.push(...listSetupProviderAuthChoiceCandidates(plugin));
212+
return choices;
131213
});
132214
}
133215

0 commit comments

Comments
 (0)