refactor(core): land plugin auth and startup cleanup
This commit is contained in:
parent
f71f44576a
commit
8ab01c5c93
@ -19,6 +19,8 @@ For model selection rules, see [/concepts/models](/concepts/models).
|
||||
- Provider plugins can inject model catalogs via `registerProvider({ catalog })`;
|
||||
OpenClaw merges that output into `models.providers` before writing
|
||||
`models.json`.
|
||||
- Provider manifests can declare `providerAuthEnvVars` so generic env-based
|
||||
auth probes do not need to load plugin runtime.
|
||||
- Provider plugins can also own provider runtime behavior via
|
||||
`resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`,
|
||||
`capabilities`, `prepareExtraParams`, `wrapStreamFn`,
|
||||
|
||||
@ -56,6 +56,9 @@ Optional keys:
|
||||
- `kind` (string): plugin kind (examples: `"memory"`, `"context-engine"`).
|
||||
- `channels` (array): channel ids registered by this plugin (example: `["matrix"]`).
|
||||
- `providers` (array): provider ids registered by this plugin.
|
||||
- `providerAuthEnvVars` (object): auth env vars keyed by provider id. Use this
|
||||
when OpenClaw should resolve provider credentials from env without loading
|
||||
plugin runtime first.
|
||||
- `skills` (array): skill directories to load (relative to the plugin root).
|
||||
- `name` (string): display name for the plugin.
|
||||
- `description` (string): short plugin summary.
|
||||
@ -84,6 +87,9 @@ Optional keys:
|
||||
- The manifest is **required for native OpenClaw plugins**, including local filesystem loads.
|
||||
- Runtime still loads the plugin module separately; the manifest is only for
|
||||
discovery + validation.
|
||||
- `providerAuthEnvVars` is the cheap metadata path for auth probes, env-marker
|
||||
validation, and similar provider-auth surfaces that should not boot plugin
|
||||
runtime just to inspect env names.
|
||||
- Exclusive plugin kinds are selected through `plugins.slots.*`.
|
||||
- `kind: "memory"` is selected by `plugins.slots.memory`.
|
||||
- `kind: "context-engine"` is selected by `plugins.slots.contextEngine`
|
||||
|
||||
@ -217,6 +217,8 @@ Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
||||
|
||||
Provider plugins now have two layers:
|
||||
|
||||
- manifest metadata: `providerAuthEnvVars` for cheap env-auth lookup before
|
||||
runtime load
|
||||
- config-time hooks: `catalog` / legacy `discovery`
|
||||
- runtime hooks: `resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`, `capabilities`, `prepareExtraParams`, `wrapStreamFn`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot`
|
||||
|
||||
@ -224,6 +226,11 @@ OpenClaw still owns the generic agent loop, failover, transcript handling, and
|
||||
tool policy. These hooks are the seam 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. Keep provider runtime `envVars` for operator-facing hints such as
|
||||
onboarding labels or OAuth client-id/client-secret setup vars.
|
||||
|
||||
### Hook order
|
||||
|
||||
For model/provider plugins, OpenClaw uses hooks in this rough order:
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "anthropic",
|
||||
"providers": ["anthropic"],
|
||||
"providerAuthEnvVars": {
|
||||
"anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "byteplus",
|
||||
"providers": ["byteplus", "byteplus-plan"],
|
||||
"providerAuthEnvVars": {
|
||||
"byteplus": ["BYTEPLUS_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "cloudflare-ai-gateway",
|
||||
"providers": ["cloudflare-ai-gateway"],
|
||||
"providerAuthEnvVars": {
|
||||
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
7
extensions/feishu/src/onboarding.ts
Normal file
7
extensions/feishu/src/onboarding.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { buildChannelOnboardingAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import { feishuPlugin } from "./channel.js";
|
||||
|
||||
export const feishuOnboardingAdapter = buildChannelOnboardingAdapterFromSetupWizard({
|
||||
plugin: feishuPlugin,
|
||||
wizard: feishuPlugin.setupWizard!,
|
||||
});
|
||||
@ -8,11 +8,8 @@ import { listProfilesForProvider } from "../../src/agents/auth-profiles/profiles
|
||||
import { ensureAuthProfileStore } from "../../src/agents/auth-profiles/store.js";
|
||||
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import { coerceSecretRef } from "../../src/config/types.secrets.js";
|
||||
import { fetchCopilotUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
import {
|
||||
DEFAULT_COPILOT_API_BASE_URL,
|
||||
resolveCopilotApiToken,
|
||||
} from "../../src/providers/github-copilot-token.js";
|
||||
import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.js";
|
||||
import { fetchCopilotUsage } from "./usage.js";
|
||||
|
||||
const PROVIDER_ID = "github-copilot";
|
||||
const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "github-copilot",
|
||||
"providers": ["github-copilot"],
|
||||
"providerAuthEnvVars": {
|
||||
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
deriveCopilotApiBaseUrlFromToken,
|
||||
resolveCopilotApiToken,
|
||||
} from "./github-copilot-token.js";
|
||||
import { deriveCopilotApiBaseUrlFromToken, resolveCopilotApiToken } from "./token.js";
|
||||
|
||||
describe("github-copilot token", () => {
|
||||
const loadJsonFile = vi.fn();
|
||||
@ -58,7 +55,7 @@ describe("github-copilot token", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const { resolveCopilotApiToken } = await import("./github-copilot-token.js");
|
||||
const { resolveCopilotApiToken } = await import("./token.js");
|
||||
|
||||
const res = await resolveCopilotApiToken({
|
||||
githubToken: "gh",
|
||||
@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
|
||||
import { resolveStateDir } from "../../src/config/paths.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../../src/infra/json-file.js";
|
||||
|
||||
const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createProviderUsageFetch, makeResponse } from "../test-utils/provider-usage-fetch.js";
|
||||
import { fetchCopilotUsage } from "./provider-usage.fetch.copilot.js";
|
||||
import {
|
||||
createProviderUsageFetch,
|
||||
makeResponse,
|
||||
} from "../../src/test-utils/provider-usage-fetch.js";
|
||||
import { fetchCopilotUsage } from "./usage.js";
|
||||
|
||||
describe("fetchCopilotUsage", () => {
|
||||
it("returns HTTP errors for failed requests", async () => {
|
||||
@ -1,6 +1,9 @@
|
||||
import { buildUsageHttpErrorSnapshot, fetchJson } from "./provider-usage.fetch.shared.js";
|
||||
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
|
||||
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
|
||||
import {
|
||||
buildUsageHttpErrorSnapshot,
|
||||
fetchJson,
|
||||
} from "../../src/infra/provider-usage.fetch.shared.js";
|
||||
import { clampPercent, PROVIDER_LABELS } from "../../src/infra/provider-usage.shared.js";
|
||||
import type { ProviderUsageSnapshot, UsageWindow } from "../../src/infra/provider-usage.types.js";
|
||||
|
||||
type CopilotUsageResponse = {
|
||||
quota_snapshots?: {
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "huggingface",
|
||||
"providers": ["huggingface"],
|
||||
"providerAuthEnvVars": {
|
||||
"huggingface": ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "kilocode",
|
||||
"providers": ["kilocode"],
|
||||
"providerAuthEnvVars": {
|
||||
"kilocode": ["KILOCODE_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "kimi-coding",
|
||||
"providers": ["kimi-coding"],
|
||||
"providerAuthEnvVars": {
|
||||
"kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
applySetupAccountConfigPatch,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
hasConfiguredSecretInput,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
|
||||
@ -175,6 +175,7 @@ const minimaxPlugin = {
|
||||
id: PORTAL_PROVIDER_ID,
|
||||
label: PROVIDER_LABEL,
|
||||
docsPath: "/providers/minimax",
|
||||
envVars: ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
|
||||
catalog: {
|
||||
run: async (ctx) => resolvePortalCatalog(ctx),
|
||||
},
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
{
|
||||
"id": "minimax",
|
||||
"providers": ["minimax", "minimax-portal"],
|
||||
"providerAuthEnvVars": {
|
||||
"minimax": ["MINIMAX_API_KEY"],
|
||||
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "mistral",
|
||||
"providers": ["mistral"],
|
||||
"providerAuthEnvVars": {
|
||||
"mistral": ["MISTRAL_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "modelstudio",
|
||||
"providers": ["modelstudio"],
|
||||
"providerAuthEnvVars": {
|
||||
"modelstudio": ["MODELSTUDIO_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "moonshot",
|
||||
"providers": ["moonshot"],
|
||||
"providerAuthEnvVars": {
|
||||
"moonshot": ["MOONSHOT_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "nvidia",
|
||||
"providers": ["nvidia"],
|
||||
"providerAuthEnvVars": {
|
||||
"nvidia": ["NVIDIA_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "ollama",
|
||||
"providers": ["ollama"],
|
||||
"providerAuthEnvVars": {
|
||||
"ollama": ["OLLAMA_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "openai",
|
||||
"providers": ["openai", "openai-codex"],
|
||||
"providerAuthEnvVars": {
|
||||
"openai": ["OPENAI_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "opencode-go",
|
||||
"providers": ["opencode-go"],
|
||||
"providerAuthEnvVars": {
|
||||
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "opencode",
|
||||
"providers": ["opencode"],
|
||||
"providerAuthEnvVars": {
|
||||
"opencode": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "openrouter",
|
||||
"providers": ["openrouter"],
|
||||
"providerAuthEnvVars": {
|
||||
"openrouter": ["OPENROUTER_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "qianfan",
|
||||
"providers": ["qianfan"],
|
||||
"providerAuthEnvVars": {
|
||||
"qianfan": ["QIANFAN_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -94,6 +94,7 @@ const qwenPortalPlugin = {
|
||||
label: PROVIDER_LABEL,
|
||||
docsPath: "/providers/qwen",
|
||||
aliases: ["qwen"],
|
||||
envVars: ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"],
|
||||
catalog: {
|
||||
run: async (ctx: ProviderCatalogContext) => resolveCatalog(ctx),
|
||||
},
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "qwen-portal-auth",
|
||||
"providers": ["qwen-portal"],
|
||||
"providerAuthEnvVars": {
|
||||
"qwen-portal": ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "sglang",
|
||||
"providers": ["sglang"],
|
||||
"providerAuthEnvVars": {
|
||||
"sglang": ["SGLANG_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "synthetic",
|
||||
"providers": ["synthetic"],
|
||||
"providerAuthEnvVars": {
|
||||
"synthetic": ["SYNTHETIC_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "together",
|
||||
"providers": ["together"],
|
||||
"providerAuthEnvVars": {
|
||||
"together": ["TOGETHER_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "venice",
|
||||
"providers": ["venice"],
|
||||
"providerAuthEnvVars": {
|
||||
"venice": ["VENICE_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "vercel-ai-gateway",
|
||||
"providers": ["vercel-ai-gateway"],
|
||||
"providerAuthEnvVars": {
|
||||
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "vllm",
|
||||
"providers": ["vllm"],
|
||||
"providerAuthEnvVars": {
|
||||
"vllm": ["VLLM_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "volcengine",
|
||||
"providers": ["volcengine", "volcengine-plan"],
|
||||
"providerAuthEnvVars": {
|
||||
"volcengine": ["VOLCANO_ENGINE_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "xiaomi",
|
||||
"providers": ["xiaomi"],
|
||||
"providerAuthEnvVars": {
|
||||
"xiaomi": ["XIAOMI_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "zai",
|
||||
"providers": ["zai"],
|
||||
"providerAuthEnvVars": {
|
||||
"zai": ["ZAI_API_KEY", "Z_AI_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,45 +1,10 @@
|
||||
export const PROVIDER_ENV_API_KEY_CANDIDATES: Record<string, string[]> = {
|
||||
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
|
||||
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
chutes: ["CHUTES_OAUTH_TOKEN", "CHUTES_API_KEY"],
|
||||
zai: ["ZAI_API_KEY", "Z_AI_API_KEY"],
|
||||
opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
"qwen-portal": ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"],
|
||||
volcengine: ["VOLCANO_ENGINE_API_KEY"],
|
||||
"volcengine-plan": ["VOLCANO_ENGINE_API_KEY"],
|
||||
byteplus: ["BYTEPLUS_API_KEY"],
|
||||
"byteplus-plan": ["BYTEPLUS_API_KEY"],
|
||||
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
|
||||
"kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
huggingface: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
google: ["GEMINI_API_KEY"],
|
||||
voyage: ["VOYAGE_API_KEY"],
|
||||
groq: ["GROQ_API_KEY"],
|
||||
deepgram: ["DEEPGRAM_API_KEY"],
|
||||
cerebras: ["CEREBRAS_API_KEY"],
|
||||
xai: ["XAI_API_KEY"],
|
||||
openrouter: ["OPENROUTER_API_KEY"],
|
||||
litellm: ["LITELLM_API_KEY"],
|
||||
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
|
||||
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
|
||||
moonshot: ["MOONSHOT_API_KEY"],
|
||||
minimax: ["MINIMAX_API_KEY"],
|
||||
nvidia: ["NVIDIA_API_KEY"],
|
||||
xiaomi: ["XIAOMI_API_KEY"],
|
||||
synthetic: ["SYNTHETIC_API_KEY"],
|
||||
venice: ["VENICE_API_KEY"],
|
||||
mistral: ["MISTRAL_API_KEY"],
|
||||
together: ["TOGETHER_API_KEY"],
|
||||
qianfan: ["QIANFAN_API_KEY"],
|
||||
modelstudio: ["MODELSTUDIO_API_KEY"],
|
||||
ollama: ["OLLAMA_API_KEY"],
|
||||
sglang: ["SGLANG_API_KEY"],
|
||||
vllm: ["VLLM_API_KEY"],
|
||||
kilocode: ["KILOCODE_API_KEY"],
|
||||
};
|
||||
import {
|
||||
PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
listKnownProviderAuthEnvVarNames,
|
||||
} from "../secrets/provider-env-vars.js";
|
||||
|
||||
export const PROVIDER_ENV_API_KEY_CANDIDATES = PROVIDER_AUTH_ENV_VAR_CANDIDATES;
|
||||
|
||||
export function listKnownProviderEnvApiKeyNames(): string[] {
|
||||
return [...new Set(Object.values(PROVIDER_ENV_API_KEY_CANDIDATES).flat())];
|
||||
return listKnownProviderAuthEnvVarNames();
|
||||
}
|
||||
|
||||
@ -426,4 +426,45 @@ describe("getApiKeyForModel", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("resolveEnvApiKey('qwen-portal') accepts QWEN_OAUTH_TOKEN", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
QWEN_OAUTH_TOKEN: "qwen-oauth-token",
|
||||
QWEN_PORTAL_API_KEY: undefined,
|
||||
},
|
||||
async () => {
|
||||
const resolved = resolveEnvApiKey("qwen");
|
||||
expect(resolved?.apiKey).toBe("qwen-oauth-token");
|
||||
expect(resolved?.source).toContain("QWEN_OAUTH_TOKEN");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("resolveEnvApiKey('minimax-portal') accepts MINIMAX_OAUTH_TOKEN", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
MINIMAX_OAUTH_TOKEN: "minimax-oauth-token",
|
||||
MINIMAX_API_KEY: undefined,
|
||||
},
|
||||
async () => {
|
||||
const resolved = resolveEnvApiKey("minimax-portal");
|
||||
expect(resolved?.apiKey).toBe("minimax-oauth-token");
|
||||
expect(resolved?.source).toContain("MINIMAX_OAUTH_TOKEN");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("resolveEnvApiKey('volcengine-plan') uses volcengine auth candidates", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
VOLCANO_ENGINE_API_KEY: "volcengine-plan-key",
|
||||
},
|
||||
async () => {
|
||||
const resolved = resolveEnvApiKey("volcengine-plan");
|
||||
expect(resolved?.apiKey).toBe("volcengine-plan-key");
|
||||
expect(resolved?.source).toContain("VOLCANO_ENGINE_API_KEY");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
isNonSecretApiKeyMarker,
|
||||
OLLAMA_LOCAL_AUTH_MARKER,
|
||||
} from "./model-auth-markers.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
import { normalizeProviderId, normalizeProviderIdForAuth } from "./model-selection.js";
|
||||
|
||||
export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js";
|
||||
|
||||
@ -400,7 +400,7 @@ export function resolveEnvApiKey(
|
||||
provider: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): EnvApiKeyResult | null {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
const normalized = normalizeProviderIdForAuth(provider);
|
||||
const applied = new Set(getShellEnvAppliedKeys());
|
||||
const pick = (envVar: string): EnvApiKeyResult | null => {
|
||||
const value = normalizeOptionalSecretInput(env[envVar]);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_COPILOT_API_BASE_URL } from "../providers/github-copilot-token.js";
|
||||
import { DEFAULT_COPILOT_API_BASE_URL } from "../../extensions/github-copilot/token.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
installModelsConfigTestHooks,
|
||||
|
||||
@ -33,7 +33,7 @@ vi.mock("../infra/backoff.js", () => ({
|
||||
sleepWithAbort: (ms: number, abortSignal?: AbortSignal) => sleepWithAbortMock(ms, abortSignal),
|
||||
}));
|
||||
|
||||
vi.mock("../providers/github-copilot-token.js", () => ({
|
||||
vi.mock("../../extensions/github-copilot/token.js", () => ({
|
||||
DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com",
|
||||
resolveCopilotApiToken: (...args: unknown[]) => resolveCopilotApiTokenMock(...args),
|
||||
}));
|
||||
|
||||
@ -4,13 +4,18 @@ import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
export type ChannelOnboardingSetupPlugin = Pick<
|
||||
ChannelPlugin,
|
||||
"id" | "meta" | "capabilities" | "config" | "setup" | "setupWizard"
|
||||
>;
|
||||
|
||||
export type SetupChannelsOptions = {
|
||||
allowDisable?: boolean;
|
||||
allowSignalInstall?: boolean;
|
||||
onSelection?: (selection: ChannelId[]) => void;
|
||||
accountIds?: Partial<Record<ChannelId, string>>;
|
||||
onAccountId?: (channel: ChannelId, accountId: string) => void;
|
||||
onResolvedPlugin?: (channel: ChannelId, plugin: ChannelPlugin) => void;
|
||||
onResolvedPlugin?: (channel: ChannelId, plugin: ChannelOnboardingSetupPlugin) => void;
|
||||
promptAccountIds?: boolean;
|
||||
whatsappAccountId?: string;
|
||||
promptWhatsAppAccountId?: boolean;
|
||||
|
||||
@ -25,10 +25,15 @@ const EMPTY_REGISTRY_FALLBACK_PLUGINS = [
|
||||
linePlugin,
|
||||
];
|
||||
|
||||
export type ChannelOnboardingSetupPlugin = Pick<
|
||||
ChannelPlugin,
|
||||
"id" | "meta" | "capabilities" | "config" | "setup" | "setupWizard"
|
||||
>;
|
||||
|
||||
const setupWizardAdapters = new WeakMap<object, ChannelOnboardingAdapter>();
|
||||
|
||||
export function resolveChannelOnboardingAdapterForPlugin(
|
||||
plugin?: ChannelPlugin,
|
||||
plugin?: ChannelOnboardingSetupPlugin,
|
||||
): ChannelOnboardingAdapter | undefined {
|
||||
if (plugin?.setupWizard) {
|
||||
const cached = setupWizardAdapters.get(plugin);
|
||||
@ -74,7 +79,7 @@ export function listChannelOnboardingAdapters(): ChannelOnboardingAdapter[] {
|
||||
|
||||
export async function loadBundledChannelOnboardingPlugin(
|
||||
channel: ChannelChoice,
|
||||
): Promise<ChannelPlugin | undefined> {
|
||||
): Promise<ChannelOnboardingSetupPlugin | undefined> {
|
||||
switch (channel) {
|
||||
case "discord":
|
||||
return discordPlugin as ChannelPlugin;
|
||||
|
||||
@ -2,6 +2,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/ag
|
||||
import { listChannelPluginCatalogEntries } from "../../channels/plugins/catalog.js";
|
||||
import { parseOptionalDelimitedEntries } from "../../channels/plugins/helpers.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import type { ChannelOnboardingSetupPlugin } from "../../channels/plugins/onboarding-types.js";
|
||||
import { moveSingleAccountChannelSectionToDefaultAccount } from "../../channels/plugins/setup-helpers.js";
|
||||
import type { ChannelId, ChannelPlugin, ChannelSetupInput } from "../../channels/plugins/types.js";
|
||||
import { writeConfigFile, type OpenClawConfig } from "../../config/config.js";
|
||||
@ -56,7 +57,7 @@ export async function channelsAddCommand(
|
||||
const prompter = createClackPrompter();
|
||||
let selection: ChannelChoice[] = [];
|
||||
const accountIds: Partial<Record<ChannelChoice, string>> = {};
|
||||
const resolvedPlugins = new Map<ChannelChoice, ChannelPlugin>();
|
||||
const resolvedPlugins = new Map<ChannelChoice, ChannelOnboardingSetupPlugin>();
|
||||
await prompter.intro("Channel setup");
|
||||
let nextConfig = await setupChannels(cfg, runtime, prompter, {
|
||||
allowDisable: false,
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import type { ChannelOnboardingSetupPlugin } from "../channels/plugins/onboarding-types.js";
|
||||
import {
|
||||
getChannelSetupPlugin,
|
||||
listChannelSetupPlugins,
|
||||
} from "../channels/plugins/setup-registry.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import {
|
||||
formatChannelPrimerLine,
|
||||
formatChannelSelectionLine,
|
||||
@ -94,7 +94,7 @@ async function promptRemovalAccountId(params: {
|
||||
prompter: WizardPrompter;
|
||||
label: string;
|
||||
channel: ChannelChoice;
|
||||
plugin?: ChannelPlugin;
|
||||
plugin?: ChannelOnboardingSetupPlugin;
|
||||
}): Promise<string> {
|
||||
const { cfg, prompter, label, channel } = params;
|
||||
const plugin = params.plugin ?? getChannelSetupPlugin(channel);
|
||||
@ -121,7 +121,7 @@ async function collectChannelStatus(params: {
|
||||
cfg: OpenClawConfig;
|
||||
options?: SetupChannelsOptions;
|
||||
accountOverrides: Partial<Record<ChannelChoice, string>>;
|
||||
installedPlugins?: ChannelPlugin[];
|
||||
installedPlugins?: ChannelOnboardingSetupPlugin[];
|
||||
resolveAdapter?: (channel: ChannelChoice) => ChannelOnboardingAdapter | undefined;
|
||||
}): Promise<ChannelStatusSummary> {
|
||||
const installedPlugins = params.installedPlugins ?? listChannelSetupPlugins();
|
||||
@ -279,7 +279,7 @@ async function maybeConfigureDmPolicies(params: {
|
||||
const { selection, prompter, accountIdsByChannel } = params;
|
||||
const resolve = params.resolveAdapter ?? (() => undefined);
|
||||
const dmPolicies = selection
|
||||
.map((channel) => resolve(channel)?.dmPolicy)
|
||||
.map((channel) => resolve?.(channel)?.dmPolicy)
|
||||
.filter(Boolean) as ChannelOnboardingDmPolicy[];
|
||||
if (dmPolicies.length === 0) {
|
||||
return params.cfg;
|
||||
@ -350,17 +350,19 @@ export async function setupChannels(
|
||||
const accountOverrides: Partial<Record<ChannelChoice, string>> = {
|
||||
...options?.accountIds,
|
||||
};
|
||||
const scopedPluginsById = new Map<ChannelChoice, ChannelPlugin>();
|
||||
const scopedPluginsById = new Map<ChannelChoice, ChannelOnboardingSetupPlugin>();
|
||||
const resolveWorkspaceDir = () => resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
|
||||
const rememberScopedPlugin = (plugin: ChannelPlugin) => {
|
||||
const rememberScopedPlugin = (plugin: ChannelOnboardingSetupPlugin) => {
|
||||
const channel = plugin.id;
|
||||
scopedPluginsById.set(channel, plugin);
|
||||
options?.onResolvedPlugin?.(channel, plugin);
|
||||
};
|
||||
const getVisibleChannelPlugin = (channel: ChannelChoice): ChannelPlugin | undefined =>
|
||||
const getVisibleChannelPlugin = (
|
||||
channel: ChannelChoice,
|
||||
): ChannelOnboardingSetupPlugin | undefined =>
|
||||
scopedPluginsById.get(channel) ?? getChannelSetupPlugin(channel);
|
||||
const listVisibleInstalledPlugins = (): ChannelPlugin[] => {
|
||||
const merged = new Map<string, ChannelPlugin>();
|
||||
const listVisibleInstalledPlugins = (): ChannelOnboardingSetupPlugin[] => {
|
||||
const merged = new Map<string, ChannelOnboardingSetupPlugin>();
|
||||
for (const plugin of listChannelSetupPlugins()) {
|
||||
merged.set(plugin.id, plugin);
|
||||
}
|
||||
@ -372,7 +374,7 @@ export async function setupChannels(
|
||||
const loadScopedChannelPlugin = async (
|
||||
channel: ChannelChoice,
|
||||
pluginId?: string,
|
||||
): Promise<ChannelPlugin | undefined> => {
|
||||
): Promise<ChannelOnboardingSetupPlugin | undefined> => {
|
||||
const existing = getVisibleChannelPlugin(channel);
|
||||
if (existing) {
|
||||
return existing;
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const childProcessMocks = vi.hoisted(() => ({
|
||||
execFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
access: vi.fn(),
|
||||
realpath: vi.fn(),
|
||||
@ -12,6 +16,10 @@ vi.mock("node:fs/promises", () => ({
|
||||
realpath: fsMocks.realpath,
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
execFileSync: childProcessMocks.execFileSync,
|
||||
}));
|
||||
|
||||
import { resolveGatewayProgramArguments } from "./program-args.js";
|
||||
|
||||
const originalArgv = [...process.argv];
|
||||
@ -87,4 +95,28 @@ describe("resolveGatewayProgramArguments", () => {
|
||||
"18789",
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses src/entry.ts for bun dev mode", async () => {
|
||||
const repoIndexPath = path.resolve("/repo/src/index.ts");
|
||||
const repoEntryPath = path.resolve("/repo/src/entry.ts");
|
||||
process.argv = ["/usr/local/bin/node", repoIndexPath];
|
||||
fsMocks.realpath.mockResolvedValue(repoIndexPath);
|
||||
fsMocks.access.mockResolvedValue(undefined);
|
||||
childProcessMocks.execFileSync.mockReturnValue("/usr/local/bin/bun\n");
|
||||
|
||||
const result = await resolveGatewayProgramArguments({
|
||||
dev: true,
|
||||
port: 18789,
|
||||
runtime: "bun",
|
||||
});
|
||||
|
||||
expect(result.programArguments).toEqual([
|
||||
"/usr/local/bin/bun",
|
||||
repoEntryPath,
|
||||
"gateway",
|
||||
"--port",
|
||||
"18789",
|
||||
]);
|
||||
expect(result.workingDirectory).toBe(path.resolve("/repo"));
|
||||
});
|
||||
});
|
||||
|
||||
@ -123,7 +123,7 @@ function resolveRepoRootForDev(): string {
|
||||
const parts = normalized.split(path.sep);
|
||||
const srcIndex = parts.lastIndexOf("src");
|
||||
if (srcIndex === -1) {
|
||||
throw new Error("Dev mode requires running from repo (src/index.ts)");
|
||||
throw new Error("Dev mode requires running from repo (src/entry.ts)");
|
||||
}
|
||||
return parts.slice(0, srcIndex).join(path.sep);
|
||||
}
|
||||
@ -180,7 +180,7 @@ async function resolveCliProgramArguments(params: {
|
||||
if (runtime === "bun") {
|
||||
if (params.dev) {
|
||||
const repoRoot = resolveRepoRootForDev();
|
||||
const devCliPath = path.join(repoRoot, "src", "index.ts");
|
||||
const devCliPath = path.join(repoRoot, "src", "entry.ts");
|
||||
await fs.access(devCliPath);
|
||||
const bunPath = isBunRuntime(execPath) ? execPath : await resolveBunPath();
|
||||
return {
|
||||
@ -213,7 +213,7 @@ async function resolveCliProgramArguments(params: {
|
||||
|
||||
// Dev mode: use bun to run TypeScript directly
|
||||
const repoRoot = resolveRepoRootForDev();
|
||||
const devCliPath = path.join(repoRoot, "src", "index.ts");
|
||||
const devCliPath = path.join(repoRoot, "src", "entry.ts");
|
||||
await fs.access(devCliPath);
|
||||
|
||||
// If already running under bun, use current execPath
|
||||
|
||||
46
src/index.test.ts
Normal file
46
src/index.test.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import fs from "node:fs";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runtimeMocks = vi.hoisted(() => ({
|
||||
runCli: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("./cli/run-main.js", () => ({
|
||||
runCli: runtimeMocks.runCli,
|
||||
}));
|
||||
|
||||
describe("legacy root entry", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("routes the package root export to the pure library entry", () => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
||||
) as {
|
||||
exports?: Record<string, unknown>;
|
||||
main?: string;
|
||||
};
|
||||
|
||||
expect(packageJson.main).toBe("dist/index.js");
|
||||
expect(packageJson.exports?.["."]).toBe("./dist/index.js");
|
||||
});
|
||||
|
||||
it("does not run CLI bootstrap when imported as a library dependency", async () => {
|
||||
const mod = await import("./index.js");
|
||||
|
||||
expect(typeof mod.runLegacyCliEntry).toBe("function");
|
||||
expect(runtimeMocks.runCli).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delegates legacy direct-entry execution to run-main", async () => {
|
||||
const mod = await import("./index.js");
|
||||
const argv = ["node", "dist/index.js", "status"];
|
||||
|
||||
await mod.runLegacyCliEntry(argv);
|
||||
|
||||
expect(runtimeMocks.runCli).toHaveBeenCalledOnce();
|
||||
expect(runtimeMocks.runCli).toHaveBeenCalledWith(argv);
|
||||
});
|
||||
});
|
||||
94
src/index.ts
94
src/index.ts
@ -1,76 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getReplyFromConfig } from "./auto-reply/reply.js";
|
||||
import { applyTemplate } from "./auto-reply/templating.js";
|
||||
import { monitorWebChannel } from "./channel-web.js";
|
||||
import { createDefaultDeps } from "./cli/deps.js";
|
||||
import { promptYesNo } from "./cli/prompt.js";
|
||||
import { waitForever } from "./cli/wait.js";
|
||||
import { loadConfig } from "./config/config.js";
|
||||
import {
|
||||
deriveSessionKey,
|
||||
loadSessionStore,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
saveSessionStore,
|
||||
} from "./config/sessions.js";
|
||||
import { ensureBinary } from "./infra/binaries.js";
|
||||
import { loadDotEnv } from "./infra/dotenv.js";
|
||||
import { normalizeEnv } from "./infra/env.js";
|
||||
import { formatUncaughtError } from "./infra/errors.js";
|
||||
import { isMainModule } from "./infra/is-main.js";
|
||||
import { ensureOpenClawCliOnPath } from "./infra/path-env.js";
|
||||
import {
|
||||
describePortOwner,
|
||||
ensurePortAvailable,
|
||||
handlePortError,
|
||||
PortInUseError,
|
||||
} from "./infra/ports.js";
|
||||
import { assertSupportedRuntime } from "./infra/runtime-guard.js";
|
||||
import { installUnhandledRejectionHandler } from "./infra/unhandled-rejections.js";
|
||||
import { enableConsoleCapture } from "./logging.js";
|
||||
import { runCommandWithTimeout, runExec } from "./process/exec.js";
|
||||
import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js";
|
||||
|
||||
loadDotEnv({ quiet: true });
|
||||
normalizeEnv();
|
||||
ensureOpenClawCliOnPath();
|
||||
const library = await import("./library.js");
|
||||
|
||||
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
||||
enableConsoleCapture();
|
||||
export const assertWebChannel = library.assertWebChannel;
|
||||
export const applyTemplate = library.applyTemplate;
|
||||
export const createDefaultDeps = library.createDefaultDeps;
|
||||
export const deriveSessionKey = library.deriveSessionKey;
|
||||
export const describePortOwner = library.describePortOwner;
|
||||
export const ensureBinary = library.ensureBinary;
|
||||
export const ensurePortAvailable = library.ensurePortAvailable;
|
||||
export const getReplyFromConfig = library.getReplyFromConfig;
|
||||
export const handlePortError = library.handlePortError;
|
||||
export const loadConfig = library.loadConfig;
|
||||
export const loadSessionStore = library.loadSessionStore;
|
||||
export const monitorWebChannel = library.monitorWebChannel;
|
||||
export const normalizeE164 = library.normalizeE164;
|
||||
export const PortInUseError = library.PortInUseError;
|
||||
export const promptYesNo = library.promptYesNo;
|
||||
export const resolveSessionKey = library.resolveSessionKey;
|
||||
export const resolveStorePath = library.resolveStorePath;
|
||||
export const runCommandWithTimeout = library.runCommandWithTimeout;
|
||||
export const runExec = library.runExec;
|
||||
export const saveSessionStore = library.saveSessionStore;
|
||||
export const toWhatsappJid = library.toWhatsappJid;
|
||||
export const waitForever = library.waitForever;
|
||||
|
||||
// Enforce the minimum supported runtime before doing any work.
|
||||
assertSupportedRuntime();
|
||||
|
||||
import { buildProgram } from "./cli/program.js";
|
||||
|
||||
const program = buildProgram();
|
||||
|
||||
export {
|
||||
assertWebChannel,
|
||||
applyTemplate,
|
||||
createDefaultDeps,
|
||||
deriveSessionKey,
|
||||
describePortOwner,
|
||||
ensureBinary,
|
||||
ensurePortAvailable,
|
||||
getReplyFromConfig,
|
||||
handlePortError,
|
||||
loadConfig,
|
||||
loadSessionStore,
|
||||
monitorWebChannel,
|
||||
normalizeE164,
|
||||
PortInUseError,
|
||||
promptYesNo,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
runCommandWithTimeout,
|
||||
runExec,
|
||||
saveSessionStore,
|
||||
toWhatsappJid,
|
||||
waitForever,
|
||||
};
|
||||
// Legacy direct file entrypoint only. Package root exports now live in library.ts.
|
||||
export async function runLegacyCliEntry(argv: string[] = process.argv): Promise<void> {
|
||||
const { runCli } = await import("./cli/run-main.js");
|
||||
await runCli(argv);
|
||||
}
|
||||
|
||||
const isMain = isMainModule({
|
||||
currentFile: fileURLToPath(import.meta.url),
|
||||
@ -86,7 +50,7 @@ if (isMain) {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
void program.parseAsync(process.argv).catch((err) => {
|
||||
void runLegacyCliEntry(process.argv).catch((err) => {
|
||||
console.error("[openclaw] CLI failed:", formatUncaughtError(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@ -26,6 +26,7 @@ describe("isGatewayArgv", () => {
|
||||
expect(isGatewayArgv(["NODE", "C:\\OpenClaw\\DIST\\ENTRY.JS", "gateway"])).toBe(true);
|
||||
expect(isGatewayArgv(["bun", "/srv/openclaw/scripts/run-node.mjs", "gateway"])).toBe(true);
|
||||
expect(isGatewayArgv(["node", "/srv/openclaw/openclaw.mjs", "gateway"])).toBe(true);
|
||||
expect(isGatewayArgv(["tsx", "/srv/openclaw/src/entry.ts", "gateway"])).toBe(true);
|
||||
expect(isGatewayArgv(["tsx", "/srv/openclaw/src/index.ts", "gateway"])).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ export function isGatewayArgv(args: string[], opts?: { allowGatewayBinary?: bool
|
||||
"dist/entry.js",
|
||||
"openclaw.mjs",
|
||||
"scripts/run-node.mjs",
|
||||
"src/entry.ts",
|
||||
"src/index.ts",
|
||||
];
|
||||
if (normalized.some((arg) => entryCandidates.some((entry) => arg.endsWith(entry)))) {
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
dedupeProfileIds,
|
||||
ensureAuthProfileStore,
|
||||
@ -14,7 +11,6 @@ import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveProviderUsageAuthWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { resolveRequiredHomeDir } from "./home-dir.js";
|
||||
import type { UsageProviderId } from "./provider-usage.types.js";
|
||||
|
||||
export type ProviderAuth = {
|
||||
@ -32,46 +28,6 @@ type UsageAuthState = {
|
||||
agentDir?: string;
|
||||
};
|
||||
|
||||
const LEGACY_OAUTH_USAGE_PROVIDERS = new Set<UsageProviderId>([
|
||||
"anthropic",
|
||||
"github-copilot",
|
||||
"google-gemini-cli",
|
||||
"openai-codex",
|
||||
]);
|
||||
|
||||
function parseGoogleToken(apiKey: string): { token: string } | null {
|
||||
try {
|
||||
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
||||
if (parsed && typeof parsed.token === "string") {
|
||||
return { token: parsed.token };
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveLegacyZaiApiKey(state: UsageAuthState): string | undefined {
|
||||
try {
|
||||
const authPath = path.join(
|
||||
resolveRequiredHomeDir(state.env, os.homedir),
|
||||
".pi",
|
||||
"agent",
|
||||
"auth.json",
|
||||
);
|
||||
if (!fs.existsSync(authPath)) {
|
||||
return undefined;
|
||||
}
|
||||
const data = JSON.parse(fs.readFileSync(authPath, "utf-8")) as Record<
|
||||
string,
|
||||
{ access?: string }
|
||||
>;
|
||||
return data["z-ai"]?.access || data.zai?.access;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProviderApiKeyFromConfigAndStore(params: {
|
||||
state: UsageAuthState;
|
||||
providerIds: string[];
|
||||
@ -236,66 +192,7 @@ export async function resolveProviderAuths(params: {
|
||||
});
|
||||
if (pluginAuth) {
|
||||
auths.push(pluginAuth);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (provider === "zai") {
|
||||
const apiKey =
|
||||
resolveProviderApiKeyFromConfigAndStore({
|
||||
state,
|
||||
providerIds: ["zai", "z-ai"],
|
||||
envDirect: [state.env.ZAI_API_KEY, state.env.Z_AI_API_KEY],
|
||||
}) ?? resolveLegacyZaiApiKey(state);
|
||||
if (apiKey) {
|
||||
auths.push({ provider, token: apiKey });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (provider === "minimax") {
|
||||
const apiKey = resolveProviderApiKeyFromConfigAndStore({
|
||||
state,
|
||||
providerIds: ["minimax"],
|
||||
envDirect: [state.env.MINIMAX_CODE_PLAN_KEY, state.env.MINIMAX_API_KEY],
|
||||
});
|
||||
if (apiKey) {
|
||||
auths.push({ provider, token: apiKey });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (provider === "xiaomi") {
|
||||
const apiKey = resolveProviderApiKeyFromConfigAndStore({
|
||||
state,
|
||||
providerIds: ["xiaomi"],
|
||||
envDirect: [state.env.XIAOMI_API_KEY],
|
||||
});
|
||||
if (apiKey) {
|
||||
auths.push({ provider, token: apiKey });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!LEGACY_OAUTH_USAGE_PROVIDERS.has(provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auth = await resolveOAuthToken({
|
||||
state,
|
||||
provider,
|
||||
});
|
||||
if (!auth) {
|
||||
continue;
|
||||
}
|
||||
if (provider === "google-gemini-cli") {
|
||||
const parsed = parseGoogleToken(auth.token);
|
||||
auths.push({
|
||||
...auth,
|
||||
token: parsed?.token ?? auth.token,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
auths.push(auth);
|
||||
}
|
||||
|
||||
return auths;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export { fetchClaudeUsage } from "./provider-usage.fetch.claude.js";
|
||||
export { fetchCodexUsage } from "./provider-usage.fetch.codex.js";
|
||||
export { fetchCopilotUsage } from "./provider-usage.fetch.copilot.js";
|
||||
export { fetchGeminiUsage } from "./provider-usage.fetch.gemini.js";
|
||||
export { fetchMinimaxUsage } from "./provider-usage.fetch.minimax.js";
|
||||
export { fetchZaiUsage } from "./provider-usage.fetch.zai.js";
|
||||
|
||||
@ -22,7 +22,7 @@ describe("provider-usage.load plugin seam", () => {
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("prefers plugin-owned usage snapshots before the legacy core switch", async () => {
|
||||
it("prefers plugin-owned usage snapshots", async () => {
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockResolvedValueOnce({
|
||||
provider: "github-copilot",
|
||||
displayName: "Copilot",
|
||||
|
||||
@ -2,14 +2,6 @@ import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveProviderUsageSnapshotWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { resolveFetch } from "./fetch.js";
|
||||
import { type ProviderAuth, resolveProviderAuths } from "./provider-usage.auth.js";
|
||||
import {
|
||||
fetchClaudeUsage,
|
||||
fetchCodexUsage,
|
||||
fetchCopilotUsage,
|
||||
fetchGeminiUsage,
|
||||
fetchMinimaxUsage,
|
||||
fetchZaiUsage,
|
||||
} from "./provider-usage.fetch.js";
|
||||
import {
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
ignoredErrors,
|
||||
@ -64,44 +56,12 @@ async function fetchProviderUsageSnapshot(params: {
|
||||
if (pluginSnapshot) {
|
||||
return pluginSnapshot;
|
||||
}
|
||||
|
||||
switch (params.auth.provider) {
|
||||
case "anthropic":
|
||||
return await fetchClaudeUsage(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
case "github-copilot":
|
||||
return await fetchCopilotUsage(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
case "google-gemini-cli":
|
||||
return await fetchGeminiUsage(
|
||||
params.auth.token,
|
||||
params.timeoutMs,
|
||||
params.fetchFn,
|
||||
params.auth.provider,
|
||||
);
|
||||
case "openai-codex":
|
||||
return await fetchCodexUsage(
|
||||
params.auth.token,
|
||||
params.auth.accountId,
|
||||
params.timeoutMs,
|
||||
params.fetchFn,
|
||||
);
|
||||
case "minimax":
|
||||
return await fetchMinimaxUsage(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
case "xiaomi":
|
||||
return {
|
||||
provider: "xiaomi",
|
||||
displayName: PROVIDER_LABELS.xiaomi,
|
||||
windows: [],
|
||||
};
|
||||
case "zai":
|
||||
return await fetchZaiUsage(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
default:
|
||||
return {
|
||||
provider: params.auth.provider,
|
||||
displayName: PROVIDER_LABELS[params.auth.provider],
|
||||
windows: [],
|
||||
error: "Unsupported provider",
|
||||
};
|
||||
}
|
||||
return {
|
||||
provider: params.auth.provider,
|
||||
displayName: PROVIDER_LABELS[params.auth.provider],
|
||||
windows: [],
|
||||
error: "Unsupported provider",
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadProviderUsageSummary(
|
||||
|
||||
48
src/library.ts
Normal file
48
src/library.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { getReplyFromConfig } from "./auto-reply/reply.js";
|
||||
import { applyTemplate } from "./auto-reply/templating.js";
|
||||
import { monitorWebChannel } from "./channel-web.js";
|
||||
import { createDefaultDeps } from "./cli/deps.js";
|
||||
import { promptYesNo } from "./cli/prompt.js";
|
||||
import { waitForever } from "./cli/wait.js";
|
||||
import { loadConfig } from "./config/config.js";
|
||||
import {
|
||||
deriveSessionKey,
|
||||
loadSessionStore,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
saveSessionStore,
|
||||
} from "./config/sessions.js";
|
||||
import { ensureBinary } from "./infra/binaries.js";
|
||||
import {
|
||||
describePortOwner,
|
||||
ensurePortAvailable,
|
||||
handlePortError,
|
||||
PortInUseError,
|
||||
} from "./infra/ports.js";
|
||||
import { runCommandWithTimeout, runExec } from "./process/exec.js";
|
||||
import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js";
|
||||
|
||||
export {
|
||||
assertWebChannel,
|
||||
applyTemplate,
|
||||
createDefaultDeps,
|
||||
deriveSessionKey,
|
||||
describePortOwner,
|
||||
ensureBinary,
|
||||
ensurePortAvailable,
|
||||
getReplyFromConfig,
|
||||
handlePortError,
|
||||
loadConfig,
|
||||
loadSessionStore,
|
||||
monitorWebChannel,
|
||||
normalizeE164,
|
||||
PortInUseError,
|
||||
promptYesNo,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
runCommandWithTimeout,
|
||||
runExec,
|
||||
saveSessionStore,
|
||||
toWhatsappJid,
|
||||
waitForever,
|
||||
};
|
||||
22
src/plugins/bundled-provider-auth-env-vars.test.ts
Normal file
22
src/plugins/bundled-provider-auth-env-vars.test.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES } from "./bundled-provider-auth-env-vars.js";
|
||||
|
||||
describe("bundled provider auth env vars", () => {
|
||||
it("reads bundled provider auth env vars from plugin manifests", () => {
|
||||
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["github-copilot"]).toEqual([
|
||||
"COPILOT_GITHUB_TOKEN",
|
||||
"GH_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
]);
|
||||
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["qwen-portal"]).toEqual([
|
||||
"QWEN_OAUTH_TOKEN",
|
||||
"QWEN_PORTAL_API_KEY",
|
||||
]);
|
||||
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["minimax-portal"]).toEqual([
|
||||
"MINIMAX_OAUTH_TOKEN",
|
||||
"MINIMAX_API_KEY",
|
||||
]);
|
||||
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.openai).toEqual(["OPENAI_API_KEY"]);
|
||||
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["openai-codex"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
91
src/plugins/bundled-provider-auth-env-vars.ts
Normal file
91
src/plugins/bundled-provider-auth-env-vars.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import ANTHROPIC_MANIFEST from "../../extensions/anthropic/openclaw.plugin.json" with { type: "json" };
|
||||
import BYTEPLUS_MANIFEST from "../../extensions/byteplus/openclaw.plugin.json" with { type: "json" };
|
||||
import CLOUDFLARE_AI_GATEWAY_MANIFEST from "../../extensions/cloudflare-ai-gateway/openclaw.plugin.json" with { type: "json" };
|
||||
import COPILOT_PROXY_MANIFEST from "../../extensions/copilot-proxy/openclaw.plugin.json" with { type: "json" };
|
||||
import GITHUB_COPILOT_MANIFEST from "../../extensions/github-copilot/openclaw.plugin.json" with { type: "json" };
|
||||
import GOOGLE_MANIFEST from "../../extensions/google/openclaw.plugin.json" with { type: "json" };
|
||||
import HUGGINGFACE_MANIFEST from "../../extensions/huggingface/openclaw.plugin.json" with { type: "json" };
|
||||
import KILOCODE_MANIFEST from "../../extensions/kilocode/openclaw.plugin.json" with { type: "json" };
|
||||
import KIMI_CODING_MANIFEST from "../../extensions/kimi-coding/openclaw.plugin.json" with { type: "json" };
|
||||
import MINIMAX_MANIFEST from "../../extensions/minimax/openclaw.plugin.json" with { type: "json" };
|
||||
import MISTRAL_MANIFEST from "../../extensions/mistral/openclaw.plugin.json" with { type: "json" };
|
||||
import MODELSTUDIO_MANIFEST from "../../extensions/modelstudio/openclaw.plugin.json" with { type: "json" };
|
||||
import MOONSHOT_MANIFEST from "../../extensions/moonshot/openclaw.plugin.json" with { type: "json" };
|
||||
import NVIDIA_MANIFEST from "../../extensions/nvidia/openclaw.plugin.json" with { type: "json" };
|
||||
import OLLAMA_MANIFEST from "../../extensions/ollama/openclaw.plugin.json" with { type: "json" };
|
||||
import OPENAI_MANIFEST from "../../extensions/openai/openclaw.plugin.json" with { type: "json" };
|
||||
import OPENCODE_GO_MANIFEST from "../../extensions/opencode-go/openclaw.plugin.json" with { type: "json" };
|
||||
import OPENCODE_MANIFEST from "../../extensions/opencode/openclaw.plugin.json" with { type: "json" };
|
||||
import OPENROUTER_MANIFEST from "../../extensions/openrouter/openclaw.plugin.json" with { type: "json" };
|
||||
import QIANFAN_MANIFEST from "../../extensions/qianfan/openclaw.plugin.json" with { type: "json" };
|
||||
import QWEN_PORTAL_AUTH_MANIFEST from "../../extensions/qwen-portal-auth/openclaw.plugin.json" with { type: "json" };
|
||||
import SGLANG_MANIFEST from "../../extensions/sglang/openclaw.plugin.json" with { type: "json" };
|
||||
import SYNTHETIC_MANIFEST from "../../extensions/synthetic/openclaw.plugin.json" with { type: "json" };
|
||||
import TOGETHER_MANIFEST from "../../extensions/together/openclaw.plugin.json" with { type: "json" };
|
||||
import VENICE_MANIFEST from "../../extensions/venice/openclaw.plugin.json" with { type: "json" };
|
||||
import VERCEL_AI_GATEWAY_MANIFEST from "../../extensions/vercel-ai-gateway/openclaw.plugin.json" with { type: "json" };
|
||||
import VLLM_MANIFEST from "../../extensions/vllm/openclaw.plugin.json" with { type: "json" };
|
||||
import VOLCENGINE_MANIFEST from "../../extensions/volcengine/openclaw.plugin.json" with { type: "json" };
|
||||
import XIAOMI_MANIFEST from "../../extensions/xiaomi/openclaw.plugin.json" with { type: "json" };
|
||||
import ZAI_MANIFEST from "../../extensions/zai/openclaw.plugin.json" with { type: "json" };
|
||||
|
||||
type ProviderAuthEnvVarManifest = {
|
||||
id?: string;
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
function collectBundledProviderAuthEnvVars(
|
||||
manifests: readonly ProviderAuthEnvVarManifest[],
|
||||
): Record<string, readonly string[]> {
|
||||
const entries: Record<string, readonly string[]> = {};
|
||||
for (const manifest of manifests) {
|
||||
const providerAuthEnvVars = manifest.providerAuthEnvVars;
|
||||
if (!providerAuthEnvVars) {
|
||||
continue;
|
||||
}
|
||||
for (const [providerId, envVars] of Object.entries(providerAuthEnvVars)) {
|
||||
const normalizedProviderId = providerId.trim();
|
||||
const normalizedEnvVars = envVars.map((value) => value.trim()).filter(Boolean);
|
||||
if (!normalizedProviderId || normalizedEnvVars.length === 0) {
|
||||
continue;
|
||||
}
|
||||
entries[normalizedProviderId] = normalizedEnvVars;
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Read bundled provider auth env metadata from manifests so env-based auth
|
||||
// lookup stays cheap and does not need to boot plugin runtime code.
|
||||
export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = collectBundledProviderAuthEnvVars([
|
||||
ANTHROPIC_MANIFEST,
|
||||
BYTEPLUS_MANIFEST,
|
||||
CLOUDFLARE_AI_GATEWAY_MANIFEST,
|
||||
COPILOT_PROXY_MANIFEST,
|
||||
GITHUB_COPILOT_MANIFEST,
|
||||
GOOGLE_MANIFEST,
|
||||
HUGGINGFACE_MANIFEST,
|
||||
KILOCODE_MANIFEST,
|
||||
KIMI_CODING_MANIFEST,
|
||||
MINIMAX_MANIFEST,
|
||||
MISTRAL_MANIFEST,
|
||||
MODELSTUDIO_MANIFEST,
|
||||
MOONSHOT_MANIFEST,
|
||||
NVIDIA_MANIFEST,
|
||||
OLLAMA_MANIFEST,
|
||||
OPENAI_MANIFEST,
|
||||
OPENCODE_GO_MANIFEST,
|
||||
OPENCODE_MANIFEST,
|
||||
OPENROUTER_MANIFEST,
|
||||
QIANFAN_MANIFEST,
|
||||
QWEN_PORTAL_AUTH_MANIFEST,
|
||||
SGLANG_MANIFEST,
|
||||
SYNTHETIC_MANIFEST,
|
||||
TOGETHER_MANIFEST,
|
||||
VENICE_MANIFEST,
|
||||
VERCEL_AI_GATEWAY_MANIFEST,
|
||||
VLLM_MANIFEST,
|
||||
VOLCENGINE_MANIFEST,
|
||||
XIAOMI_MANIFEST,
|
||||
ZAI_MANIFEST,
|
||||
]);
|
||||
@ -213,4 +213,9 @@ describe("resolveEnableState", () => {
|
||||
reason: "workspace plugin (disabled by default)",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bundled provider plugins enabled when they are bundled-default providers", () => {
|
||||
const state = resolveEnableState("google", "bundled", normalizePluginsConfig({}));
|
||||
expect(state).toEqual({ enabled: true });
|
||||
});
|
||||
});
|
||||
|
||||
@ -29,6 +29,7 @@ export const BUNDLED_ENABLED_BY_DEFAULT = new Set<string>([
|
||||
"cloudflare-ai-gateway",
|
||||
"device-pair",
|
||||
"github-copilot",
|
||||
"google",
|
||||
"huggingface",
|
||||
"kilocode",
|
||||
"kimi-coding",
|
||||
|
||||
@ -28,7 +28,7 @@ import { isPathInside, safeStatSync } from "./path-safety.js";
|
||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import { setActivePluginRegistry } from "./runtime.js";
|
||||
import { createPluginRuntime, type CreatePluginRuntimeOptions } from "./runtime/index.js";
|
||||
import type { CreatePluginRuntimeOptions } from "./runtime/index.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { validateJsonSchemaValue } from "./schema-validator.js";
|
||||
import type {
|
||||
@ -163,6 +163,25 @@ const resolveExtensionApiAlias = (params: { modulePath?: string } = {}): string
|
||||
return null;
|
||||
};
|
||||
|
||||
function resolvePluginRuntimeModulePath(params: { modulePath?: string } = {}): string | null {
|
||||
try {
|
||||
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
|
||||
const moduleDir = path.dirname(modulePath);
|
||||
const candidates = [
|
||||
path.join(moduleDir, "runtime", "index.ts"),
|
||||
path.join(moduleDir, "runtime", "index.js"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedPluginSdkExportedSubpaths = new Map<string, string[]>();
|
||||
|
||||
function listPluginSdkExportedSubpaths(params: { modulePath?: string } = {}): string[] {
|
||||
@ -747,11 +766,58 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
clearPluginInteractiveHandlers();
|
||||
}
|
||||
|
||||
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
|
||||
let jitiLoader: ReturnType<typeof createJiti> | null = null;
|
||||
const getJiti = () => {
|
||||
if (jitiLoader) {
|
||||
return jitiLoader;
|
||||
}
|
||||
const pluginSdkAlias = resolvePluginSdkAlias();
|
||||
const extensionApiAlias = resolveExtensionApiAlias();
|
||||
const aliasMap = {
|
||||
...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}),
|
||||
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
|
||||
...resolvePluginSdkScopedAliasMap(),
|
||||
};
|
||||
jitiLoader = createJiti(import.meta.url, {
|
||||
interopDefault: true,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
...(Object.keys(aliasMap).length > 0
|
||||
? {
|
||||
alias: aliasMap,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
return jitiLoader;
|
||||
};
|
||||
|
||||
let createPluginRuntimeFactory: ((options?: CreatePluginRuntimeOptions) => PluginRuntime) | null =
|
||||
null;
|
||||
const resolveCreatePluginRuntime = (): ((
|
||||
options?: CreatePluginRuntimeOptions,
|
||||
) => PluginRuntime) => {
|
||||
if (createPluginRuntimeFactory) {
|
||||
return createPluginRuntimeFactory;
|
||||
}
|
||||
const runtimeModulePath = resolvePluginRuntimeModulePath();
|
||||
if (!runtimeModulePath) {
|
||||
throw new Error("Unable to resolve plugin runtime module");
|
||||
}
|
||||
const runtimeModule = getJiti()(runtimeModulePath) as {
|
||||
createPluginRuntime?: (options?: CreatePluginRuntimeOptions) => PluginRuntime;
|
||||
};
|
||||
if (typeof runtimeModule.createPluginRuntime !== "function") {
|
||||
throw new Error("Plugin runtime module missing createPluginRuntime export");
|
||||
}
|
||||
createPluginRuntimeFactory = runtimeModule.createPluginRuntime;
|
||||
return createPluginRuntimeFactory;
|
||||
};
|
||||
|
||||
// Lazily initialize the runtime so startup paths that discover/skip plugins do
|
||||
// not eagerly load every channel runtime dependency.
|
||||
// not eagerly load every channel/runtime dependency tree.
|
||||
let resolvedRuntime: PluginRuntime | null = null;
|
||||
const resolveRuntime = (): PluginRuntime => {
|
||||
resolvedRuntime ??= createPluginRuntime(options.runtimeOptions);
|
||||
resolvedRuntime ??= resolveCreatePluginRuntime()(options.runtimeOptions);
|
||||
return resolvedRuntime;
|
||||
};
|
||||
const runtime = new Proxy({} as PluginRuntime, {
|
||||
@ -780,6 +846,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
return Reflect.getPrototypeOf(resolveRuntime() as object);
|
||||
},
|
||||
});
|
||||
|
||||
const { registry, createApi } = createPluginRegistry({
|
||||
logger,
|
||||
runtime,
|
||||
@ -823,31 +890,6 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
env,
|
||||
});
|
||||
|
||||
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
|
||||
let jitiLoader: ReturnType<typeof createJiti> | null = null;
|
||||
const getJiti = () => {
|
||||
if (jitiLoader) {
|
||||
return jitiLoader;
|
||||
}
|
||||
const pluginSdkAlias = resolvePluginSdkAlias();
|
||||
const extensionApiAlias = resolveExtensionApiAlias();
|
||||
const aliasMap = {
|
||||
...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}),
|
||||
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
|
||||
...resolvePluginSdkScopedAliasMap(),
|
||||
};
|
||||
jitiLoader = createJiti(import.meta.url, {
|
||||
interopDefault: true,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
...(Object.keys(aliasMap).length > 0
|
||||
? {
|
||||
alias: aliasMap,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
return jitiLoader;
|
||||
};
|
||||
|
||||
const manifestByRoot = new Map(
|
||||
manifestRegistry.plugins.map((record) => [record.rootDir, record]),
|
||||
);
|
||||
|
||||
@ -199,6 +199,28 @@ describe("loadPluginManifestRegistry", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves provider auth env metadata from plugin manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "openai",
|
||||
providers: ["openai", "openai-codex"],
|
||||
providerAuthEnvVars: {
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
},
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const registry = loadSingleCandidateRegistry({
|
||||
idHint: "openai",
|
||||
rootDir: dir,
|
||||
origin: "bundled",
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
});
|
||||
});
|
||||
|
||||
it("reports bundled plugins as the duplicate winner for auto-discovered globals", () => {
|
||||
const bundledDir = makeTempDir();
|
||||
const globalDir = makeTempDir();
|
||||
|
||||
@ -41,6 +41,7 @@ export type PluginManifestRecord = {
|
||||
kind?: PluginKind;
|
||||
channels: string[];
|
||||
providers: string[];
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
skills: string[];
|
||||
settingsFiles?: string[];
|
||||
hooks: string[];
|
||||
@ -152,6 +153,7 @@ function buildRecord(params: {
|
||||
kind: params.manifest.kind,
|
||||
channels: params.manifest.channels ?? [],
|
||||
providers: params.manifest.providers ?? [],
|
||||
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
|
||||
skills: params.manifest.skills ?? [],
|
||||
settingsFiles: [],
|
||||
hooks: [],
|
||||
|
||||
@ -14,6 +14,7 @@ export type PluginManifest = {
|
||||
kind?: PluginKind;
|
||||
channels?: string[];
|
||||
providers?: string[];
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
skills?: string[];
|
||||
name?: string;
|
||||
description?: string;
|
||||
@ -32,6 +33,25 @@ function normalizeStringList(value: unknown): string[] {
|
||||
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeStringListRecord(value: unknown): Record<string, string[]> | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized: Record<string, string[]> = {};
|
||||
for (const [key, rawValues] of Object.entries(value)) {
|
||||
const providerId = typeof key === "string" ? key.trim() : "";
|
||||
if (!providerId) {
|
||||
continue;
|
||||
}
|
||||
const values = normalizeStringList(rawValues);
|
||||
if (values.length === 0) {
|
||||
continue;
|
||||
}
|
||||
normalized[providerId] = values;
|
||||
}
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginManifestPath(rootDir: string): string {
|
||||
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
|
||||
const candidate = path.join(rootDir, filename);
|
||||
@ -93,6 +113,7 @@ export function loadPluginManifest(
|
||||
const version = typeof raw.version === "string" ? raw.version.trim() : undefined;
|
||||
const channels = normalizeStringList(raw.channels);
|
||||
const providers = normalizeStringList(raw.providers);
|
||||
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
|
||||
const skills = normalizeStringList(raw.skills);
|
||||
|
||||
let uiHints: Record<string, PluginConfigUiHint> | undefined;
|
||||
@ -108,6 +129,7 @@ export function loadPluginManifest(
|
||||
kind,
|
||||
channels,
|
||||
providers,
|
||||
providerAuthEnvVars,
|
||||
skills,
|
||||
name,
|
||||
description,
|
||||
|
||||
@ -2,9 +2,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderPlugin, ProviderRuntimeModel } from "./types.js";
|
||||
|
||||
const resolvePluginProvidersMock = vi.fn((_: unknown) => [] as ProviderPlugin[]);
|
||||
const resolveOwningPluginIdsForProviderMock = vi.fn(
|
||||
(_: unknown) => undefined as string[] | undefined,
|
||||
);
|
||||
|
||||
vi.mock("./providers.js", () => ({
|
||||
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
|
||||
resolveOwningPluginIdsForProvider: (params: unknown) =>
|
||||
resolveOwningPluginIdsForProviderMock(params as never),
|
||||
}));
|
||||
|
||||
import {
|
||||
@ -41,6 +46,8 @@ describe("provider-runtime", () => {
|
||||
beforeEach(() => {
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue([]);
|
||||
resolveOwningPluginIdsForProviderMock.mockReset();
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("matches providers by alias for runtime hook lookup", () => {
|
||||
@ -56,9 +63,13 @@ describe("provider-runtime", () => {
|
||||
const plugin = resolveProviderRuntimePlugin({ provider: "Open Router" });
|
||||
|
||||
expect(plugin?.id).toBe("openrouter");
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
||||
expect(resolveOwningPluginIdsForProviderMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "Open Router",
|
||||
}),
|
||||
);
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
}),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolvePluginProviders } from "./providers.js";
|
||||
import { resolveOwningPluginIdsForProvider, resolvePluginProviders } from "./providers.js";
|
||||
import type {
|
||||
ProviderAugmentModelCatalogContext,
|
||||
ProviderBuildMissingAuthMessageContext,
|
||||
@ -60,9 +60,15 @@ export function resolveProviderRuntimePlugin(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin | undefined {
|
||||
return resolveProviderPluginsForHooks(params).find((plugin) =>
|
||||
matchesProviderId(plugin, params.provider),
|
||||
);
|
||||
return resolveProviderPluginsForHooks({
|
||||
...params,
|
||||
onlyPluginIds: resolveOwningPluginIdsForProvider({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}),
|
||||
}).find((plugin) => matchesProviderId(plugin, params.provider));
|
||||
}
|
||||
|
||||
export function runProviderDynamicModel(params: {
|
||||
|
||||
@ -1,18 +1,28 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolvePluginProviders } from "./providers.js";
|
||||
import { resolveOwningPluginIdsForProvider, resolvePluginProviders } from "./providers.js";
|
||||
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
const loadPluginManifestRegistryMock = vi.fn();
|
||||
|
||||
vi.mock("./loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: (...args: unknown[]) => loadPluginManifestRegistryMock(...args),
|
||||
}));
|
||||
|
||||
describe("resolvePluginProviders", () => {
|
||||
beforeEach(() => {
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
providers: [{ pluginId: "google", provider: { id: "demo-provider" } }],
|
||||
});
|
||||
loadPluginManifestRegistryMock.mockReset();
|
||||
loadPluginManifestRegistryMock.mockReturnValue({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards an explicit env to plugin loading", () => {
|
||||
@ -86,4 +96,18 @@ describe("resolvePluginProviders", () => {
|
||||
expect(allow).toContain("google");
|
||||
expect(allow).not.toContain("google-gemini-cli-auth");
|
||||
});
|
||||
|
||||
it("maps provider ids to owning plugin ids via manifests", () => {
|
||||
loadPluginManifestRegistryMock.mockReturnValue({
|
||||
plugins: [
|
||||
{ id: "minimax", providers: ["minimax", "minimax-portal"] },
|
||||
{ id: "openai", providers: ["openai", "openai-codex"] },
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
expect(resolveOwningPluginIdsForProvider({ provider: "minimax-portal" })).toEqual(["minimax"]);
|
||||
expect(resolveOwningPluginIdsForProvider({ provider: "openai-codex" })).toEqual(["openai"]);
|
||||
expect(resolveOwningPluginIdsForProvider({ provider: "gemini-cli" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { withBundledPluginAllowlistCompat } from "./bundled-compat.js";
|
||||
import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { ProviderPlugin } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
@ -86,6 +88,32 @@ function withBundledProviderVitestCompat(params: {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveOwningPluginIdsForProvider(params: {
|
||||
provider: string;
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] | undefined {
|
||||
const normalizedProvider = normalizeProviderId(params.provider);
|
||||
if (!normalizedProvider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const pluginIds = registry.plugins
|
||||
.filter((plugin) =>
|
||||
plugin.providers.some((providerId) => normalizeProviderId(providerId) === normalizedProvider),
|
||||
)
|
||||
.map((plugin) => plugin.id);
|
||||
|
||||
return pluginIds.length > 0 ? pluginIds : undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
|
||||
@ -337,8 +337,6 @@ export type ProviderResolvedUsageAuth = {
|
||||
* This hook runs after `resolveUsageAuth` succeeds. Core still owns summary
|
||||
* fan-out, timeout wrapping, filtering, and formatting; the provider plugin
|
||||
* owns the provider-specific HTTP request + response normalization.
|
||||
*
|
||||
* Return `null`/`undefined` to fall back to legacy core fetchers.
|
||||
*/
|
||||
export type ProviderFetchUsageSnapshotContext = {
|
||||
config: OpenClawConfig;
|
||||
@ -499,6 +497,12 @@ export type ProviderPlugin = {
|
||||
label: string;
|
||||
docsPath?: string;
|
||||
aliases?: string[];
|
||||
/**
|
||||
* Provider-related env vars shown in onboarding/search/help surfaces.
|
||||
*
|
||||
* Keep entries in preferred display order. This can include direct auth env
|
||||
* vars or setup inputs such as OAuth client id/secret vars.
|
||||
*/
|
||||
envVars?: string[];
|
||||
auth: ProviderAuthMethod[];
|
||||
/**
|
||||
@ -584,10 +588,9 @@ export type ProviderPlugin = {
|
||||
/**
|
||||
* Usage/billing auth resolution hook.
|
||||
*
|
||||
* Called by provider-usage surfaces (`/usage`, status snapshots, reporting)
|
||||
* before OpenClaw falls back to legacy core auth resolution. Use this when a
|
||||
* provider's usage endpoint needs provider-owned token extraction, blob
|
||||
* parsing, or alias handling.
|
||||
* Called by provider-usage surfaces (`/usage`, status snapshots, reporting).
|
||||
* Use this when a provider's usage endpoint needs provider-owned token
|
||||
* extraction, blob parsing, or alias handling.
|
||||
*/
|
||||
resolveUsageAuth?: (
|
||||
ctx: ProviderResolveUsageAuthContext,
|
||||
|
||||
@ -10,10 +10,12 @@ describe("provider env vars", () => {
|
||||
expect(listKnownProviderAuthEnvVarNames()).toEqual(
|
||||
expect.arrayContaining(["GITHUB_TOKEN", "GH_TOKEN", "ANTHROPIC_OAUTH_TOKEN"]),
|
||||
);
|
||||
expect(listKnownSecretEnvVarNames()).not.toEqual(listKnownProviderAuthEnvVarNames());
|
||||
expect(listKnownSecretEnvVarNames()).not.toEqual(
|
||||
expect(listKnownSecretEnvVarNames()).toEqual(
|
||||
expect.arrayContaining(["GITHUB_TOKEN", "GH_TOKEN", "ANTHROPIC_OAUTH_TOKEN"]),
|
||||
);
|
||||
expect(listKnownProviderAuthEnvVarNames()).toEqual(
|
||||
expect.arrayContaining(["MINIMAX_CODE_PLAN_KEY"]),
|
||||
);
|
||||
expect(listKnownSecretEnvVarNames()).not.toContain("OPENCLAW_API_KEY");
|
||||
});
|
||||
|
||||
|
||||
@ -1,50 +1,42 @@
|
||||
export const PROVIDER_ENV_VARS: Record<string, readonly string[]> = {
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
anthropic: ["ANTHROPIC_API_KEY"],
|
||||
import { BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES } from "../plugins/bundled-provider-auth-env-vars.js";
|
||||
|
||||
const CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
|
||||
chutes: ["CHUTES_OAUTH_TOKEN", "CHUTES_API_KEY"],
|
||||
google: ["GEMINI_API_KEY"],
|
||||
minimax: ["MINIMAX_API_KEY"],
|
||||
"minimax-cn": ["MINIMAX_API_KEY"],
|
||||
moonshot: ["MOONSHOT_API_KEY"],
|
||||
"kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
synthetic: ["SYNTHETIC_API_KEY"],
|
||||
venice: ["VENICE_API_KEY"],
|
||||
zai: ["ZAI_API_KEY", "Z_AI_API_KEY"],
|
||||
xiaomi: ["XIAOMI_API_KEY"],
|
||||
openrouter: ["OPENROUTER_API_KEY"],
|
||||
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
|
||||
voyage: ["VOYAGE_API_KEY"],
|
||||
groq: ["GROQ_API_KEY"],
|
||||
deepgram: ["DEEPGRAM_API_KEY"],
|
||||
cerebras: ["CEREBRAS_API_KEY"],
|
||||
litellm: ["LITELLM_API_KEY"],
|
||||
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
|
||||
opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
together: ["TOGETHER_API_KEY"],
|
||||
huggingface: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
|
||||
qianfan: ["QIANFAN_API_KEY"],
|
||||
xai: ["XAI_API_KEY"],
|
||||
mistral: ["MISTRAL_API_KEY"],
|
||||
kilocode: ["KILOCODE_API_KEY"],
|
||||
modelstudio: ["MODELSTUDIO_API_KEY"],
|
||||
volcengine: ["VOLCANO_ENGINE_API_KEY"],
|
||||
byteplus: ["BYTEPLUS_API_KEY"],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Provider auth env candidates used by generic auth resolution.
|
||||
*
|
||||
* Order matters: the first non-empty value wins for helpers such as
|
||||
* `resolveEnvApiKey()`. Bundled providers source this from plugin manifest
|
||||
* metadata so auth probes do not need to load plugin runtime.
|
||||
*/
|
||||
export const PROVIDER_AUTH_ENV_VAR_CANDIDATES: Record<string, readonly string[]> = {
|
||||
...BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
...CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
};
|
||||
|
||||
const EXTRA_PROVIDER_AUTH_ENV_VARS = [
|
||||
"VOYAGE_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
"DEEPGRAM_API_KEY",
|
||||
"CEREBRAS_API_KEY",
|
||||
"NVIDIA_API_KEY",
|
||||
"COPILOT_GITHUB_TOKEN",
|
||||
"GH_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
"ANTHROPIC_OAUTH_TOKEN",
|
||||
"CHUTES_OAUTH_TOKEN",
|
||||
"CHUTES_API_KEY",
|
||||
"QWEN_OAUTH_TOKEN",
|
||||
"QWEN_PORTAL_API_KEY",
|
||||
"MINIMAX_OAUTH_TOKEN",
|
||||
"OLLAMA_API_KEY",
|
||||
"VLLM_API_KEY",
|
||||
] as const;
|
||||
/**
|
||||
* Provider env vars used for onboarding/default secret refs and broad secret
|
||||
* scrubbing. This can include non-model providers and may intentionally choose
|
||||
* a different preferred first env var than auth resolution.
|
||||
*/
|
||||
export const PROVIDER_ENV_VARS: Record<string, readonly string[]> = {
|
||||
...PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
|
||||
chutes: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
|
||||
google: ["GEMINI_API_KEY"],
|
||||
"minimax-cn": ["MINIMAX_API_KEY"],
|
||||
xai: ["XAI_API_KEY"],
|
||||
};
|
||||
|
||||
const EXTRA_PROVIDER_AUTH_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY"] as const;
|
||||
|
||||
const KNOWN_SECRET_ENV_VARS = [
|
||||
...new Set(Object.values(PROVIDER_ENV_VARS).flatMap((keys) => keys)),
|
||||
@ -53,7 +45,11 @@ const KNOWN_SECRET_ENV_VARS = [
|
||||
// OPENCLAW_API_KEY authenticates the local OpenClaw bridge itself and must
|
||||
// remain available to child bridge/runtime processes.
|
||||
const KNOWN_PROVIDER_AUTH_ENV_VARS = [
|
||||
...new Set([...KNOWN_SECRET_ENV_VARS, ...EXTRA_PROVIDER_AUTH_ENV_VARS]),
|
||||
...new Set([
|
||||
...Object.values(PROVIDER_AUTH_ENV_VAR_CANDIDATES).flatMap((keys) => keys),
|
||||
...KNOWN_SECRET_ENV_VARS,
|
||||
...EXTRA_PROVIDER_AUTH_ENV_VARS,
|
||||
]),
|
||||
];
|
||||
|
||||
export function listKnownProviderAuthEnvVarNames(): string[] {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user