mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -287,6 +287,9 @@ vi.mock("@slack/bolt", () => {
|
||||
command() {
|
||||
/* no-op */
|
||||
}
|
||||
use() {
|
||||
/* no-op */
|
||||
}
|
||||
start = vi.fn().mockResolvedValue(undefined);
|
||||
stop = vi.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user