mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:50:44 +00:00
feat(plugins): read setup provider env vars (#71226)
* feat(plugins): read setup provider env vars * fix(plugins): mark provider env compat deprecation
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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<string, string[]>` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. |
|
||||
| `providerAuthEnvVars` | No | `Record<string, string[]>` | 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<string, string>` | 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<string, string[]>` | 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 <package>`).
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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<string, string[]>;
|
||||
/** Provider ids that should reuse another provider id for auth lookup. */
|
||||
providerAuthAliases?: Record<string, string>;
|
||||
|
||||
@@ -15,6 +15,12 @@ type MockManifestRegistry = {
|
||||
kind?: "memory" | "context-engine" | Array<"memory" | "context-engine">;
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
providerAuthAliases?: Record<string, string>;
|
||||
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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user