Merge branch 'main' into fix/token-usage-input-output-breakdown

This commit is contained in:
jiarung 2026-03-16 12:13:03 +08:00 committed by GitHub
commit fc2aeb017c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
89 changed files with 3818 additions and 894 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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}

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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).

View File

@ -76,7 +76,7 @@ openclaw pairing approve whatsapp <CODE>
</Steps>
<Note>
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.)
</Note>
## Deployment patterns

View File

@ -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:

View File

@ -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:

View File

@ -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`,

View File

@ -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`,
@ -778,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.

View File

@ -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<ProviderAuthResult> {
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),

View File

@ -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!,
});

View File

@ -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,

View File

@ -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, {

View File

@ -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) {

View File

@ -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({

View File

@ -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,

View File

@ -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");
}

View File

@ -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") {

View File

@ -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),
});
},
};

View File

@ -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");
});

View File

@ -0,0 +1 @@
export const DEFAULT_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"];

View File

@ -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
// ============================================================================

View File

@ -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;

View File

@ -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;

View File

@ -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") {

View File

@ -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;

View File

@ -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) {

View File

@ -19,6 +19,7 @@ const opencodeGoPlugin = {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
},
isModernModelRef: () => true,
});
},
};

View File

@ -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),
});
},
};

View File

@ -110,6 +110,7 @@ const openRouterPlugin = {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
},
isModernModelRef: () => true,
wrapStreamFn: (ctx) => {
let streamFn = ctx.streamFn;
const providerRouting =

View File

@ -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"],

View File

@ -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);
}

View File

@ -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<void>((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(
{

View File

@ -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<Api> =>
({
@ -32,43 +39,6 @@ function supportsStrictMode(model: Model<Api>): boolean | undefined {
return (model.compat as { supportsStrictMode?: boolean } | undefined)?.supportsStrictMode;
}
function createTemplateModel(provider: string, id: string): Model<Api> {
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<Api>;
}
function createOpenAITemplateModel(id: string): Model<Api> {
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<Api>;
}
function createRegistry(models: Record<string, Model<Api>>): ModelRegistry {
return {
find(provider: string, modelId: string) {
return models[`${provider}/${modelId}`] ?? null;
},
} as ModelRegistry;
}
function expectSupportsDeveloperRoleForcedOff(overrides?: Partial<Model<Api>>): void {
const model = { ...baseModel(), ...overrides };
delete (model as { compat?: unknown }).compat;
@ -90,14 +60,10 @@ function expectSupportsStrictModeForcedOff(overrides?: Partial<Model<Api>>): voi
expect(supportsStrictMode(normalized)).toBe(false);
}
function expectResolvedForwardCompat(
model: Model<Api> | 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<Api> =>
@ -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();
});
});

View File

@ -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<Api>>;
}): Model<Api> | undefined {
const { normalizedProvider, trimmedModelId, templateIds, modelRegistry } = params;
for (const templateId of [...new Set(templateIds)].filter(Boolean)) {
const template = modelRegistry.find(normalizedProvider, templateId) as Model<Api> | null;
if (!template) {
continue;
}
return normalizeModelCompat({
...template,
id: trimmedModelId,
name: trimmedModelId,
...params.patch,
} as Model<Api>);
}
return undefined;
}
function resolveGoogle31ForwardCompatModel(
provider: string,
modelId: string,
modelRegistry: ModelRegistry,
): Model<Api> | 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<Api> | 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<Api> | null;
if (!template) {
continue;
}
return normalizeModelCompat({
...template,
id: trimmed,
name: trimmed,
reasoning: true,
} as Model<Api>);
}
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<Api>);
}
export function resolveForwardCompatModel(
provider: string,
modelId: string,
modelRegistry: ModelRegistry,
): Model<Api> | undefined {
return (
resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ??
resolveGoogle31ForwardCompatModel(provider, modelId, modelRegistry)
);
}

View File

@ -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;
}

View File

@ -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" }),

View File

@ -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";
}

View File

@ -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,
}));

View File

@ -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";

View File

@ -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<Record<ChannelId, string>>;
onAccountId?: (channel: ChannelId, accountId: string) => void;
onResolvedPlugin?: (channel: ChannelId, plugin: ChannelOnboardingSetupPlugin) => void;
onResolvedPlugin?: (channel: ChannelId, plugin: ChannelSetupPlugin) => void;
promptAccountIds?: boolean;
whatsappAccountId?: string;
promptWhatsAppAccountId?: boolean;

View File

@ -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() {}

View File

@ -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"]);
});

View File

@ -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;

View File

@ -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<Record<ChannelChoice, string>> = {};
const resolvedPlugins = new Map<ChannelChoice, ChannelOnboardingSetupPlugin>();
const resolvedPlugins = new Map<ChannelChoice, ChannelSetupPlugin>();
await prompter.intro("Channel setup");
let nextConfig = await setupChannels(cfg, runtime, prompter, {
allowDisable: false,

View File

@ -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<typeof import("../onboard-auth.js")>();
return {
...actual,
writeOAuthCredentials: mocks.writeOAuthCredentials,
};
});
vi.mock("./shared.js", async (importActual) => {
const actual = await importActual<typeof import("./shared.js")>();
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<ProviderPlugin["auth"]>[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<typeof vi.fn>;
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",
});
});
});

View File

@ -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 <T>(params: Parameters<typeof clackSelect<T>>[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<ResolvedModelsAuthContext> {
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<typeof createClackPrompter>;
}) {
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<typeof createClackPrompter>;
}) {
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<typeof createClackPrompter>;
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<typeof createClackPrompter>;
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<string, never>, 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<string, never>, 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<typeof createClackPrompter>;
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 <id> 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 <id> 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");
}
}

View File

@ -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<string> {
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<Record<ChannelChoice, string>>;
installedPlugins?: ChannelOnboardingSetupPlugin[];
installedPlugins?: ChannelSetupPlugin[];
resolveAdapter?: (channel: ChannelChoice) => ChannelSetupFlowAdapter | undefined;
}): Promise<ChannelStatusSummary> {
const installedPlugins = params.installedPlugins ?? listChannelSetupPlugins();
@ -347,19 +347,17 @@ export async function setupChannels(
const accountOverrides: Partial<Record<ChannelChoice, string>> = {
...options?.accountIds,
};
const scopedPluginsById = new Map<ChannelChoice, ChannelOnboardingSetupPlugin>();
const scopedPluginsById = new Map<ChannelChoice, ChannelSetupPlugin>();
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<string, ChannelOnboardingSetupPlugin>();
const listVisibleInstalledPlugins = (): ChannelSetupPlugin[] => {
const merged = new Map<string, ChannelSetupPlugin>();
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<ChannelOnboardingSetupPlugin | undefined> => {
): Promise<ChannelSetupPlugin | undefined> => {
const existing = getVisibleChannelPlugin(channel);
if (existing) {
return existing;

100
src/commands/status-json.ts Normal file
View File

@ -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<typeof import("../infra/provider-usage.js")> | 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<HeartbeatEventPayload | null>({
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,
),
);
}

View File

@ -0,0 +1,2 @@
export { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
export { buildChannelsTable } from "./status-all/channels.js";

View File

@ -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,
}));

View File

@ -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<typeof import("../cli/plugin-registry.js")> | undefined;
let statusScanRuntimeModulePromise: Promise<typeof import("./status.scan.runtime.js")> | undefined;
function loadPluginRegistryModule() {
pluginRegistryModulePromise ??= import("../cli/plugin-registry.js");
return pluginRegistryModulePromise;
}
function loadStatusScanRuntimeModule() {
statusScanRuntimeModulePromise ??= import("./status.scan.runtime.js");
return statusScanRuntimeModulePromise;
}
function deferResult<T>(promise: Promise<T>): Promise<DeferredResult<T>> {
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();

View File

@ -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;
};

View File

@ -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");

View File

@ -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);
}
});

View File

@ -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,

View File

@ -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 {

View File

@ -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");

View File

@ -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"),

View File

@ -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",

View File

@ -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;

View File

@ -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<Array<ModelCatalogEntry> | ReadonlyArray<ModelCatalogEntry> | 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<OAuthCredential>;

View File

@ -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";

View File

@ -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<typeof import("./audit-channel.runtime.js")>
| undefined;
function loadAuditChannelRuntimeModule() {
auditChannelRuntimeModulePromise ??= import("./audit-channel.runtime.js");
return auditChannelRuntimeModulePromise;
}
function normalizeAllowFromList(list: Array<string | number> | undefined | null): string[] {
return normalizeStringEntries(Array.isArray(list) ? list : undefined);
@ -32,12 +32,13 @@ function addDiscordNameBasedEntries(params: {
target: Set<string>;
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<string>;
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<string, unknown>)) {
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<string>;
}): void {
}): Promise<void> {
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<string, unknown> } | null)?.config ??
({} as Record<string, unknown>);
@ -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<string, unknown> | 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<string, unknown> } | null)?.config ??
({} as Record<string, unknown>);
@ -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<string, unknown>; dm?: Record<string, unknown> } | null)
?.config ?? ({} as Record<string, unknown>);
@ -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<string>();
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<string, unknown>;
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<string, unknown>).some((topicValue) => {
for (const topicValue of Object.values(topics as Record<string, unknown>)) {
if (!topicValue || typeof topicValue !== "object") {
return false;
continue;
}
const topic = topicValue as Record<string, unknown>;
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;

View File

@ -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<typeof import("../channels/plugins/index.js")> | 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<Secu
}
if (context.includeChannelSecurity && hasPotentialConfiguredChannels(cfg, env)) {
const plugins = context.plugins ?? listChannelPlugins();
const plugins = context.plugins ?? (await loadChannelPlugins()).listChannelPlugins();
findings.push(
...(await collectChannelSecurityFindings({
cfg,

View File

@ -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 {

View File

@ -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();

25
ui/src/local-storage.ts Normal file
View File

@ -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;
}

View File

@ -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) {
@ -583,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 =

View File

@ -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
}

View File

@ -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
}

View File

@ -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();

View File

@ -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
}

View File

@ -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<string> | 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<string> {

View File

@ -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
}

View File

@ -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<DeviceIdentity> {
}
export async function loadOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
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<DeviceIdentity> {
...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<DeviceIdentity> {
privateKey: identity.privateKey,
createdAtMs: Date.now(),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
storage?.setItem(STORAGE_KEY, JSON.stringify(stored));
return identity;
}

View File

@ -16,6 +16,7 @@ type PersistedUiSettings = Omit<UiSettings, "token" | "sessionKey" | "lastActive
};
import { isSupportedLocale } from "../i18n/index.ts";
import { getSafeLocalStorage } from "../local-storage.ts";
import { inferBasePathFromPathname, normalizeBasePath } from "./navigation.ts";
import { parseThemeSelection, type ThemeMode, type ThemeName } from "./theme.ts";
@ -168,6 +169,7 @@ function persistSessionToken(gatewayUrl: string, token: string) {
export function loadSettings(): UiSettings {
const { pageUrl: pageDerivedUrl, effectiveUrl: defaultUrl } = deriveDefaultGatewayUrl();
const storage = getSafeLocalStorage();
const defaults: UiSettings = {
gatewayUrl: defaultUrl,
@ -186,7 +188,7 @@ export function loadSettings(): UiSettings {
};
try {
const raw = localStorage.getItem(KEY);
const raw = storage?.getItem(KEY);
if (!raw) {
return defaults;
}
@ -252,10 +254,11 @@ export function saveSettings(next: UiSettings) {
function persistSettings(next: UiSettings) {
persistSessionToken(next.gatewayUrl, next.token);
const storage = getSafeLocalStorage();
const scope = normalizeGatewayTokenScope(next.gatewayUrl);
let existingSessionsByGateway: Record<string, ScopedSessionSelection> = {};
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));
}

View File

@ -316,6 +316,7 @@ export type PresenceEntry = {
};
export type GatewaySessionsDefaults = {
modelProvider: string | null;
model: string | null;
contextTokens: number | null;
};

View File

@ -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 */
}