feat: add external auth provider contracts

This commit is contained in:
Shakker
2026-04-23 06:34:12 +01:00
committed by Shakker
parent 4d1d0cd021
commit 47ae15c059
4 changed files with 98 additions and 0 deletions

View File

@@ -47,6 +47,7 @@ import { resolvePluginCacheInputs } from "./roots.js";
type PluginManifestContractListKey =
| "speechProviders"
| "externalAuthProviders"
| "mediaUnderstandingProviders"
| "realtimeVoiceProviders"
| "realtimeTranscriptionProviders"

View File

@@ -233,6 +233,12 @@ export type PluginManifest = {
export type PluginManifestContracts = {
embeddedExtensionFactories?: string[];
/**
* Provider ids whose external auth profile hook can contribute runtime-only
* credentials. Declaring this lets auth-store overlays load only the owning
* plugin instead of every provider plugin.
*/
externalAuthProviders?: string[];
memoryEmbeddingProviders?: string[];
speechProviders?: string[];
realtimeTranscriptionProviders?: string[];
@@ -420,6 +426,7 @@ function normalizeManifestContracts(value: unknown): PluginManifestContracts | u
}
const embeddedExtensionFactories = normalizeTrimmedStringList(value.embeddedExtensionFactories);
const externalAuthProviders = normalizeTrimmedStringList(value.externalAuthProviders);
const memoryEmbeddingProviders = normalizeTrimmedStringList(value.memoryEmbeddingProviders);
const speechProviders = normalizeTrimmedStringList(value.speechProviders);
const realtimeTranscriptionProviders = normalizeTrimmedStringList(
@@ -435,6 +442,7 @@ function normalizeManifestContracts(value: unknown): PluginManifestContracts | u
const tools = normalizeTrimmedStringList(value.tools);
const contracts = {
...(embeddedExtensionFactories.length > 0 ? { embeddedExtensionFactories } : {}),
...(externalAuthProviders.length > 0 ? { externalAuthProviders } : {}),
...(memoryEmbeddingProviders.length > 0 ? { memoryEmbeddingProviders } : {}),
...(speechProviders.length > 0 ? { speechProviders } : {}),
...(realtimeTranscriptionProviders.length > 0 ? { realtimeTranscriptionProviders } : {}),

View File

@@ -10,6 +10,8 @@ type LoadOpenClawPlugins = typeof import("./loader.js").loadOpenClawPlugins;
type IsPluginRegistryLoadInFlight = typeof import("./loader.js").isPluginRegistryLoadInFlight;
type LoadPluginManifestRegistry =
typeof import("./manifest-registry.js").loadPluginManifestRegistry;
type ResolveManifestContractPluginIds =
typeof import("./manifest-registry.js").resolveManifestContractPluginIds;
type ApplyPluginAutoEnable = typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable;
type SetActivePluginRegistry = typeof import("./runtime.js").setActivePluginRegistry;
@@ -17,12 +19,30 @@ const resolveRuntimePluginRegistryMock = vi.fn<ResolveRuntimePluginRegistry>();
const loadOpenClawPluginsMock = vi.fn<LoadOpenClawPlugins>();
const isPluginRegistryLoadInFlightMock = vi.fn<IsPluginRegistryLoadInFlight>((_) => false);
const loadPluginManifestRegistryMock = vi.fn<LoadPluginManifestRegistry>();
const resolveManifestContractPluginIdsMock = vi.fn<ResolveManifestContractPluginIds>((params) => {
const onlyPluginIds =
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
return loadPluginManifestRegistryMock({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter(
(plugin) =>
(!params.origin || plugin.origin === params.origin) &&
(!onlyPluginIds || onlyPluginIds.has(plugin.id)) &&
(plugin.contracts?.[params.contract] ?? []).length > 0,
)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
});
const applyPluginAutoEnableMock = vi.fn<ApplyPluginAutoEnable>();
let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider;
let resolveOwningPluginIdsForModelRef: typeof import("./providers.js").resolveOwningPluginIdsForModelRef;
let resolveActivatableProviderOwnerPluginIds: typeof import("./providers.js").resolveActivatableProviderOwnerPluginIds;
let resolveEnabledProviderPluginIds: typeof import("./providers.js").resolveEnabledProviderPluginIds;
let resolveExternalAuthProfileProviderPluginIds: typeof import("./providers.js").resolveExternalAuthProfileProviderPluginIds;
let resolveDiscoveredProviderPluginIds: typeof import("./providers.js").resolveDiscoveredProviderPluginIds;
let resolveDiscoverableProviderOwnerPluginIds: typeof import("./providers.js").resolveDiscoverableProviderOwnerPluginIds;
let resolvePluginProviders: typeof import("./providers.runtime.js").resolvePluginProviders;
@@ -37,6 +57,7 @@ function createManifestProviderPlugin(params: {
modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] };
activation?: PluginManifestRecord["activation"];
setup?: PluginManifestRecord["setup"];
contracts?: PluginManifestRecord["contracts"];
}): PluginManifestRecord {
return {
id: params.id,
@@ -47,6 +68,7 @@ function createManifestProviderPlugin(params: {
modelSupport: params.modelSupport,
activation: params.activation,
setup: params.setup,
contracts: params.contracts,
skills: [],
hooks: [],
origin: params.origin ?? "bundled",
@@ -285,12 +307,15 @@ describe("resolvePluginProviders", () => {
vi.doMock("./manifest-registry.js", () => ({
loadPluginManifestRegistry: (...args: Parameters<LoadPluginManifestRegistry>) =>
loadPluginManifestRegistryMock(...args),
resolveManifestContractPluginIds: (...args: Parameters<ResolveManifestContractPluginIds>) =>
resolveManifestContractPluginIdsMock(...args),
}));
({
resolveActivatableProviderOwnerPluginIds,
resolveOwningPluginIdsForProvider,
resolveOwningPluginIdsForModelRef,
resolveEnabledProviderPluginIds,
resolveExternalAuthProfileProviderPluginIds,
resolveDiscoveredProviderPluginIds,
resolveDiscoverableProviderOwnerPluginIds,
} = await import("./providers.js"));
@@ -321,6 +346,7 @@ describe("resolvePluginProviders", () => {
resolveRuntimePluginRegistryMock.mockReturnValue(registry);
loadOpenClawPluginsMock.mockReturnValue(registry);
loadPluginManifestRegistryMock.mockReset();
resolveManifestContractPluginIdsMock.mockClear();
applyPluginAutoEnableMock.mockReset();
applyPluginAutoEnableMock.mockImplementation(
(params): PluginAutoEnableResult => ({
@@ -387,6 +413,28 @@ describe("resolvePluginProviders", () => {
]);
});
it("resolves external auth hook plugin ids from manifest contracts without runtime loading", () => {
setManifestPlugins([
createManifestProviderPlugin({
id: "external-auth-owner",
providerIds: ["demo"],
contracts: { externalAuthProviders: ["demo"] },
}),
createManifestProviderPlugin({
id: "regular-provider",
providerIds: ["regular"],
}),
]);
expect(
resolveExternalAuthProfileProviderPluginIds({
config: {},
env: {} as NodeJS.ProcessEnv,
}),
).toEqual(["external-auth-owner"]);
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
});
it("treats explicit empty provider scopes as scoped-empty in provider helpers", () => {
expect(
resolveEnabledProviderPluginIds({

View File

@@ -8,6 +8,7 @@ import {
} from "./manifest-owner-policy.js";
import {
loadPluginManifestRegistry,
resolveManifestContractPluginIds,
type PluginManifestRecord,
type PluginManifestRegistry,
} from "./manifest-registry.js";
@@ -118,6 +119,44 @@ export function resolveEnabledProviderPluginIds(params: {
);
}
export function resolveExternalAuthProfileProviderPluginIds(params: {
config?: PluginLoadOptions["config"];
workspaceDir?: string;
env?: PluginLoadOptions["env"];
}): string[] {
return resolveManifestContractPluginIds({
contract: "externalAuthProviders",
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
}
export function resolveExternalAuthProfileCompatFallbackPluginIds(params: {
config?: PluginLoadOptions["config"];
workspaceDir?: string;
env?: PluginLoadOptions["env"];
}): string[] {
// Compatibility fallback for provider plugins that implemented the public
// external auth hook before contracts.externalAuthProviders existed. Remove
// this with the warning path in provider-runtime after the deprecation window.
const declaredPluginIds = new Set(resolveExternalAuthProfileProviderPluginIds(params));
const registry = loadProviderManifestRegistry(params);
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
return listManifestPluginIds(
registry,
(plugin) =>
plugin.origin !== "bundled" &&
plugin.providers.length > 0 &&
!declaredPluginIds.has(plugin.id) &&
isProviderPluginEligibleForRuntimeOwnerActivation({
plugin,
normalizedConfig,
rootConfig: params.config,
}),
);
}
export function resolveDiscoveredProviderPluginIds(params: {
config?: PluginLoadOptions["config"];
workspaceDir?: string;
@@ -229,6 +268,8 @@ export function resolveActivatableProviderOwnerPluginIds(params: {
export const __testing = {
resolveActivatableProviderOwnerPluginIds,
resolveEnabledProviderPluginIds,
resolveExternalAuthProfileCompatFallbackPluginIds,
resolveExternalAuthProfileProviderPluginIds,
resolveDiscoveredProviderPluginIds,
resolveDiscoverableProviderOwnerPluginIds,
resolveBundledProviderCompatPluginIds,