From 7536993397c69742facb1b2405f7743aa750924c Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 24 Apr 2026 12:59:02 -0700 Subject: [PATCH] feat(plugins): read setup provider env vars (#71226) * feat(plugins): read setup provider env vars * fix(plugins): mark provider env compat deprecation --- CHANGELOG.md | 1 + docs/plugins/architecture-internals.md | 20 +++-- docs/plugins/manifest.md | 10 ++- src/plugins/manifest-registry.test.ts | 32 ++++++++ src/plugins/manifest-registry.ts | 25 +++++++ src/plugins/manifest.ts | 8 +- src/secrets/provider-env-vars.dynamic.test.ts | 75 +++++++++++++++++++ src/secrets/provider-env-vars.ts | 14 ++-- 8 files changed, 168 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5140c81b8..14432a5ae9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai - Plugins/setup: honor explicit `setup.requiresRuntime: false` as a descriptor-only setup contract while keeping omitted values on the legacy setup-api fallback path. Thanks @vincentkoc. - 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. - TUI/dependencies: remove direct `cli-highlight` usage from the OpenClaw TUI code-block renderer, keeping themed code coloring without the extra root dependency. Thanks @vincentkoc. - Diagnostics/OTEL: export run, model-call, and tool-execution diagnostic lifecycle events as OTEL spans without retaining live span state. Thanks @vincentkoc. - Providers/Anthropic Vertex: move the Vertex SDK runtime behind the bundled provider plugin so core no longer owns that provider-specific dependency. Thanks @vincentkoc. diff --git a/docs/plugins/architecture-internals.md b/docs/plugins/architecture-internals.md index 3bbc2294a6c..25be338182b 100644 --- a/docs/plugins/architecture-internals.md +++ b/docs/plugins/architecture-internals.md @@ -166,7 +166,8 @@ conversation, and it runs after core approval handling finishes. Provider plugins have three layers: -- **Manifest metadata** for cheap pre-runtime lookup: `providerAuthEnvVars`, +- **Manifest metadata** for cheap pre-runtime lookup: + `setup.providers[].envVars`, deprecated compatibility `providerAuthEnvVars`, `providerAuthAliases`, `providerAuthChoices`, and `channelEnvVars`. - **Config-time hooks**: `catalog` (legacy `discovery`) plus `applyConfigDefaults`. @@ -178,13 +179,16 @@ OpenClaw still owns the generic agent loop, failover, transcript handling, and tool policy. These hooks are the extension surface for provider-specific behavior without needing a whole custom inference transport. -Use manifest `providerAuthEnvVars` when the provider has env-based credentials -that generic auth/status/model-picker paths should see without loading plugin -runtime. Use manifest `providerAuthAliases` when one provider id should reuse -another provider id's env vars, auth profiles, config-backed auth, and API-key -onboarding choice. Use manifest `providerAuthChoices` when onboarding/auth-choice -CLI surfaces should know the provider's choice id, group labels, and simple -one-flag auth wiring without loading provider runtime. Keep provider runtime +Use manifest `setup.providers[].envVars` when the provider has env-based +credentials that generic auth/status/model-picker paths should see without +loading plugin runtime. Deprecated `providerAuthEnvVars` is still read by the +compatibility adapter during the deprecation window, and non-bundled plugins +that use it receive a manifest diagnostic. Use manifest `providerAuthAliases` +when one provider id should reuse another provider id's env vars, auth profiles, +config-backed auth, and API-key onboarding choice. Use manifest +`providerAuthChoices` when onboarding/auth-choice CLI surfaces should know the +provider's choice id, group labels, and simple one-flag auth wiring without +loading provider runtime. Keep provider runtime `envVars` for operator-facing hints such as onboarding labels or OAuth client-id/client-secret setup vars. diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index 7aa849067ee..1dcca6c6bc9 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -148,7 +148,7 @@ or npm install metadata. Those belong in your plugin code and `package.json`. | `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. | | `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. | | `commandAliases` | No | `object[]` | Command names owned by this plugin that should produce plugin-aware config and CLI diagnostics before runtime loads. | -| `providerAuthEnvVars` | No | `Record` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. | +| `providerAuthEnvVars` | No | `Record` | Deprecated compatibility env metadata for provider auth/status lookup. Prefer `setup.providers[].envVars` for new plugins; OpenClaw still reads this during the deprecation window. | | `providerAuthAliases` | No | `Record` | Provider ids that should reuse another provider id for auth lookup, for example a coding provider that shares the base provider API key and auth profiles. | | `channelEnvVars` | No | `Record` | Cheap channel env metadata that OpenClaw can inspect without loading plugin code. Use this for env-driven channel setup or auth surfaces that generic startup/config helpers should see. | | `providerAuthChoices` | No | `object[]` | Cheap auth-choice metadata for onboarding pickers, preferred-provider resolution, and simple CLI flag wiring. | @@ -327,6 +327,12 @@ narrows the candidate plugin and setup still needs richer setup-time runtime hooks, set `requiresRuntime: true` and keep `setup-api` in place as the fallback execution path. +OpenClaw also includes `setup.providers[].envVars` in generic provider auth and +env-var lookups. `providerAuthEnvVars` remains supported through a compatibility +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`. + 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` for setup lookup. Omitted `requiresRuntime` @@ -725,7 +731,7 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s - `channels`, `providers`, `cliBackends`, and `skills` can all be omitted when a plugin does not need them. - `providerDiscoveryEntry` must stay lightweight and should not import broad runtime code; use it for static provider catalog metadata or narrow discovery descriptors, not request-time execution. - Exclusive plugin kinds are selected through `plugins.slots.*`: `kind: "memory"` via `plugins.slots.memory`, `kind: "context-engine"` via `plugins.slots.contextEngine` (default `legacy`). -- Env-var metadata (`providerAuthEnvVars`, `channelEnvVars`) is declarative only. Status, audit, cron delivery validation, and other read-only surfaces still apply plugin trust and effective activation policy before treating an env var as configured. +- Env-var metadata (`setup.providers[].envVars`, deprecated `providerAuthEnvVars`, and `channelEnvVars`) is declarative only. Status, audit, cron delivery validation, and other read-only surfaces still apply plugin trust and effective activation policy before treating an env var as configured. - For runtime wizard metadata that requires provider code, see [Provider runtime hooks](/plugins/architecture-internals#provider-runtime-hooks). - If your plugin depends on native modules, document the build steps and any package-manager allowlist requirements (for example, pnpm `allow-build-scripts` + `pnpm rebuild `). diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index aab97ca5f0c..2984ae84f94 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -478,6 +478,38 @@ describe("loadPluginManifestRegistry", () => { ]); }); + it("reports non-bundled providerAuthEnvVars as deprecated compat metadata", () => { + const dir = makeTempDir(); + writeManifest(dir, { + id: "external-openai", + providers: ["openai"], + providerAuthEnvVars: { + openai: ["OPENAI_API_KEY"], + }, + configSchema: { type: "object" }, + }); + + const registry = loadSingleCandidateRegistry({ + idHint: "external-openai", + rootDir: dir, + origin: "global", + }); + + expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({ + openai: ["OPENAI_API_KEY"], + }); + expect(registry.diagnostics).toContainEqual( + expect.objectContaining({ + level: "warn", + pluginId: "external-openai", + source: path.join(dir, "openclaw.plugin.json"), + message: expect.stringContaining( + "providerAuthEnvVars is deprecated compatibility metadata", + ), + }), + ); + }); + it("falls back providerDiscoverySource from .ts to emitted .js files", () => { const dir = makeTempDir(); writeManifest(dir, { diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 142747c39c1..bc3cd7b9b3f 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -450,6 +450,28 @@ function buildBundleRecord(params: { }; } +function pushProviderAuthEnvVarsCompatDiagnostic(params: { + record: PluginManifestRecord; + diagnostics: PluginDiagnostic[]; +}): void { + if (params.record.origin === "bundled" || !params.record.providerAuthEnvVars) { + return; + } + const providerIds = Object.entries(params.record.providerAuthEnvVars) + .filter(([providerId, envVars]) => providerId.trim() && envVars.length > 0) + .map(([providerId]) => providerId) + .toSorted((left, right) => left.localeCompare(right)); + if (providerIds.length === 0) { + return; + } + params.diagnostics.push({ + level: "warn", + pluginId: params.record.id, + source: params.record.manifestPath, + message: `providerAuthEnvVars is deprecated compatibility metadata for provider env-var lookup; mirror ${providerIds.join(", ")} env vars to setup.providers[].envVars before the deprecation window closes`, + }); +} + function matchesInstalledPluginRecord(params: { pluginId: string; candidate: PluginCandidate; @@ -642,6 +664,7 @@ export function loadPluginManifestRegistry( if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) { records[existing.recordIndex] = record; seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex }); + pushProviderAuthEnvVarsCompatDiagnostic({ record, diagnostics }); } continue; } @@ -664,6 +687,7 @@ export function loadPluginManifestRegistry( if (candidateWins) { records[existing.recordIndex] = record; seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex }); + pushProviderAuthEnvVarsCompatDiagnostic({ record, diagnostics }); } diagnostics.push({ level: "warn", @@ -676,6 +700,7 @@ export function loadPluginManifestRegistry( seenIds.set(manifest.id, { candidate, recordIndex: records.length }); records.push(record); + pushProviderAuthEnvVarsCompatDiagnostic({ record, diagnostics }); } const registry = { plugins: records, diagnostics }; diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index b4e57a5b781..f5e43742dd3 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -194,7 +194,13 @@ export type PluginManifest = { * config diagnostics before runtime loads. */ commandAliases?: PluginManifestCommandAlias[]; - /** Cheap provider-auth env lookup without booting plugin runtime. */ + /** + * Cheap provider-auth env lookup without booting plugin runtime. + * + * @deprecated Prefer setup.providers[].envVars for generic setup/status env + * metadata. This field remains supported through the provider env-var + * compatibility adapter during the deprecation window. + */ providerAuthEnvVars?: Record; /** Provider ids that should reuse another provider id for auth lookup. */ providerAuthAliases?: Record; diff --git a/src/secrets/provider-env-vars.dynamic.test.ts b/src/secrets/provider-env-vars.dynamic.test.ts index 94485213047..103216954f1 100644 --- a/src/secrets/provider-env-vars.dynamic.test.ts +++ b/src/secrets/provider-env-vars.dynamic.test.ts @@ -15,6 +15,12 @@ type MockManifestRegistry = { kind?: "memory" | "context-engine" | Array<"memory" | "context-engine">; providerAuthEnvVars?: Record; providerAuthAliases?: Record; + setup?: { + providers?: Array<{ + id: string; + envVars?: string[]; + }>; + }; }>; diagnostics: unknown[]; }; @@ -57,6 +63,55 @@ describe("provider env vars dynamic manifest metadata", () => { expect(listKnownSecretEnvVarNames()).toContain("FIREWORKS_ALT_API_KEY"); }); + it("includes setup provider env vars without loading setup runtime", async () => { + loadPluginManifestRegistry.mockReturnValue({ + plugins: [ + { + id: "external-model-studio", + origin: "global", + setup: { + providers: [ + { + id: "model-studio", + envVars: ["MODEL_STUDIO_API_KEY", "MODEL_STUDIO_API_KEY"], + }, + ], + }, + }, + ], + diagnostics: [], + }); + + expect(getProviderEnvVars("model-studio")).toEqual(["MODEL_STUDIO_API_KEY"]); + expect(listKnownProviderAuthEnvVarNames()).toContain("MODEL_STUDIO_API_KEY"); + expect(listKnownSecretEnvVarNames()).toContain("MODEL_STUDIO_API_KEY"); + }); + + it("appends setup provider env vars after explicit provider auth env vars", async () => { + loadPluginManifestRegistry.mockReturnValue({ + plugins: [ + { + id: "external-fireworks", + origin: "global", + providerAuthEnvVars: { + fireworks: ["FIREWORKS_API_KEY"], + }, + setup: { + providers: [ + { + id: "fireworks", + envVars: ["FIREWORKS_SETUP_KEY", "FIREWORKS_API_KEY"], + }, + ], + }, + }, + ], + diagnostics: [], + }); + + expect(getProviderEnvVars("fireworks")).toEqual(["FIREWORKS_API_KEY", "FIREWORKS_SETUP_KEY"]); + }); + it("keeps lazy manifest-backed exports cold until accessed and resolves them once", async () => { loadPluginManifestRegistry.mockReturnValue({ plugins: [ @@ -111,6 +166,14 @@ describe("provider env vars dynamic manifest metadata", () => { providerAuthEnvVars: { whisperx: ["AWS_SECRET_ACCESS_KEY"], }, + setup: { + providers: [ + { + id: "workspace-setup", + envVars: ["WORKSPACE_SETUP_SECRET"], + }, + ], + }, }, ], diagnostics: [], @@ -124,12 +187,24 @@ describe("provider env vars dynamic manifest metadata", () => { includeUntrustedWorkspacePlugins: false, }), ).toEqual([]); + expect( + mod.getProviderEnvVars("workspace-setup", { + config: { plugins: {} }, + includeUntrustedWorkspacePlugins: false, + }), + ).toEqual([]); expect( mod.listKnownProviderAuthEnvVarNames({ config: { plugins: {} }, includeUntrustedWorkspacePlugins: false, }), ).not.toContain("AWS_SECRET_ACCESS_KEY"); + expect( + mod.listKnownProviderAuthEnvVarNames({ + config: { plugins: {} }, + includeUntrustedWorkspacePlugins: false, + }), + ).not.toContain("WORKSPACE_SETUP_SECRET"); }); it("keeps explicitly trusted workspace plugin env vars when requested", async () => { diff --git a/src/secrets/provider-env-vars.ts b/src/secrets/provider-env-vars.ts index dd5528851af..962e4140eab 100644 --- a/src/secrets/provider-env-vars.ts +++ b/src/secrets/provider-env-vars.ts @@ -86,13 +86,15 @@ function resolveManifestProviderAuthEnvVarCandidates( if (!shouldUsePluginProviderEnvVars(plugin, params)) { continue; } - if (!plugin.providerAuthEnvVars) { - continue; + if (plugin.providerAuthEnvVars) { + for (const [providerId, keys] of Object.entries(plugin.providerAuthEnvVars).toSorted( + ([left], [right]) => left.localeCompare(right), + )) { + appendUniqueEnvVarCandidates(candidates, providerId, keys); + } } - for (const [providerId, keys] of Object.entries(plugin.providerAuthEnvVars).toSorted( - ([left], [right]) => left.localeCompare(right), - )) { - appendUniqueEnvVarCandidates(candidates, providerId, keys); + for (const provider of plugin.setup?.providers ?? []) { + appendUniqueEnvVarCandidates(candidates, provider.id, provider.envVars ?? []); } } const aliases = resolveProviderAuthAliasMap(params);