Merge branch 'main' into fix/token-usage-input-output-breakdown
This commit is contained in:
commit
fc2aeb017c
File diff suppressed because it is too large
Load Diff
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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).
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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!,
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
63
extensions/google/provider-models.ts
Normal file
63
extensions/google/provider-models.ts
Normal 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");
|
||||
}
|
||||
@ -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") {
|
||||
@ -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),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -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");
|
||||
});
|
||||
|
||||
1
extensions/nostr/src/default-relays.ts
Normal file
1
extensions/nostr/src/default-relays.ts
Normal file
@ -0,0 +1 @@
|
||||
export const DEFAULT_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"];
|
||||
@ -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
|
||||
// ============================================================================
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -19,6 +19,7 @@ const opencodeGoPlugin = {
|
||||
geminiThoughtSignatureSanitization: true,
|
||||
geminiThoughtSignatureModelHints: ["gemini"],
|
||||
},
|
||||
isModernModelRef: () => true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -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),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -110,6 +110,7 @@ const openRouterPlugin = {
|
||||
geminiThoughtSignatureSanitization: true,
|
||||
geminiThoughtSignatureModelHints: ["gemini"],
|
||||
},
|
||||
isModernModelRef: () => true,
|
||||
wrapStreamFn: (ctx) => {
|
||||
let streamFn = ctx.streamFn;
|
||||
const providerRouting =
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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" }),
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() {}
|
||||
|
||||
@ -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"]);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
100
src/commands/status-json.ts
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
2
src/commands/status.scan.runtime.ts
Normal file
2
src/commands/status.scan.runtime.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
export { buildChannelsTable } from "./status-all/channels.js";
|
||||
@ -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,
|
||||
}));
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
|
||||
9
src/security/audit-channel.runtime.ts
Normal file
9
src/security/audit-channel.runtime.ts
Normal 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";
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
25
ui/src/local-storage.ts
Normal 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;
|
||||
}
|
||||
@ -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 =
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -316,6 +316,7 @@ export type PresenceEntry = {
|
||||
};
|
||||
|
||||
export type GatewaySessionsDefaults = {
|
||||
modelProvider: string | null;
|
||||
model: string | null;
|
||||
contextTokens: number | null;
|
||||
};
|
||||
|
||||
@ -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 */
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user