From ca2f0466686d5ff39ef75d1e77d0c88f07ca5383 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 20:56:44 -0700 Subject: [PATCH 01/14] Status: route JSON through lean command --- src/cli/program/routes.test.ts | 25 +++++++++ src/cli/program/routes.ts | 5 ++ src/commands/status-json.ts | 100 +++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 src/commands/status-json.ts diff --git a/src/cli/program/routes.test.ts b/src/cli/program/routes.test.ts index 896dcb6757a..65cba06e299 100644 --- a/src/cli/program/routes.test.ts +++ b/src/cli/program/routes.test.ts @@ -6,6 +6,7 @@ const runConfigUnsetMock = vi.hoisted(() => vi.fn(async () => {})); const modelsListCommandMock = vi.hoisted(() => vi.fn(async () => {})); const modelsStatusCommandMock = vi.hoisted(() => vi.fn(async () => {})); const gatewayStatusCommandMock = vi.hoisted(() => vi.fn(async () => {})); +const statusJsonCommandMock = vi.hoisted(() => vi.fn(async () => {})); vi.mock("../config-cli.js", () => ({ runConfigGet: runConfigGetMock, @@ -21,6 +22,10 @@ vi.mock("../../commands/gateway-status.js", () => ({ gatewayStatusCommand: gatewayStatusCommandMock, })); +vi.mock("../../commands/status-json.js", () => ({ + statusJsonCommand: statusJsonCommandMock, +})); + describe("program routes", () => { beforeEach(() => { vi.clearAllMocks(); @@ -124,6 +129,26 @@ describe("program routes", () => { await expectRunFalse(["status"], ["node", "openclaw", "status", "--timeout"]); }); + it("routes status --json through the lean JSON command", async () => { + const route = expectRoute(["status"]); + await expect( + route?.run([ + "node", + "openclaw", + "status", + "--json", + "--deep", + "--usage", + "--timeout", + "5000", + ]), + ).resolves.toBe(true); + expect(statusJsonCommandMock).toHaveBeenCalledWith( + { deep: true, all: false, usage: true, timeoutMs: 5000 }, + expect.any(Object), + ); + }); + it("returns false for sessions route when --store value is missing", async () => { await expectRunFalse(["sessions"], ["node", "openclaw", "sessions", "--store"]); }); diff --git a/src/cli/program/routes.ts b/src/cli/program/routes.ts index 353c9b8f11d..913f84dd2e4 100644 --- a/src/cli/program/routes.ts +++ b/src/cli/program/routes.ts @@ -47,6 +47,11 @@ const routeStatus: RouteSpec = { if (timeoutMs === null) { return false; } + if (json) { + const { statusJsonCommand } = await import("../../commands/status-json.js"); + await statusJsonCommand({ deep, all, usage, timeoutMs }, defaultRuntime); + return true; + } const { statusCommand } = await import("../../commands/status.js"); await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime); return true; diff --git a/src/commands/status-json.ts b/src/commands/status-json.ts new file mode 100644 index 00000000000..035f2c71245 --- /dev/null +++ b/src/commands/status-json.ts @@ -0,0 +1,100 @@ +import { callGateway } from "../gateway/call.js"; +import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js"; +import { normalizeUpdateChannel, resolveUpdateChannelDisplay } from "../infra/update-channels.js"; +import type { RuntimeEnv } from "../runtime.js"; +import { runSecurityAudit } from "../security/audit.js"; +import { getDaemonStatusSummary, getNodeDaemonStatusSummary } from "./status.daemon.js"; +import { scanStatus } from "./status.scan.js"; + +let providerUsagePromise: Promise | undefined; + +function loadProviderUsage() { + providerUsagePromise ??= import("../infra/provider-usage.js"); + return providerUsagePromise; +} + +export async function statusJsonCommand( + opts: { + deep?: boolean; + usage?: boolean; + timeoutMs?: number; + all?: boolean; + }, + runtime: RuntimeEnv, +) { + const scan = await scanStatus({ json: true, timeoutMs: opts.timeoutMs, all: opts.all }, runtime); + const securityAudit = await runSecurityAudit({ + config: scan.cfg, + sourceConfig: scan.sourceConfig, + deep: false, + includeFilesystem: true, + includeChannelSecurity: true, + }); + + const usage = opts.usage + ? await loadProviderUsage().then(({ loadProviderUsageSummary }) => + loadProviderUsageSummary({ timeoutMs: opts.timeoutMs }), + ) + : undefined; + const health = opts.deep + ? await callGateway({ + method: "health", + params: { probe: true }, + timeoutMs: opts.timeoutMs, + config: scan.cfg, + }).catch(() => undefined) + : undefined; + const lastHeartbeat = + opts.deep && scan.gatewayReachable + ? await callGateway({ + method: "last-heartbeat", + params: {}, + timeoutMs: opts.timeoutMs, + config: scan.cfg, + }).catch(() => null) + : null; + + const [daemon, nodeDaemon] = await Promise.all([ + getDaemonStatusSummary(), + getNodeDaemonStatusSummary(), + ]); + const channelInfo = resolveUpdateChannelDisplay({ + configChannel: normalizeUpdateChannel(scan.cfg.update?.channel), + installKind: scan.update.installKind, + gitTag: scan.update.git?.tag ?? null, + gitBranch: scan.update.git?.branch ?? null, + }); + + runtime.log( + JSON.stringify( + { + ...scan.summary, + os: scan.osSummary, + update: scan.update, + updateChannel: channelInfo.channel, + updateChannelSource: channelInfo.source, + memory: scan.memory, + memoryPlugin: scan.memoryPlugin, + gateway: { + mode: scan.gatewayMode, + url: scan.gatewayConnection.url, + urlSource: scan.gatewayConnection.urlSource, + misconfigured: scan.remoteUrlMissing, + reachable: scan.gatewayReachable, + connectLatencyMs: scan.gatewayProbe?.connectLatencyMs ?? null, + self: scan.gatewaySelf, + error: scan.gatewayProbe?.error ?? null, + authWarning: scan.gatewayProbeAuthWarning ?? null, + }, + gatewayService: daemon, + nodeService: nodeDaemon, + agents: scan.agentStatus, + securityAudit, + secretDiagnostics: scan.secretDiagnostics, + ...(health || usage || lastHeartbeat ? { health, usage, lastHeartbeat } : {}), + }, + null, + 2, + ), + ); +} From a33caab280f3e289005e4d37bc6449208a0d3d8d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Mar 2026 20:58:59 -0700 Subject: [PATCH 02/14] refactor(plugins): move auth and model policy to providers --- docs/concepts/model-providers.md | 21 +- docs/tools/plugin.md | 56 +- extensions/anthropic/index.ts | 74 ++- extensions/github-copilot/index.ts | 3 + extensions/google/gemini-cli-provider.test.ts | 67 ++- extensions/google/gemini-cli-provider.ts | 58 +- extensions/google/index.ts | 11 + extensions/google/openclaw.plugin.json | 5 +- extensions/google/provider-models.ts | 63 ++ extensions/minimax/index.ts | 6 + extensions/openai/openai-codex-provider.ts | 63 +- extensions/openai/openai-provider.ts | 11 +- extensions/openai/shared.ts | 8 + extensions/opencode-go/index.ts | 1 + extensions/opencode/index.ts | 10 + extensions/openrouter/index.ts | 1 + extensions/zai/index.ts | 10 + src/agents/live-model-filter.ts | 15 + src/agents/model-compat.test.ts | 136 +---- src/agents/model-forward-compat.ts | 123 ---- src/agents/pi-embedded-runner/model.ts | 50 -- src/auto-reply/thinking.test.ts | 43 +- src/auto-reply/thinking.ts | 60 +- src/commands/models/auth.test.ts | 139 +++-- src/commands/models/auth.ts | 538 ++++++++++-------- src/plugin-sdk/core.ts | 3 + src/plugin-sdk/index.ts | 3 + src/plugins/provider-runtime.test.ts | 49 ++ src/plugins/provider-runtime.ts | 43 ++ src/plugins/types.ts | 63 ++ 30 files changed, 1080 insertions(+), 653 deletions(-) create mode 100644 extensions/google/provider-models.ts delete mode 100644 src/agents/model-forward-compat.ts diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index aa4b90fd41f..eb0f8a1c6a2 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -25,8 +25,10 @@ For model selection rules, see [/concepts/models](/concepts/models). `resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`, `capabilities`, `prepareExtraParams`, `wrapStreamFn`, `isCacheTtlEligible`, `buildMissingAuthMessage`, - `suppressBuiltInModel`, `augmentModelCatalog`, `prepareRuntimeAuth`, - `resolveUsageAuth`, and `fetchUsageSnapshot`. + `suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`, + `supportsXHighThinking`, `resolveDefaultThinkingLevel`, + `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, and + `fetchUsageSnapshot`. ## Plugin-owned provider behavior @@ -51,6 +53,11 @@ Typical split: vendor-owned error for direct resolution failures - `augmentModelCatalog`: provider appends synthetic/final catalog rows after discovery and config merging +- `isBinaryThinking`: provider owns binary on/off thinking UX +- `supportsXHighThinking`: provider opts selected models into `xhigh` +- `resolveDefaultThinkingLevel`: provider owns default `/think` policy for a + model family +- `isModernModelRef`: provider owns live/smoke preferred-model matching - `prepareRuntimeAuth`: provider turns a configured credential into a short lived runtime token - `resolveUsageAuth`: provider resolves usage/quota credentials for `/usage` @@ -68,14 +75,16 @@ Current bundled examples: hints, runtime token exchange, and usage endpoint fetching - `openai`: GPT-5.4 forward-compat fallback, direct OpenAI transport normalization, Codex-aware missing-auth hints, Spark suppression, synthetic - OpenAI/Codex catalog rows, and provider-family metadata -- `google-gemini-cli`: Gemini 3.1 forward-compat fallback plus usage-token - parsing and quota endpoint fetching for usage surfaces + OpenAI/Codex catalog rows, thinking/live-model policy, and + provider-family metadata +- `google` and `google-gemini-cli`: Gemini 3.1 forward-compat fallback and + modern-model matching; Gemini CLI OAuth also owns usage-token parsing and + quota endpoint fetching for usage surfaces - `moonshot`: shared transport, plugin-owned thinking payload normalization - `kilocode`: shared transport, plugin-owned request headers, reasoning payload normalization, Gemini transcript hints, and cache-TTL policy - `zai`: GLM-5 forward-compat fallback, `tool_stream` defaults, cache-TTL - policy, and usage auth + quota fetching + policy, binary-thinking/live-model policy, and usage auth + quota fetching - `mistral`, `opencode`, and `opencode-go`: plugin-owned capability metadata - `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`, `minimax-portal`, `modelstudio`, `nvidia`, `qianfan`, `qwen-portal`, diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index c39401bebfc..62350fb9dd4 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -220,7 +220,7 @@ 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` +- runtime hooks: `resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`, `capabilities`, `prepareExtraParams`, `wrapStreamFn`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`, `supportsXHighThinking`, `resolveDefaultThinkingLevel`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot` OpenClaw still owns the generic agent loop, failover, transcript handling, and tool policy. These hooks are the seam for provider-specific behavior without @@ -263,13 +263,22 @@ For model/provider plugins, OpenClaw uses hooks in this rough order: error hint. 12. `augmentModelCatalog` Provider-owned synthetic/final catalog rows appended after discovery. -13. `prepareRuntimeAuth` +13. `isBinaryThinking` + Provider-owned on/off reasoning toggle for binary-thinking providers. +14. `supportsXHighThinking` + Provider-owned `xhigh` reasoning support for selected models. +15. `resolveDefaultThinkingLevel` + Provider-owned default `/think` level for a specific model family. +16. `isModernModelRef` + Provider-owned modern-model matcher used by live profile filters and smoke + selection. +17. `prepareRuntimeAuth` Exchanges a configured credential into the actual runtime token/key just before inference. -14. `resolveUsageAuth` +18. `resolveUsageAuth` Resolves usage/billing credentials for `/usage` and related status surfaces. -15. `fetchUsageSnapshot` +19. `fetchUsageSnapshot` Fetches and normalizes provider-specific usage/quota snapshots after auth is resolved. @@ -286,6 +295,10 @@ For model/provider plugins, OpenClaw uses hooks in this rough order: - `buildMissingAuthMessage`: replace the generic auth-store error with a provider-specific recovery hint - `suppressBuiltInModel`: hide stale upstream rows and optionally return a provider-owned error for direct resolution failures - `augmentModelCatalog`: append synthetic/final catalog rows after discovery and config merging +- `isBinaryThinking`: expose binary on/off reasoning UX without hardcoding provider ids in `/think` +- `supportsXHighThinking`: opt specific models into the `xhigh` reasoning level +- `resolveDefaultThinkingLevel`: keep provider/model default reasoning policy out of core +- `isModernModelRef`: keep live/smoke model family inclusion rules with the provider - `prepareRuntimeAuth`: exchange a configured credential into the actual short-lived runtime token/key used for requests - `resolveUsageAuth`: resolve provider-owned credentials for usage/billing endpoints without hardcoding token parsing in core - `fetchUsageSnapshot`: own provider-specific usage endpoint fetch/parsing while core keeps summary fan-out and formatting @@ -303,6 +316,10 @@ Rule of thumb: - provider needs a provider-specific missing-auth recovery hint: use `buildMissingAuthMessage` - provider needs to hide stale upstream rows or replace them with a vendor hint: use `suppressBuiltInModel` - provider needs synthetic forward-compat rows in `models list` and pickers: use `augmentModelCatalog` +- provider exposes only binary thinking on/off: use `isBinaryThinking` +- provider wants `xhigh` on only a subset of models: use `supportsXHighThinking` +- provider owns default `/think` policy for a model family: use `resolveDefaultThinkingLevel` +- provider owns live/smoke preferred-model matching: use `isModernModelRef` - provider needs a token exchange or short-lived request credential: use `prepareRuntimeAuth` - provider needs custom usage/quota token parsing or a different usage credential: use `resolveUsageAuth` - provider needs a provider-specific usage endpoint or payload parser: use `fetchUsageSnapshot` @@ -368,14 +385,17 @@ api.registerProvider({ ### Built-in examples - Anthropic uses `resolveDynamicModel`, `capabilities`, `resolveUsageAuth`, - `fetchUsageSnapshot`, and `isCacheTtlEligible` because it owns Claude 4.6 - forward-compat, provider-family hints, usage endpoint integration, and - prompt-cache eligibility. + `fetchUsageSnapshot`, `isCacheTtlEligible`, `resolveDefaultThinkingLevel`, + and `isModernModelRef` because it owns Claude 4.6 forward-compat, + provider-family hints, usage endpoint integration, prompt-cache + eligibility, and Claude default/adaptive thinking policy. - OpenAI uses `resolveDynamicModel`, `normalizeResolvedModel`, and - `capabilities` plus `buildMissingAuthMessage`, `suppressBuiltInModel`, and - `augmentModelCatalog` because it owns GPT-5.4 forward-compat, the direct - OpenAI `openai-completions` -> `openai-responses` normalization, Codex-aware - auth hints, Spark suppression, and synthetic OpenAI list rows. + `capabilities` plus `buildMissingAuthMessage`, `suppressBuiltInModel`, + `augmentModelCatalog`, `supportsXHighThinking`, and `isModernModelRef` + because it owns GPT-5.4 forward-compat, the direct OpenAI + `openai-completions` -> `openai-responses` normalization, Codex-aware auth + hints, Spark suppression, synthetic OpenAI list rows, and GPT-5 thinking / + live-model policy. - OpenRouter uses `catalog` plus `resolveDynamicModel` and `prepareDynamicModel` because the provider is pass-through and may expose new model ids before OpenClaw's static catalog updates. @@ -389,9 +409,10 @@ api.registerProvider({ still runs on core OpenAI transports but owns its transport/base URL normalization, default transport choice, synthetic Codex catalog rows, and ChatGPT usage endpoint integration. -- Gemini CLI OAuth uses `resolveDynamicModel`, `resolveUsageAuth`, and - `fetchUsageSnapshot` because it owns Gemini 3.1 forward-compat fallback plus - the token parsing and quota endpoint wiring needed by `/usage`. +- Google AI Studio and Gemini CLI OAuth use `resolveDynamicModel` and + `isModernModelRef` because they own Gemini 3.1 forward-compat fallback and + modern-model matching; Gemini CLI OAuth also uses `resolveUsageAuth` and + `fetchUsageSnapshot` for token parsing and quota endpoint wiring. - OpenRouter uses `capabilities`, `wrapStreamFn`, and `isCacheTtlEligible` to keep provider-specific request headers, routing metadata, reasoning patches, and prompt-cache policy out of core. @@ -402,9 +423,10 @@ api.registerProvider({ reasoning payload normalization, Gemini transcript hints, and Anthropic cache-TTL gating. - Z.AI uses `resolveDynamicModel`, `prepareExtraParams`, `wrapStreamFn`, - `isCacheTtlEligible`, `resolveUsageAuth`, and `fetchUsageSnapshot` because it - owns GLM-5 fallback, `tool_stream` defaults, and both usage auth + quota - fetching. + `isCacheTtlEligible`, `isBinaryThinking`, `isModernModelRef`, + `resolveUsageAuth`, and `fetchUsageSnapshot` because it owns GLM-5 fallback, + `tool_stream` defaults, binary thinking UX, modern-model matching, and both + usage auth + quota fetching. - Mistral, OpenCode Zen, and OpenCode Go use `capabilities` only to keep transcript/tooling quirks out of core. - Catalog-only bundled providers such as `byteplus`, `cloudflare-ai-gateway`, diff --git a/extensions/anthropic/index.ts b/extensions/anthropic/index.ts index bb17f9d4dc1..5ea7e20b6d9 100644 --- a/extensions/anthropic/index.ts +++ b/extensions/anthropic/index.ts @@ -1,11 +1,14 @@ import { emptyPluginConfigSchema, type OpenClawPluginApi, + type ProviderAuthContext, type ProviderResolveDynamicModelContext, type ProviderRuntimeModel, } from "openclaw/plugin-sdk/core"; import { normalizeModelCompat } from "../../src/agents/model-compat.js"; +import { buildTokenProfileId, validateAnthropicSetupToken } from "../../src/commands/auth-token.js"; import { fetchClaudeUsage } from "../../src/infra/provider-usage.fetch.js"; +import type { ProviderAuthResult } from "../../src/plugins/types.js"; const PROVIDER_ID = "anthropic"; const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6"; @@ -14,6 +17,13 @@ const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6"; const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6"; const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const; +const ANTHROPIC_MODERN_MODEL_PREFIXES = [ + "claude-opus-4-6", + "claude-sonnet-4-6", + "claude-opus-4-5", + "claude-sonnet-4-5", + "claude-haiku-4-5", +] as const; function cloneFirstTemplateModel(params: { modelId: string; @@ -96,6 +106,51 @@ function resolveAnthropicForwardCompatModel( ); } +function matchesAnthropicModernModel(modelId: string): boolean { + const lower = modelId.trim().toLowerCase(); + return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix)); +} + +async function runAnthropicSetupToken(ctx: ProviderAuthContext): Promise { + await ctx.prompter.note( + ["Run `claude setup-token` in your terminal.", "Then paste the generated token below."].join( + "\n", + ), + "Anthropic setup-token", + ); + + const tokenRaw = await ctx.prompter.text({ + message: "Paste Anthropic setup-token", + validate: (value) => validateAnthropicSetupToken(String(value ?? "")), + }); + const token = String(tokenRaw ?? "").trim(); + const tokenError = validateAnthropicSetupToken(token); + if (tokenError) { + throw new Error(tokenError); + } + + const profileNameRaw = await ctx.prompter.text({ + message: "Token name (blank = default)", + placeholder: "default", + }); + + return { + profiles: [ + { + profileId: buildTokenProfileId({ + provider: PROVIDER_ID, + name: String(profileNameRaw ?? ""), + }), + credential: { + type: "token", + provider: PROVIDER_ID, + token, + }, + }, + ], + }; +} + const anthropicPlugin = { id: PROVIDER_ID, name: "Anthropic Provider", @@ -107,12 +162,29 @@ const anthropicPlugin = { label: "Anthropic", docsPath: "/providers/models", envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"], - auth: [], + auth: [ + { + id: "setup-token", + label: "setup-token (claude)", + hint: "Paste a setup-token from `claude setup-token`", + kind: "token", + run: async (ctx: ProviderAuthContext) => await runAnthropicSetupToken(ctx), + }, + ], resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx), capabilities: { providerFamily: "anthropic", dropThinkingBlockModelHints: ["claude"], }, + isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId), + resolveDefaultThinkingLevel: ({ modelId }) => + matchesAnthropicModernModel(modelId) && + (modelId.toLowerCase().startsWith(ANTHROPIC_OPUS_46_MODEL_ID) || + modelId.toLowerCase().startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID) || + modelId.toLowerCase().startsWith(ANTHROPIC_SONNET_46_MODEL_ID) || + modelId.toLowerCase().startsWith(ANTHROPIC_SONNET_46_DOT_MODEL_ID)) + ? "adaptive" + : undefined, resolveUsageAuth: async (ctx) => await ctx.resolveOAuthToken(), fetchUsageSnapshot: async (ctx) => await fetchClaudeUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn), diff --git a/extensions/github-copilot/index.ts b/extensions/github-copilot/index.ts index 038ed70aec9..41c9deed5ec 100644 --- a/extensions/github-copilot/index.ts +++ b/extensions/github-copilot/index.ts @@ -15,6 +15,7 @@ const PROVIDER_ID = "github-copilot"; const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]; const CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex"; const CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const; +const COPILOT_XHIGH_MODEL_IDS = ["gpt-5.2", "gpt-5.2-codex"] as const; function resolveFirstGithubToken(params: { agentDir?: string; env: NodeJS.ProcessEnv }): { githubToken: string; @@ -117,6 +118,8 @@ const githubCopilotPlugin = { capabilities: { dropThinkingBlockModelHints: ["claude"], }, + supportsXHighThinking: ({ modelId }) => + COPILOT_XHIGH_MODEL_IDS.includes(modelId.trim().toLowerCase() as never), prepareRuntimeAuth: async (ctx) => { const token = await resolveCopilotApiToken({ githubToken: ctx.apiKey, diff --git a/extensions/google/gemini-cli-provider.test.ts b/extensions/google/gemini-cli-provider.test.ts index 341ecd9e0b9..21e7f505521 100644 --- a/extensions/google/gemini-cli-provider.test.ts +++ b/extensions/google/gemini-cli-provider.test.ts @@ -7,8 +7,16 @@ import { } from "../../src/test-utils/provider-usage-fetch.js"; import googlePlugin from "./index.js"; +function findProvider(providers: ProviderPlugin[], id: string): ProviderPlugin { + const provider = providers.find((candidate) => candidate.id === id); + if (!provider) { + throw new Error(`provider ${id} missing`); + } + return provider; +} + function registerGooglePlugin(): { - provider: ProviderPlugin; + providers: ProviderPlugin[]; webSearchProvider: { id: string; envVars: string[]; @@ -18,13 +26,12 @@ function registerGooglePlugin(): { } { const captured = createCapturedPluginRegistration(); googlePlugin.register(captured.api); - const provider = captured.providers[0]; - if (!provider) { + if (captured.providers.length === 0) { throw new Error("provider registration missing"); } const webSearchProvider = captured.webSearchProviders[0] ?? null; return { - provider, + providers: captured.providers, webSearchProviderRegistered: webSearchProvider !== null, webSearchProvider: webSearchProvider === null @@ -38,10 +45,13 @@ function registerGooglePlugin(): { } describe("google plugin", () => { - it("registers both Gemini CLI auth and Gemini web search", () => { + it("registers Google direct, Gemini CLI auth, and Gemini web search", () => { const result = registerGooglePlugin(); - expect(result.provider.id).toBe("google-gemini-cli"); + expect(result.providers.map((provider) => provider.id)).toEqual([ + "google", + "google-gemini-cli", + ]); expect(result.webSearchProviderRegistered).toBe(true); expect(result.webSearchProvider).toMatchObject({ id: "gemini", @@ -50,8 +60,43 @@ describe("google plugin", () => { }); }); - it("owns gemini 3.1 forward-compat resolution", () => { - const { provider } = registerGooglePlugin(); + it("owns google direct gemini 3.1 forward-compat resolution", () => { + const { providers } = registerGooglePlugin(); + const provider = findProvider(providers, "google"); + const model = provider.resolveDynamicModel?.({ + provider: "google", + modelId: "gemini-3.1-pro-preview", + modelRegistry: { + find: (_provider: string, id: string) => + id === "gemini-3-pro-preview" + ? { + id, + name: id, + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_048_576, + maxTokens: 65_536, + } + : null, + } as never, + }); + + expect(model).toMatchObject({ + id: "gemini-3.1-pro-preview", + provider: "google", + api: "google-generative-ai", + baseUrl: "https://generativelanguage.googleapis.com", + reasoning: true, + }); + }); + + it("owns gemini cli 3.1 forward-compat resolution", () => { + const { providers } = registerGooglePlugin(); + const provider = findProvider(providers, "google-gemini-cli"); const model = provider.resolveDynamicModel?.({ provider: "google-gemini-cli", modelId: "gemini-3.1-pro-preview", @@ -82,7 +127,8 @@ describe("google plugin", () => { }); it("owns usage-token parsing", async () => { - const { provider } = registerGooglePlugin(); + const { providers } = registerGooglePlugin(); + const provider = findProvider(providers, "google-gemini-cli"); await expect( provider.resolveUsageAuth?.({ config: {} as never, @@ -101,7 +147,8 @@ describe("google plugin", () => { }); it("owns usage snapshot fetching", async () => { - const { provider } = registerGooglePlugin(); + const { providers } = registerGooglePlugin(); + const provider = findProvider(providers, "google-gemini-cli"); const mockFetch = createProviderUsageFetch(async (url) => { if (url.includes("cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota")) { return makeResponse(200, { diff --git a/extensions/google/gemini-cli-provider.ts b/extensions/google/gemini-cli-provider.ts index b4bb58f7d80..5a3d784a866 100644 --- a/extensions/google/gemini-cli-provider.ts +++ b/extensions/google/gemini-cli-provider.ts @@ -1,22 +1,16 @@ -import { normalizeModelCompat } from "../../src/agents/model-compat.js"; import { fetchGeminiUsage } from "../../src/infra/provider-usage.fetch.js"; import { buildOauthProviderAuthResult } from "../../src/plugin-sdk/provider-auth-result.js"; import type { OpenClawPluginApi, ProviderAuthContext, ProviderFetchUsageSnapshotContext, - ProviderResolveDynamicModelContext, - ProviderRuntimeModel, } from "../../src/plugins/types.js"; import { loginGeminiCliOAuth } from "./oauth.js"; +import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js"; const PROVIDER_ID = "google-gemini-cli"; const PROVIDER_LABEL = "Gemini CLI OAuth"; const DEFAULT_MODEL = "google-gemini-cli/gemini-3.1-pro-preview"; -const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro"; -const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash"; -const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const; -const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const; const ENV_VARS = [ "OPENCLAW_GEMINI_OAUTH_CLIENT_ID", "OPENCLAW_GEMINI_OAUTH_CLIENT_SECRET", @@ -24,30 +18,6 @@ const ENV_VARS = [ "GEMINI_CLI_OAUTH_CLIENT_SECRET", ]; -function cloneFirstTemplateModel(params: { - modelId: string; - templateIds: readonly string[]; - ctx: ProviderResolveDynamicModelContext; -}): ProviderRuntimeModel | undefined { - const trimmedModelId = params.modelId.trim(); - for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) { - const template = params.ctx.modelRegistry.find( - PROVIDER_ID, - templateId, - ) as ProviderRuntimeModel | null; - if (!template) { - continue; - } - return normalizeModelCompat({ - ...template, - id: trimmedModelId, - name: trimmedModelId, - reasoning: true, - } as ProviderRuntimeModel); - } - return undefined; -} - function parseGoogleUsageToken(apiKey: string): string { try { const parsed = JSON.parse(apiKey) as { token?: unknown }; @@ -64,28 +34,6 @@ async function fetchGeminiCliUsage(ctx: ProviderFetchUsageSnapshotContext) { return await fetchGeminiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn, PROVIDER_ID); } -function resolveGeminiCliForwardCompatModel( - ctx: ProviderResolveDynamicModelContext, -): ProviderRuntimeModel | undefined { - const trimmed = ctx.modelId.trim(); - const lower = trimmed.toLowerCase(); - - let templateIds: readonly string[]; - if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) { - templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS; - } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) { - templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS; - } else { - return undefined; - } - - return cloneFirstTemplateModel({ - modelId: trimmed, - templateIds, - ctx, - }); -} - export function registerGoogleGeminiCliProvider(api: OpenClawPluginApi) { api.registerProvider({ id: PROVIDER_ID, @@ -133,7 +81,9 @@ export function registerGoogleGeminiCliProvider(api: OpenClawPluginApi) { }, }, ], - resolveDynamicModel: (ctx) => resolveGeminiCliForwardCompatModel(ctx), + resolveDynamicModel: (ctx) => + resolveGoogle31ForwardCompatModel({ providerId: PROVIDER_ID, ctx }), + isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId), resolveUsageAuth: async (ctx) => { const auth = await ctx.resolveOAuthToken(); if (!auth) { diff --git a/extensions/google/index.ts b/extensions/google/index.ts index 806133b6419..0afa07e2ce0 100644 --- a/extensions/google/index.ts +++ b/extensions/google/index.ts @@ -6,6 +6,7 @@ import { import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js"; import type { OpenClawPluginApi } from "../../src/plugins/types.js"; import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js"; +import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js"; const googlePlugin = { id: "google", @@ -13,6 +14,16 @@ const googlePlugin = { description: "Bundled Google plugin", configSchema: emptyPluginConfigSchema(), register(api: OpenClawPluginApi) { + api.registerProvider({ + id: "google", + label: "Google AI Studio", + docsPath: "/providers/models", + envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"], + auth: [], + resolveDynamicModel: (ctx) => + resolveGoogle31ForwardCompatModel({ providerId: "google", ctx }), + isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId), + }); registerGoogleGeminiCliProvider(api); api.registerWebSearchProvider( createPluginBackedWebSearchProvider({ diff --git a/extensions/google/openclaw.plugin.json b/extensions/google/openclaw.plugin.json index 1a6d0dcd196..0d64bb18c14 100644 --- a/extensions/google/openclaw.plugin.json +++ b/extensions/google/openclaw.plugin.json @@ -1,6 +1,9 @@ { "id": "google", - "providers": ["google-gemini-cli"], + "providers": ["google", "google-gemini-cli"], + "providerAuthEnvVars": { + "google": ["GEMINI_API_KEY", "GOOGLE_API_KEY"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/google/provider-models.ts b/extensions/google/provider-models.ts new file mode 100644 index 00000000000..0a086780b1a --- /dev/null +++ b/extensions/google/provider-models.ts @@ -0,0 +1,63 @@ +import { normalizeModelCompat } from "../../src/agents/model-compat.js"; +import type { + ProviderResolveDynamicModelContext, + ProviderRuntimeModel, +} from "../../src/plugins/types.js"; + +const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro"; +const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash"; +const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const; +const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const; + +function cloneFirstTemplateModel(params: { + providerId: string; + modelId: string; + templateIds: readonly string[]; + ctx: ProviderResolveDynamicModelContext; +}): ProviderRuntimeModel | undefined { + const trimmedModelId = params.modelId.trim(); + for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) { + const template = params.ctx.modelRegistry.find( + params.providerId, + templateId, + ) as ProviderRuntimeModel | null; + if (!template) { + continue; + } + return normalizeModelCompat({ + ...template, + id: trimmedModelId, + name: trimmedModelId, + reasoning: true, + } as ProviderRuntimeModel); + } + return undefined; +} + +export function resolveGoogle31ForwardCompatModel(params: { + providerId: string; + ctx: ProviderResolveDynamicModelContext; +}): ProviderRuntimeModel | undefined { + const trimmed = params.ctx.modelId.trim(); + const lower = trimmed.toLowerCase(); + + let templateIds: readonly string[]; + if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) { + templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS; + } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) { + templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS; + } else { + return undefined; + } + + return cloneFirstTemplateModel({ + providerId: params.providerId, + modelId: trimmed, + templateIds, + ctx: params.ctx, + }); +} + +export function isModernGoogleModel(modelId: string): boolean { + return modelId.trim().toLowerCase().startsWith("gemini-3"); +} diff --git a/extensions/minimax/index.ts b/extensions/minimax/index.ts index e99f5bf15b2..0231fd86236 100644 --- a/extensions/minimax/index.ts +++ b/extensions/minimax/index.ts @@ -30,6 +30,10 @@ function modelRef(modelId: string): string { return `${PORTAL_PROVIDER_ID}/${modelId}`; } +function isModernMiniMaxModel(modelId: string): boolean { + return modelId.trim().toLowerCase().startsWith("minimax-m2.5"); +} + function buildPortalProviderCatalog(params: { baseUrl: string; apiKey: string }) { return { ...buildMinimaxPortalProvider(), @@ -167,6 +171,7 @@ const minimaxPlugin = { }); return apiKey ? { token: apiKey } : null; }, + isModernModelRef: ({ modelId }) => isModernMiniMaxModel(modelId), fetchUsageSnapshot: async (ctx) => await fetchMinimaxUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn), }); @@ -195,6 +200,7 @@ const minimaxPlugin = { run: createOAuthHandler("cn"), }, ], + isModernModelRef: ({ modelId }) => isModernMiniMaxModel(modelId), }); }, }; diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index af5f85d4d21..68058170f19 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -1,4 +1,5 @@ import type { + ProviderAuthContext, ProviderResolveDynamicModelContext, ProviderRuntimeModel, } from "openclaw/plugin-sdk/core"; @@ -8,9 +9,16 @@ import { DEFAULT_CONTEXT_TOKENS } from "../../src/agents/defaults.js"; import { normalizeModelCompat } from "../../src/agents/model-compat.js"; import { normalizeProviderId } from "../../src/agents/model-selection.js"; import { buildOpenAICodexProvider } from "../../src/agents/models-config.providers.static.js"; +import { loginOpenAICodexOAuth } from "../../src/commands/openai-codex-oauth.js"; import { fetchCodexUsage } from "../../src/infra/provider-usage.fetch.js"; +import { buildOauthProviderAuthResult } from "../../src/plugin-sdk/provider-auth-result.js"; import type { ProviderPlugin } from "../../src/plugins/types.js"; -import { cloneFirstTemplateModel, findCatalogTemplate, isOpenAIApiBaseUrl } from "./shared.js"; +import { + cloneFirstTemplateModel, + findCatalogTemplate, + isOpenAIApiBaseUrl, + matchesExactOrPrefix, +} from "./shared.js"; const PROVIDER_ID = "openai-codex"; const OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api"; @@ -23,6 +31,24 @@ const OPENAI_CODEX_GPT_53_SPARK_MODEL_ID = "gpt-5.3-codex-spark"; const OPENAI_CODEX_GPT_53_SPARK_CONTEXT_TOKENS = 128_000; const OPENAI_CODEX_GPT_53_SPARK_MAX_TOKENS = 128_000; const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const; +const OPENAI_CODEX_DEFAULT_MODEL = `${PROVIDER_ID}/${OPENAI_CODEX_GPT_54_MODEL_ID}`; +const OPENAI_CODEX_XHIGH_MODEL_IDS = [ + OPENAI_CODEX_GPT_54_MODEL_ID, + OPENAI_CODEX_GPT_53_MODEL_ID, + OPENAI_CODEX_GPT_53_SPARK_MODEL_ID, + "gpt-5.2-codex", + "gpt-5.1-codex", +] as const; +const OPENAI_CODEX_MODERN_MODEL_IDS = [ + OPENAI_CODEX_GPT_54_MODEL_ID, + "gpt-5.2", + "gpt-5.2-codex", + OPENAI_CODEX_GPT_53_MODEL_ID, + OPENAI_CODEX_GPT_53_SPARK_MODEL_ID, + "gpt-5.1-codex", + "gpt-5.1-codex-mini", + "gpt-5.1-codex-max", +] as const; function isOpenAICodexBaseUrl(baseUrl?: string): boolean { const trimmed = baseUrl?.trim(); @@ -106,12 +132,42 @@ function resolveCodexForwardCompatModel( ); } +async function runOpenAICodexOAuth(ctx: ProviderAuthContext) { + const creds = await loginOpenAICodexOAuth({ + prompter: ctx.prompter, + runtime: ctx.runtime, + isRemote: ctx.isRemote, + openUrl: ctx.openUrl, + localBrowserMessage: "Complete sign-in in browser…", + }); + if (!creds) { + throw new Error("OpenAI Codex OAuth did not return credentials."); + } + + return buildOauthProviderAuthResult({ + providerId: PROVIDER_ID, + defaultModel: OPENAI_CODEX_DEFAULT_MODEL, + access: creds.access, + refresh: creds.refresh, + expires: creds.expires, + email: typeof creds.email === "string" ? creds.email : undefined, + }); +} + export function buildOpenAICodexProviderPlugin(): ProviderPlugin { return { id: PROVIDER_ID, label: "OpenAI Codex", docsPath: "/providers/models", - auth: [], + auth: [ + { + id: "oauth", + label: "ChatGPT OAuth", + hint: "Browser sign-in", + kind: "oauth", + run: async (ctx) => await runOpenAICodexOAuth(ctx), + }, + ], catalog: { order: "profile", run: async (ctx) => { @@ -130,6 +186,9 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { capabilities: { providerFamily: "openai", }, + supportsXHighThinking: ({ modelId }) => + matchesExactOrPrefix(modelId, OPENAI_CODEX_XHIGH_MODEL_IDS), + isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_CODEX_MODERN_MODEL_IDS), prepareExtraParams: (ctx) => { const transport = ctx.extraParams?.transport; if (transport === "auto" || transport === "sse" || transport === "websocket") { diff --git a/extensions/openai/openai-provider.ts b/extensions/openai/openai-provider.ts index 9ce61e2a2b8..be406f26bbb 100644 --- a/extensions/openai/openai-provider.ts +++ b/extensions/openai/openai-provider.ts @@ -5,7 +5,12 @@ import { import { normalizeModelCompat } from "../../src/agents/model-compat.js"; import { normalizeProviderId } from "../../src/agents/model-selection.js"; import type { ProviderPlugin } from "../../src/plugins/types.js"; -import { cloneFirstTemplateModel, findCatalogTemplate, isOpenAIApiBaseUrl } from "./shared.js"; +import { + cloneFirstTemplateModel, + findCatalogTemplate, + isOpenAIApiBaseUrl, + matchesExactOrPrefix, +} from "./shared.js"; const PROVIDER_ID = "openai"; const OPENAI_GPT_54_MODEL_ID = "gpt-5.4"; @@ -14,6 +19,8 @@ const OPENAI_GPT_54_CONTEXT_TOKENS = 1_050_000; const OPENAI_GPT_54_MAX_TOKENS = 128_000; const OPENAI_GPT_54_TEMPLATE_MODEL_IDS = ["gpt-5.2"] as const; const OPENAI_GPT_54_PRO_TEMPLATE_MODEL_IDS = ["gpt-5.2-pro", "gpt-5.2"] as const; +const OPENAI_XHIGH_MODEL_IDS = ["gpt-5.4", "gpt-5.4-pro", "gpt-5.2"] as const; +const OPENAI_MODERN_MODEL_IDS = ["gpt-5.4", "gpt-5.4-pro", "gpt-5.2", "gpt-5.0"] as const; const OPENAI_DIRECT_SPARK_MODEL_ID = "gpt-5.3-codex-spark"; const SUPPRESSED_SPARK_PROVIDERS = new Set(["openai", "azure-openai-responses"]); @@ -93,6 +100,8 @@ export function buildOpenAIProvider(): ProviderPlugin { capabilities: { providerFamily: "openai", }, + supportsXHighThinking: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_XHIGH_MODEL_IDS), + isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_MODERN_MODEL_IDS), buildMissingAuthMessage: (ctx) => { if (ctx.provider !== PROVIDER_ID || ctx.listProfileIds("openai-codex").length === 0) { return undefined; diff --git a/extensions/openai/shared.ts b/extensions/openai/shared.ts index c8654be2f9b..4e4c8c2d850 100644 --- a/extensions/openai/shared.ts +++ b/extensions/openai/shared.ts @@ -6,6 +6,14 @@ import type { export const OPENAI_API_BASE_URL = "https://api.openai.com/v1"; +export function matchesExactOrPrefix(id: string, values: readonly string[]): boolean { + const normalizedId = id.trim().toLowerCase(); + return values.some((value) => { + const normalizedValue = value.trim().toLowerCase(); + return normalizedId === normalizedValue || normalizedId.startsWith(normalizedValue); + }); +} + export function isOpenAIApiBaseUrl(baseUrl?: string): boolean { const trimmed = baseUrl?.trim(); if (!trimmed) { diff --git a/extensions/opencode-go/index.ts b/extensions/opencode-go/index.ts index 3740c0190c4..87e52eab53e 100644 --- a/extensions/opencode-go/index.ts +++ b/extensions/opencode-go/index.ts @@ -19,6 +19,7 @@ const opencodeGoPlugin = { geminiThoughtSignatureSanitization: true, geminiThoughtSignatureModelHints: ["gemini"], }, + isModernModelRef: () => true, }); }, }; diff --git a/extensions/opencode/index.ts b/extensions/opencode/index.ts index 81175fc5613..c800961ab36 100644 --- a/extensions/opencode/index.ts +++ b/extensions/opencode/index.ts @@ -1,6 +1,15 @@ import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core"; const PROVIDER_ID = "opencode"; +const MINIMAX_PREFIX = "minimax-m2.5"; + +function isModernOpencodeModel(modelId: string): boolean { + const lower = modelId.trim().toLowerCase(); + if (lower.endsWith("-free") || lower === "alpha-glm-4.7") { + return false; + } + return !lower.startsWith(MINIMAX_PREFIX); +} const opencodePlugin = { id: PROVIDER_ID, @@ -19,6 +28,7 @@ const opencodePlugin = { geminiThoughtSignatureSanitization: true, geminiThoughtSignatureModelHints: ["gemini"], }, + isModernModelRef: ({ modelId }) => isModernOpencodeModel(modelId), }); }, }; diff --git a/extensions/openrouter/index.ts b/extensions/openrouter/index.ts index faa7b338cf1..92521cb3984 100644 --- a/extensions/openrouter/index.ts +++ b/extensions/openrouter/index.ts @@ -110,6 +110,7 @@ const openRouterPlugin = { geminiThoughtSignatureSanitization: true, geminiThoughtSignatureModelHints: ["gemini"], }, + isModernModelRef: () => true, wrapStreamFn: (ctx) => { let streamFn = ctx.streamFn; const providerRouting = diff --git a/extensions/zai/index.ts b/extensions/zai/index.ts index d9b81b87dda..f4fd60ad5c3 100644 --- a/extensions/zai/index.ts +++ b/extensions/zai/index.ts @@ -98,6 +98,16 @@ const zaiPlugin = { }, wrapStreamFn: (ctx) => createZaiToolStreamWrapper(ctx.streamFn, ctx.extraParams?.tool_stream !== false), + isBinaryThinking: () => true, + isModernModelRef: ({ modelId }) => { + const lower = modelId.trim().toLowerCase(); + return ( + lower.startsWith("glm-5") || + lower.startsWith("glm-4.7") || + lower.startsWith("glm-4.7-flash") || + lower.startsWith("glm-4.7-flashx") + ); + }, resolveUsageAuth: async (ctx) => { const apiKey = ctx.resolveApiKeyFromConfigAndStore({ providerIds: [PROVIDER_ID, "z-ai"], diff --git a/src/agents/live-model-filter.ts b/src/agents/live-model-filter.ts index 059e12d9711..e047d70dbde 100644 --- a/src/agents/live-model-filter.ts +++ b/src/agents/live-model-filter.ts @@ -1,3 +1,5 @@ +import { resolveProviderModernModelRef } from "../plugins/provider-runtime.js"; + export type ModelRef = { provider?: string | null; id?: string | null; @@ -41,6 +43,19 @@ export function isModernModelRef(ref: ModelRef): boolean { return false; } + const pluginDecision = resolveProviderModernModelRef({ + provider, + context: { + provider, + modelId: id, + }, + }); + if (typeof pluginDecision === "boolean") { + return pluginDecision; + } + + // Compatibility fallback for core-owned providers and tests that disable + // bundled provider runtime hooks. if (provider === "anthropic") { return matchesPrefix(id, ANTHROPIC_PREFIXES); } diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index 9bb1bf76eff..c473aadf8e6 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -1,9 +1,16 @@ import type { Api, Model } from "@mariozechner/pi-ai"; -import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const providerRuntimeMocks = vi.hoisted(() => ({ + resolveProviderModernModelRef: vi.fn(), +})); + +vi.mock("../plugins/provider-runtime.js", () => ({ + resolveProviderModernModelRef: providerRuntimeMocks.resolveProviderModernModelRef, +})); + import { isModernModelRef } from "./live-model-filter.js"; import { normalizeModelCompat } from "./model-compat.js"; -import { resolveForwardCompatModel } from "./model-forward-compat.js"; const baseModel = (): Model => ({ @@ -32,43 +39,6 @@ function supportsStrictMode(model: Model): boolean | undefined { return (model.compat as { supportsStrictMode?: boolean } | undefined)?.supportsStrictMode; } -function createTemplateModel(provider: string, id: string): Model { - return { - id, - name: id, - provider, - api: "anthropic-messages", - input: ["text"], - reasoning: true, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200_000, - maxTokens: 8_192, - } as Model; -} - -function createOpenAITemplateModel(id: string): Model { - return { - id, - name: id, - provider: "openai", - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - input: ["text", "image"], - reasoning: true, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 400_000, - maxTokens: 32_768, - } as Model; -} - -function createRegistry(models: Record>): ModelRegistry { - return { - find(provider: string, modelId: string) { - return models[`${provider}/${modelId}`] ?? null; - }, - } as ModelRegistry; -} - function expectSupportsDeveloperRoleForcedOff(overrides?: Partial>): void { const model = { ...baseModel(), ...overrides }; delete (model as { compat?: unknown }).compat; @@ -90,14 +60,10 @@ function expectSupportsStrictModeForcedOff(overrides?: Partial>): voi expect(supportsStrictMode(normalized)).toBe(false); } -function expectResolvedForwardCompat( - model: Model | undefined, - expected: { provider: string; id: string }, -): void { - expect(model?.id).toBe(expected.id); - expect(model?.name).toBe(expected.id); - expect(model?.provider).toBe(expected.provider); -} +beforeEach(() => { + providerRuntimeMocks.resolveProviderModernModelRef.mockReset(); + providerRuntimeMocks.resolveProviderModernModelRef.mockReturnValue(undefined); +}); describe("normalizeModelCompat — Anthropic baseUrl", () => { const anthropicBase = (): Model => @@ -373,6 +339,12 @@ describe("normalizeModelCompat", () => { }); describe("isModernModelRef", () => { + it("uses provider runtime hooks before fallback heuristics", () => { + providerRuntimeMocks.resolveProviderModernModelRef.mockReturnValue(false); + + expect(isModernModelRef({ provider: "openrouter", id: "claude-opus-4-6" })).toBe(false); + }); + it("includes OpenAI gpt-5.4 variants in modern selection", () => { expect(isModernModelRef({ provider: "openai", id: "gpt-5.4" })).toBe(true); expect(isModernModelRef({ provider: "openai", id: "gpt-5.4-pro" })).toBe(true); @@ -395,71 +367,3 @@ describe("isModernModelRef", () => { expect(isModernModelRef({ provider: "opencode-go", id: "minimax-m2.5" })).toBe(true); }); }); - -describe("resolveForwardCompatModel", () => { - it("resolves openai gpt-5.4 via gpt-5.2 template", () => { - const registry = createRegistry({ - "openai/gpt-5.2": createOpenAITemplateModel("gpt-5.2"), - }); - const model = resolveForwardCompatModel("openai", "gpt-5.4", registry); - expectResolvedForwardCompat(model, { provider: "openai", id: "gpt-5.4" }); - expect(model?.api).toBe("openai-responses"); - expect(model?.baseUrl).toBe("https://api.openai.com/v1"); - expect(model?.contextWindow).toBe(1_050_000); - expect(model?.maxTokens).toBe(128_000); - }); - - it("resolves openai gpt-5.4 without templates using normalized fallback defaults", () => { - const registry = createRegistry({}); - - const model = resolveForwardCompatModel("openai", "gpt-5.4", registry); - - expectResolvedForwardCompat(model, { provider: "openai", id: "gpt-5.4" }); - expect(model?.api).toBe("openai-responses"); - expect(model?.baseUrl).toBe("https://api.openai.com/v1"); - expect(model?.input).toEqual(["text", "image"]); - expect(model?.reasoning).toBe(true); - expect(model?.contextWindow).toBe(1_050_000); - expect(model?.maxTokens).toBe(128_000); - expect(model?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }); - }); - - it("resolves openai gpt-5.4-pro via template fallback", () => { - const registry = createRegistry({ - "openai/gpt-5.2": createOpenAITemplateModel("gpt-5.2"), - }); - const model = resolveForwardCompatModel("openai", "gpt-5.4-pro", registry); - expectResolvedForwardCompat(model, { provider: "openai", id: "gpt-5.4-pro" }); - expect(model?.api).toBe("openai-responses"); - expect(model?.baseUrl).toBe("https://api.openai.com/v1"); - expect(model?.contextWindow).toBe(1_050_000); - expect(model?.maxTokens).toBe(128_000); - }); - - it("resolves anthropic opus 4.6 via 4.5 template", () => { - const registry = createRegistry({ - "anthropic/claude-opus-4-5": createTemplateModel("anthropic", "claude-opus-4-5"), - }); - const model = resolveForwardCompatModel("anthropic", "claude-opus-4-6", registry); - expectResolvedForwardCompat(model, { provider: "anthropic", id: "claude-opus-4-6" }); - }); - - it("resolves anthropic sonnet 4.6 dot variant with suffix", () => { - const registry = createRegistry({ - "anthropic/claude-sonnet-4.5-20260219": createTemplateModel( - "anthropic", - "claude-sonnet-4.5-20260219", - ), - }); - const model = resolveForwardCompatModel("anthropic", "claude-sonnet-4.6-20260219", registry); - expectResolvedForwardCompat(model, { provider: "anthropic", id: "claude-sonnet-4.6-20260219" }); - }); - - it("does not resolve anthropic 4.6 fallback for other providers", () => { - const registry = createRegistry({ - "anthropic/claude-opus-4-5": createTemplateModel("anthropic", "claude-opus-4-5"), - }); - const model = resolveForwardCompatModel("openai", "claude-opus-4-6", registry); - expect(model).toBeUndefined(); - }); -}); diff --git a/src/agents/model-forward-compat.ts b/src/agents/model-forward-compat.ts deleted file mode 100644 index 5319d30423e..00000000000 --- a/src/agents/model-forward-compat.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { Api, Model } from "@mariozechner/pi-ai"; -import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; -import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js"; -import { normalizeModelCompat } from "./model-compat.js"; -import { normalizeProviderId } from "./model-selection.js"; - -const ZAI_GLM5_MODEL_ID = "glm-5"; -const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"] as const; - -// gemini-3.1-pro-preview / gemini-3.1-flash-preview are not present in some pi-ai -// Google catalogs yet. Clone the nearest gemini-3 template so users don't get -// "Unknown model" errors when Google ships new minor-version models before pi-ai -// updates its built-in registry. -const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro"; -const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash"; -const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const; -const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const; - -function cloneFirstTemplateModel(params: { - normalizedProvider: string; - trimmedModelId: string; - templateIds: string[]; - modelRegistry: ModelRegistry; - patch?: Partial>; -}): Model | undefined { - const { normalizedProvider, trimmedModelId, templateIds, modelRegistry } = params; - for (const templateId of [...new Set(templateIds)].filter(Boolean)) { - const template = modelRegistry.find(normalizedProvider, templateId) as Model | null; - if (!template) { - continue; - } - return normalizeModelCompat({ - ...template, - id: trimmedModelId, - name: trimmedModelId, - ...params.patch, - } as Model); - } - return undefined; -} - -function resolveGoogle31ForwardCompatModel( - provider: string, - modelId: string, - modelRegistry: ModelRegistry, -): Model | undefined { - const normalizedProvider = normalizeProviderId(provider); - if (normalizedProvider !== "google" && normalizedProvider !== "google-gemini-cli") { - return undefined; - } - const trimmed = modelId.trim(); - const lower = trimmed.toLowerCase(); - - let templateIds: readonly string[]; - if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) { - templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS; - } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) { - templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS; - } else { - return undefined; - } - - return cloneFirstTemplateModel({ - normalizedProvider, - trimmedModelId: trimmed, - templateIds: [...templateIds], - modelRegistry, - patch: { reasoning: true }, - }); -} - -// Z.ai's GLM-5 may not be present in pi-ai's built-in model catalog yet. -// When a user configures zai/glm-5 without a models.json entry, clone glm-4.7 as a forward-compat fallback. -function resolveZaiGlm5ForwardCompatModel( - provider: string, - modelId: string, - modelRegistry: ModelRegistry, -): Model | undefined { - if (normalizeProviderId(provider) !== "zai") { - return undefined; - } - const trimmed = modelId.trim(); - const lower = trimmed.toLowerCase(); - if (lower !== ZAI_GLM5_MODEL_ID && !lower.startsWith(`${ZAI_GLM5_MODEL_ID}-`)) { - return undefined; - } - - for (const templateId of ZAI_GLM5_TEMPLATE_MODEL_IDS) { - const template = modelRegistry.find("zai", templateId) as Model | null; - if (!template) { - continue; - } - return normalizeModelCompat({ - ...template, - id: trimmed, - name: trimmed, - reasoning: true, - } as Model); - } - - return normalizeModelCompat({ - id: trimmed, - name: trimmed, - api: "openai-completions", - provider: "zai", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: DEFAULT_CONTEXT_TOKENS, - maxTokens: DEFAULT_CONTEXT_TOKENS, - } as Model); -} - -export function resolveForwardCompatModel( - provider: string, - modelId: string, - modelRegistry: ModelRegistry, -): Model | undefined { - return ( - resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ?? - resolveGoogle31ForwardCompatModel(provider, modelId, modelRegistry) - ); -} diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index ed6356a361f..5bf97a683d0 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -13,7 +13,6 @@ import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; import { buildModelAliasLines } from "../model-alias-lines.js"; import { isSecretRefHeaderValueMarker } from "../model-auth-markers.js"; import { normalizeModelCompat } from "../model-compat.js"; -import { resolveForwardCompatModel } from "../model-forward-compat.js"; import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js"; import { buildSuppressedBuiltInModelError, @@ -34,8 +33,6 @@ type InlineProviderConfig = { headers?: unknown; }; -const PLUGIN_FIRST_DYNAMIC_PROVIDERS = new Set(["google-gemini-cli", "zai"]); - function sanitizeModelHeaders( headers: unknown, opts?: { stripSecretRefMarkers?: boolean }, @@ -232,53 +229,6 @@ function resolveExplicitModelWithRegistry(params: { }; } - if (PLUGIN_FIRST_DYNAMIC_PROVIDERS.has(normalizeProviderId(provider))) { - // Give migrated provider plugins first shot at ids that still keep a core - // forward-compat fallback for disabled-plugin/test compatibility. - const pluginDynamicModel = runProviderDynamicModel({ - provider, - config: cfg, - context: { - config: cfg, - agentDir, - provider, - modelId, - modelRegistry, - providerConfig, - }, - }); - if (pluginDynamicModel) { - return { - kind: "resolved", - model: normalizeResolvedModel({ - provider, - cfg, - agentDir, - model: pluginDynamicModel, - }), - }; - } - } - - // Forward-compat fallbacks must be checked BEFORE the generic providerCfg fallback. - // Otherwise, configured providers can default to a generic API and break specific transports. - const forwardCompat = resolveForwardCompatModel(provider, modelId, modelRegistry); - if (forwardCompat) { - return { - kind: "resolved", - model: normalizeResolvedModel({ - provider, - cfg, - agentDir, - model: applyConfiguredProviderOverrides({ - discoveredModel: forwardCompat, - providerConfig, - modelId, - }), - }), - }; - } - return undefined; } diff --git a/src/auto-reply/thinking.test.ts b/src/auto-reply/thinking.test.ts index d4814a263e9..48113b3ce72 100644 --- a/src/auto-reply/thinking.test.ts +++ b/src/auto-reply/thinking.test.ts @@ -1,4 +1,16 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const providerRuntimeMocks = vi.hoisted(() => ({ + resolveProviderBinaryThinking: vi.fn(), + resolveProviderDefaultThinkingLevel: vi.fn(), + resolveProviderXHighThinking: vi.fn(), +})); + +vi.mock("../plugins/provider-runtime.js", () => ({ + resolveProviderBinaryThinking: providerRuntimeMocks.resolveProviderBinaryThinking, + resolveProviderDefaultThinkingLevel: providerRuntimeMocks.resolveProviderDefaultThinkingLevel, + resolveProviderXHighThinking: providerRuntimeMocks.resolveProviderXHighThinking, +})); import { listThinkingLevelLabels, listThinkingLevels, @@ -7,6 +19,15 @@ import { resolveThinkingDefaultForModel, } from "./thinking.js"; +beforeEach(() => { + providerRuntimeMocks.resolveProviderBinaryThinking.mockReset(); + providerRuntimeMocks.resolveProviderBinaryThinking.mockReturnValue(undefined); + providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReset(); + providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined); + providerRuntimeMocks.resolveProviderXHighThinking.mockReset(); + providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(undefined); +}); + describe("normalizeThinkLevel", () => { it("accepts mid as medium", () => { expect(normalizeThinkLevel("mid")).toBe("medium"); @@ -43,6 +64,12 @@ describe("normalizeThinkLevel", () => { }); describe("listThinkingLevels", () => { + it("uses provider runtime hooks for xhigh support", () => { + providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(true); + + expect(listThinkingLevels("demo", "demo-model")).toContain("xhigh"); + }); + it("includes xhigh for codex models", () => { expect(listThinkingLevels(undefined, "gpt-5.2-codex")).toContain("xhigh"); expect(listThinkingLevels(undefined, "gpt-5.3-codex")).toContain("xhigh"); @@ -75,6 +102,12 @@ describe("listThinkingLevels", () => { }); describe("listThinkingLevelLabels", () => { + it("uses provider runtime hooks for binary thinking providers", () => { + providerRuntimeMocks.resolveProviderBinaryThinking.mockReturnValue(true); + + expect(listThinkingLevelLabels("demo", "demo-model")).toEqual(["off", "on"]); + }); + it("returns on/off for ZAI", () => { expect(listThinkingLevelLabels("zai", "glm-4.7")).toEqual(["off", "on"]); }); @@ -86,6 +119,14 @@ describe("listThinkingLevelLabels", () => { }); describe("resolveThinkingDefaultForModel", () => { + it("uses provider runtime hooks for default thinking levels", () => { + providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReturnValue("adaptive"); + + expect(resolveThinkingDefaultForModel({ provider: "demo", model: "demo-model" })).toBe( + "adaptive", + ); + }); + it("defaults Claude 4.6 models to adaptive", () => { expect( resolveThinkingDefaultForModel({ provider: "anthropic", model: "claude-opus-4-6" }), diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 639db68eafb..9c03086ab91 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -1,3 +1,9 @@ +import { + resolveProviderBinaryThinking, + resolveProviderDefaultThinkingLevel, + resolveProviderXHighThinking, +} from "../plugins/provider-runtime.js"; + export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; export type VerboseLevel = "off" | "on" | "full"; export type NoticeLevel = "off" | "on" | "full"; @@ -27,8 +33,24 @@ function normalizeProviderId(provider?: string | null): string { return normalized; } -export function isBinaryThinkingProvider(provider?: string | null): boolean { - return normalizeProviderId(provider) === "zai"; +export function isBinaryThinkingProvider(provider?: string | null, model?: string | null): boolean { + const normalizedProvider = normalizeProviderId(provider); + if (!normalizedProvider) { + return false; + } + + const pluginDecision = resolveProviderBinaryThinking({ + provider: normalizedProvider, + context: { + provider: normalizedProvider, + modelId: model?.trim() ?? "", + }, + }); + if (typeof pluginDecision === "boolean") { + return pluginDecision; + } + + return normalizedProvider === "zai"; } export const XHIGH_MODEL_REFS = [ @@ -95,7 +117,19 @@ export function supportsXHighThinking(provider?: string | null, model?: string | if (!modelKey) { return false; } - const providerKey = provider?.trim().toLowerCase(); + const providerKey = normalizeProviderId(provider); + if (providerKey) { + const pluginDecision = resolveProviderXHighThinking({ + provider: providerKey, + context: { + provider: providerKey, + modelId: modelKey, + }, + }); + if (typeof pluginDecision === "boolean") { + return pluginDecision; + } + } if (providerKey) { return XHIGH_MODEL_SET.has(`${providerKey}/${modelKey}`); } @@ -112,7 +146,7 @@ export function listThinkingLevels(provider?: string | null, model?: string | nu } export function listThinkingLevelLabels(provider?: string | null, model?: string | null): string[] { - if (isBinaryThinkingProvider(provider)) { + if (isBinaryThinkingProvider(provider, model)) { return ["off", "on"]; } return listThinkingLevels(provider, model); @@ -147,6 +181,21 @@ export function resolveThinkingDefaultForModel(params: { }): ThinkLevel { const normalizedProvider = normalizeProviderId(params.provider); const modelLower = params.model.trim().toLowerCase(); + const candidate = params.catalog?.find( + (entry) => entry.provider === params.provider && entry.id === params.model, + ); + const pluginDecision = resolveProviderDefaultThinkingLevel({ + provider: normalizedProvider, + context: { + provider: normalizedProvider, + modelId: params.model, + reasoning: candidate?.reasoning, + }, + }); + if (pluginDecision) { + return pluginDecision; + } + const isAnthropicFamilyModel = normalizedProvider === "anthropic" || normalizedProvider === "amazon-bedrock" || @@ -155,9 +204,6 @@ export function resolveThinkingDefaultForModel(params: { if (isAnthropicFamilyModel && CLAUDE_46_MODEL_RE.test(modelLower)) { return "adaptive"; } - const candidate = params.catalog?.find( - (entry) => entry.provider === params.provider && entry.id === params.model, - ); if (candidate?.reasoning) { return "low"; } diff --git a/src/commands/models/auth.test.ts b/src/commands/models/auth.test.ts index bf8195b5284..6bb052ba3d6 100644 --- a/src/commands/models/auth.test.ts +++ b/src/commands/models/auth.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; +import type { ProviderPlugin } from "../../plugins/types.js"; import type { RuntimeEnv } from "../../runtime.js"; const mocks = vi.hoisted(() => ({ @@ -15,8 +16,6 @@ const mocks = vi.hoisted(() => ({ upsertAuthProfile: vi.fn(), resolvePluginProviders: vi.fn(), createClackPrompter: vi.fn(), - loginOpenAICodexOAuth: vi.fn(), - writeOAuthCredentials: vi.fn(), loadValidConfigOrThrow: vi.fn(), updateConfig: vi.fn(), logConfigUpdated: vi.fn(), @@ -59,18 +58,6 @@ vi.mock("../../wizard/clack-prompter.js", () => ({ createClackPrompter: mocks.createClackPrompter, })); -vi.mock("../openai-codex-oauth.js", () => ({ - loginOpenAICodexOAuth: mocks.loginOpenAICodexOAuth, -})); - -vi.mock("../onboard-auth.js", async (importActual) => { - const actual = await importActual(); - return { - ...actual, - writeOAuthCredentials: mocks.writeOAuthCredentials, - }; -}); - vi.mock("./shared.js", async (importActual) => { const actual = await importActual(); return { @@ -88,7 +75,8 @@ vi.mock("../onboard-helpers.js", () => ({ openUrl: mocks.openUrl, })); -const { modelsAuthLoginCommand, modelsAuthPasteTokenCommand } = await import("./auth.js"); +const { modelsAuthLoginCommand, modelsAuthPasteTokenCommand, modelsAuthSetupTokenCommand } = + await import("./auth.js"); function createRuntime(): RuntimeEnv { return { @@ -116,10 +104,30 @@ function withInteractiveStdin() { }; } +function createProvider(params: { + id: string; + label?: string; + run: NonNullable[number]["run"]; +}): ProviderPlugin { + return { + id: params.id, + label: params.label ?? params.id, + auth: [ + { + id: "oauth", + label: "OAuth", + kind: "oauth", + run: params.run, + }, + ], + }; +} + describe("modelsAuthLoginCommand", () => { let restoreStdin: (() => void) | null = null; let currentConfig: OpenClawConfig; let lastUpdatedConfig: OpenClawConfig | null; + let runProviderAuth: ReturnType; beforeEach(() => { vi.clearAllMocks(); @@ -151,16 +159,29 @@ describe("modelsAuthLoginCommand", () => { note: vi.fn(async () => {}), select: vi.fn(), }); - mocks.loginOpenAICodexOAuth.mockResolvedValue({ - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - email: "user@example.com", + runProviderAuth = vi.fn().mockResolvedValue({ + profiles: [ + { + profileId: "openai-codex:user@example.com", + credential: { + type: "oauth", + provider: "openai-codex", + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + email: "user@example.com", + }, + }, + ], + defaultModel: "openai-codex/gpt-5.4", }); - mocks.writeOAuthCredentials.mockResolvedValue("openai-codex:user@example.com"); - mocks.resolvePluginProviders.mockReturnValue([]); + mocks.resolvePluginProviders.mockReturnValue([ + createProvider({ + id: "openai-codex", + label: "OpenAI Codex", + run: runProviderAuth as ProviderPlugin["auth"][number]["run"], + }), + ]); mocks.loadAuthProfileStoreForRuntime.mockReturnValue({ profiles: {}, usageStats: {} }); mocks.listProfilesForProvider.mockReturnValue([]); mocks.clearAuthProfileCooldown.mockResolvedValue(undefined); @@ -171,19 +192,20 @@ describe("modelsAuthLoginCommand", () => { restoreStdin = null; }); - it("supports built-in openai-codex login without provider plugins", async () => { + it("runs plugin-owned openai-codex login", async () => { const runtime = createRuntime(); await modelsAuthLoginCommand({ provider: "openai-codex" }, runtime); - expect(mocks.loginOpenAICodexOAuth).toHaveBeenCalledOnce(); - expect(mocks.writeOAuthCredentials).toHaveBeenCalledWith( - "openai-codex", - expect.any(Object), - "/tmp/openclaw/agents/main", - { syncSiblingAgents: true }, - ); - expect(mocks.resolvePluginProviders).not.toHaveBeenCalled(); + expect(runProviderAuth).toHaveBeenCalledOnce(); + expect(mocks.upsertAuthProfile).toHaveBeenCalledWith({ + profileId: "openai-codex:user@example.com", + credential: expect.objectContaining({ + type: "oauth", + provider: "openai-codex", + }), + agentDir: "/tmp/openclaw/agents/main", + }); expect(lastUpdatedConfig?.auth?.profiles?.["openai-codex:user@example.com"]).toMatchObject({ provider: "openai-codex", mode: "oauth", @@ -236,7 +258,7 @@ describe("modelsAuthLoginCommand", () => { }); // Verify clearing happens before login attempt const clearOrder = mocks.clearAuthProfileCooldown.mock.invocationCallOrder[0]; - const loginOrder = mocks.loginOpenAICodexOAuth.mock.invocationCallOrder[0]; + const loginOrder = runProviderAuth.mock.invocationCallOrder[0]; expect(clearOrder).toBeLessThan(loginOrder); }); @@ -248,7 +270,7 @@ describe("modelsAuthLoginCommand", () => { await modelsAuthLoginCommand({ provider: "openai-codex" }, runtime); - expect(mocks.loginOpenAICodexOAuth).toHaveBeenCalledOnce(); + expect(runProviderAuth).toHaveBeenCalledOnce(); }); it("loads lockout state from the agent-scoped store", async () => { @@ -261,11 +283,11 @@ describe("modelsAuthLoginCommand", () => { expect(mocks.loadAuthProfileStoreForRuntime).toHaveBeenCalledWith("/tmp/openclaw/agents/main"); }); - it("keeps existing plugin error behavior for non built-in providers", async () => { + it("reports loaded plugin providers when requested provider is unavailable", async () => { const runtime = createRuntime(); await expect(modelsAuthLoginCommand({ provider: "anthropic" }, runtime)).rejects.toThrow( - "No provider plugins found.", + 'Unknown provider "anthropic". Loaded providers: openai-codex. Verify plugins via `openclaw plugins list --json`.', ); }); @@ -292,4 +314,47 @@ describe("modelsAuthLoginCommand", () => { exitSpy.mockRestore(); } }); + + it("runs token auth for any token-capable provider plugin", async () => { + const runtime = createRuntime(); + const runTokenAuth = vi.fn().mockResolvedValue({ + profiles: [ + { + profileId: "moonshot:token", + credential: { + type: "token", + provider: "moonshot", + token: "moonshot-token", + }, + }, + ], + }); + mocks.resolvePluginProviders.mockReturnValue([ + { + id: "moonshot", + label: "Moonshot", + auth: [ + { + id: "setup-token", + label: "setup-token", + kind: "token", + run: runTokenAuth, + }, + ], + }, + ]); + + await modelsAuthSetupTokenCommand({ provider: "moonshot", yes: true }, runtime); + + expect(runTokenAuth).toHaveBeenCalledOnce(); + expect(mocks.upsertAuthProfile).toHaveBeenCalledWith({ + profileId: "moonshot:token", + credential: { + type: "token", + provider: "moonshot", + token: "moonshot-token", + }, + agentDir: "/tmp/openclaw/agents/main", + }); + }); }); diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index c9b54b2f753..46ad67c41ef 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -21,22 +21,21 @@ import { normalizeProviderId } from "../../agents/model-selection.js"; import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; import { formatCliCommand } from "../../cli/command-format.js"; import { parseDurationMs } from "../../cli/parse-duration.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { logConfigUpdated } from "../../config/logging.js"; import { resolvePluginProviders } from "../../plugins/providers.js"; -import type { ProviderAuthResult, ProviderPlugin } from "../../plugins/types.js"; +import type { + ProviderAuthMethod, + ProviderAuthResult, + ProviderPlugin, +} from "../../plugins/types.js"; import type { RuntimeEnv } from "../../runtime.js"; import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js"; import { createClackPrompter } from "../../wizard/clack-prompter.js"; -import { validateAnthropicSetupToken } from "../auth-token.js"; import { isRemoteEnvironment } from "../oauth-env.js"; import { createVpsAwareOAuthHandlers } from "../oauth-flow.js"; -import { applyAuthProfileConfig, writeOAuthCredentials } from "../onboard-auth.js"; +import { applyAuthProfileConfig } from "../onboard-auth.js"; import { openUrl } from "../onboard-helpers.js"; -import { - applyOpenAICodexModelDefault, - OPENAI_CODEX_DEFAULT_MODEL, -} from "../openai-codex-model-default.js"; -import { loginOpenAICodexOAuth } from "../openai-codex-oauth.js"; import { applyDefaultModel, mergeConfigPatch, @@ -78,40 +77,250 @@ const select = async (params: Parameters>[0]) => }), ); -type TokenProvider = "anthropic"; - -function resolveTokenProvider(raw?: string): TokenProvider | "custom" | null { - const trimmed = raw?.trim(); - if (!trimmed) { - return null; - } - const normalized = normalizeProviderId(trimmed); - if (normalized === "anthropic") { - return "anthropic"; - } - return "custom"; -} - function resolveDefaultTokenProfileId(provider: string): string { return `${normalizeProviderId(provider)}:manual`; } +type ResolvedModelsAuthContext = { + config: OpenClawConfig; + agentDir: string; + workspaceDir: string; + providers: ProviderPlugin[]; +}; + +function listProvidersWithAuthMethods(providers: ProviderPlugin[]): ProviderPlugin[] { + return providers.filter((provider) => provider.auth.length > 0); +} + +function listTokenAuthMethods(provider: ProviderPlugin): ProviderAuthMethod[] { + return provider.auth.filter((method) => method.kind === "token"); +} + +function listProvidersWithTokenMethods(providers: ProviderPlugin[]): ProviderPlugin[] { + return providers.filter((provider) => listTokenAuthMethods(provider).length > 0); +} + +async function resolveModelsAuthContext(): Promise { + const config = await loadValidConfigOrThrow(); + const defaultAgentId = resolveDefaultAgentId(config); + const agentDir = resolveAgentDir(config, defaultAgentId); + const workspaceDir = + resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir(); + const providers = resolvePluginProviders({ config, workspaceDir }); + return { config, agentDir, workspaceDir, providers }; +} + +function resolveRequestedProviderOrThrow( + providers: ProviderPlugin[], + rawProvider?: string, +): ProviderPlugin | null { + const requested = rawProvider?.trim(); + if (!requested) { + return null; + } + const matched = resolveProviderMatch(providers, requested); + if (matched) { + return matched; + } + const available = providers + .map((provider) => provider.id) + .filter(Boolean) + .toSorted((a, b) => a.localeCompare(b)); + const availableText = available.length > 0 ? available.join(", ") : "(none)"; + throw new Error( + `Unknown provider "${requested}". Loaded providers: ${availableText}. Verify plugins via \`${formatCliCommand("openclaw plugins list --json")}\`.`, + ); +} + +function resolveTokenMethodOrThrow( + provider: ProviderPlugin, + rawMethod?: string, +): ProviderAuthMethod | null { + const tokenMethods = listTokenAuthMethods(provider); + if (rawMethod?.trim()) { + const matched = pickAuthMethod(provider, rawMethod); + if (matched && matched.kind === "token") { + return matched; + } + const available = tokenMethods.map((method) => method.id).join(", ") || "(none)"; + throw new Error( + `Unknown token auth method "${rawMethod}" for provider "${provider.id}". Available token methods: ${available}.`, + ); + } + return null; +} + +async function pickProviderAuthMethod(params: { + provider: ProviderPlugin; + requestedMethod?: string; + prompter: ReturnType; +}) { + const requestedMethod = pickAuthMethod(params.provider, params.requestedMethod); + if (requestedMethod) { + return requestedMethod; + } + if (params.provider.auth.length === 1) { + return params.provider.auth[0] ?? null; + } + return await params.prompter + .select({ + message: `Auth method for ${params.provider.label}`, + options: params.provider.auth.map((method) => ({ + value: method.id, + label: method.label, + hint: method.hint, + })), + }) + .then((id) => params.provider.auth.find((method) => method.id === String(id)) ?? null); +} + +async function pickProviderTokenMethod(params: { + provider: ProviderPlugin; + requestedMethod?: string; + prompter: ReturnType; +}) { + const explicitTokenMethod = resolveTokenMethodOrThrow(params.provider, params.requestedMethod); + if (explicitTokenMethod) { + return explicitTokenMethod; + } + const tokenMethods = listTokenAuthMethods(params.provider); + if (tokenMethods.length === 0) { + return null; + } + const setupTokenMethod = tokenMethods.find((method) => method.id === "setup-token"); + if (setupTokenMethod) { + return setupTokenMethod; + } + if (tokenMethods.length === 1) { + return tokenMethods[0] ?? null; + } + return await params.prompter + .select({ + message: `Token method for ${params.provider.label}`, + options: tokenMethods.map((method) => ({ + value: method.id, + label: method.label, + hint: method.hint, + })), + }) + .then((id) => tokenMethods.find((method) => method.id === String(id)) ?? null); +} + +async function persistProviderAuthResult(params: { + result: ProviderAuthResult; + agentDir: string; + runtime: RuntimeEnv; + prompter: ReturnType; + setDefault?: boolean; +}) { + for (const profile of params.result.profiles) { + upsertAuthProfile({ + profileId: profile.profileId, + credential: profile.credential, + agentDir: params.agentDir, + }); + } + + await updateConfig((cfg) => { + let next = cfg; + if (params.result.configPatch) { + next = mergeConfigPatch(next, params.result.configPatch); + } + for (const profile of params.result.profiles) { + next = applyAuthProfileConfig(next, { + profileId: profile.profileId, + provider: profile.credential.provider, + mode: credentialMode(profile.credential), + }); + } + if (params.setDefault && params.result.defaultModel) { + next = applyDefaultModel(next, params.result.defaultModel); + } + return next; + }); + + logConfigUpdated(params.runtime); + for (const profile of params.result.profiles) { + params.runtime.log( + `Auth profile: ${profile.profileId} (${profile.credential.provider}/${credentialMode(profile.credential)})`, + ); + } + if (params.result.defaultModel) { + params.runtime.log( + params.setDefault + ? `Default model set to ${params.result.defaultModel}` + : `Default model available: ${params.result.defaultModel} (use --set-default to apply)`, + ); + } + if (params.result.notes && params.result.notes.length > 0) { + await params.prompter.note(params.result.notes.join("\n"), "Provider notes"); + } +} + +async function runProviderAuthMethod(params: { + config: OpenClawConfig; + agentDir: string; + workspaceDir: string; + provider: ProviderPlugin; + method: ProviderAuthMethod; + runtime: RuntimeEnv; + prompter: ReturnType; + setDefault?: boolean; +}) { + await clearStaleProfileLockouts(params.provider.id, params.agentDir); + + const result = await params.method.run({ + config: params.config, + agentDir: params.agentDir, + workspaceDir: params.workspaceDir, + prompter: params.prompter, + runtime: params.runtime, + isRemote: isRemoteEnvironment(), + openUrl: async (url) => { + await openUrl(url); + }, + oauth: { + createVpsAwareHandlers: (runtimeParams) => createVpsAwareOAuthHandlers(runtimeParams), + }, + }); + + await persistProviderAuthResult({ + result, + agentDir: params.agentDir, + runtime: params.runtime, + prompter: params.prompter, + setDefault: params.setDefault, + }); +} + export async function modelsAuthSetupTokenCommand( opts: { provider?: string; yes?: boolean }, runtime: RuntimeEnv, ) { - const provider = resolveTokenProvider(opts.provider ?? "anthropic"); - if (provider !== "anthropic") { - throw new Error("Only --provider anthropic is supported for setup-token."); - } - if (!process.stdin.isTTY) { throw new Error("setup-token requires an interactive TTY."); } + const { config, agentDir, workspaceDir, providers } = await resolveModelsAuthContext(); + const tokenProviders = listProvidersWithTokenMethods(providers); + if (tokenProviders.length === 0) { + throw new Error( + `No provider token-auth plugins found. Install one via \`${formatCliCommand("openclaw plugins install")}\`.`, + ); + } + + const provider = + resolveRequestedProviderOrThrow(tokenProviders, opts.provider ?? "anthropic") ?? + tokenProviders.find((candidate) => normalizeProviderId(candidate.id) === "anthropic") ?? + tokenProviders[0] ?? + null; + if (!provider) { + throw new Error("No token-capable provider is available."); + } + if (!opts.yes) { const proceed = await confirm({ - message: "Have you run `claude setup-token` and copied the token?", + message: `Continue with ${provider.label} token auth?`, initialValue: true, }); if (!proceed) { @@ -119,32 +328,21 @@ export async function modelsAuthSetupTokenCommand( } } - const tokenInput = await text({ - message: "Paste Anthropic setup-token", - validate: (value) => validateAnthropicSetupToken(String(value ?? "")), + const prompter = createClackPrompter(); + const method = await pickProviderTokenMethod({ provider, prompter }); + if (!method) { + throw new Error(`Provider "${provider.id}" does not expose a token auth method.`); + } + + await runProviderAuthMethod({ + config, + agentDir, + workspaceDir, + provider, + method, + runtime, + prompter, }); - const token = String(tokenInput ?? "").trim(); - const profileId = resolveDefaultTokenProfileId(provider); - - upsertAuthProfile({ - profileId, - credential: { - type: "token", - provider, - token, - }, - }); - - await updateConfig((cfg) => - applyAuthProfileConfig(cfg, { - profileId, - provider, - mode: "token", - }), - ); - - logConfigUpdated(runtime); - runtime.log(`Auth profile: ${profileId} (${provider}/token)`); } export async function modelsAuthPasteTokenCommand( @@ -190,10 +388,17 @@ export async function modelsAuthPasteTokenCommand( } export async function modelsAuthAddCommand(_opts: Record, runtime: RuntimeEnv) { + const { config, agentDir, workspaceDir, providers } = await resolveModelsAuthContext(); + const tokenProviders = listProvidersWithTokenMethods(providers); + const provider = await select({ message: "Token provider", options: [ - { value: "anthropic", label: "anthropic" }, + ...tokenProviders.map((providerPlugin) => ({ + value: providerPlugin.id, + label: providerPlugin.id, + hint: providerPlugin.docsPath ? `Docs: ${providerPlugin.docsPath}` : undefined, + })), { value: "custom", label: "custom (type provider id)" }, ], }); @@ -210,25 +415,41 @@ export async function modelsAuthAddCommand(_opts: Record, runtime ) : provider; - const method = (await select({ - message: "Token method", - options: [ - ...(providerId === "anthropic" - ? [ - { - value: "setup-token", - label: "setup-token (claude)", - hint: "Paste a setup-token from `claude setup-token`", - }, - ] - : []), - { value: "paste", label: "paste token" }, - ], - })) as "setup-token" | "paste"; - - if (method === "setup-token") { - await modelsAuthSetupTokenCommand({ provider: providerId }, runtime); - return; + const providerPlugin = + provider === "custom" ? null : resolveRequestedProviderOrThrow(tokenProviders, providerId); + if (providerPlugin) { + const tokenMethods = listTokenAuthMethods(providerPlugin); + const methodId = + tokenMethods.length > 0 + ? await select({ + message: "Token method", + options: [ + ...tokenMethods.map((method) => ({ + value: method.id, + label: method.label, + hint: method.hint, + })), + { value: "paste", label: "paste token" }, + ], + }) + : "paste"; + if (methodId !== "paste") { + const prompter = createClackPrompter(); + const method = tokenMethods.find((candidate) => candidate.id === methodId); + if (!method) { + throw new Error(`Unknown token auth method "${String(methodId)}".`); + } + await runProviderAuthMethod({ + config, + agentDir, + workspaceDir, + provider: providerPlugin, + method, + runtime, + prompter, + }); + return; + } } const profileIdDefault = resolveDefaultTokenProfileId(providerId); @@ -292,22 +513,7 @@ export function resolveRequestedLoginProviderOrThrow( providers: ProviderPlugin[], rawProvider?: string, ): ProviderPlugin | null { - const requested = rawProvider?.trim(); - if (!requested) { - return null; - } - const matched = resolveProviderMatch(providers, requested); - if (matched) { - return matched; - } - const available = providers - .map((provider) => provider.id) - .filter(Boolean) - .toSorted((a, b) => a.localeCompare(b)); - const availableText = available.length > 0 ? available.join(", ") : "(none)"; - throw new Error( - `Unknown provider "${requested}". Loaded providers: ${availableText}. Verify plugins via \`${formatCliCommand("openclaw plugins list --json")}\`.`, - ); + return resolveRequestedProviderOrThrow(providers, rawProvider); } function credentialMode(credential: AuthProfileCredential): "api_key" | "oauth" | "token" { @@ -320,177 +526,55 @@ function credentialMode(credential: AuthProfileCredential): "api_key" | "oauth" return "oauth"; } -async function runBuiltInOpenAICodexLogin(params: { - opts: LoginOptions; - runtime: RuntimeEnv; - prompter: ReturnType; - agentDir: string; -}) { - const creds = await loginOpenAICodexOAuth({ - prompter: params.prompter, - runtime: params.runtime, - isRemote: isRemoteEnvironment(), - openUrl: async (url) => { - await openUrl(url); - }, - localBrowserMessage: "Complete sign-in in browser…", - }); - if (!creds) { - throw new Error("OpenAI Codex OAuth did not return credentials."); - } - - const profileId = await writeOAuthCredentials("openai-codex", creds, params.agentDir, { - syncSiblingAgents: true, - }); - await updateConfig((cfg) => { - let next = applyAuthProfileConfig(cfg, { - profileId, - provider: "openai-codex", - mode: "oauth", - }); - if (params.opts.setDefault) { - next = applyOpenAICodexModelDefault(next).next; - } - return next; - }); - - logConfigUpdated(params.runtime); - params.runtime.log(`Auth profile: ${profileId} (openai-codex/oauth)`); - if (params.opts.setDefault) { - params.runtime.log(`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`); - } else { - params.runtime.log( - `Default model available: ${OPENAI_CODEX_DEFAULT_MODEL} (use --set-default to apply)`, - ); - } -} - export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: RuntimeEnv) { if (!process.stdin.isTTY) { throw new Error("models auth login requires an interactive TTY."); } - const config = await loadValidConfigOrThrow(); - const defaultAgentId = resolveDefaultAgentId(config); - const agentDir = resolveAgentDir(config, defaultAgentId); - const workspaceDir = - resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir(); - const requestedProviderId = normalizeProviderId(String(opts.provider ?? "")); + const { config, agentDir, workspaceDir, providers } = await resolveModelsAuthContext(); const prompter = createClackPrompter(); - - if (requestedProviderId === "openai-codex") { - await clearStaleProfileLockouts("openai-codex", agentDir); - await runBuiltInOpenAICodexLogin({ - opts, - runtime, - prompter, - agentDir, - }); - return; - } - - const providers = resolvePluginProviders({ config, workspaceDir }); - if (providers.length === 0) { + const authProviders = listProvidersWithAuthMethods(providers); + if (authProviders.length === 0) { throw new Error( `No provider plugins found. Install one via \`${formatCliCommand("openclaw plugins install")}\`.`, ); } - const requestedProvider = resolveRequestedLoginProviderOrThrow(providers, opts.provider); + const requestedProvider = resolveRequestedLoginProviderOrThrow(authProviders, opts.provider); const selectedProvider = requestedProvider ?? (await prompter .select({ message: "Select a provider", - options: providers.map((provider) => ({ + options: authProviders.map((provider) => ({ value: provider.id, label: provider.label, hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined, })), }) - .then((id) => resolveProviderMatch(providers, String(id)))); + .then((id) => resolveProviderMatch(authProviders, String(id)))); if (!selectedProvider) { throw new Error("Unknown provider. Use --provider to pick a provider plugin."); } - - await clearStaleProfileLockouts(selectedProvider.id, agentDir); - - const chosenMethod = - pickAuthMethod(selectedProvider, opts.method) ?? - (selectedProvider.auth.length === 1 - ? selectedProvider.auth[0] - : await prompter - .select({ - message: `Auth method for ${selectedProvider.label}`, - options: selectedProvider.auth.map((method) => ({ - value: method.id, - label: method.label, - hint: method.hint, - })), - }) - .then((id) => selectedProvider.auth.find((method) => method.id === String(id)))); + const chosenMethod = await pickProviderAuthMethod({ + provider: selectedProvider, + requestedMethod: opts.method, + prompter, + }); if (!chosenMethod) { throw new Error("Unknown auth method. Use --method to select one."); } - const isRemote = isRemoteEnvironment(); - const result: ProviderAuthResult = await chosenMethod.run({ + await runProviderAuthMethod({ config, agentDir, workspaceDir, - prompter, + provider: selectedProvider, + method: chosenMethod, runtime, - isRemote, - openUrl: async (url) => { - await openUrl(url); - }, - oauth: { - createVpsAwareHandlers: (params) => createVpsAwareOAuthHandlers(params), - }, + prompter, + setDefault: opts.setDefault, }); - - for (const profile of result.profiles) { - upsertAuthProfile({ - profileId: profile.profileId, - credential: profile.credential, - agentDir, - }); - } - - await updateConfig((cfg) => { - let next = cfg; - if (result.configPatch) { - next = mergeConfigPatch(next, result.configPatch); - } - for (const profile of result.profiles) { - next = applyAuthProfileConfig(next, { - profileId: profile.profileId, - provider: profile.credential.provider, - mode: credentialMode(profile.credential), - }); - } - if (opts.setDefault && result.defaultModel) { - next = applyDefaultModel(next, result.defaultModel); - } - return next; - }); - - logConfigUpdated(runtime); - for (const profile of result.profiles) { - runtime.log( - `Auth profile: ${profile.profileId} (${profile.credential.provider}/${credentialMode(profile.credential)})`, - ); - } - if (result.defaultModel) { - runtime.log( - opts.setDefault - ? `Default model set to ${result.defaultModel}` - : `Default model available: ${result.defaultModel} (use --set-default to apply)`, - ); - } - if (result.notes && result.notes.length > 0) { - await prompter.note(result.notes.join("\n"), "Provider notes"); - } } diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index a792af23816..f3a6d1ca16b 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -10,7 +10,9 @@ export type { ProviderBuiltInModelSuppressionResult, ProviderBuildMissingAuthMessageContext, ProviderCacheTtlEligibilityContext, + ProviderDefaultThinkingPolicyContext, ProviderFetchUsageSnapshotContext, + ProviderModernModelPolicyContext, ProviderPreparedRuntimeAuth, ProviderResolvedUsageAuth, ProviderPrepareExtraParamsContext, @@ -20,6 +22,7 @@ export type { ProviderResolveDynamicModelContext, ProviderNormalizeResolvedModelContext, ProviderRuntimeModel, + ProviderThinkingPolicyContext, ProviderWrapStreamFnContext, OpenClawPluginService, ProviderAuthContext, diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index ba5583d2c4a..6ad093eec91 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -114,7 +114,9 @@ export type { ProviderBuiltInModelSuppressionResult, ProviderBuildMissingAuthMessageContext, ProviderCacheTtlEligibilityContext, + ProviderDefaultThinkingPolicyContext, ProviderFetchUsageSnapshotContext, + ProviderModernModelPolicyContext, ProviderPreparedRuntimeAuth, ProviderResolvedUsageAuth, ProviderPrepareExtraParamsContext, @@ -124,6 +126,7 @@ export type { ProviderResolveDynamicModelContext, ProviderNormalizeResolvedModelContext, ProviderRuntimeModel, + ProviderThinkingPolicyContext, ProviderWrapStreamFnContext, } from "../plugins/types.js"; export type { diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index e38d6553080..23234be8109 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -17,10 +17,14 @@ import { buildProviderMissingAuthMessageWithPlugin, prepareProviderExtraParams, resolveProviderCacheTtlEligibility, + resolveProviderBinaryThinking, resolveProviderBuiltInModelSuppression, + resolveProviderDefaultThinkingLevel, + resolveProviderModernModelRef, resolveProviderUsageSnapshotWithPlugin, resolveProviderCapabilitiesWithPlugin, resolveProviderUsageAuthWithPlugin, + resolveProviderXHighThinking, normalizeProviderResolvedModelWithPlugin, prepareProviderDynamicModel, prepareProviderRuntimeAuth, @@ -143,6 +147,10 @@ describe("provider-runtime", () => { resolveUsageAuth, fetchUsageSnapshot, isCacheTtlEligible: ({ modelId }) => modelId.startsWith("anthropic/"), + isBinaryThinking: () => true, + supportsXHighThinking: ({ modelId }) => modelId === "gpt-5.4", + resolveDefaultThinkingLevel: ({ reasoning }) => (reasoning ? "low" : "off"), + isModernModelRef: ({ modelId }) => modelId.startsWith("gpt-5"), }, ]; }); @@ -278,6 +286,47 @@ describe("provider-runtime", () => { }), ).toBe(true); + expect( + resolveProviderBinaryThinking({ + provider: "demo", + context: { + provider: "demo", + modelId: "glm-5", + }, + }), + ).toBe(true); + + expect( + resolveProviderXHighThinking({ + provider: "demo", + context: { + provider: "demo", + modelId: "gpt-5.4", + }, + }), + ).toBe(true); + + expect( + resolveProviderDefaultThinkingLevel({ + provider: "demo", + context: { + provider: "demo", + modelId: "gpt-5.4", + reasoning: true, + }, + }), + ).toBe("low"); + + expect( + resolveProviderModernModelRef({ + provider: "demo", + context: { + provider: "demo", + modelId: "gpt-5.4", + }, + }), + ).toBe(true); + expect( buildProviderMissingAuthMessageWithPlugin({ provider: "openai", diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 9e5104f7f86..8997011a7c9 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -6,7 +6,9 @@ import type { ProviderBuildMissingAuthMessageContext, ProviderBuiltInModelSuppressionContext, ProviderCacheTtlEligibilityContext, + ProviderDefaultThinkingPolicyContext, ProviderFetchUsageSnapshotContext, + ProviderModernModelPolicyContext, ProviderPrepareExtraParamsContext, ProviderPrepareDynamicModelContext, ProviderPrepareRuntimeAuthContext, @@ -14,6 +16,7 @@ import type { ProviderPlugin, ProviderResolveDynamicModelContext, ProviderRuntimeModel, + ProviderThinkingPolicyContext, ProviderWrapStreamFnContext, } from "./types.js"; @@ -179,6 +182,46 @@ export function resolveProviderCacheTtlEligibility(params: { return resolveProviderRuntimePlugin(params)?.isCacheTtlEligible?.(params.context); } +export function resolveProviderBinaryThinking(params: { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + context: ProviderThinkingPolicyContext; +}) { + return resolveProviderRuntimePlugin(params)?.isBinaryThinking?.(params.context); +} + +export function resolveProviderXHighThinking(params: { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + context: ProviderThinkingPolicyContext; +}) { + return resolveProviderRuntimePlugin(params)?.supportsXHighThinking?.(params.context); +} + +export function resolveProviderDefaultThinkingLevel(params: { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + context: ProviderDefaultThinkingPolicyContext; +}) { + return resolveProviderRuntimePlugin(params)?.resolveDefaultThinkingLevel?.(params.context); +} + +export function resolveProviderModernModelRef(params: { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + context: ProviderModernModelPolicyContext; +}) { + return resolveProviderRuntimePlugin(params)?.isModernModelRef?.(params.context); +} + export function buildProviderMissingAuthMessageWithPlugin(params: { provider: string; config?: OpenClawConfig; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 685858a9b6e..df7e00734d5 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -426,6 +426,40 @@ export type ProviderBuiltInModelSuppressionResult = { errorMessage?: string; }; +/** + * Provider-owned thinking policy input. + * + * Used by shared `/think`, ACP controls, and directive parsing to ask a + * provider whether a model supports special reasoning UX such as xhigh or a + * binary on/off toggle. + */ +export type ProviderThinkingPolicyContext = { + provider: string; + modelId: string; +}; + +/** + * Provider-owned default thinking policy input. + * + * `reasoning` is the merged catalog hint for the selected model when one is + * available. Providers can use it to keep "reasoning model => low" behavior + * without re-reading the catalog themselves. + */ +export type ProviderDefaultThinkingPolicyContext = ProviderThinkingPolicyContext & { + reasoning?: boolean; +}; + +/** + * Provider-owned "modern model" policy input. + * + * Live smoke/model-profile selection uses this to keep provider-specific + * inclusion/exclusion rules out of core. + */ +export type ProviderModernModelPolicyContext = { + provider: string; + modelId: string; +}; + /** * Final catalog augmentation hook. * @@ -651,6 +685,35 @@ export type ProviderPlugin = { | Promise | ReadonlyArray | null | undefined> | null | undefined; + /** + * Provider-owned binary thinking toggle. + * + * Return true when the provider exposes a coarse on/off reasoning control + * instead of the normal multi-level ladder shown by `/think`. + */ + isBinaryThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; + /** + * Provider-owned xhigh reasoning support. + * + * Return true only for models that should expose the `xhigh` thinking level. + */ + supportsXHighThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; + /** + * Provider-owned default thinking level. + * + * Use this to keep model-family defaults (for example Claude 4.6 => + * adaptive) out of core command logic. + */ + resolveDefaultThinkingLevel?: ( + ctx: ProviderDefaultThinkingPolicyContext, + ) => "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | null | undefined; + /** + * Provider-owned "modern model" matcher used by live profile/smoke filters. + * + * Return true when the given provider/model ref should be treated as a + * preferred modern model candidate. + */ + isModernModelRef?: (ctx: ProviderModernModelPolicyContext) => boolean | undefined; wizard?: ProviderPluginWizard; formatApiKey?: (cred: AuthProfileCredential) => string; refreshOAuth?: (cred: OAuthCredential) => Promise; From 01456f95bc5fd41243d02a33fb71abef57eb6c67 Mon Sep 17 00:00:00 2001 From: Christopher Chamaletsos Date: Sun, 15 Mar 2026 20:21:04 +0200 Subject: [PATCH 03/14] fix: control UI sends correct provider prefix when switching models The model selector was using just the model ID (e.g. "gpt-5.2") as the option value. When sent to sessions.patch, the server would fall back to the session's current provider ("anthropic") yielding "anthropic/gpt-5.2" instead of "openai/gpt-5.2". Now option values use "provider/model" format, and resolveModelOverrideValue and resolveDefaultModelValue also return the full provider-prefixed key so selected state stays consistent. --- ui/src/ui/app-render.helpers.ts | 19 ++++++++++++++----- ui/src/ui/types.ts | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 77ba247a26d..db6dfc40861 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -529,16 +529,24 @@ function resolveModelOverrideValue(state: AppViewState): string { return ""; } // No local override recorded yet — fall back to server data. + // Include provider prefix so the value matches option keys (provider/model). const activeRow = resolveActiveSessionRow(state); - if (activeRow) { - return typeof activeRow.model === "string" ? activeRow.model.trim() : ""; + if (activeRow && typeof activeRow.model === "string" && activeRow.model.trim()) { + const provider = activeRow.modelProvider?.trim(); + const model = activeRow.model.trim(); + return provider ? `${provider}/${model}` : model; } return ""; } function resolveDefaultModelValue(state: AppViewState): string { - const model = state.sessionsResult?.defaults?.model; - return typeof model === "string" ? model.trim() : ""; + const defaults = state.sessionsResult?.defaults; + const model = defaults?.model; + if (typeof model !== "string" || !model.trim()) { + return ""; + } + const provider = defaults?.modelProvider?.trim(); + return provider ? `${provider}/${model.trim()}` : model.trim(); } function buildChatModelOptions( @@ -563,7 +571,8 @@ function buildChatModelOptions( for (const entry of catalog) { const provider = entry.provider?.trim(); - addOption(entry.id, provider ? `${entry.id} · ${provider}` : entry.id); + const value = provider ? `${provider}/${entry.id}` : entry.id; + addOption(value, provider ? `${entry.id} · ${provider}` : entry.id); } if (currentOverride) { diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index d9764a024e6..82c97c6744a 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -316,6 +316,7 @@ export type PresenceEntry = { }; export type GatewaySessionsDefaults = { + modelProvider: string | null; model: string | null; contextTokens: number | null; }; From d9fb50e7772177e7f739f7598401f30da5ad0bc8 Mon Sep 17 00:00:00 2001 From: Christopher Chamaletsos Date: Sun, 15 Mar 2026 21:05:24 +0200 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20format=20default=20model=20label?= =?UTF-8?q?=20as=20'model=20=C2=B7=20provider'=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default option showed 'Default (openai/gpt-5.2)' while individual options used the friendlier 'gpt-5.2 · openai' format. --- ui/src/ui/app-render.helpers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index db6dfc40861..12e239cb50d 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -592,7 +592,10 @@ function renderChatModelSelect(state: AppViewState) { currentOverride, defaultModel, ); - const defaultLabel = defaultModel ? `Default (${defaultModel})` : "Default model"; + const defaultDisplay = defaultModel.includes("/") + ? `${defaultModel.slice(defaultModel.indexOf("/") + 1)} · ${defaultModel.slice(0, defaultModel.indexOf("/"))}` + : defaultModel; + const defaultLabel = defaultModel ? `Default (${defaultDisplay})` : "Default model"; const busy = state.chatLoading || state.chatSending || Boolean(state.chatRunId) || state.chatStream !== null; const disabled = From 31e6cb0df6293fa8e46fd894b9c51c9d465457df Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 20:46:49 -0700 Subject: [PATCH 05/14] Nostr: break setup-surface import cycle --- extensions/nostr/src/default-relays.ts | 1 + extensions/nostr/src/nostr-bus.ts | 3 +-- extensions/nostr/src/setup-surface.ts | 3 ++- extensions/nostr/src/types.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 extensions/nostr/src/default-relays.ts diff --git a/extensions/nostr/src/default-relays.ts b/extensions/nostr/src/default-relays.ts new file mode 100644 index 00000000000..f9b6be01cba --- /dev/null +++ b/extensions/nostr/src/default-relays.ts @@ -0,0 +1 @@ +export const DEFAULT_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"]; diff --git a/extensions/nostr/src/nostr-bus.ts b/extensions/nostr/src/nostr-bus.ts index 0b015dad29f..f7fa1d4d94f 100644 --- a/extensions/nostr/src/nostr-bus.ts +++ b/extensions/nostr/src/nostr-bus.ts @@ -8,6 +8,7 @@ import { } from "nostr-tools"; import { decrypt, encrypt } from "nostr-tools/nip04"; import type { NostrProfile } from "./config-schema.js"; +import { DEFAULT_RELAYS } from "./default-relays.js"; import { createMetrics, createNoopMetrics, @@ -25,8 +26,6 @@ import { } from "./nostr-state-store.js"; import { createSeenTracker, type SeenTracker } from "./seen-tracker.js"; -export const DEFAULT_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"]; - // ============================================================================ // Constants // ============================================================================ diff --git a/extensions/nostr/src/setup-surface.ts b/extensions/nostr/src/setup-surface.ts index 800b2705258..84c78743cb3 100644 --- a/extensions/nostr/src/setup-surface.ts +++ b/extensions/nostr/src/setup-surface.ts @@ -13,7 +13,8 @@ import type { DmPolicy } from "../../../src/config/types.js"; import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js"; import { formatDocsLink } from "../../../src/terminal/links.js"; import type { WizardPrompter } from "../../../src/wizard/prompts.js"; -import { DEFAULT_RELAYS, getPublicKeyFromPrivate, normalizePubkey } from "./nostr-bus.js"; +import { DEFAULT_RELAYS } from "./default-relays.js"; +import { getPublicKeyFromPrivate, normalizePubkey } from "./nostr-bus.js"; import { resolveNostrAccount } from "./types.js"; const channel = "nostr" as const; diff --git a/extensions/nostr/src/types.ts b/extensions/nostr/src/types.ts index 9baf78a0ca8..e2419c44ac3 100644 --- a/extensions/nostr/src/types.ts +++ b/extensions/nostr/src/types.ts @@ -5,8 +5,8 @@ import { } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/nostr"; import type { NostrProfile } from "./config-schema.js"; +import { DEFAULT_RELAYS } from "./default-relays.js"; import { getPublicKeyFromPrivate } from "./nostr-bus.js"; -import { DEFAULT_RELAYS } from "./nostr-bus.js"; export interface NostrAccountConfig { enabled?: boolean; From 7d5e26b4a283882787f71ef4d7151f03a2976a05 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 20:47:20 -0700 Subject: [PATCH 06/14] Tests: stabilize bundle MCP env on Windows --- src/plugins/bundle-mcp.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plugins/bundle-mcp.test.ts b/src/plugins/bundle-mcp.test.ts index 122c7a83c5c..ef109f4abfb 100644 --- a/src/plugins/bundle-mcp.test.ts +++ b/src/plugins/bundle-mcp.test.ts @@ -24,11 +24,14 @@ afterEach(async () => { describe("loadEnabledBundleMcpConfig", () => { it("loads enabled Claude bundle MCP config and absolutizes relative args", async () => { - const env = captureEnv(["HOME"]); + const env = captureEnv(["HOME", "USERPROFILE", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]); try { const homeDir = await createTempDir("openclaw-bundle-mcp-home-"); const workspaceDir = await createTempDir("openclaw-bundle-mcp-workspace-"); process.env.HOME = homeDir; + process.env.USERPROFILE = homeDir; + delete process.env.OPENCLAW_HOME; + delete process.env.OPENCLAW_STATE_DIR; const pluginRoot = path.join(homeDir, ".openclaw", "extensions", "bundle-probe"); const serverPath = path.join(pluginRoot, "servers", "probe.mjs"); @@ -80,11 +83,14 @@ describe("loadEnabledBundleMcpConfig", () => { }); it("merges inline bundle MCP servers and skips disabled bundles", async () => { - const env = captureEnv(["HOME"]); + const env = captureEnv(["HOME", "USERPROFILE", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]); try { const homeDir = await createTempDir("openclaw-bundle-inline-home-"); const workspaceDir = await createTempDir("openclaw-bundle-inline-workspace-"); process.env.HOME = homeDir; + process.env.USERPROFILE = homeDir; + delete process.env.OPENCLAW_HOME; + delete process.env.OPENCLAW_STATE_DIR; const enabledRoot = path.join(homeDir, ".openclaw", "extensions", "inline-enabled"); const disabledRoot = path.join(homeDir, ".openclaw", "extensions", "inline-disabled"); From 270ba54c4747e2f3b1d0c6f3c0f4f019d958e657 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 20:48:07 -0700 Subject: [PATCH 07/14] Status: lazy-load channel security and summaries --- src/security/audit.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/security/audit.ts b/src/security/audit.ts index d3c1337e042..b304f658d68 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -6,7 +6,7 @@ import { redactCdpUrl } from "../browser/cdp.helpers.js"; import { resolveBrowserConfig, resolveProfile } from "../browser/config.js"; import { resolveBrowserControlAuth } from "../browser/control-auth.js"; import { hasPotentialConfiguredChannels } from "../channels/config-presence.js"; -import { listChannelPlugins } from "../channels/plugins/index.js"; +import type { listChannelPlugins } from "../channels/plugins/index.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { ConfigFileSnapshot, OpenClawConfig } from "../config/config.js"; import { resolveConfigPath, resolveStateDir } from "../config/paths.js"; @@ -137,6 +137,13 @@ type AuditExecutionContext = { deepProbeAuth?: { token?: string; password?: string }; }; +let channelPluginsModulePromise: Promise | undefined; + +async function loadChannelPlugins() { + channelPluginsModulePromise ??= import("../channels/plugins/index.js"); + return await channelPluginsModulePromise; +} + function countBySeverity(findings: SecurityAuditFinding[]): SecurityAuditSummary { let critical = 0; let warn = 0; @@ -1244,7 +1251,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise Date: Sun, 15 Mar 2026 20:52:33 -0700 Subject: [PATCH 08/14] Docs: refresh generated config baseline --- docs/.generated/config-baseline.json | 2110 ++++++++++++++++++++++++- docs/.generated/config-baseline.jsonl | 173 +- 2 files changed, 2252 insertions(+), 31 deletions(-) diff --git a/docs/.generated/config-baseline.json b/docs/.generated/config-baseline.json index f6f854b2946..6dc7cc100f2 100644 --- a/docs/.generated/config-baseline.json +++ b/docs/.generated/config-baseline.json @@ -2956,6 +2956,16 @@ "tags": [], "hasChildren": true }, + { + "path": "agents.defaults.sandbox.backend", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "agents.defaults.sandbox.browser", "kind": "core", @@ -5048,6 +5058,16 @@ "tags": [], "hasChildren": true }, + { + "path": "agents.list.*.sandbox.backend", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "agents.list.*.sandbox.browser", "kind": "core", @@ -30047,6 +30067,16 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.telegram.accounts.*.actions.editForumTopic", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.telegram.accounts.*.actions.editMessage", "kind": "channel", @@ -31930,6 +31960,16 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.telegram.actions.editForumTopic", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.telegram.actions.editMessage", "kind": "channel", @@ -44497,6 +44537,75 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.anthropic", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/anthropic-provider", + "help": "OpenClaw Anthropic provider plugin (plugin: anthropic)", + "hasChildren": true + }, + { + "path": "plugins.entries.anthropic.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/anthropic-provider Config", + "help": "Plugin-defined config payload for anthropic.", + "hasChildren": false + }, + { + "path": "plugins.entries.anthropic.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/anthropic-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.anthropic.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.anthropic.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.bluebubbles", "kind": "plugin", @@ -44566,6 +44675,213 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.brave", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/brave-plugin", + "help": "OpenClaw Brave plugin (plugin: brave)", + "hasChildren": true + }, + { + "path": "plugins.entries.brave.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/brave-plugin Config", + "help": "Plugin-defined config payload for brave.", + "hasChildren": false + }, + { + "path": "plugins.entries.brave.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/brave-plugin", + "hasChildren": false + }, + { + "path": "plugins.entries.brave.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.brave.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.byteplus", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/byteplus-provider", + "help": "OpenClaw BytePlus provider plugin (plugin: byteplus)", + "hasChildren": true + }, + { + "path": "plugins.entries.byteplus.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/byteplus-provider Config", + "help": "Plugin-defined config payload for byteplus.", + "hasChildren": false + }, + { + "path": "plugins.entries.byteplus.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/byteplus-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.byteplus.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.byteplus.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.cloudflare-ai-gateway", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/cloudflare-ai-gateway-provider", + "help": "OpenClaw Cloudflare AI Gateway provider plugin (plugin: cloudflare-ai-gateway)", + "hasChildren": true + }, + { + "path": "plugins.entries.cloudflare-ai-gateway.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/cloudflare-ai-gateway-provider Config", + "help": "Plugin-defined config payload for cloudflare-ai-gateway.", + "hasChildren": false + }, + { + "path": "plugins.entries.cloudflare-ai-gateway.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/cloudflare-ai-gateway-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.cloudflare-ai-gateway.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.cloudflare-ai-gateway.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.copilot-proxy", "kind": "plugin", @@ -45332,7 +45648,7 @@ "hasChildren": false }, { - "path": "plugins.entries.google-gemini-cli-auth", + "path": "plugins.entries.github-copilot", "kind": "plugin", "type": "object", "required": false, @@ -45341,12 +45657,12 @@ "tags": [ "advanced" ], - "label": "@openclaw/google-gemini-cli-auth", - "help": "OpenClaw Gemini CLI OAuth provider plugin (plugin: google-gemini-cli-auth)", + "label": "@openclaw/github-copilot-provider", + "help": "OpenClaw GitHub Copilot provider plugin (plugin: github-copilot)", "hasChildren": true }, { - "path": "plugins.entries.google-gemini-cli-auth.config", + "path": "plugins.entries.github-copilot.config", "kind": "plugin", "type": "object", "required": false, @@ -45355,12 +45671,12 @@ "tags": [ "advanced" ], - "label": "@openclaw/google-gemini-cli-auth Config", - "help": "Plugin-defined config payload for google-gemini-cli-auth.", + "label": "@openclaw/github-copilot-provider Config", + "help": "Plugin-defined config payload for github-copilot.", "hasChildren": false }, { - "path": "plugins.entries.google-gemini-cli-auth.enabled", + "path": "plugins.entries.github-copilot.enabled", "kind": "plugin", "type": "boolean", "required": false, @@ -45369,11 +45685,11 @@ "tags": [ "advanced" ], - "label": "Enable @openclaw/google-gemini-cli-auth", + "label": "Enable @openclaw/github-copilot-provider", "hasChildren": false }, { - "path": "plugins.entries.google-gemini-cli-auth.hooks", + "path": "plugins.entries.github-copilot.hooks", "kind": "plugin", "type": "object", "required": false, @@ -45387,7 +45703,76 @@ "hasChildren": true }, { - "path": "plugins.entries.google-gemini-cli-auth.hooks.allowPromptInjection", + "path": "plugins.entries.github-copilot.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.google", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/google-plugin", + "help": "OpenClaw Google plugin (plugin: google)", + "hasChildren": true + }, + { + "path": "plugins.entries.google.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/google-plugin Config", + "help": "Plugin-defined config payload for google.", + "hasChildren": false + }, + { + "path": "plugins.entries.google.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/google-plugin", + "hasChildren": false + }, + { + "path": "plugins.entries.google.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.google.hooks.allowPromptInjection", "kind": "plugin", "type": "boolean", "required": false, @@ -45469,6 +45854,75 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.huggingface", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/huggingface-provider", + "help": "OpenClaw Hugging Face provider plugin (plugin: huggingface)", + "hasChildren": true + }, + { + "path": "plugins.entries.huggingface.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/huggingface-provider Config", + "help": "Plugin-defined config payload for huggingface.", + "hasChildren": false + }, + { + "path": "plugins.entries.huggingface.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/huggingface-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.huggingface.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.huggingface.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.imessage", "kind": "plugin", @@ -45607,6 +46061,144 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.kilocode", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/kilocode-provider", + "help": "OpenClaw Kilo Gateway provider plugin (plugin: kilocode)", + "hasChildren": true + }, + { + "path": "plugins.entries.kilocode.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/kilocode-provider Config", + "help": "Plugin-defined config payload for kilocode.", + "hasChildren": false + }, + { + "path": "plugins.entries.kilocode.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/kilocode-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.kilocode.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.kilocode.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.kimi-coding", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/kimi-coding-provider", + "help": "OpenClaw Kimi Coding provider plugin (plugin: kimi-coding)", + "hasChildren": true + }, + { + "path": "plugins.entries.kimi-coding.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/kimi-coding-provider Config", + "help": "Plugin-defined config payload for kimi-coding.", + "hasChildren": false + }, + { + "path": "plugins.entries.kimi-coding.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/kimi-coding-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.kimi-coding.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.kimi-coding.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.line", "kind": "plugin", @@ -46290,7 +46882,7 @@ "hasChildren": false }, { - "path": "plugins.entries.minimax-portal-auth", + "path": "plugins.entries.minimax", "kind": "plugin", "type": "object", "required": false, @@ -46299,12 +46891,12 @@ "tags": [ "performance" ], - "label": "@openclaw/minimax-portal-auth", - "help": "OpenClaw MiniMax Portal OAuth provider plugin (plugin: minimax-portal-auth)", + "label": "@openclaw/minimax-provider", + "help": "OpenClaw MiniMax provider and OAuth plugin (plugin: minimax)", "hasChildren": true }, { - "path": "plugins.entries.minimax-portal-auth.config", + "path": "plugins.entries.minimax.config", "kind": "plugin", "type": "object", "required": false, @@ -46313,12 +46905,12 @@ "tags": [ "performance" ], - "label": "@openclaw/minimax-portal-auth Config", - "help": "Plugin-defined config payload for minimax-portal-auth.", + "label": "@openclaw/minimax-provider Config", + "help": "Plugin-defined config payload for minimax.", "hasChildren": false }, { - "path": "plugins.entries.minimax-portal-auth.enabled", + "path": "plugins.entries.minimax.enabled", "kind": "plugin", "type": "boolean", "required": false, @@ -46327,11 +46919,11 @@ "tags": [ "performance" ], - "label": "Enable @openclaw/minimax-portal-auth", + "label": "Enable @openclaw/minimax-provider", "hasChildren": false }, { - "path": "plugins.entries.minimax-portal-auth.hooks", + "path": "plugins.entries.minimax.hooks", "kind": "plugin", "type": "object", "required": false, @@ -46345,7 +46937,214 @@ "hasChildren": true }, { - "path": "plugins.entries.minimax-portal-auth.hooks.allowPromptInjection", + "path": "plugins.entries.minimax.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.mistral", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/mistral-provider", + "help": "OpenClaw Mistral provider plugin (plugin: mistral)", + "hasChildren": true + }, + { + "path": "plugins.entries.mistral.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/mistral-provider Config", + "help": "Plugin-defined config payload for mistral.", + "hasChildren": false + }, + { + "path": "plugins.entries.mistral.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/mistral-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.mistral.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.mistral.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.modelstudio", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/modelstudio-provider", + "help": "OpenClaw Model Studio provider plugin (plugin: modelstudio)", + "hasChildren": true + }, + { + "path": "plugins.entries.modelstudio.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/modelstudio-provider Config", + "help": "Plugin-defined config payload for modelstudio.", + "hasChildren": false + }, + { + "path": "plugins.entries.modelstudio.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/modelstudio-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.modelstudio.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.modelstudio.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.moonshot", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/moonshot-provider", + "help": "OpenClaw Moonshot provider plugin (plugin: moonshot)", + "hasChildren": true + }, + { + "path": "plugins.entries.moonshot.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/moonshot-provider Config", + "help": "Plugin-defined config payload for moonshot.", + "hasChildren": false + }, + { + "path": "plugins.entries.moonshot.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/moonshot-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.moonshot.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.moonshot.hooks.allowPromptInjection", "kind": "plugin", "type": "boolean", "required": false, @@ -46565,6 +47364,75 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.nvidia", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/nvidia-provider", + "help": "OpenClaw NVIDIA provider plugin (plugin: nvidia)", + "hasChildren": true + }, + { + "path": "plugins.entries.nvidia.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/nvidia-provider Config", + "help": "Plugin-defined config payload for nvidia.", + "hasChildren": false + }, + { + "path": "plugins.entries.nvidia.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/nvidia-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.nvidia.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.nvidia.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.ollama", "kind": "plugin", @@ -46703,6 +47571,587 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.openai", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/openai-provider", + "help": "OpenClaw OpenAI provider plugins (plugin: openai)", + "hasChildren": true + }, + { + "path": "plugins.entries.openai.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/openai-provider Config", + "help": "Plugin-defined config payload for openai.", + "hasChildren": false + }, + { + "path": "plugins.entries.openai.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/openai-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.openai.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.openai.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.opencode", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/opencode-provider", + "help": "OpenClaw OpenCode Zen provider plugin (plugin: opencode)", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode-go", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/opencode-go-provider", + "help": "OpenClaw OpenCode Go provider plugin (plugin: opencode-go)", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode-go.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/opencode-go-provider Config", + "help": "Plugin-defined config payload for opencode-go.", + "hasChildren": false + }, + { + "path": "plugins.entries.opencode-go.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/opencode-go-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.opencode-go.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode-go.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.opencode.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/opencode-provider Config", + "help": "Plugin-defined config payload for opencode.", + "hasChildren": false + }, + { + "path": "plugins.entries.opencode.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/opencode-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.opencode.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.openrouter", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/openrouter-provider", + "help": "OpenClaw OpenRouter provider plugin (plugin: openrouter)", + "hasChildren": true + }, + { + "path": "plugins.entries.openrouter.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/openrouter-provider Config", + "help": "Plugin-defined config payload for openrouter.", + "hasChildren": false + }, + { + "path": "plugins.entries.openrouter.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/openrouter-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.openrouter.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.openrouter.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "OpenShell Sandbox", + "help": "Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-based command execution. (plugin: openshell)", + "hasChildren": true + }, + { + "path": "plugins.entries.openshell.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "OpenShell Sandbox Config", + "help": "Plugin-defined config payload for openshell.", + "hasChildren": true + }, + { + "path": "plugins.entries.openshell.config.autoProviders", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Auto-create Providers", + "help": "When enabled, pass --auto-providers during sandbox create.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.command", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "OpenShell Command", + "help": "Path or command name for the openshell CLI.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.from", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Sandbox Source", + "help": "OpenShell sandbox source for first-time create. Defaults to openclaw.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.gateway", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Gateway Name", + "help": "Optional OpenShell gateway name passed as --gateway.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.gatewayEndpoint", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Gateway Endpoint", + "help": "Optional OpenShell gateway endpoint passed as --gateway-endpoint.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.gpu", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "GPU", + "help": "Request GPU resources when creating the sandbox.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.policy", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Policy File", + "help": "Optional path to a custom OpenShell sandbox policy YAML.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.providers", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Providers", + "help": "Provider names to attach when a sandbox is created.", + "hasChildren": true + }, + { + "path": "plugins.entries.openshell.config.providers.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.remoteAgentWorkspaceDir", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced", + "storage" + ], + "label": "Remote Agent Dir", + "help": "Mirror path for the real agent workspace when workspaceAccess is read-only.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.remoteWorkspaceDir", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced", + "storage" + ], + "label": "Remote Workspace Dir", + "help": "Primary writable workspace inside the OpenShell sandbox.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.config.timeoutSeconds", + "kind": "plugin", + "type": "number", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced", + "performance" + ], + "label": "Command Timeout Seconds", + "help": "Timeout for openshell CLI operations such as create/upload/download.", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable OpenShell Sandbox", + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.openshell.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.perplexity", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/perplexity-plugin", + "help": "OpenClaw Perplexity plugin (plugin: perplexity)", + "hasChildren": true + }, + { + "path": "plugins.entries.perplexity.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/perplexity-plugin Config", + "help": "Plugin-defined config payload for perplexity.", + "hasChildren": false + }, + { + "path": "plugins.entries.perplexity.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/perplexity-plugin", + "hasChildren": false + }, + { + "path": "plugins.entries.perplexity.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.perplexity.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.phone-control", "kind": "plugin", @@ -46772,6 +48221,75 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.qianfan", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/qianfan-provider", + "help": "OpenClaw Qianfan provider plugin (plugin: qianfan)", + "hasChildren": true + }, + { + "path": "plugins.entries.qianfan.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/qianfan-provider Config", + "help": "Plugin-defined config payload for qianfan.", + "hasChildren": false + }, + { + "path": "plugins.entries.qianfan.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/qianfan-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.qianfan.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.qianfan.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.qwen-portal-auth", "kind": "plugin", @@ -47117,6 +48635,75 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.synthetic", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/synthetic-provider", + "help": "OpenClaw Synthetic provider plugin (plugin: synthetic)", + "hasChildren": true + }, + { + "path": "plugins.entries.synthetic.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/synthetic-provider Config", + "help": "Plugin-defined config payload for synthetic.", + "hasChildren": false + }, + { + "path": "plugins.entries.synthetic.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/synthetic-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.synthetic.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.synthetic.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.talk-voice", "kind": "plugin", @@ -47431,6 +49018,75 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.together", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/together-provider", + "help": "OpenClaw Together provider plugin (plugin: together)", + "hasChildren": true + }, + { + "path": "plugins.entries.together.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/together-provider Config", + "help": "Plugin-defined config payload for together.", + "hasChildren": false + }, + { + "path": "plugins.entries.together.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/together-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.together.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.together.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.twitch", "kind": "plugin", @@ -47500,6 +49156,144 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.venice", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/venice-provider", + "help": "OpenClaw Venice provider plugin (plugin: venice)", + "hasChildren": true + }, + { + "path": "plugins.entries.venice.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/venice-provider Config", + "help": "Plugin-defined config payload for venice.", + "hasChildren": false + }, + { + "path": "plugins.entries.venice.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/venice-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.venice.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.venice.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.vercel-ai-gateway", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/vercel-ai-gateway-provider", + "help": "OpenClaw Vercel AI Gateway provider plugin (plugin: vercel-ai-gateway)", + "hasChildren": true + }, + { + "path": "plugins.entries.vercel-ai-gateway.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/vercel-ai-gateway-provider Config", + "help": "Plugin-defined config payload for vercel-ai-gateway.", + "hasChildren": false + }, + { + "path": "plugins.entries.vercel-ai-gateway.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/vercel-ai-gateway-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.vercel-ai-gateway.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.vercel-ai-gateway.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.vllm", "kind": "plugin", @@ -48999,6 +50793,75 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.volcengine", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/volcengine-provider", + "help": "OpenClaw Volcengine provider plugin (plugin: volcengine)", + "hasChildren": true + }, + { + "path": "plugins.entries.volcengine.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/volcengine-provider Config", + "help": "Plugin-defined config payload for volcengine.", + "hasChildren": false + }, + { + "path": "plugins.entries.volcengine.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/volcengine-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.volcengine.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.volcengine.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.whatsapp", "kind": "plugin", @@ -49068,6 +50931,213 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.xai", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/xai-plugin", + "help": "OpenClaw xAI plugin (plugin: xai)", + "hasChildren": true + }, + { + "path": "plugins.entries.xai.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/xai-plugin Config", + "help": "Plugin-defined config payload for xai.", + "hasChildren": false + }, + { + "path": "plugins.entries.xai.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/xai-plugin", + "hasChildren": false + }, + { + "path": "plugins.entries.xai.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.xai.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.xiaomi", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/xiaomi-provider", + "help": "OpenClaw Xiaomi provider plugin (plugin: xiaomi)", + "hasChildren": true + }, + { + "path": "plugins.entries.xiaomi.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/xiaomi-provider Config", + "help": "Plugin-defined config payload for xiaomi.", + "hasChildren": false + }, + { + "path": "plugins.entries.xiaomi.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/xiaomi-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.xiaomi.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.xiaomi.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.zai", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/zai-provider", + "help": "OpenClaw Z.AI provider plugin (plugin: zai)", + "hasChildren": true + }, + { + "path": "plugins.entries.zai.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/zai-provider Config", + "help": "Plugin-defined config payload for zai.", + "hasChildren": false + }, + { + "path": "plugins.entries.zai.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/zai-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.zai.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.zai.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, { "path": "plugins.entries.zalo", "kind": "plugin", diff --git a/docs/.generated/config-baseline.jsonl b/docs/.generated/config-baseline.jsonl index 18baeac12b9..65552724518 100644 --- a/docs/.generated/config-baseline.jsonl +++ b/docs/.generated/config-baseline.jsonl @@ -1,4 +1,4 @@ -{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":4889} +{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5040} {"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true} {"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true} {"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -245,6 +245,7 @@ {"recordType":"path","path":"agents.defaults.pdfModel.primary","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"PDF Model","help":"Optional PDF model (provider/model) for the PDF analysis tool. Defaults to imageModel, then session model.","hasChildren":false} {"recordType":"path","path":"agents.defaults.repoRoot","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Repo Root","help":"Optional repository root shown in the system prompt runtime line (overrides auto-detect).","hasChildren":false} {"recordType":"path","path":"agents.defaults.sandbox","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} +{"recordType":"path","path":"agents.defaults.sandbox.backend","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"agents.defaults.sandbox.browser","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"agents.defaults.sandbox.browser.allowHostControl","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"agents.defaults.sandbox.browser.autoStart","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -445,6 +446,7 @@ {"recordType":"path","path":"agents.list.*.runtime.acp.mode","kind":"core","type":"string","required":false,"enumValues":["persistent","oneshot"],"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent ACP Mode","help":"Optional ACP session mode default for this agent (persistent or oneshot).","hasChildren":false} {"recordType":"path","path":"agents.list.*.runtime.type","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent Runtime Type","help":"Runtime type for this agent: \"embedded\" (default OpenClaw runtime) or \"acp\" (ACP harness defaults).","hasChildren":false} {"recordType":"path","path":"agents.list.*.sandbox","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} +{"recordType":"path","path":"agents.list.*.sandbox.backend","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"agents.list.*.sandbox.browser","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"agents.list.*.sandbox.browser.allowHostControl","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"agents.list.*.sandbox.browser.autoStart","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -2708,6 +2710,7 @@ {"recordType":"path","path":"channels.telegram.accounts.*.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.telegram.accounts.*.actions.createForumTopic","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.accounts.*.actions.deleteMessage","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.telegram.accounts.*.actions.editForumTopic","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.accounts.*.actions.editMessage","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.accounts.*.actions.poll","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.accounts.*.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -2883,6 +2886,7 @@ {"recordType":"path","path":"channels.telegram.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.telegram.actions.createForumTopic","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.actions.deleteMessage","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.telegram.actions.editForumTopic","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.actions.editMessage","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.actions.poll","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.telegram.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -3940,11 +3944,31 @@ {"recordType":"path","path":"plugins.entries.acpx.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable ACPX Runtime","hasChildren":false} {"recordType":"path","path":"plugins.entries.acpx.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.acpx.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.anthropic","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/anthropic-provider","help":"OpenClaw Anthropic provider plugin (plugin: anthropic)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.anthropic.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/anthropic-provider Config","help":"Plugin-defined config payload for anthropic.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.anthropic.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/anthropic-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.anthropic.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.anthropic.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.bluebubbles","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/bluebubbles","help":"OpenClaw BlueBubbles channel plugin (plugin: bluebubbles)","hasChildren":true} {"recordType":"path","path":"plugins.entries.bluebubbles.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/bluebubbles Config","help":"Plugin-defined config payload for bluebubbles.","hasChildren":false} {"recordType":"path","path":"plugins.entries.bluebubbles.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/bluebubbles","hasChildren":false} {"recordType":"path","path":"plugins.entries.bluebubbles.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.bluebubbles.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.brave","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/brave-plugin","help":"OpenClaw Brave plugin (plugin: brave)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.brave.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/brave-plugin Config","help":"Plugin-defined config payload for brave.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.brave.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/brave-plugin","hasChildren":false} +{"recordType":"path","path":"plugins.entries.brave.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.brave.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.byteplus","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/byteplus-provider","help":"OpenClaw BytePlus provider plugin (plugin: byteplus)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.byteplus.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/byteplus-provider Config","help":"Plugin-defined config payload for byteplus.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.byteplus.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/byteplus-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.byteplus.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.byteplus.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider","help":"OpenClaw Cloudflare AI Gateway provider plugin (plugin: cloudflare-ai-gateway)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider Config","help":"Plugin-defined config payload for cloudflare-ai-gateway.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/cloudflare-ai-gateway-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.copilot-proxy","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/copilot-proxy","help":"OpenClaw Copilot Proxy provider plugin (plugin: copilot-proxy)","hasChildren":true} {"recordType":"path","path":"plugins.entries.copilot-proxy.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/copilot-proxy Config","help":"Plugin-defined config payload for copilot-proxy.","hasChildren":false} {"recordType":"path","path":"plugins.entries.copilot-proxy.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/copilot-proxy","hasChildren":false} @@ -3998,16 +4022,26 @@ {"recordType":"path","path":"plugins.entries.feishu.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/feishu","hasChildren":false} {"recordType":"path","path":"plugins.entries.feishu.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.feishu.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} -{"recordType":"path","path":"plugins.entries.google-gemini-cli-auth","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-gemini-cli-auth","help":"OpenClaw Gemini CLI OAuth provider plugin (plugin: google-gemini-cli-auth)","hasChildren":true} -{"recordType":"path","path":"plugins.entries.google-gemini-cli-auth.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-gemini-cli-auth Config","help":"Plugin-defined config payload for google-gemini-cli-auth.","hasChildren":false} -{"recordType":"path","path":"plugins.entries.google-gemini-cli-auth.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/google-gemini-cli-auth","hasChildren":false} -{"recordType":"path","path":"plugins.entries.google-gemini-cli-auth.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} -{"recordType":"path","path":"plugins.entries.google-gemini-cli-auth.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.github-copilot","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/github-copilot-provider","help":"OpenClaw GitHub Copilot provider plugin (plugin: github-copilot)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.github-copilot.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/github-copilot-provider Config","help":"Plugin-defined config payload for github-copilot.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.github-copilot.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/github-copilot-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.github-copilot.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.github-copilot.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.google","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-plugin","help":"OpenClaw Google plugin (plugin: google)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.google.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-plugin Config","help":"Plugin-defined config payload for google.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.google.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/google-plugin","hasChildren":false} +{"recordType":"path","path":"plugins.entries.google.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.google.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.googlechat","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/googlechat","help":"OpenClaw Google Chat channel plugin (plugin: googlechat)","hasChildren":true} {"recordType":"path","path":"plugins.entries.googlechat.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/googlechat Config","help":"Plugin-defined config payload for googlechat.","hasChildren":false} {"recordType":"path","path":"plugins.entries.googlechat.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/googlechat","hasChildren":false} {"recordType":"path","path":"plugins.entries.googlechat.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.googlechat.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.huggingface","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/huggingface-provider","help":"OpenClaw Hugging Face provider plugin (plugin: huggingface)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.huggingface.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/huggingface-provider Config","help":"Plugin-defined config payload for huggingface.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.huggingface.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/huggingface-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.huggingface.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.huggingface.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.imessage","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/imessage","help":"OpenClaw iMessage channel plugin (plugin: imessage)","hasChildren":true} {"recordType":"path","path":"plugins.entries.imessage.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/imessage Config","help":"Plugin-defined config payload for imessage.","hasChildren":false} {"recordType":"path","path":"plugins.entries.imessage.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/imessage","hasChildren":false} @@ -4018,6 +4052,16 @@ {"recordType":"path","path":"plugins.entries.irc.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/irc","hasChildren":false} {"recordType":"path","path":"plugins.entries.irc.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.irc.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kilocode","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kilocode-provider","help":"OpenClaw Kilo Gateway provider plugin (plugin: kilocode)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kilocode.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kilocode-provider Config","help":"Plugin-defined config payload for kilocode.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kilocode.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/kilocode-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kilocode.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kilocode.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kimi-coding","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-coding-provider","help":"OpenClaw Kimi Coding provider plugin (plugin: kimi-coding)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kimi-coding.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-coding-provider Config","help":"Plugin-defined config payload for kimi-coding.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kimi-coding.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/kimi-coding-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kimi-coding.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kimi-coding.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.line","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/line","help":"OpenClaw LINE channel plugin (plugin: line)","hasChildren":true} {"recordType":"path","path":"plugins.entries.line.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/line Config","help":"Plugin-defined config payload for line.","hasChildren":false} {"recordType":"path","path":"plugins.entries.line.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/line","hasChildren":false} @@ -4069,11 +4113,26 @@ {"recordType":"path","path":"plugins.entries.memory-lancedb.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Enable @openclaw/memory-lancedb","hasChildren":false} {"recordType":"path","path":"plugins.entries.memory-lancedb.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.memory-lancedb.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} -{"recordType":"path","path":"plugins.entries.minimax-portal-auth","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-portal-auth","help":"OpenClaw MiniMax Portal OAuth provider plugin (plugin: minimax-portal-auth)","hasChildren":true} -{"recordType":"path","path":"plugins.entries.minimax-portal-auth.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-portal-auth Config","help":"Plugin-defined config payload for minimax-portal-auth.","hasChildren":false} -{"recordType":"path","path":"plugins.entries.minimax-portal-auth.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Enable @openclaw/minimax-portal-auth","hasChildren":false} -{"recordType":"path","path":"plugins.entries.minimax-portal-auth.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} -{"recordType":"path","path":"plugins.entries.minimax-portal-auth.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.minimax","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-provider","help":"OpenClaw MiniMax provider and OAuth plugin (plugin: minimax)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.minimax.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-provider Config","help":"Plugin-defined config payload for minimax.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.minimax.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Enable @openclaw/minimax-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.minimax.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.minimax.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.mistral","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/mistral-provider","help":"OpenClaw Mistral provider plugin (plugin: mistral)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.mistral.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/mistral-provider Config","help":"Plugin-defined config payload for mistral.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.mistral.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/mistral-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.mistral.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.mistral.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.modelstudio","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/modelstudio-provider","help":"OpenClaw Model Studio provider plugin (plugin: modelstudio)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.modelstudio.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/modelstudio-provider Config","help":"Plugin-defined config payload for modelstudio.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.modelstudio.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/modelstudio-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.modelstudio.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.modelstudio.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.moonshot","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/moonshot-provider","help":"OpenClaw Moonshot provider plugin (plugin: moonshot)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.moonshot.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/moonshot-provider Config","help":"Plugin-defined config payload for moonshot.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.moonshot.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/moonshot-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.moonshot.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.moonshot.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.msteams","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/msteams","help":"OpenClaw Microsoft Teams channel plugin (plugin: msteams)","hasChildren":true} {"recordType":"path","path":"plugins.entries.msteams.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/msteams Config","help":"Plugin-defined config payload for msteams.","hasChildren":false} {"recordType":"path","path":"plugins.entries.msteams.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/msteams","hasChildren":false} @@ -4089,6 +4148,11 @@ {"recordType":"path","path":"plugins.entries.nostr.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/nostr","hasChildren":false} {"recordType":"path","path":"plugins.entries.nostr.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.nostr.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.nvidia","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nvidia-provider","help":"OpenClaw NVIDIA provider plugin (plugin: nvidia)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nvidia.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nvidia-provider Config","help":"Plugin-defined config payload for nvidia.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.nvidia.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/nvidia-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.nvidia.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nvidia.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.ollama","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/ollama-provider","help":"OpenClaw Ollama provider plugin (plugin: ollama)","hasChildren":true} {"recordType":"path","path":"plugins.entries.ollama.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/ollama-provider Config","help":"Plugin-defined config payload for ollama.","hasChildren":false} {"recordType":"path","path":"plugins.entries.ollama.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/ollama-provider","hasChildren":false} @@ -4099,11 +4163,58 @@ {"recordType":"path","path":"plugins.entries.open-prose.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable OpenProse","hasChildren":false} {"recordType":"path","path":"plugins.entries.open-prose.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.open-prose.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openai","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openai-provider","help":"OpenClaw OpenAI provider plugins (plugin: openai)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openai-provider Config","help":"Plugin-defined config payload for openai.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openai.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/openai-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openai.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openai.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-provider","help":"OpenClaw OpenCode Zen provider plugin (plugin: opencode)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode-go","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-go-provider","help":"OpenClaw OpenCode Go provider plugin (plugin: opencode-go)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode-go.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-go-provider Config","help":"Plugin-defined config payload for opencode-go.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode-go.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/opencode-go-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode-go.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode-go.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-provider Config","help":"Plugin-defined config payload for opencode.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/opencode-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openrouter","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openrouter-provider","help":"OpenClaw OpenRouter provider plugin (plugin: openrouter)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openrouter.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openrouter-provider Config","help":"Plugin-defined config payload for openrouter.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openrouter.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/openrouter-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openrouter.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openrouter.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"OpenShell Sandbox","help":"Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-based command execution. (plugin: openshell)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openshell.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"OpenShell Sandbox Config","help":"Plugin-defined config payload for openshell.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openshell.config.autoProviders","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Auto-create Providers","help":"When enabled, pass --auto-providers during sandbox create.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.command","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"OpenShell Command","help":"Path or command name for the openshell CLI.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.from","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Sandbox Source","help":"OpenShell sandbox source for first-time create. Defaults to openclaw.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.gateway","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Gateway Name","help":"Optional OpenShell gateway name passed as --gateway.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.gatewayEndpoint","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Gateway Endpoint","help":"Optional OpenShell gateway endpoint passed as --gateway-endpoint.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.gpu","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"GPU","help":"Request GPU resources when creating the sandbox.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.policy","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Policy File","help":"Optional path to a custom OpenShell sandbox policy YAML.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.providers","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Providers","help":"Provider names to attach when a sandbox is created.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openshell.config.providers.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.remoteAgentWorkspaceDir","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced","storage"],"label":"Remote Agent Dir","help":"Mirror path for the real agent workspace when workspaceAccess is read-only.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.remoteWorkspaceDir","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced","storage"],"label":"Remote Workspace Dir","help":"Primary writable workspace inside the OpenShell sandbox.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.config.timeoutSeconds","kind":"plugin","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":["advanced","performance"],"label":"Command Timeout Seconds","help":"Timeout for openshell CLI operations such as create/upload/download.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable OpenShell Sandbox","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openshell.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.perplexity","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/perplexity-plugin","help":"OpenClaw Perplexity plugin (plugin: perplexity)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.perplexity.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/perplexity-plugin Config","help":"Plugin-defined config payload for perplexity.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.perplexity.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/perplexity-plugin","hasChildren":false} +{"recordType":"path","path":"plugins.entries.perplexity.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.perplexity.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.phone-control","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Phone Control","help":"Arm/disarm high-risk phone node commands (camera/screen/writes) with an optional auto-expiry. (plugin: phone-control)","hasChildren":true} {"recordType":"path","path":"plugins.entries.phone-control.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Phone Control Config","help":"Plugin-defined config payload for phone-control.","hasChildren":false} {"recordType":"path","path":"plugins.entries.phone-control.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable Phone Control","hasChildren":false} {"recordType":"path","path":"plugins.entries.phone-control.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.phone-control.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.qianfan","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/qianfan-provider","help":"OpenClaw Qianfan provider plugin (plugin: qianfan)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.qianfan.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/qianfan-provider Config","help":"Plugin-defined config payload for qianfan.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.qianfan.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/qianfan-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.qianfan.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.qianfan.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.qwen-portal-auth","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"qwen-portal-auth","help":"Plugin entry for qwen-portal-auth.","hasChildren":true} {"recordType":"path","path":"plugins.entries.qwen-portal-auth.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"qwen-portal-auth Config","help":"Plugin-defined config payload for qwen-portal-auth.","hasChildren":false} {"recordType":"path","path":"plugins.entries.qwen-portal-auth.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable qwen-portal-auth","hasChildren":false} @@ -4129,6 +4240,11 @@ {"recordType":"path","path":"plugins.entries.synology-chat.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/synology-chat","hasChildren":false} {"recordType":"path","path":"plugins.entries.synology-chat.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.synology-chat.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.synthetic","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/synthetic-provider","help":"OpenClaw Synthetic provider plugin (plugin: synthetic)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.synthetic.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/synthetic-provider Config","help":"Plugin-defined config payload for synthetic.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.synthetic.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/synthetic-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.synthetic.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.synthetic.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.talk-voice","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Talk Voice","help":"Manage Talk voice selection (list/set). (plugin: talk-voice)","hasChildren":true} {"recordType":"path","path":"plugins.entries.talk-voice.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Talk Voice Config","help":"Plugin-defined config payload for talk-voice.","hasChildren":false} {"recordType":"path","path":"plugins.entries.talk-voice.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable Talk Voice","hasChildren":false} @@ -4152,11 +4268,26 @@ {"recordType":"path","path":"plugins.entries.tlon.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/tlon","hasChildren":false} {"recordType":"path","path":"plugins.entries.tlon.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.tlon.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.together","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/together-provider","help":"OpenClaw Together provider plugin (plugin: together)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.together.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/together-provider Config","help":"Plugin-defined config payload for together.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.together.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/together-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.together.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.together.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.twitch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/twitch","help":"OpenClaw Twitch channel plugin (plugin: twitch)","hasChildren":true} {"recordType":"path","path":"plugins.entries.twitch.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/twitch Config","help":"Plugin-defined config payload for twitch.","hasChildren":false} {"recordType":"path","path":"plugins.entries.twitch.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/twitch","hasChildren":false} {"recordType":"path","path":"plugins.entries.twitch.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.twitch.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.venice","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/venice-provider","help":"OpenClaw Venice provider plugin (plugin: venice)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.venice.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/venice-provider Config","help":"Plugin-defined config payload for venice.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.venice.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/venice-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.venice.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.venice.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vercel-ai-gateway-provider","help":"OpenClaw Vercel AI Gateway provider plugin (plugin: vercel-ai-gateway)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vercel-ai-gateway-provider Config","help":"Plugin-defined config payload for vercel-ai-gateway.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/vercel-ai-gateway-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.vllm","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vllm-provider","help":"OpenClaw vLLM provider plugin (plugin: vllm)","hasChildren":true} {"recordType":"path","path":"plugins.entries.vllm.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vllm-provider Config","help":"Plugin-defined config payload for vllm.","hasChildren":false} {"recordType":"path","path":"plugins.entries.vllm.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/vllm-provider","hasChildren":false} @@ -4283,11 +4414,31 @@ {"recordType":"path","path":"plugins.entries.voice-call.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/voice-call","hasChildren":false} {"recordType":"path","path":"plugins.entries.voice-call.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.voice-call.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.volcengine","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/volcengine-provider","help":"OpenClaw Volcengine provider plugin (plugin: volcengine)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.volcengine.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/volcengine-provider Config","help":"Plugin-defined config payload for volcengine.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.volcengine.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/volcengine-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.volcengine.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.volcengine.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.whatsapp","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/whatsapp","help":"OpenClaw WhatsApp channel plugin (plugin: whatsapp)","hasChildren":true} {"recordType":"path","path":"plugins.entries.whatsapp.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/whatsapp Config","help":"Plugin-defined config payload for whatsapp.","hasChildren":false} {"recordType":"path","path":"plugins.entries.whatsapp.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/whatsapp","hasChildren":false} {"recordType":"path","path":"plugins.entries.whatsapp.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.whatsapp.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xai","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xai-plugin","help":"OpenClaw xAI plugin (plugin: xai)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xai-plugin Config","help":"Plugin-defined config payload for xai.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xai.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/xai-plugin","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xai.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xai.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xiaomi","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xiaomi-provider","help":"OpenClaw Xiaomi provider plugin (plugin: xiaomi)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xiaomi.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xiaomi-provider Config","help":"Plugin-defined config payload for xiaomi.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xiaomi.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/xiaomi-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xiaomi.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xiaomi.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.zai","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zai-provider","help":"OpenClaw Z.AI provider plugin (plugin: zai)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zai-provider Config","help":"Plugin-defined config payload for zai.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.zai.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/zai-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.zai.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zai.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalo","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zalo","help":"OpenClaw Zalo channel plugin (plugin: zalo)","hasChildren":true} {"recordType":"path","path":"plugins.entries.zalo.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zalo Config","help":"Plugin-defined config payload for zalo.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalo.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/zalo","hasChildren":false} From 0218045818ec951bf58b9052707a783aee8a0c6e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Mar 2026 21:02:25 -0700 Subject: [PATCH 09/14] test: silence vitest warning noise --- src/cli/program.test-mocks.ts | 164 +++++++++++++++++------------ src/infra/warning-filter.test.ts | 9 ++ src/plugins/loader.test.ts | 14 +-- ui/src/i18n/lib/translate.ts | 9 +- ui/src/i18n/test/translate.test.ts | 16 +++ ui/src/local-storage.ts | 25 +++++ ui/src/ui/app-render.ts | 5 +- ui/src/ui/chat/deleted-messages.ts | 6 +- ui/src/ui/chat/grouped-render.ts | 5 +- ui/src/ui/chat/pinned-messages.ts | 6 +- ui/src/ui/controllers/usage.ts | 10 +- ui/src/ui/device-auth.ts | 5 +- ui/src/ui/device-identity.ts | 8 +- ui/src/ui/storage.ts | 9 +- ui/src/ui/views/chat.test.ts | 5 +- 15 files changed, 190 insertions(+), 106 deletions(-) create mode 100644 ui/src/local-storage.ts diff --git a/src/cli/program.test-mocks.ts b/src/cli/program.test-mocks.ts index ab0d6b497bf..cf71122749f 100644 --- a/src/cli/program.test-mocks.ts +++ b/src/cli/program.test-mocks.ts @@ -1,78 +1,104 @@ -import { Mock, vi } from "vitest"; +import { vi, type Mock } from "vitest"; -export const messageCommand: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const statusCommand: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const configureCommand: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const configureCommandWithSections: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const setupCommand: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const onboardCommand: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const callGateway: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const runChannelLogin: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const runChannelLogout: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const runTui: Mock<(...args: unknown[]) => unknown> = vi.fn(); +type AnyMock = Mock<(...args: unknown[]) => unknown>; -export const loadAndMaybeMigrateDoctorConfig: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const ensureConfigReady: Mock<(...args: unknown[]) => unknown> = vi.fn(); -export const ensurePluginRegistryLoaded: Mock<(...args: unknown[]) => unknown> = vi.fn(); +const programMocks = vi.hoisted(() => ({ + messageCommand: vi.fn(), + statusCommand: vi.fn(), + configureCommand: vi.fn(), + configureCommandWithSections: vi.fn(), + setupCommand: vi.fn(), + onboardCommand: vi.fn(), + callGateway: vi.fn(), + runChannelLogin: vi.fn(), + runChannelLogout: vi.fn(), + runTui: vi.fn(), + loadAndMaybeMigrateDoctorConfig: vi.fn(), + ensureConfigReady: vi.fn(), + ensurePluginRegistryLoaded: vi.fn(), + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(() => { + throw new Error("exit"); + }), + }, +})); -export const runtime: { +export const messageCommand = programMocks.messageCommand as AnyMock; +export const statusCommand = programMocks.statusCommand as AnyMock; +export const configureCommand = programMocks.configureCommand as AnyMock; +export const configureCommandWithSections = programMocks.configureCommandWithSections as AnyMock; +export const setupCommand = programMocks.setupCommand as AnyMock; +export const onboardCommand = programMocks.onboardCommand as AnyMock; +export const callGateway = programMocks.callGateway as AnyMock; +export const runChannelLogin = programMocks.runChannelLogin as AnyMock; +export const runChannelLogout = programMocks.runChannelLogout as AnyMock; +export const runTui = programMocks.runTui as AnyMock; +export const loadAndMaybeMigrateDoctorConfig = + programMocks.loadAndMaybeMigrateDoctorConfig as AnyMock; +export const ensureConfigReady = programMocks.ensureConfigReady as AnyMock; +export const ensurePluginRegistryLoaded = programMocks.ensurePluginRegistryLoaded as AnyMock; + +export const runtime = programMocks.runtime as { log: Mock<(...args: unknown[]) => void>; error: Mock<(...args: unknown[]) => void>; exit: Mock<(...args: unknown[]) => never>; -} = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(() => { - throw new Error("exit"); - }), }; -export function installBaseProgramMocks() { - vi.mock("../commands/message.js", () => ({ messageCommand })); - vi.mock("../commands/status.js", () => ({ statusCommand })); - vi.mock("../commands/configure.js", () => ({ - CONFIGURE_WIZARD_SECTIONS: [ - "workspace", - "model", - "web", - "gateway", - "daemon", - "channels", - "skills", - "health", - ], - configureCommand, - configureCommandWithSections, - configureCommandFromSectionsArg: (sections: unknown, runtime: unknown) => { - const resolved = Array.isArray(sections) ? sections : []; - if (resolved.length > 0) { - return configureCommandWithSections(resolved, runtime); - } - return configureCommand({}, runtime); - }, - })); - vi.mock("../commands/setup.js", () => ({ setupCommand })); - vi.mock("../commands/onboard.js", () => ({ onboardCommand })); - vi.mock("../runtime.js", () => ({ defaultRuntime: runtime })); - vi.mock("./channel-auth.js", () => ({ runChannelLogin, runChannelLogout })); - vi.mock("../tui/tui.js", () => ({ runTui })); - vi.mock("../gateway/call.js", () => ({ - callGateway, - randomIdempotencyKey: () => "idem-test", - buildGatewayConnectionDetails: () => ({ - url: "ws://127.0.0.1:1234", - urlSource: "test", - message: "Gateway target: ws://127.0.0.1:1234", - }), - })); - vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) })); -} +// Keep these mocks at top level so Vitest does not warn about hoisted nested mocks. +vi.mock("../commands/message.js", () => ({ messageCommand: programMocks.messageCommand })); +vi.mock("../commands/status.js", () => ({ statusCommand: programMocks.statusCommand })); +vi.mock("../commands/configure.js", () => ({ + CONFIGURE_WIZARD_SECTIONS: [ + "workspace", + "model", + "web", + "gateway", + "daemon", + "channels", + "skills", + "health", + ], + configureCommand: programMocks.configureCommand, + configureCommandWithSections: programMocks.configureCommandWithSections, + configureCommandFromSectionsArg: (sections: unknown, runtime: unknown) => { + const resolved = Array.isArray(sections) ? sections : []; + if (resolved.length > 0) { + return programMocks.configureCommandWithSections(resolved, runtime); + } + return programMocks.configureCommand({}, runtime); + }, +})); +vi.mock("../commands/setup.js", () => ({ setupCommand: programMocks.setupCommand })); +vi.mock("../commands/onboard.js", () => ({ onboardCommand: programMocks.onboardCommand })); +vi.mock("../runtime.js", () => ({ defaultRuntime: programMocks.runtime })); +vi.mock("./channel-auth.js", () => ({ + runChannelLogin: programMocks.runChannelLogin, + runChannelLogout: programMocks.runChannelLogout, +})); +vi.mock("../tui/tui.js", () => ({ runTui: programMocks.runTui })); +vi.mock("../gateway/call.js", () => ({ + callGateway: programMocks.callGateway, + randomIdempotencyKey: () => "idem-test", + buildGatewayConnectionDetails: () => ({ + url: "ws://127.0.0.1:1234", + urlSource: "test", + message: "Gateway target: ws://127.0.0.1:1234", + }), +})); +vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) })); +vi.mock("./plugin-registry.js", () => ({ + ensurePluginRegistryLoaded: programMocks.ensurePluginRegistryLoaded, +})); +vi.mock("../commands/doctor-config-flow.js", () => ({ + loadAndMaybeMigrateDoctorConfig: programMocks.loadAndMaybeMigrateDoctorConfig, +})); +vi.mock("./program/config-guard.js", () => ({ + ensureConfigReady: programMocks.ensureConfigReady, +})); +vi.mock("./preaction.js", () => ({ registerPreActionHooks: () => {} })); -export function installSmokeProgramMocks() { - vi.mock("./plugin-registry.js", () => ({ ensurePluginRegistryLoaded })); - vi.mock("../commands/doctor-config-flow.js", () => ({ - loadAndMaybeMigrateDoctorConfig, - })); - vi.mock("./program/config-guard.js", () => ({ ensureConfigReady })); - vi.mock("./preaction.js", () => ({ registerPreActionHooks: () => {} })); -} +export function installBaseProgramMocks() {} + +export function installSmokeProgramMocks() {} diff --git a/src/infra/warning-filter.test.ts b/src/infra/warning-filter.test.ts index 7ce9854aa9a..da4b9dad163 100644 --- a/src/infra/warning-filter.test.ts +++ b/src/infra/warning-filter.test.ts @@ -74,6 +74,7 @@ describe("warning filter", () => { it("installs once and suppresses known warnings at emit time", async () => { const seenWarnings: Array<{ code?: string; name: string; message: string }> = []; + const stderrWrites: string[] = []; const onWarning = (warning: Error & { code?: string }) => { seenWarnings.push({ code: warning.code, @@ -81,6 +82,12 @@ describe("warning filter", () => { message: warning.message, }); }; + const stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation((( + chunk: string | Uint8Array, + ) => { + stderrWrites.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")); + return true; + }) as typeof process.stderr.write); process.on("warning", onWarning); try { @@ -135,7 +142,9 @@ describe("warning filter", () => { warning.code === "DEP0040" && warning.message === "The punycode module is deprecated.", ), ).toBeDefined(); + expect(stderrWrites.join("")).toContain("Visible warning"); } finally { + stderrWriteSpy.mockRestore(); process.off("warning", onWarning); } }); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 45710ef08bf..d442685a3ff 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -7,13 +7,13 @@ import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { withEnv } from "../test-utils/env.js"; async function importFreshPluginTestModules() { vi.resetModules(); - vi.unmock("node:fs"); - vi.unmock("node:fs/promises"); - vi.unmock("node:module"); - vi.unmock("./hook-runner-global.js"); - vi.unmock("./hooks.js"); - vi.unmock("./loader.js"); - vi.unmock("jiti"); + vi.doUnmock("node:fs"); + vi.doUnmock("node:fs/promises"); + vi.doUnmock("node:module"); + vi.doUnmock("./hook-runner-global.js"); + vi.doUnmock("./hooks.js"); + vi.doUnmock("./loader.js"); + vi.doUnmock("jiti"); const [loader, hookRunnerGlobal, hooks, runtime, registry] = await Promise.all([ import("./loader.js"), import("./hook-runner-global.js"), diff --git a/ui/src/i18n/lib/translate.ts b/ui/src/i18n/lib/translate.ts index fc18f36c8e5..11759bc6d8d 100644 --- a/ui/src/i18n/lib/translate.ts +++ b/ui/src/i18n/lib/translate.ts @@ -1,3 +1,4 @@ +import { getSafeLocalStorage } from "../../local-storage.ts"; import { en } from "../locales/en.ts"; import { DEFAULT_LOCALE, @@ -22,8 +23,8 @@ class I18nManager { } private readStoredLocale(): string | null { - const storage = globalThis.localStorage; - if (!storage || typeof storage.getItem !== "function") { + const storage = getSafeLocalStorage(); + if (!storage) { return null; } try { @@ -34,8 +35,8 @@ class I18nManager { } private persistLocale(locale: Locale) { - const storage = globalThis.localStorage; - if (!storage || typeof storage.setItem !== "function") { + const storage = getSafeLocalStorage(); + if (!storage) { return; } try { diff --git a/ui/src/i18n/test/translate.test.ts b/ui/src/i18n/test/translate.test.ts index d373d3a47c9..14344b9079b 100644 --- a/ui/src/i18n/test/translate.test.ts +++ b/ui/src/i18n/test/translate.test.ts @@ -92,6 +92,22 @@ describe("i18n", () => { expect(fresh.t("common.health")).toBe("健康状况"); }); + it("skips node localStorage accessors that warn without a storage file", async () => { + vi.resetModules(); + vi.unstubAllGlobals(); + vi.stubGlobal("navigator", { language: "en-US" } as Navigator); + const warningSpy = vi.spyOn(process, "emitWarning"); + + const fresh = await import("../lib/translate.ts"); + + expect(fresh.i18n.getLocale()).toBe("en"); + expect(warningSpy).not.toHaveBeenCalledWith( + "`--localstorage-file` was provided without a valid path", + expect.anything(), + expect.anything(), + ); + }); + it("keeps the version label available in shipped locales", () => { expect((pt_BR.common as { version?: string }).version).toBeTruthy(); expect((zh_CN.common as { version?: string }).version).toBeTruthy(); diff --git a/ui/src/local-storage.ts b/ui/src/local-storage.ts new file mode 100644 index 00000000000..a1e80d9d32a --- /dev/null +++ b/ui/src/local-storage.ts @@ -0,0 +1,25 @@ +function isStorage(value: unknown): value is Storage { + return ( + Boolean(value) && + typeof (value as Storage).getItem === "function" && + typeof (value as Storage).setItem === "function" + ); +} + +export function getSafeLocalStorage(): Storage | null { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage"); + + if (process.env.VITEST) { + return descriptor && !descriptor.get && isStorage(descriptor.value) ? descriptor.value : null; + } + + if (typeof window !== "undefined" && typeof document !== "undefined") { + try { + return isStorage(window.localStorage) ? window.localStorage : null; + } catch { + return null; + } + } + + return descriptor && !descriptor.get && isStorage(descriptor.value) ? descriptor.value : null; +} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 328f2cb6e33..11bcacae1ee 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -4,6 +4,7 @@ import { parseAgentSessionKey, } from "../../../src/routing/session-key.js"; import { t } from "../i18n/index.ts"; +import { getSafeLocalStorage } from "../local-storage.ts"; import { refreshChatAvatar } from "./app-chat.ts"; import { renderUsageTab } from "./app-render-usage-tab.ts"; import { @@ -181,7 +182,7 @@ type DismissedUpdateBanner = { function loadDismissedUpdateBanner(): DismissedUpdateBanner | null { try { - const raw = localStorage.getItem(UPDATE_BANNER_DISMISS_KEY); + const raw = getSafeLocalStorage()?.getItem(UPDATE_BANNER_DISMISS_KEY); if (!raw) { return null; } @@ -225,7 +226,7 @@ function dismissUpdateBanner(updateAvailable: unknown) { dismissedAtMs: Date.now(), }; try { - localStorage.setItem(UPDATE_BANNER_DISMISS_KEY, JSON.stringify(payload)); + getSafeLocalStorage()?.setItem(UPDATE_BANNER_DISMISS_KEY, JSON.stringify(payload)); } catch { // ignore } diff --git a/ui/src/ui/chat/deleted-messages.ts b/ui/src/ui/chat/deleted-messages.ts index 21094bb9e83..316b659baa8 100644 --- a/ui/src/ui/chat/deleted-messages.ts +++ b/ui/src/ui/chat/deleted-messages.ts @@ -1,3 +1,5 @@ +import { getSafeLocalStorage } from "../../local-storage.ts"; + const PREFIX = "openclaw:deleted:"; export class DeletedMessages { @@ -30,7 +32,7 @@ export class DeletedMessages { private load(): void { try { - const raw = localStorage.getItem(this.key); + const raw = getSafeLocalStorage()?.getItem(this.key); if (!raw) { return; } @@ -45,7 +47,7 @@ export class DeletedMessages { private save(): void { try { - localStorage.setItem(this.key, JSON.stringify([...this._keys])); + getSafeLocalStorage()?.setItem(this.key, JSON.stringify([...this._keys])); } catch { // ignore } diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 5b7549c8d64..7dcc0b62e19 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -1,5 +1,6 @@ import { html, nothing } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import { getSafeLocalStorage } from "../../local-storage.ts"; import type { AssistantIdentity } from "../assistant-identity.ts"; import { icons } from "../icons.ts"; import { toSanitizedMarkdownHtml } from "../markdown.ts"; @@ -322,7 +323,7 @@ type DeleteConfirmSide = "left" | "right"; function shouldSkipDeleteConfirm(): boolean { try { - return localStorage.getItem(SKIP_DELETE_CONFIRM_KEY) === "1"; + return getSafeLocalStorage()?.getItem(SKIP_DELETE_CONFIRM_KEY) === "1"; } catch { return false; } @@ -370,7 +371,7 @@ function renderDeleteButton(onDelete: () => void, side: DeleteConfirmSide) { yes.addEventListener("click", () => { if (check.checked) { try { - localStorage.setItem(SKIP_DELETE_CONFIRM_KEY, "1"); + getSafeLocalStorage()?.setItem(SKIP_DELETE_CONFIRM_KEY, "1"); } catch {} } popover.remove(); diff --git a/ui/src/ui/chat/pinned-messages.ts b/ui/src/ui/chat/pinned-messages.ts index a3e77a9483b..3bd7b9d6603 100644 --- a/ui/src/ui/chat/pinned-messages.ts +++ b/ui/src/ui/chat/pinned-messages.ts @@ -1,3 +1,5 @@ +import { getSafeLocalStorage } from "../../local-storage.ts"; + const PREFIX = "openclaw:pinned:"; export class PinnedMessages { @@ -42,7 +44,7 @@ export class PinnedMessages { private load(): void { try { - const raw = localStorage.getItem(this.key); + const raw = getSafeLocalStorage()?.getItem(this.key); if (!raw) { return; } @@ -57,7 +59,7 @@ export class PinnedMessages { private save(): void { try { - localStorage.setItem(this.key, JSON.stringify([...this._indices])); + getSafeLocalStorage()?.setItem(this.key, JSON.stringify([...this._indices])); } catch { // ignore } diff --git a/ui/src/ui/controllers/usage.ts b/ui/src/ui/controllers/usage.ts index 0fe257ae8e7..5862bd82e72 100644 --- a/ui/src/ui/controllers/usage.ts +++ b/ui/src/ui/controllers/usage.ts @@ -1,3 +1,4 @@ +import { getSafeLocalStorage } from "../../local-storage.ts"; import type { GatewayBrowserClient } from "../gateway.ts"; import type { SessionsUsageResult, CostUsageSummary, SessionUsageTimeSeries } from "../types.ts"; import type { SessionLogEntry } from "../views/usage.ts"; @@ -39,14 +40,7 @@ const LEGACY_USAGE_DATE_PARAMS_INVALID_RE = /invalid sessions\.usage params/i; let legacyUsageDateParamsCache: Set | null = null; function getLocalStorage(): Storage | null { - // Support browser runtime and node tests (when localStorage is stubbed globally). - if (typeof window !== "undefined" && window.localStorage) { - return window.localStorage; - } - if (typeof localStorage !== "undefined") { - return localStorage; - } - return null; + return getSafeLocalStorage(); } function loadLegacyUsageDateParamsCache(): Set { diff --git a/ui/src/ui/device-auth.ts b/ui/src/ui/device-auth.ts index 1adcf7deda9..1238a859f1c 100644 --- a/ui/src/ui/device-auth.ts +++ b/ui/src/ui/device-auth.ts @@ -5,12 +5,13 @@ import { storeDeviceAuthTokenInStore, } from "../../../src/shared/device-auth-store.js"; import type { DeviceAuthStore } from "../../../src/shared/device-auth.js"; +import { getSafeLocalStorage } from "../local-storage.ts"; const STORAGE_KEY = "openclaw.device.auth.v1"; function readStore(): DeviceAuthStore | null { try { - const raw = window.localStorage.getItem(STORAGE_KEY); + const raw = getSafeLocalStorage()?.getItem(STORAGE_KEY); if (!raw) { return null; } @@ -32,7 +33,7 @@ function readStore(): DeviceAuthStore | null { function writeStore(store: DeviceAuthStore) { try { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); + getSafeLocalStorage()?.setItem(STORAGE_KEY, JSON.stringify(store)); } catch { // best-effort } diff --git a/ui/src/ui/device-identity.ts b/ui/src/ui/device-identity.ts index 947b8185038..ff20c68649e 100644 --- a/ui/src/ui/device-identity.ts +++ b/ui/src/ui/device-identity.ts @@ -1,4 +1,5 @@ import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519"; +import { getSafeLocalStorage } from "../local-storage.ts"; type StoredIdentity = { version: 1; @@ -58,8 +59,9 @@ async function generateIdentity(): Promise { } export async function loadOrCreateDeviceIdentity(): Promise { + const storage = getSafeLocalStorage(); try { - const raw = localStorage.getItem(STORAGE_KEY); + const raw = storage?.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw) as StoredIdentity; if ( @@ -74,7 +76,7 @@ export async function loadOrCreateDeviceIdentity(): Promise { ...parsed, deviceId: derivedId, }; - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + storage?.setItem(STORAGE_KEY, JSON.stringify(updated)); return { deviceId: derivedId, publicKey: parsed.publicKey, @@ -100,7 +102,7 @@ export async function loadOrCreateDeviceIdentity(): Promise { privateKey: identity.privateKey, createdAtMs: Date.now(), }; - localStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); + storage?.setItem(STORAGE_KEY, JSON.stringify(stored)); return identity; } diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index 450c5124592..0b23b3436a4 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -16,6 +16,7 @@ type PersistedUiSettings = Omit = {}; try { - const raw = localStorage.getItem(KEY); + const raw = storage?.getItem(KEY); if (raw) { const parsed = JSON.parse(raw) as PersistedUiSettings; if (parsed.sessionsByGateway && typeof parsed.sessionsByGateway === "object") { @@ -291,5 +294,5 @@ function persistSettings(next: UiSettings) { sessionsByGateway, ...(next.locale ? { locale: next.locale } : {}), }; - localStorage.setItem(KEY, JSON.stringify(persisted)); + storage?.setItem(KEY, JSON.stringify(persisted)); } diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 860727c1927..ab55db6935f 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -2,6 +2,7 @@ import { render } from "lit"; import { describe, expect, it, vi } from "vitest"; +import { getSafeLocalStorage } from "../../local-storage.ts"; import { renderChatSessionSelect } from "../app-render.helpers.ts"; import type { AppViewState } from "../app-view-state.ts"; import type { GatewayBrowserClient } from "../gateway.ts"; @@ -482,7 +483,7 @@ describe("chat view", () => { it("opens delete confirm on the left for user messages", () => { try { - localStorage.removeItem("openclaw:skipDeleteConfirm"); + getSafeLocalStorage()?.removeItem("openclaw:skipDeleteConfirm"); } catch { /* noop */ } @@ -515,7 +516,7 @@ describe("chat view", () => { it("opens delete confirm on the right for assistant messages", () => { try { - localStorage.removeItem("openclaw:skipDeleteConfirm"); + getSafeLocalStorage()?.removeItem("openclaw:skipDeleteConfirm"); } catch { /* noop */ } From 350b42d3424e216d384744ac7fdd855d128fce97 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 21:01:31 -0700 Subject: [PATCH 10/14] Status: lazy-load text scan helpers --- src/commands/status.scan.runtime.ts | 2 ++ src/commands/status.scan.test.ts | 5 +++++ src/commands/status.scan.ts | 10 ++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/commands/status.scan.runtime.ts diff --git a/src/commands/status.scan.runtime.ts b/src/commands/status.scan.runtime.ts new file mode 100644 index 00000000000..372b31f4803 --- /dev/null +++ b/src/commands/status.scan.runtime.ts @@ -0,0 +1,2 @@ +export { collectChannelStatusIssues } from "../infra/channels-status-issues.js"; +export { buildChannelsTable } from "./status-all/channels.js"; diff --git a/src/commands/status.scan.test.ts b/src/commands/status.scan.test.ts index 122e10076bf..7dccbefb621 100644 --- a/src/commands/status.scan.test.ts +++ b/src/commands/status.scan.test.ts @@ -30,6 +30,11 @@ vi.mock("./status-all/channels.js", () => ({ buildChannelsTable: mocks.buildChannelsTable, })); +vi.mock("./status.scan.runtime.js", () => ({ + buildChannelsTable: mocks.buildChannelsTable, + collectChannelStatusIssues: vi.fn(() => []), +})); + vi.mock("./status.update.js", () => ({ getUpdateCheckResult: mocks.getUpdateCheckResult, })); diff --git a/src/commands/status.scan.ts b/src/commands/status.scan.ts index 7f1380964d5..64a17e2b371 100644 --- a/src/commands/status.scan.ts +++ b/src/commands/status.scan.ts @@ -7,14 +7,12 @@ import { readBestEffortConfig } from "../config/config.js"; import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js"; import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js"; import { probeGateway } from "../gateway/probe.js"; -import { collectChannelStatusIssues } from "../infra/channels-status-issues.js"; import { resolveOsSummary } from "../infra/os-summary.js"; import { getTailnetHostname } from "../infra/tailscale.js"; import { getMemorySearchManager } from "../memory/index.js"; import type { MemoryProviderStatus } from "../memory/types.js"; import { runExec } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; -import { buildChannelsTable } from "./status-all/channels.js"; import { getAgentLocalStatuses } from "./status.agent-local.js"; import { pickGatewaySelfPresence, @@ -48,12 +46,18 @@ type GatewayProbeSnapshot = { }; let pluginRegistryModulePromise: Promise | undefined; +let statusScanRuntimeModulePromise: Promise | undefined; function loadPluginRegistryModule() { pluginRegistryModulePromise ??= import("../cli/plugin-registry.js"); return pluginRegistryModulePromise; } +function loadStatusScanRuntimeModule() { + statusScanRuntimeModulePromise ??= import("./status.scan.runtime.js"); + return statusScanRuntimeModulePromise; +} + function deferResult(promise: Promise): Promise> { return promise.then( (value) => ({ ok: true, value }), @@ -360,6 +364,8 @@ export async function scanStatus( progress.setLabel("Querying channel status…"); const channelsStatus = await resolveChannelsStatus({ cfg, gatewayReachable, opts }); + const { collectChannelStatusIssues, buildChannelsTable } = + await loadStatusScanRuntimeModule(); const channelIssues = channelsStatus ? collectChannelStatusIssues(channelsStatus) : []; progress.tick(); From 53ccc78c636322f2b17649e83e67862d913dda9c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Mar 2026 21:06:55 -0700 Subject: [PATCH 11/14] refactor: rename setup helper surfaces --- extensions/feishu/src/onboarding.ts | 7 ------- ...ng.status.test.ts => setup-status.test.ts} | 0 ...boarding.test.ts => setup-surface.test.ts} | 0 ...boarding.test.ts => setup-surface.test.ts} | 2 +- ...boarding.test.ts => setup-surface.test.ts} | 0 ...ng.status.test.ts => setup-status.test.ts} | 0 .../plugins/setup-flow-helpers.test.ts | 2 +- src/channels/plugins/setup-flow-helpers.ts | 2 +- src/channels/plugins/setup-flow-types.ts | 4 ++-- src/commands/channels/add.ts | 4 ++-- src/commands/onboard-channels.ts | 20 +++++++++---------- src/plugin-sdk/{onboarding.ts => setup.ts} | 0 12 files changed, 16 insertions(+), 25 deletions(-) delete mode 100644 extensions/feishu/src/onboarding.ts rename extensions/feishu/src/{onboarding.status.test.ts => setup-status.test.ts} (100%) rename extensions/feishu/src/{onboarding.test.ts => setup-surface.test.ts} (100%) rename extensions/irc/src/{onboarding.test.ts => setup-surface.test.ts} (98%) rename extensions/whatsapp/src/{onboarding.test.ts => setup-surface.test.ts} (100%) rename extensions/zalo/src/{onboarding.status.test.ts => setup-status.test.ts} (100%) rename src/plugin-sdk/{onboarding.ts => setup.ts} (100%) diff --git a/extensions/feishu/src/onboarding.ts b/extensions/feishu/src/onboarding.ts deleted file mode 100644 index ae247b30f76..00000000000 --- a/extensions/feishu/src/onboarding.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { buildChannelSetupFlowAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; -import { feishuPlugin } from "./channel.js"; - -export const feishuOnboardingAdapter = buildChannelSetupFlowAdapterFromSetupWizard({ - plugin: feishuPlugin, - wizard: feishuPlugin.setupWizard!, -}); diff --git a/extensions/feishu/src/onboarding.status.test.ts b/extensions/feishu/src/setup-status.test.ts similarity index 100% rename from extensions/feishu/src/onboarding.status.test.ts rename to extensions/feishu/src/setup-status.test.ts diff --git a/extensions/feishu/src/onboarding.test.ts b/extensions/feishu/src/setup-surface.test.ts similarity index 100% rename from extensions/feishu/src/onboarding.test.ts rename to extensions/feishu/src/setup-surface.test.ts diff --git a/extensions/irc/src/onboarding.test.ts b/extensions/irc/src/setup-surface.test.ts similarity index 98% rename from extensions/irc/src/onboarding.test.ts rename to extensions/irc/src/setup-surface.test.ts index 883f15fe1b1..92cca5f0f35 100644 --- a/extensions/irc/src/onboarding.test.ts +++ b/extensions/irc/src/setup-surface.test.ts @@ -33,7 +33,7 @@ const ircConfigureAdapter = buildChannelSetupFlowAdapterFromSetupWizard({ }); describe("irc setup wizard", () => { - it("configures host and nick via onboarding prompts", async () => { + it("configures host and nick via setup prompts", async () => { const prompter = createPrompter({ text: vi.fn(async ({ message }: { message: string }) => { if (message === "IRC server host") { diff --git a/extensions/whatsapp/src/onboarding.test.ts b/extensions/whatsapp/src/setup-surface.test.ts similarity index 100% rename from extensions/whatsapp/src/onboarding.test.ts rename to extensions/whatsapp/src/setup-surface.test.ts diff --git a/extensions/zalo/src/onboarding.status.test.ts b/extensions/zalo/src/setup-status.test.ts similarity index 100% rename from extensions/zalo/src/onboarding.status.test.ts rename to extensions/zalo/src/setup-status.test.ts diff --git a/src/channels/plugins/setup-flow-helpers.test.ts b/src/channels/plugins/setup-flow-helpers.test.ts index 3b24600372c..d13ce6a3b6b 100644 --- a/src/channels/plugins/setup-flow-helpers.test.ts +++ b/src/channels/plugins/setup-flow-helpers.test.ts @@ -3,7 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; const promptAccountIdSdkMock = vi.hoisted(() => vi.fn(async () => "default")); -vi.mock("../../plugin-sdk/onboarding.js", () => ({ +vi.mock("../../plugin-sdk/setup.js", () => ({ promptAccountId: promptAccountIdSdkMock, })); diff --git a/src/channels/plugins/setup-flow-helpers.ts b/src/channels/plugins/setup-flow-helpers.ts index 87a208a9a21..b0519b8f35d 100644 --- a/src/channels/plugins/setup-flow-helpers.ts +++ b/src/channels/plugins/setup-flow-helpers.ts @@ -5,7 +5,7 @@ import { import type { OpenClawConfig } from "../../config/config.js"; import type { DmPolicy, GroupPolicy } from "../../config/types.js"; import type { SecretInput } from "../../config/types.secrets.js"; -import { promptAccountId as promptAccountIdSdk } from "../../plugin-sdk/onboarding.js"; +import { promptAccountId as promptAccountIdSdk } from "../../plugin-sdk/setup.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; import type { PromptAccountId, PromptAccountIdParams } from "./setup-flow-types.js"; diff --git a/src/channels/plugins/setup-flow-types.ts b/src/channels/plugins/setup-flow-types.ts index a3887cc7ef2..53766d72af6 100644 --- a/src/channels/plugins/setup-flow-types.ts +++ b/src/channels/plugins/setup-flow-types.ts @@ -4,7 +4,7 @@ import type { RuntimeEnv } from "../../runtime.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; import type { ChannelId, ChannelPlugin } from "./types.js"; -export type ChannelOnboardingSetupPlugin = Pick< +export type ChannelSetupPlugin = Pick< ChannelPlugin, "id" | "meta" | "capabilities" | "config" | "setup" | "setupWizard" >; @@ -15,7 +15,7 @@ export type SetupChannelsOptions = { onSelection?: (selection: ChannelId[]) => void; accountIds?: Partial>; onAccountId?: (channel: ChannelId, accountId: string) => void; - onResolvedPlugin?: (channel: ChannelId, plugin: ChannelOnboardingSetupPlugin) => void; + onResolvedPlugin?: (channel: ChannelId, plugin: ChannelSetupPlugin) => void; promptAccountIds?: boolean; whatsappAccountId?: string; promptWhatsAppAccountId?: boolean; diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index d4175cf100b..b4f8205ae3a 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -2,7 +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/setup-flow-types.js"; +import type { ChannelSetupPlugin } from "../../channels/plugins/setup-flow-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"; @@ -57,7 +57,7 @@ export async function channelsAddCommand( const prompter = createClackPrompter(); let selection: ChannelChoice[] = []; const accountIds: Partial> = {}; - const resolvedPlugins = new Map(); + const resolvedPlugins = new Map(); await prompter.intro("Channel setup"); let nextConfig = await setupChannels(cfg, runtime, prompter, { allowDisable: false, diff --git a/src/commands/onboard-channels.ts b/src/commands/onboard-channels.ts index 67c78e7a72c..564e056b053 100644 --- a/src/commands/onboard-channels.ts +++ b/src/commands/onboard-channels.ts @@ -1,7 +1,7 @@ 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/setup-flow-types.js"; +import type { ChannelSetupPlugin } from "../channels/plugins/setup-flow-types.js"; import { getChannelSetupPlugin, listChannelSetupPlugins, @@ -91,7 +91,7 @@ async function promptRemovalAccountId(params: { prompter: WizardPrompter; label: string; channel: ChannelChoice; - plugin?: ChannelOnboardingSetupPlugin; + plugin?: ChannelSetupPlugin; }): Promise { const { cfg, prompter, label, channel } = params; const plugin = params.plugin ?? getChannelSetupPlugin(channel); @@ -118,7 +118,7 @@ async function collectChannelStatus(params: { cfg: OpenClawConfig; options?: SetupChannelsOptions; accountOverrides: Partial>; - installedPlugins?: ChannelOnboardingSetupPlugin[]; + installedPlugins?: ChannelSetupPlugin[]; resolveAdapter?: (channel: ChannelChoice) => ChannelSetupFlowAdapter | undefined; }): Promise { const installedPlugins = params.installedPlugins ?? listChannelSetupPlugins(); @@ -347,19 +347,17 @@ export async function setupChannels( const accountOverrides: Partial> = { ...options?.accountIds, }; - const scopedPluginsById = new Map(); + const scopedPluginsById = new Map(); const resolveWorkspaceDir = () => resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next)); - const rememberScopedPlugin = (plugin: ChannelOnboardingSetupPlugin) => { + const rememberScopedPlugin = (plugin: ChannelSetupPlugin) => { const channel = plugin.id; scopedPluginsById.set(channel, plugin); options?.onResolvedPlugin?.(channel, plugin); }; - const getVisibleChannelPlugin = ( - channel: ChannelChoice, - ): ChannelOnboardingSetupPlugin | undefined => + const getVisibleChannelPlugin = (channel: ChannelChoice): ChannelSetupPlugin | undefined => scopedPluginsById.get(channel) ?? getChannelSetupPlugin(channel); - const listVisibleInstalledPlugins = (): ChannelOnboardingSetupPlugin[] => { - const merged = new Map(); + const listVisibleInstalledPlugins = (): ChannelSetupPlugin[] => { + const merged = new Map(); for (const plugin of listChannelSetupPlugins()) { merged.set(plugin.id, plugin); } @@ -371,7 +369,7 @@ export async function setupChannels( const loadScopedChannelPlugin = async ( channel: ChannelChoice, pluginId?: string, - ): Promise => { + ): Promise => { const existing = getVisibleChannelPlugin(channel); if (existing) { return existing; diff --git a/src/plugin-sdk/onboarding.ts b/src/plugin-sdk/setup.ts similarity index 100% rename from src/plugin-sdk/onboarding.ts rename to src/plugin-sdk/setup.ts From 0f43dc46808bca5cd9ff355469030ad322c6044e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Mar 2026 21:07:05 -0700 Subject: [PATCH 12/14] test: fix fetch mock typing --- extensions/msteams/src/graph-upload.test.ts | 7 +++--- src/agents/model-auth.test.ts | 23 +++++++++++-------- src/infra/fetch.test.ts | 2 +- src/infra/provider-usage.fetch.shared.test.ts | 5 ++-- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/extensions/msteams/src/graph-upload.test.ts b/extensions/msteams/src/graph-upload.test.ts index 484075984dd..b79086f54ca 100644 --- a/extensions/msteams/src/graph-upload.test.ts +++ b/extensions/msteams/src/graph-upload.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { withFetchPreconnect } from "../../../src/test-utils/fetch-mock.js"; import { uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js"; describe("graph upload helpers", () => { @@ -22,7 +23,7 @@ describe("graph upload helpers", () => { buffer: Buffer.from("hello"), filename: "a.txt", tokenProvider, - fetchFn: fetchFn as typeof fetch, + fetchFn: withFetchPreconnect(fetchFn), }); expect(fetchFn).toHaveBeenCalledWith( @@ -59,7 +60,7 @@ describe("graph upload helpers", () => { filename: "b.txt", siteId: "site-123", tokenProvider, - fetchFn: fetchFn as typeof fetch, + fetchFn: withFetchPreconnect(fetchFn), }); expect(fetchFn).toHaveBeenCalledWith( @@ -94,7 +95,7 @@ describe("graph upload helpers", () => { filename: "bad.txt", siteId: "site-123", tokenProvider, - fetchFn: fetchFn as typeof fetch, + fetchFn: withFetchPreconnect(fetchFn), }), ).rejects.toThrow("SharePoint upload response missing required fields"); }); diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index de8f0f1b752..31fdee5496c 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -1,5 +1,6 @@ import { streamSimpleOpenAICompletions, type Model } from "@mariozechner/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { withFetchPreconnect } from "../test-utils/fetch-mock.js"; import type { AuthProfileStore } from "./auth-profiles.js"; import { CUSTOM_LOCAL_AUTH_MARKER, NON_ENV_SECRETREF_MARKER } from "./model-auth-markers.js"; import { @@ -503,16 +504,18 @@ describe("applyLocalNoAuthHeaderOverride", () => { const requestSeen = new Promise((resolve) => { resolveRequest = resolve; }); - globalThis.fetch = vi.fn(async (_input, init) => { - const headers = new Headers(init?.headers); - capturedAuthorization = headers.get("Authorization"); - capturedXTest = headers.get("X-Test"); - resolveRequest?.(); - return new Response(JSON.stringify({ error: { message: "unauthorized" } }), { - status: 401, - headers: { "content-type": "application/json" }, - }); - }) as typeof fetch; + globalThis.fetch = withFetchPreconnect( + vi.fn(async (_input, init) => { + const headers = new Headers(init?.headers); + capturedAuthorization = headers.get("Authorization"); + capturedXTest = headers.get("X-Test"); + resolveRequest?.(); + return new Response(JSON.stringify({ error: { message: "unauthorized" } }), { + status: 401, + headers: { "content-type": "application/json" }, + }); + }), + ); const model = applyLocalNoAuthHeaderOverride( { diff --git a/src/infra/fetch.test.ts b/src/infra/fetch.test.ts index deef81f551f..820325c0e70 100644 --- a/src/infra/fetch.test.ts +++ b/src/infra/fetch.test.ts @@ -293,7 +293,7 @@ describe("wrapFetchWithAbortSignal", () => { }); it("exposes a no-op preconnect when the source fetch has none", () => { - const fetchImpl = vi.fn(async () => ({ ok: true }) as Response) as typeof fetch; + const fetchImpl = withFetchPreconnect(vi.fn(async () => ({ ok: true }) as Response)); const wrapped = wrapFetchWithAbortSignal(fetchImpl) as typeof fetch & { preconnect: (url: string, init?: { credentials?: RequestCredentials }) => unknown; }; diff --git a/src/infra/provider-usage.fetch.shared.test.ts b/src/infra/provider-usage.fetch.shared.test.ts index 692a57705db..b287f1fad04 100644 --- a/src/infra/provider-usage.fetch.shared.test.ts +++ b/src/infra/provider-usage.fetch.shared.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { withFetchPreconnect } from "../test-utils/fetch-mock.js"; import { buildUsageErrorSnapshot, buildUsageHttpErrorSnapshot, @@ -36,7 +37,7 @@ describe("provider usage fetch shared helpers", () => { async (_input: URL | RequestInfo, init?: RequestInit) => new Response(JSON.stringify({ aborted: init?.signal?.aborted ?? false }), { status: 200 }), ); - const fetchFn = fetchFnMock as typeof fetch; + const fetchFn = withFetchPreconnect(fetchFnMock); const response = await fetchJson( "https://example.com/usage", @@ -71,7 +72,7 @@ describe("provider usage fetch shared helpers", () => { }); }), ); - const fetchFn = fetchFnMock as typeof fetch; + const fetchFn = withFetchPreconnect(fetchFnMock); const request = fetchJson("https://example.com/usage", {}, 50, fetchFn); const rejection = expect(request).rejects.toThrow("aborted by timeout"); From c4a5fd8465cbee23e2f3b6986c16f448763b34ee Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Mar 2026 21:07:18 -0700 Subject: [PATCH 13/14] docs: update channel setup wording --- docs/channels/feishu.md | 4 ++-- docs/channels/matrix.md | 4 ++-- docs/channels/mattermost.md | 2 +- docs/channels/msteams.md | 2 +- docs/channels/nextcloud-talk.md | 4 ++-- docs/channels/nostr.md | 2 +- docs/channels/telegram.md | 2 +- docs/channels/whatsapp.md | 2 +- docs/channels/zalo.md | 6 +++--- docs/channels/zalouser.md | 4 ++-- docs/tools/plugin.md | 2 +- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/channels/feishu.md b/docs/channels/feishu.md index 2fc16aed5d4..3768906d940 100644 --- a/docs/channels/feishu.md +++ b/docs/channels/feishu.md @@ -30,9 +30,9 @@ openclaw plugins install @openclaw/feishu There are two ways to add the Feishu channel: -### Method 1: onboarding wizard (recommended) +### Method 1: setup wizard (recommended) -If you just installed OpenClaw, run the wizard: +If you just installed OpenClaw, run the setup wizard: ```bash openclaw onboard diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 9bb56d1ddb7..1536a7c08ac 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -31,7 +31,7 @@ Local checkout (when running from a git repo): openclaw plugins install ./extensions/matrix ``` -If you choose Matrix during configure/onboarding and a git checkout is detected, +If you choose Matrix during setup and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) @@ -72,7 +72,7 @@ Details: [Plugins](/tools/plugin) - If both are set, config takes precedence. - With access token: user ID is fetched automatically via `/whoami`. - When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`). -5. Restart the gateway (or finish onboarding). +5. Restart the gateway (or finish setup). 6. Start a DM with the bot or invite it to a room from any Matrix client (Element, Beeper, etc.; see [https://matrix.org/ecosystem/clients/](https://matrix.org/ecosystem/clients/)). Beeper requires E2EE, so set `channels.matrix.encryption: true` and verify the device. diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md index 1e3e3f4bad2..2ceb6c17626 100644 --- a/docs/channels/mattermost.md +++ b/docs/channels/mattermost.md @@ -28,7 +28,7 @@ Local checkout (when running from a git repo): openclaw plugins install ./extensions/mattermost ``` -If you choose Mattermost during configure/onboarding and a git checkout is detected, +If you choose Mattermost during setup and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md index a24f20c69df..88cba3ce6aa 100644 --- a/docs/channels/msteams.md +++ b/docs/channels/msteams.md @@ -33,7 +33,7 @@ Local checkout (when running from a git repo): openclaw plugins install ./extensions/msteams ``` -If you choose Teams during configure/onboarding and a git checkout is detected, +If you choose Teams during setup and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) diff --git a/docs/channels/nextcloud-talk.md b/docs/channels/nextcloud-talk.md index 7797b1276ff..f8be8d74f0c 100644 --- a/docs/channels/nextcloud-talk.md +++ b/docs/channels/nextcloud-talk.md @@ -25,7 +25,7 @@ Local checkout (when running from a git repo): openclaw plugins install ./extensions/nextcloud-talk ``` -If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected, +If you choose Nextcloud Talk during setup and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) @@ -43,7 +43,7 @@ Details: [Plugins](/tools/plugin) 4. Configure OpenClaw: - Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret` - Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only) -5. Restart the gateway (or finish onboarding). +5. Restart the gateway (or finish setup). Minimal config: diff --git a/docs/channels/nostr.md b/docs/channels/nostr.md index 760704b589f..46888da0352 100644 --- a/docs/channels/nostr.md +++ b/docs/channels/nostr.md @@ -16,7 +16,7 @@ Nostr is a decentralized protocol for social networking. This channel enables Op ### Onboarding (recommended) -- The onboarding wizard (`openclaw onboard`) and `openclaw channels add` list optional channel plugins. +- The setup wizard (`openclaw onboard`) and `openclaw channels add` list optional channel plugins. - Selecting Nostr prompts you to install the plugin on demand. Install defaults: diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 37be3bf1111..b5700213830 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -115,7 +115,7 @@ Token resolution order is account-aware. In practice, config values win over env `channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized. `dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation. - The onboarding wizard accepts `@username` input and resolves it to numeric IDs. + The setup wizard accepts `@username` input and resolves it to numeric IDs. If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token). If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can recover entries into `channels.telegram.allowFrom` in allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet). diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index cad9fe77ee3..850d88ffcac 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -76,7 +76,7 @@ openclaw pairing approve whatsapp -OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and onboarding flow are optimized for that setup, but personal-number setups are also supported.) +OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and setup flow are optimized for that setup, but personal-number setups are also supported.) ## Deployment patterns diff --git a/docs/channels/zalo.md b/docs/channels/zalo.md index cf53b574e42..b327f596f74 100644 --- a/docs/channels/zalo.md +++ b/docs/channels/zalo.md @@ -14,7 +14,7 @@ Status: experimental. DMs are supported. The [Capabilities](#capabilities) secti Zalo ships as a plugin and is not bundled with the core install. - Install via CLI: `openclaw plugins install @openclaw/zalo` -- Or select **Zalo** during onboarding and confirm the install prompt +- Or select **Zalo** during setup and confirm the install prompt - Details: [Plugins](/tools/plugin) ## Quick setup (beginner) @@ -22,11 +22,11 @@ Zalo ships as a plugin and is not bundled with the core install. 1. Install the Zalo plugin: - From a source checkout: `openclaw plugins install ./extensions/zalo` - From npm (if published): `openclaw plugins install @openclaw/zalo` - - Or pick **Zalo** in onboarding and confirm the install prompt + - Or pick **Zalo** in setup and confirm the install prompt 2. Set the token: - Env: `ZALO_BOT_TOKEN=...` - Or config: `channels.zalo.accounts.default.botToken: "..."`. -3. Restart the gateway (or finish onboarding). +3. Restart the gateway (or finish setup). 4. DM access is pairing by default; approve the pairing code on first contact. Minimal config: diff --git a/docs/channels/zalouser.md b/docs/channels/zalouser.md index 58bd2a43923..4847430c8ac 100644 --- a/docs/channels/zalouser.md +++ b/docs/channels/zalouser.md @@ -41,7 +41,7 @@ No external `zca`/`openzca` CLI binary is required. } ``` -4. Restart the Gateway (or finish onboarding). +4. Restart the Gateway (or finish setup). 5. DM access defaults to pairing; approve the pairing code on first contact. ## What it is @@ -74,7 +74,7 @@ openclaw directory groups list --channel zalouser --query "work" `channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`). -`channels.zalouser.allowFrom` accepts user IDs or names. During onboarding, names are resolved to IDs using the plugin's in-process contact lookup. +`channels.zalouser.allowFrom` accepts user IDs or names. During setup, names are resolved to IDs using the plugin's in-process contact lookup. Approve via: diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 62350fb9dd4..8be0743c57c 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -800,7 +800,7 @@ trees "pure JS/TS" and avoid packages that require `postinstall` builds. Optional: `openclaw.setupEntry` can point at a lightweight setup-only module. When OpenClaw needs setup surfaces for a disabled channel plugin, or when a channel plugin is enabled but still unconfigured, it loads `setupEntry` -instead of the full plugin entry. This keeps startup and onboarding lighter +instead of the full plugin entry. This keeps startup and setup lighter when your main plugin entry also wires tools, hooks, or other runtime-only code. From 093e51f2b35b90d6ed6b8ea808835ab30dac98e6 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 21:08:41 -0700 Subject: [PATCH 14/14] Security: lazy-load channel audit provider helpers --- src/security/audit-channel.runtime.ts | 9 ++++ src/security/audit-channel.ts | 77 ++++++++++++++++----------- 2 files changed, 56 insertions(+), 30 deletions(-) create mode 100644 src/security/audit-channel.runtime.ts diff --git a/src/security/audit-channel.runtime.ts b/src/security/audit-channel.runtime.ts new file mode 100644 index 00000000000..147f686862a --- /dev/null +++ b/src/security/audit-channel.runtime.ts @@ -0,0 +1,9 @@ +export { + isNumericTelegramUserId, + normalizeTelegramAllowFromEntry, +} from "../../extensions/telegram/src/allow-from.js"; +export { readChannelAllowFromStore } from "../pairing/pairing-store.js"; +export { + isDiscordMutableAllowEntry, + isZalouserMutableGroupEntry, +} from "./mutable-allowlist-detectors.js"; diff --git a/src/security/audit-channel.ts b/src/security/audit-channel.ts index ce1484f6513..56f3b139f87 100644 --- a/src/security/audit-channel.ts +++ b/src/security/audit-channel.ts @@ -1,7 +1,3 @@ -import { - isNumericTelegramUserId, - normalizeTelegramAllowFromEntry, -} from "../../extensions/telegram/src/allow-from.js"; import { hasConfiguredUnavailableCredentialStatus, hasResolvedCredentialValue, @@ -15,14 +11,18 @@ import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../con import type { OpenClawConfig } from "../config/config.js"; import { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; import { formatErrorMessage } from "../infra/errors.js"; -import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { normalizeStringEntries } from "../shared/string-normalization.js"; import type { SecurityAuditFinding, SecurityAuditSeverity } from "./audit.js"; import { resolveDmAllowState } from "./dm-policy-shared.js"; -import { - isDiscordMutableAllowEntry, - isZalouserMutableGroupEntry, -} from "./mutable-allowlist-detectors.js"; + +let auditChannelRuntimeModulePromise: + | Promise + | undefined; + +function loadAuditChannelRuntimeModule() { + auditChannelRuntimeModulePromise ??= import("./audit-channel.runtime.js"); + return auditChannelRuntimeModulePromise; +} function normalizeAllowFromList(list: Array | undefined | null): string[] { return normalizeStringEntries(Array.isArray(list) ? list : undefined); @@ -32,12 +32,13 @@ function addDiscordNameBasedEntries(params: { target: Set; values: unknown; source: string; + isDiscordMutableAllowEntry: (value: string) => boolean; }): void { if (!Array.isArray(params.values)) { return; } for (const value of params.values) { - if (!isDiscordMutableAllowEntry(String(value))) { + if (!params.isDiscordMutableAllowEntry(String(value))) { continue; } const text = String(value).trim(); @@ -52,25 +53,28 @@ function addZalouserMutableGroupEntries(params: { target: Set; groups: unknown; source: string; + isZalouserMutableGroupEntry: (value: string) => boolean; }): void { if (!params.groups || typeof params.groups !== "object" || Array.isArray(params.groups)) { return; } for (const key of Object.keys(params.groups as Record)) { - if (!isZalouserMutableGroupEntry(key)) { + if (!params.isZalouserMutableGroupEntry(key)) { continue; } params.target.add(`${params.source}:${key}`); } } -function collectInvalidTelegramAllowFromEntries(params: { +async function collectInvalidTelegramAllowFromEntries(params: { entries: unknown; target: Set; -}): void { +}): Promise { if (!Array.isArray(params.entries)) { return; } + const { isNumericTelegramUserId, normalizeTelegramAllowFromEntry } = + await loadAuditChannelRuntimeModule(); for (const entry of params.entries) { const normalized = normalizeTelegramAllowFromEntry(entry); if (!normalized || normalized === "*") { @@ -383,6 +387,8 @@ export async function collectChannelSecurityFindings(params: { } if (plugin.id === "discord") { + const { isDiscordMutableAllowEntry, readChannelAllowFromStore } = + await loadAuditChannelRuntimeModule(); const discordCfg = (account as { config?: Record } | null)?.config ?? ({} as Record); @@ -401,16 +407,19 @@ export async function collectChannelSecurityFindings(params: { target: discordNameBasedAllowEntries, values: discordCfg.allowFrom, source: `${discordPathPrefix}.allowFrom`, + isDiscordMutableAllowEntry, }); addDiscordNameBasedEntries({ target: discordNameBasedAllowEntries, values: (discordCfg.dm as { allowFrom?: unknown } | undefined)?.allowFrom, source: `${discordPathPrefix}.dm.allowFrom`, + isDiscordMutableAllowEntry, }); addDiscordNameBasedEntries({ target: discordNameBasedAllowEntries, values: storeAllowFrom, source: "~/.openclaw/credentials/discord-allowFrom.json", + isDiscordMutableAllowEntry, }); const discordGuildEntries = (discordCfg.guilds as Record | undefined) ?? {}; @@ -423,6 +432,7 @@ export async function collectChannelSecurityFindings(params: { target: discordNameBasedAllowEntries, values: guild.users, source: `${discordPathPrefix}.guilds.${guildKey}.users`, + isDiscordMutableAllowEntry, }); const channels = guild.channels; if (!channels || typeof channels !== "object") { @@ -439,6 +449,7 @@ export async function collectChannelSecurityFindings(params: { target: discordNameBasedAllowEntries, values: channel.users, source: `${discordPathPrefix}.guilds.${guildKey}.channels.${channelKey}.users`, + isDiscordMutableAllowEntry, }); } } @@ -547,6 +558,7 @@ export async function collectChannelSecurityFindings(params: { } if (plugin.id === "zalouser") { + const { isZalouserMutableGroupEntry } = await loadAuditChannelRuntimeModule(); const zalouserCfg = (account as { config?: Record } | null)?.config ?? ({} as Record); @@ -560,6 +572,7 @@ export async function collectChannelSecurityFindings(params: { target: mutableGroupEntries, groups: zalouserCfg.groups, source: `${zalouserPathPrefix}.groups`, + isZalouserMutableGroupEntry, }); if (mutableGroupEntries.size > 0) { const examples = Array.from(mutableGroupEntries).slice(0, 5); @@ -586,6 +599,7 @@ export async function collectChannelSecurityFindings(params: { } if (plugin.id === "slack") { + const { readChannelAllowFromStore } = await loadAuditChannelRuntimeModule(); const slackCfg = (account as { config?: Record; dm?: Record } | null) ?.config ?? ({} as Record); @@ -724,6 +738,7 @@ export async function collectChannelSecurityFindings(params: { continue; } + const { readChannelAllowFromStore } = await loadAuditChannelRuntimeModule(); const storeAllowFrom = await readChannelAllowFromStore( "telegram", process.env, @@ -731,7 +746,7 @@ export async function collectChannelSecurityFindings(params: { ).catch(() => []); const storeHasWildcard = storeAllowFrom.some((v) => String(v).trim() === "*"); const invalidTelegramAllowFromEntries = new Set(); - collectInvalidTelegramAllowFromEntries({ + await collectInvalidTelegramAllowFromEntries({ entries: storeAllowFrom, target: invalidTelegramAllowFromEntries, }); @@ -739,48 +754,50 @@ export async function collectChannelSecurityFindings(params: { ? telegramCfg.groupAllowFrom : []; const groupAllowFromHasWildcard = groupAllowFrom.some((v) => String(v).trim() === "*"); - collectInvalidTelegramAllowFromEntries({ + await collectInvalidTelegramAllowFromEntries({ entries: groupAllowFrom, target: invalidTelegramAllowFromEntries, }); const dmAllowFrom = Array.isArray(telegramCfg.allowFrom) ? telegramCfg.allowFrom : []; - collectInvalidTelegramAllowFromEntries({ + await collectInvalidTelegramAllowFromEntries({ entries: dmAllowFrom, target: invalidTelegramAllowFromEntries, }); - const anyGroupOverride = Boolean( - groups && - Object.values(groups).some((value) => { + let anyGroupOverride = false; + if (groups) { + for (const value of Object.values(groups)) { if (!value || typeof value !== "object") { - return false; + continue; } const group = value as Record; const allowFrom = Array.isArray(group.allowFrom) ? group.allowFrom : []; if (allowFrom.length > 0) { - collectInvalidTelegramAllowFromEntries({ + anyGroupOverride = true; + await collectInvalidTelegramAllowFromEntries({ entries: allowFrom, target: invalidTelegramAllowFromEntries, }); - return true; } const topics = group.topics; if (!topics || typeof topics !== "object") { - return false; + continue; } - return Object.values(topics as Record).some((topicValue) => { + for (const topicValue of Object.values(topics as Record)) { if (!topicValue || typeof topicValue !== "object") { - return false; + continue; } const topic = topicValue as Record; const topicAllow = Array.isArray(topic.allowFrom) ? topic.allowFrom : []; - collectInvalidTelegramAllowFromEntries({ + if (topicAllow.length > 0) { + anyGroupOverride = true; + } + await collectInvalidTelegramAllowFromEntries({ entries: topicAllow, target: invalidTelegramAllowFromEntries, }); - return topicAllow.length > 0; - }); - }), - ); + } + } + } const hasAnySenderAllowlist = storeAllowFrom.length > 0 || groupAllowFrom.length > 0 || anyGroupOverride;