mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-21 06:22:28 +08:00
Compare commits
125 Commits
qa-recreat
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af826ae7fc | ||
|
|
f33b5426aa | ||
|
|
df1a008584 | ||
|
|
cc7cbc0580 | ||
|
|
2dc0ea95d7 | ||
|
|
224f195709 | ||
|
|
c422f10aef | ||
|
|
619da391ab | ||
|
|
2d886f03a7 | ||
|
|
6a53001e2f | ||
|
|
0843ad5ad1 | ||
|
|
960a631b25 | ||
|
|
607d341451 | ||
|
|
b6da5443fc | ||
|
|
a84858d315 | ||
|
|
23549694f7 | ||
|
|
4a5fe2e0e7 | ||
|
|
32ae0eba54 | ||
|
|
24f76d04eb | ||
|
|
5b53ddcc5f | ||
|
|
51ff658586 | ||
|
|
be85d9aaec | ||
|
|
fd1b355f84 | ||
|
|
b9a9290dfc | ||
|
|
b141da4ca9 | ||
|
|
4857f9d0c2 | ||
|
|
aa7c67e6a9 | ||
|
|
fdf381f1a7 | ||
|
|
5cff2ff94b | ||
|
|
ac66507ccb | ||
|
|
3dec7f2596 | ||
|
|
83f47a4d0a | ||
|
|
a9dbaa1124 | ||
|
|
367f52f483 | ||
|
|
b371af76a3 | ||
|
|
3584d28141 | ||
|
|
c1b1d14218 | ||
|
|
79348f73c8 | ||
|
|
cef64f0b5a | ||
|
|
e91405ebf9 | ||
|
|
bfa1fa1700 | ||
|
|
54ad458267 | ||
|
|
c6d3ee70e2 | ||
|
|
8a841b531f | ||
|
|
1582bbbfc5 | ||
|
|
4780788bbb | ||
|
|
eb6d0ce2c2 | ||
|
|
b5fc435bd5 | ||
|
|
8e1c81e707 | ||
|
|
17a324b0de | ||
|
|
bb60b53124 | ||
|
|
d7f75ee087 | ||
|
|
b58f9c5258 | ||
|
|
a234157337 | ||
|
|
f30c087fdf | ||
|
|
1a3eb38aaf | ||
|
|
2ed2dbba00 | ||
|
|
48611ec40a | ||
|
|
471d056e2f | ||
|
|
1351bacaa4 | ||
|
|
f7e76e31f3 | ||
|
|
1703bdcaf6 | ||
|
|
a62193c09e | ||
|
|
5e0b58fbc6 | ||
|
|
4ed60d950d | ||
|
|
05f9dd7a01 | ||
|
|
d6d8d1716f | ||
|
|
7f1b159c03 | ||
|
|
6a57f5403d | ||
|
|
53c52124b9 | ||
|
|
0655e173c4 | ||
|
|
dea3ab0aa9 | ||
|
|
94256ea1a0 | ||
|
|
e29d370969 | ||
|
|
06f9677b5b | ||
|
|
beed40e918 | ||
|
|
c73aeed929 | ||
|
|
a4a1cfc8c2 | ||
|
|
39b05c4920 | ||
|
|
08492dfeee | ||
|
|
2f72363984 | ||
|
|
64f889cd4b | ||
|
|
a2a9fa7f6f | ||
|
|
cd564bf5a5 | ||
|
|
c11e7a7420 | ||
|
|
00372508b5 | ||
|
|
ca94f02959 | ||
|
|
a2376462e9 | ||
|
|
d66960206b | ||
|
|
c2a8aac282 | ||
|
|
5a6d80da7f | ||
|
|
afb89b439a | ||
|
|
d624ec3a0b | ||
|
|
9ce4abfe55 | ||
|
|
a213a580d5 | ||
|
|
a78c4de737 | ||
|
|
7b62fcd87d | ||
|
|
d1c7d9af80 | ||
|
|
fbbe2a1675 | ||
|
|
82710f2add | ||
|
|
516a43f9f2 | ||
|
|
57d1685a65 | ||
|
|
b0c7bac9ce | ||
|
|
e7407f8178 | ||
|
|
1033db4d31 | ||
|
|
3a7a67b218 | ||
|
|
2176b68e50 | ||
|
|
b4e5d91941 | ||
|
|
5586b3fd19 | ||
|
|
d7f3af3b06 | ||
|
|
d83dd9b536 | ||
|
|
d3e67a0de7 | ||
|
|
932194b7d5 | ||
|
|
52146f8803 | ||
|
|
aa464f8573 | ||
|
|
8279375bdf | ||
|
|
58f95b8000 | ||
|
|
8a43223014 | ||
|
|
9b7002ee59 | ||
|
|
456ad889c7 | ||
|
|
ce8492f9a0 | ||
|
|
a8e827856a | ||
|
|
9bc43b61bf | ||
|
|
2a4eea58a9 | ||
|
|
a4f16f572c |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -1,5 +1,7 @@
|
||||
name: CI
|
||||
|
||||
# Keep PR CI synchronized on branch updates.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
93
.github/workflows/control-ui-locale-refresh.yml
vendored
93
.github/workflows/control-ui-locale-refresh.yml
vendored
@@ -6,9 +6,11 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- ui/src/i18n/locales/en.ts
|
||||
- ui/src/i18n/locales/*.ts
|
||||
- ui/src/i18n/.i18n/*
|
||||
- ui/src/i18n/lib/types.ts
|
||||
- ui/src/i18n/lib/registry.ts
|
||||
- scripts/control-ui-i18n.ts
|
||||
- package.json
|
||||
- .github/workflows/control-ui-locale-refresh.yml
|
||||
release:
|
||||
types:
|
||||
@@ -25,24 +27,87 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
if: github.repository == 'openclaw/openclaw' && (github.event_name != 'push' || github.actor != 'github-actions[bot]')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_locales: ${{ steps.plan.outputs.has_locales }}
|
||||
locales_json: ${{ steps.plan.outputs.locales_json }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Plan locale matrix
|
||||
id: plan
|
||||
env:
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
all_locales_json='["zh-CN","zh-TW","pt-BR","de","es","ja-JP","ko","fr","tr","uk","id","pl"]'
|
||||
|
||||
if [ "$EVENT_NAME" != "push" ]; then
|
||||
echo "has_locales=true" >> "$GITHUB_OUTPUT"
|
||||
echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
before_ref="$BEFORE_SHA"
|
||||
if [ -z "$before_ref" ] || [ "$before_ref" = "0000000000000000000000000000000000000000" ]; then
|
||||
before_ref="$(git rev-parse HEAD^)"
|
||||
fi
|
||||
|
||||
changed_files="$(git diff --name-only "$before_ref" HEAD)"
|
||||
echo "changed files:"
|
||||
printf '%s\n' "$changed_files"
|
||||
|
||||
if printf '%s\n' "$changed_files" | grep -Eq '^(ui/src/i18n/locales/en\.ts|ui/src/i18n/lib/types\.ts|ui/src/i18n/lib/registry\.ts|scripts/control-ui-i18n\.ts|\.github/workflows/control-ui-locale-refresh\.yml)$'; then
|
||||
echo "has_locales=true" >> "$GITHUB_OUTPUT"
|
||||
echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
locales_json="$(printf '%s\n' "$changed_files" | node <<'EOF'
|
||||
const fs = require("node:fs");
|
||||
const changed = fs.readFileSync(0, "utf8").split(/\r?\n/).filter(Boolean);
|
||||
const locales = new Set();
|
||||
for (const file of changed) {
|
||||
let match = file.match(/^ui\/src\/i18n\/locales\/(.+)\.ts$/);
|
||||
if (match && match[1] !== "en") {
|
||||
locales.add(match[1]);
|
||||
continue;
|
||||
}
|
||||
match = file.match(/^ui\/src\/i18n\/\.i18n\/(.+)\.(?:meta\.json|tm\.jsonl)$/);
|
||||
if (match) {
|
||||
locales.add(match[1]);
|
||||
}
|
||||
}
|
||||
process.stdout.write(JSON.stringify([...locales]));
|
||||
EOF
|
||||
)"
|
||||
|
||||
if [ "$locales_json" = "[]" ]; then
|
||||
echo "has_locales=false" >> "$GITHUB_OUTPUT"
|
||||
echo "locales_json=[]" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_locales=true" >> "$GITHUB_OUTPUT"
|
||||
echo "locales_json=$locales_json" >> "$GITHUB_OUTPUT"
|
||||
|
||||
refresh:
|
||||
if: github.repository == 'openclaw/openclaw'
|
||||
needs: plan
|
||||
if: github.repository == 'openclaw/openclaw' && needs.plan.outputs.has_locales == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
locale:
|
||||
- zh-CN
|
||||
- zh-TW
|
||||
- pt-BR
|
||||
- de
|
||||
- es
|
||||
- ja-JP
|
||||
- ko
|
||||
- fr
|
||||
- tr
|
||||
- id
|
||||
- pl
|
||||
locale: ${{ fromJson(needs.plan.outputs.locales_json) }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Refresh ${{ matrix.locale }}
|
||||
steps:
|
||||
|
||||
105
CHANGELOG.md
105
CHANGELOG.md
@@ -10,47 +10,78 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Providers/Amazon Bedrock Mantle: add a bundled OpenAI-compatible Mantle provider with bearer-token discovery, automatic OSS model catalog loading, and Bedrock Mantle region detection for hosted GPT-OSS, Qwen, Kimi, GLM, and similar routes. (#61296) Thanks @wirjo.
|
||||
- Providers/Amazon Bedrock: discover regional and global inference profiles, inherit their backing model capabilities, and inject the Bedrock request region automatically so cross-region Claude profiles work without manual provider overrides. (#61299) Thanks @wirjo.
|
||||
- Providers/Anthropic: remove the Claude CLI backend, have `openclaw doctor` convert stale `anthropic:claude-cli` state back to Anthropic token/OAuth when stored credential bytes still exist (or delete the stale Claude CLI config when they do not), and steer Anthropic setup to API keys or legacy setup-token with the correct Extra Usage billing guidance.
|
||||
- Providers/Anthropic: remove setup-token from new onboarding and auth-command setup paths, keep existing configured legacy token profiles runnable, and steer new Anthropic setup to API keys.
|
||||
- Providers/Fireworks: add a bundled Fireworks AI provider plugin with `FIREWORKS_API_KEY` onboarding, Fire Pass Kimi defaults, and dynamic Fireworks model-id support.
|
||||
- Providers/Qwen: add a bundled Qwen provider plugin with dedicated onboarding, media understanding, and video generation support.
|
||||
- Providers/StepFun: add the bundled StepFun provider plugin with standard and Step Plan endpoints, China/global onboarding choices, `step-3.5-flash` on both catalogs, and `step-3.5-flash-2603` currently exposed on Step Plan. (#60032) Thanks @hengm3467.
|
||||
- MiniMax/TTS: add a bundled MiniMax speech provider backed by the T2A v2 API so speech synthesis can run through MiniMax-native voices and auth. (#55921) Thanks @duncanita.
|
||||
- Providers/Ollama: add a bundled Ollama Web Search provider for key-free `web_search` via your configured Ollama host and `ollama signin`. (#59318) Thanks @BruceMacD.
|
||||
- Tools/web_search: add a bundled MiniMax Search provider backed by the Coding Plan search API, with region reuse from `MINIMAX_API_HOST` and plugin-owned credential config. (#54648) Thanks @fengmk2.
|
||||
- Memory/dreaming (experimental): add weighted short-term recall promotion, managed dreaming modes (`off|core|rem|deep`), a `/dreaming` command, Dreams UI, multilingual conceptual tagging, and doctor/status repair support so durable memory promotion can run in the background with less manual setup. (#60569, #60697) Thanks @vignesh07.
|
||||
- Memory/dreaming (experimental): add weighted short-term recall promotion, a `/dreaming` command, Dreams UI, multilingual conceptual tagging, and doctor/status repair support, while refactoring dreaming from competing modes into three cooperative phases (light, deep, REM) with independent schedules and recovery behavior so durable memory promotion can run in the background with less manual setup. (#60569, #60697) Thanks @vignesh07.
|
||||
- Memory/dreaming: add configurable aging controls (`recencyHalfLifeDays`, `maxAgeDays`) plus optional verbose logging so operators can tune recall decay and inspect promotion decisions more easily.
|
||||
- Memory/dreaming: add REM preview tooling (`openclaw memory rem-harness`, `promote-explain`), surface possible lasting truths during REM staging, and make deep promotion replay-safe so reruns reconcile instead of duplicating `MEMORY.md` entries.
|
||||
- Agents/video generation: add the built-in `video_generate` tool so agents can create videos through configured providers and return the generated media directly in the reply.
|
||||
- Control UI/multilingual: add localized control UI support for Simplified Chinese, Traditional Chinese, Brazilian Portuguese, German, Spanish, Japanese, Korean, French, Turkish, Indonesian, Polish, and Ukrainian. Thanks @vincentkoc.
|
||||
- iOS/exec approvals: add generic APNs approval notifications that open an in-app exec approval modal, fetch command details only after authenticated operator reconnect, and clear stale notification state when the approval resolves. (#60239) Thanks @ngutman.
|
||||
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
|
||||
- Control UI/skills: add ClawHub search, detail, and install flows directly in the Skills panel. (#60134) Thanks @samzong.
|
||||
- Control UI/multilingual: add localized control UI support for Simplified Chinese, Traditional Chinese, Brazilian Portuguese, German, Spanish, Japanese, Korean, French, Turkish, Indonesian, and Polish. Thanks @vincentkoc.
|
||||
- Plugins: add plugin-config TUI prompts to guided onboarding/setup flows, and add `openclaw plugins install --force` so existing plugin and hook-pack targets can be replaced without using the dangerous-code override flag. (#60590, #60544)
|
||||
- Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so supplemental quote, thread, and fetched history context can be filtered by sender allowlists instead of always passing through as received.
|
||||
- Plugins/install: add `openclaw plugins install --force` to overwrite existing plugin and hook-pack install targets without using the dangerous-code override flag. (#60544) Thanks @gumadeiras.
|
||||
- Plugins/onboarding: add plugin config TUI prompts to onboard and configure wizards so more plugin setup can stay in the guided flow. (#60590) Thanks @odysseus0.
|
||||
- Config/schema: enrich the exported `openclaw config schema` JSON Schema with field titles and descriptions so editors, agents, and other schema consumers receive the same config help metadata. (#60067) Thanks @solavrc.
|
||||
- Providers: add bundled Qwen, Fireworks AI, and StepFun providers, plus MiniMax TTS, Ollama Web Search, and MiniMax Search integrations for chat, speech, and search workflows. (#60032, #55921, #59318, #54648)
|
||||
- Providers/Amazon Bedrock: add bundled Mantle support plus inference-profile discovery and automatic request-region injection so Bedrock-hosted Claude, GPT-OSS, Qwen, Kimi, GLM, and similar routes work with less manual setup. (#61296, #61299) Thanks @wirjo.
|
||||
- Providers/request overrides: add shared model and media request transport overrides across OpenAI-, Anthropic-, Google-, and compatible provider paths, including headers, auth, proxy, and TLS controls. (#60200)
|
||||
- Prompt caching: keep prompt prefixes more reusable across transport fallback, deterministic MCP tool ordering, compaction, and embedded image history so follow-up turns hit cache more reliably. (#58036, #58037, #58038, #59054, #60603, #60691) Thanks @bcherny.
|
||||
- Agents/Claude CLI: expose OpenClaw tools to background Claude CLI runs through a loopback MCP bridge that reuses gateway tool policy, honors session/account/channel scoping, and only advertises the bridge when the local runtime is actually live. (#35676) Thanks @mylukin.
|
||||
- Agents/Claude CLI: switch bundled Claude CLI runs to stdin + `stream-json` partial-message streaming so prompts stop riding argv, long replies show live progress, and final session/usage metadata still land cleanly.
|
||||
- ACPX/runtime: embed the ACP runtime directly in the bundled `acpx` plugin, remove the extra external ACP CLI hop, and harden live ACP session binding and reuse. (#61319)
|
||||
- Prompt caching: keep prompt prefixes more reusable across transport fallback, deterministic MCP tool ordering, compaction, embedded image history, normalized system-prompt fingerprints, `openclaw status --verbose` cache diagnostics, and the removal of duplicate in-band tool inventories from agent system prompts so follow-up turns hit cache more reliably. (#58036, #58037, #58038, #59054, #60603, #60691) Thanks @bcherny and @vincentkoc.
|
||||
- Providers/OpenAI: add forward-compat `openai-codex/gpt-5.4-mini`, an opt-in GPT personality, and provider-owned GPT-5 prompt contributions so Codex/GPT runs stay cache-stable and compatible with bundled catalog lag.
|
||||
- Providers/Anthropic: remove the Claude CLI backend and setup-token from new onboarding, keep existing configured legacy profiles runnable, and have `openclaw doctor` repair or remove stale `anthropic:claude-cli` state during migration.
|
||||
- Agents/progress: add experimental structured plan updates and structured execution item events so compatible UIs can show clearer step-by-step progress during long-running runs.
|
||||
- Agents/tool prompts: remove the duplicate in-band tool inventory from agent system prompts so tool-calling models rely on the structured tool definitions as the single source of truth, improving prompt stability and reducing stale tool guidance.
|
||||
- Agents/Claude CLI: expose OpenClaw tools to background Claude CLI runs through a loopback MCP bridge and switch bundled runs to stdin + `stream-json` partial-message streaming so prompts stop riding argv, long replies show live progress, and final session/usage metadata still land cleanly. (#35676) Thanks @mylukin.
|
||||
- ACPX/runtime: embed the ACP runtime directly in the bundled `acpx` plugin, remove the extra external ACP CLI hop, harden live ACP session binding and reuse, and add a generic `reply_dispatch` hook so bundled plugins like ACPX can own reply interception without hardcoded ACP paths in core auto-reply routing. (#61319)
|
||||
- Config/schema: enrich the exported `openclaw config schema` JSON Schema with field titles and descriptions so editors, agents, and other schema consumers receive the same config help metadata. (#60067) Thanks @solavrc.
|
||||
- Agents/cache: diagnostics: add prompt-cache break diagnostics, trace live cache scenarios through embedded runner paths, and show cache reuse explicitly in `openclaw status --verbose`. Thanks @vincentkoc.
|
||||
- Agents/cache: stabilize cache-relevant system prompt fingerprints by normalizing equivalent structured prompt whitespace, line endings, hook-added system context, and runtime capability ordering so semantically unchanged prompts reuse KV/cache more reliably. Thanks @vincentkoc.
|
||||
- Agents/tool prompts: remove the duplicate in-band tool inventory from agent system prompts so tool-calling models rely on the structured tool definitions as the single source of truth, improving prompt stability and reducing stale tool guidance.
|
||||
- Tools/video generation: add bundled xAI (`grok-imagine-video`) and Alibaba Model Studio Wan video providers, plus live-test/default model wiring for both.
|
||||
- Providers/CLI: remove bundled CLI text-provider backends and the `agents.defaults.cliBackends` surface, while keeping ACP harness sessions and Gemini media understanding on the native bundled providers.
|
||||
- Providers/OpenAI Codex: add forward-compat `openai-codex/gpt-5.4-mini` synthesis across provider runtime, model catalog, and model listing so Codex mini works before bundled Pi catalog updates land.
|
||||
- Providers/OpenAI: add an opt-in GPT personality and move GPT-5 prompt tuning onto provider-owned system-prompt contributions so cache-stable guidance stays above the prompt cache boundary and embedded runner paths reuse the same provider-specific prompt behavior.
|
||||
- Docs/IRC: replace public IRC hostname examples with `irc.example.com` and recommend private servers for bot coordination while listing common public networks for intentional use.
|
||||
- Memory/dreaming: add configurable aging controls (`recencyHalfLifeDays`, `maxAgeDays`) plus optional verbose logging so operators can tune recall decay and inspect promotion decisions more easily.
|
||||
- Plugins/reply dispatch: add a generic `reply_dispatch` hook so bundled plugins like ACPX can own reply interception without hardcoded ACP paths in core auto-reply routing.
|
||||
- Memory/dreaming: refactor dreaming from competing modes (`off|core|rem|deep`) to three cooperative phases (light, deep, REM) with independent schedules, per-phase enable/disable, deep-only `MEMORY.md` writes, light/REM daily-note staging, deep recovery, and per-phase execution overrides.
|
||||
- Matrix/exec approvals: clarify unavailable-approval replies so Matrix no longer claims chat approvals are unsupported when native exec approvals are merely unconfigured. (#61424) Thanks @gumadeiras.
|
||||
- Docs/IRC: replace public IRC hostname examples with `irc.example.com` and recommend private servers for bot coordination while listing common public networks for intentional use.
|
||||
- Memory/dreaming: write dreaming trail content to top-level `DREAMS.md` instead of daily memory notes, update `/dreaming` help text to point there, and keep `DREAMS.md` available for explicit reads without pulling it into default recall. Thanks @davemorin.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security: preserve restrictive plugin-only tool allowlists, require owner access for `/allowlist add` and `/allowlist remove`, fail closed when `before_tool_call` hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120) Thanks @eleqtrizit and @pgondhi987.
|
||||
- Providers/OpenAI: make GPT-5 and Codex runs act sooner with lower-verbosity defaults, visible progress during tool work, and a one-shot retry when a turn only narrates the plan instead of taking action.
|
||||
- Providers/OpenAI and reply delivery: preserve native `reasoning.effort: "none"` and strict schemas where supported, add GPT-5.4 assistant `phase` metadata across replay and the Gateway `/v1/responses` layer, and keep commentary buffered until `final_answer` so web chat, session previews, embedded replies, and Telegram partials stop leaking planning text. Fixes #59150, #59643, #61282.
|
||||
- Telegram: fix current-model checks in the model picker, HTML-format non-default `/model` confirmations, explicit topic replies, persisted reaction ownership across restarts, caption-media placeholder and `file_id` preservation on download failure, and upgraded-install inbound image reads. (#60384, #60042, #59634, #59207, #59948, #59971) Thanks @sfuminya, @GitZhangChi, @dashhuang, @samzong, @v1p0r, and @neeravmakwana.
|
||||
- Telegram: restore DM voice-note preflight transcription so direct-message audio stops arriving as raw `<media:audio>` placeholders. (#61008) Thanks @manueltarouca.
|
||||
- Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly `reasoning:stream`, so hidden `<think>` traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc.
|
||||
- Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more `/` entries visible. (#61129) Thanks @neeravmakwana.
|
||||
- Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan.
|
||||
- Discord/reply tags: strip leaked `[[reply_to_current]]` control tags from preview text and honor explicit reply-tag threading during final delivery, so Discord replies stay attached to the triggering message instead of printing reply metadata into chat.
|
||||
- Discord/replies: replace the unshipped `replyToOnlyWhenBatched` flag with `replyToMode: "batched"` so native reply references only attach on debounced multi-message turns while explicit reply tags still work.
|
||||
- Discord/image generation: include the real generated `MEDIA:` paths in tool output, avoid duplicate plain-output media requeueing, and persist volatile workspace-generated media into durable outbound media before final reply delivery so generated image replies stop pointing at missing local files.
|
||||
- Slack: route live DM replies back to the concrete inbound DM channel while keeping persisted routing metadata user-scoped, so normal assistant replies stop disappearing when pairing and system messages still arrive. (#59030) Thanks @afurm.
|
||||
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr.
|
||||
- Android/Talk Mode: cancel in-flight `talk.speak` playback when speech is explicitly stopped, and restore spoken replies on both node-scoped and gateway-backed sessions by keeping reply routing and embedded transport overrides aligned with the current playback path. (#60306, #61164, #61214)
|
||||
- Voice-call/OpenAI: pass full plugin config into realtime transcription provider resolution so streaming calls can discover the bundled OpenAI realtime transcription provider again. Fixes #60936. Thanks @sliekens and @vincentkoc.
|
||||
- Matrix/exec approvals: anchor seeded approval reactions to the primary Matrix prompt event, resolve them from event metadata instead of prompt text, and clean up chunked approval prompts correctly. (#60931) Thanks @gumadeiras.
|
||||
- Matrix: recover more reliably when secret storage or recovery keys are missing by recreating secret storage during repair and backup reset, hold crypto snapshot locks during persistence, and surface explicit too-large attachment markers. (#59846, #59851, #60599, #60289) Thanks @al3mart, @emonty, and @efe-arv.
|
||||
- Matrix/DM sessions: add `channels.matrix.dm.sessionScope`, shared-session collision notices, and aligned outbound session reuse so separate Matrix DM rooms can keep distinct context when configured. (#61373) Thanks @gumadeiras.
|
||||
- Matrix: move legacy top-level `avatarUrl` into the default account during multi-account promotion and keep env-backed account setup avatar config persisted. (#61437) Thanks @gumadeiras.
|
||||
- MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin.
|
||||
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
|
||||
- Control UI/chat: add a per-session thinking-level picker in the chat header and mobile chat settings, and keep the browser bundle on UI-local thinking/session-key helpers so Safari no longer crashes on Node-only imports before rendering chat controls.
|
||||
- Sandbox/SSH: reject hardlinked files during cross-device rename fallback so EXDEV file copies preserve the same pinned file-boundary checks as direct reads.
|
||||
- Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267) Thanks @chziyue and @frankekn.
|
||||
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
|
||||
- Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.
|
||||
- Control UI/Overview: prevent gateway access token/password visibility toggle buttons from overlapping their inputs at narrow widths. (#56924) Thanks @bbddbb1.
|
||||
- Auto-reply: unify reply lifecycle ownership across preflight compaction, session rotation, CLI-backed runs, and gateway restart handling so `/stop` and same-session overlap checks target the right active turn and restart-interrupted turns return the restart notice instead of being silently dropped. (#61267) Thanks @dutifulbob.
|
||||
- Reply delivery: prevent duplicate block replies on `text_end` channels so providers that emit explicit text-end boundaries no longer double-send the same final message. (#61530)
|
||||
- Gateway/startup: default `gateway.mode` to `local` when unset, detect PID recycling in gateway lock files on Windows and macOS, and show startup progress so healthy restarts stop getting blocked by stale locks. (#54801, #60085, #59843) Thanks @BradGroux and @TonyDerek-dot.
|
||||
- Gateway/macOS: let launchd `KeepAlive` own in-process gateway restarts again, adding a short supervised-exit delay so rapid restarts avoid launchd crash-loop unloads while `openclaw gateway restart` still reports real LaunchAgent errors synchronously.
|
||||
- Gateway/macOS: re-bootstrap the LaunchAgent if `launchctl kickstart -k` unloads it during restart so failed restarts do not leave the gateway unmanaged until manual repair.
|
||||
- Gateway/macOS: recover installed-but-unloaded LaunchAgents during `openclaw gateway start` and `restart`, while still preferring live unmanaged gateways during restart recovery. (#43766) Thanks @HenryC-3.
|
||||
- Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loudly when `/Run` does not start, and report fast failed restarts accurately instead of pretending they timed out after 60 seconds. (#59335) Thanks @tmimmanuel.
|
||||
- Windows/restart: fall back to the installed Startup-entry launcher when the scheduled task was never registered, so `/restart` can relaunch the gateway on Windows setups where `schtasks` install fell back during onboarding. (#58943) Thanks @imechZhangLY.
|
||||
- Windows/restart: clean up stale gateway listeners before Windows self-restart and treat listener and argv probe failures as inconclusive, so scheduled-task relaunch no longer falls into an `EADDRINUSE` retry loop. (#60480) Thanks @arifahmedjoy.
|
||||
- Update/npm: prefer the npm binary that owns the installed global OpenClaw prefix so mixed Homebrew-plus-nvm setups update the right install. (#60153) Thanks @jayeshp19.
|
||||
- CLI/skills JSON: route `skills list --json`, `skills info --json`, and `skills check --json` output to stdout instead of stderr so machine-readable consumers receive JSON on the expected stream again. (#60914; fixes #57599; landed from contributor PR #57611 by @Aftabbs) Thanks @Aftabbs.
|
||||
- CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.
|
||||
- Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth.
|
||||
- Cron: send failure notifications through the job's primary delivery channel using the same session context as successful delivery when no explicit `failureDestination` is configured. (#60622) Thanks @artwalker.
|
||||
- Exec/remote skills: stop advertising `exec host=node` when the current exec policy cannot route to a node, and clarify blocked exec-host override errors with both the requested host and allowed config path.
|
||||
- Agents/Claude CLI/security: clear inherited Claude Code config-root and plugin-root env overrides like `CLAUDE_CONFIG_DIR` and `CLAUDE_CODE_PLUGIN_*`, so OpenClaw-launched Claude CLI runs cannot be silently pointed at an alternate Claude config/plugin tree with different hooks, plugins, or auth context. Thanks @vincentkoc.
|
||||
- Agents/Claude CLI/security: clear inherited Claude Code provider-routing and managed-auth env overrides, and mark OpenClaw-launched Claude CLI runs as host-managed, so Claude CLI backdoor sessions cannot be silently redirected to proxy, Bedrock, Vertex, Foundry, or parent-managed token contexts. Thanks @vincentkoc.
|
||||
@@ -58,6 +89,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Claude CLI: treat malformed bare `--permission-mode` backend overrides as missing and fail safe back to `bypassPermissions`, so custom `cliBackends.claude-cli.args` security config cannot accidentally consume the next flag as a bogus permission mode. Thanks @vincentkoc.
|
||||
- Gateway/device pairing: require non-admin paired-device sessions to manage only their own device for token rotate/revoke and paired-device removal, blocking cross-device token theft inside pairing-scoped sessions. (#50627) Thanks @coygeek.
|
||||
- Gateway/plugin routes: keep gateway-auth plugin runtime routes on write-only fallback scopes unless a trusted-proxy caller explicitly declares narrower `x-openclaw-scopes`, so plugin HTTP handlers no longer mint admin-level runtime scopes on missing or untrusted HTTP scope headers. (#59815) Thanks @pgondhi987.
|
||||
- Build/types: fix the Node `createRequire(...)` helper typing so provider-runtime lazy loads compile cleanly again and `pnpm build` no longer fails in the Pi embedded provider error-pattern path.
|
||||
- Gateway/security: scope loopback browser-origin auth throttling by normalized origin so one localhost Control UI tab cannot lock out a different localhost browser origin after repeated auth failures.
|
||||
- Gateway/auth: serialize async shared-secret auth attempts per client so concurrent Tailscale-capable failures cannot overrun the intended auth rate-limit budget. Thanks @Telecaster2147.
|
||||
- Device pairing/security: keep non-operator device scope checks bound to the requested role prefix so bootstrap verification cannot redeem `operator.*` scopes through `node` auth. (#57258) Thanks @jlapenna.
|
||||
@@ -72,14 +104,12 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/marketplace: block remote marketplace symlink escapes without breaking ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit.
|
||||
- Telegram/local Bot API: honor `channels.telegram.apiRoot` for buffered media downloads, add `channels.telegram.network.dangerouslyAllowPrivateNetwork` for trusted fake-IP setups, and require `channels.telegram.trustedLocalFileRoots` before reading absolute Bot API `file_path` values. (#59544, #60705) Thanks @SARAMALI15792 and @obviyus.
|
||||
- Outbound/sanitizer: strip leaked `<tool_call>`, `<function_calls>`, and model special tokens from shared user-visible assistant text, including truncated tool-call streams, so internal scaffolding no longer bleeds into replies across surfaces. (#60619) Thanks @oliviareid-svg.
|
||||
- Agents/output delivery: suppress `phase:”commentary”` assistant text at the embedded subscribe boundary so internal planning text cannot leak into user-visible replies or Telegram partials. (#61282) Thanks @mbelinky.
|
||||
- Agents/streaming: keep commentary-only partials hidden until `final_answer` is available and buffer OpenAI Responses websocket text deltas until phase metadata arrives, so commentary does not leak into visible embedded replies. (#59643) Thanks @ringlochid.
|
||||
- Agents/errors: surface an explicit disk-full message when local session or transcript writes fail with `ENOSPC`/`disk full`, so those runs stop degrading into opaque `NO_REPLY`-style failures. Thanks @vincentkoc.
|
||||
- Exec approvals: remove heuristic command-obfuscation gating from host exec so gateway and node runs rely on explicit policy, allowlist, and strict inline-eval rules only.
|
||||
- Agents/tool results: cap live tool-result persistence and overflow-recovery truncation at 40k characters so oversized tool output stays bounded without discarding recent context entirely.
|
||||
- Discord/video replies: split text-plus-video deliveries into a text reply followed by a media-only send, and let live provider auth checks honor manifest-declared API key env vars like `MODELSTUDIO_API_KEY`.
|
||||
- Config/All Settings: keep the raw config view intact when sensitive fields are blank instead of corrupting or dropping the rendered snapshot. (#28214) Thanks @solodmd.
|
||||
- Plugin SDK/facades: back-fill bundled plugin facade sentinels before plugin-id tracking re-enters config loading, so CLI/provider startup no longer crashes with `shouldNormalizeGoogleProviderConfig is not a function` or other empty-facade reads during bundled plugin re-entry. Thanks @adam91holt.
|
||||
- Discord/image generation: include the real generated `MEDIA:` paths in tool output and avoid duplicate plain-output media requeueing so Discord image replies stop pointing at missing local files.
|
||||
- Discord/replies: replace the unshipped `replyToOnlyWhenBatched` flag with `replyToMode: "batched"` so native reply references only attach on debounced multi-message turns while explicit reply tags still work.
|
||||
- Plugins/facades: back-fill facade sentinels before tracked-plugin resolution re-enters config loading, so facade exports stay defined during circular provider normalization. (#61180) Thanks @adam91holt.
|
||||
- Discord/image generation: include the real generated `MEDIA:` paths in tool output and avoid duplicate plain-output media requeueing so Discord image replies stop pointing at missing local files.
|
||||
- Slack: route live DM replies back to the concrete inbound DM channel while keeping persisted routing metadata user-scoped, so normal assistant replies stop disappearing when pairing and system messages still arrive. (#59030) Thanks @afurm.
|
||||
@@ -117,9 +147,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Exec/heartbeat: use the canonical `exec-event` wake reason for `notifyOnExit` so background exec completions still trigger follow-up turns when `HEARTBEAT.md` is empty or comments-only. (#41479) Thanks @rstar327.
|
||||
- Heartbeat: skip wake delivery when the target session lane is already busy so the pending event is retried instead of getting drained too early. (#40526) Thanks @lucky7323.
|
||||
- Group chats/agent prompts: tell models to minimize empty lines and use normal chat-style spacing so group replies avoid document-style blank-line formatting.
|
||||
- Providers/OpenAI: make GPT-5 and Codex runs act sooner with lower-verbosity defaults, visible progress during tool work, and a one-shot retry when a turn only narrates the plan instead of taking action.
|
||||
- Providers/OpenAI: preserve native `reasoning.effort: “none”` and strict tool schemas on direct OpenAI-family endpoints, keep compat routes on compat shaping, fix Responses WebSocket warm-up behavior, keep stable session and turn metadata, and fall back more gracefully after early WebSocket failures.
|
||||
- Providers/OpenAI: support GPT-5.4 assistant `phase` metadata across OpenAI-family Responses replay and the Gateway `/v1/responses` compatibility layer, including `commentary` tool preambles and `final_answer` replies.
|
||||
- Providers/OpenAI GPT: treat short approval turns like `ok do it` and `go ahead` as immediate action turns, and trim overly memo-like GPT-5 chat confirmations so OpenAI replies stay shorter and more conversational by default.
|
||||
- Providers/OpenAI Codex: split native `contextWindow` from runtime `contextTokens`, keep the default effective cap at `272000`, and expose a per-model `contextTokens` override on `models.providers.*.models[]`.
|
||||
- Providers/OpenAI-compatible WS: compute fallback token totals from normalized usage when providers omit or zero `total_tokens`, so DashScope-compatible sessions stop storing zero totals after alias normalization. (#54940) Thanks @lyfuci.
|
||||
@@ -173,13 +200,7 @@ Docs: https://docs.openclaw.ai
|
||||
- ACP/agents: inherit the target agent workspace for cross-agent ACP spawns and fall back safely when the inherited workspace no longer exists. (#58438) Thanks @zssggle-rgb.
|
||||
- ACPX/Windows: preserve backslashes and absolute `.exe` paths in Claude CLI parsing, and fail fast on wrapper-script targets with guidance to use `cmd.exe /c`, `powershell.exe -File`, or `node <script>`. (#60689) Thanks @steipete.
|
||||
- Auth/failover: persist selected fallback overrides before retrying, shorten `auth_permanent` lockouts, and refresh websocket/shared-auth sessions only when real auth changes occur so retries and secret rotations behave predictably. (#60404, #60323, #60387) Thanks @extrasmall0 and @mappel-nv.
|
||||
- Gateway/startup: default `gateway.mode` to `local` when unset, detect PID recycling in gateway lock files on Windows and macOS, and show startup progress so healthy restarts stop getting blocked by stale locks. (#54801, #60085, #59843) Thanks @BradGroux and @TonyDerek-dot.
|
||||
- Gateway/macOS: let launchd `KeepAlive` own in-process gateway restarts again, adding a short supervised-exit delay so rapid restarts avoid launchd crash-loop unloads while `openclaw gateway restart` still reports real LaunchAgent errors synchronously.
|
||||
- Gateway/macOS: re-bootstrap the LaunchAgent if `launchctl kickstart -k` unloads it during restart so failed restarts do not leave the gateway unmanaged until manual repair.
|
||||
- Gateway/macOS: recover installed-but-unloaded LaunchAgents during `openclaw gateway start` and `restart`, while still preferring live unmanaged gateways during restart recovery. (#43766) Thanks @HenryC-3.
|
||||
- Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loudly when `/Run` does not start, and report fast failed restarts accurately instead of pretending they timed out after 60 seconds. (#59335) Thanks @tmimmanuel.
|
||||
- Gateway/channels: pin the initial startup channel registry before later plugin-registry churn so configured channels stay visible and `channels.status` stops falling back to empty `channelOrder` / `channels` payloads after runtime plugin loads.
|
||||
- Windows/restart: fall back to the installed Startup-entry launcher when the scheduled task was never registered, so `/restart` can relaunch the gateway on Windows setups where `schtasks` install fell back during onboarding. (#58943) Thanks @imechZhangLY.
|
||||
- Prompt caching: order stable workspace project-context files before `HEARTBEAT.md` and keep `HEARTBEAT.md` below the system-prompt cache boundary so heartbeat churn does not invalidate the stable project-context prefix. (#58979) Thanks @yozu and @vincentkoc.
|
||||
- Prompt caching: route Codex Responses and Anthropic Vertex through boundary-aware cache shaping, and report the actual outbound system prompt in cache traces so cache reuse and misses line up with what providers really receive. Thanks @vincentkoc.
|
||||
- Agents/cache: preserve the full 3-turn prompt-cache image window across tool loops, keep colliding bundled MCP tool definitions deterministic, and reapply Anthropic Vertex cache shaping after payload hook replacements so KV/cache reuse stays stable. Thanks @vincentkoc.
|
||||
@@ -196,13 +217,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/cache: inherit the active gateway workspace for provider, web-search, and web-fetch snapshot loads when callers omit `workspaceDir`, so compatible plugin registries and snapshot caches stop missing on gateway-owned runtime paths. (#61138) Thanks @jzakirov.
|
||||
- Plugin SDK/context engines: export the missing context-engine result and subagent lifecycle types from `openclaw/plugin-sdk` so context engine plugins can type `ContextEngine` implementations without local workarounds. (#61251) Thanks @DaevMithran.
|
||||
- Tasks/maintenance: reconcile stale cron and chat-backed CLI task rows against live cron-job and agent-run ownership instead of treating any persisted session key as proof that the task is still running. (#60310) Thanks @lml2468.
|
||||
- Update/npm: prefer the npm binary that owns the installed global OpenClaw prefix so mixed Homebrew-plus-nvm setups update the right install. (#60153) Thanks @jayeshp19.
|
||||
- Windows/restart: clean up stale gateway listeners before Windows self-restart and treat listener and argv probe failures as inconclusive, so scheduled-task relaunch no longer falls into an `EADDRINUSE` retry loop. (#60480) Thanks @arifahmedjoy.
|
||||
- Plugins: suppress trust-warning noise during non-activating snapshot and CLI metadata loads. (#61427) Thanks @gumadeiras.
|
||||
- Agents/video generation: accept `agents.defaults.videoGenerationModel` in strict config validation and `openclaw config set/get`, so gateways using `video_generate` no longer fail to boot after enabling a video model.
|
||||
- Discord/image generation: persist volatile workspace-generated media into durable outbound media before final reply delivery so generated image replies stop failing with missing local workspace paths.
|
||||
- Matrix: move legacy top-level `avatarUrl` into the default account during multi-account promotion and keep env-backed account setup avatar config persisted. (#61437) Thanks @gumadeiras.
|
||||
- Matrix/DM sessions: add `channels.matrix.dm.sessionScope`, shared-session collision notices, and aligned outbound session reuse so separate Matrix DM rooms can keep distinct context when configured. (#61373) Thanks @gumadeiras.
|
||||
- Matrix/streaming: add a quiet preview mode for streamed Matrix replies, keep legacy `partial` preview-first behavior, and finalize quiet media captions correctly so previews stop notifying early without dropping final text semantics. (#61450) Thanks @gumadeiras.
|
||||
|
||||
## 2026.4.2
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (o
|
||||
OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boundary.
|
||||
|
||||
- Authenticated Gateway callers are treated as trusted operators for that gateway instance.
|
||||
- Direct localhost/loopback Control UI and Gateway WebSocket sessions authenticated with the shared gateway secret (`token` / `password`) are in that same trusted-operator bucket. Local auto-paired device sessions on that path are expected to retain full localhost operator capability; they do not create a separate `operator.write` vs `operator.admin` security boundary.
|
||||
- The HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) and direct tool endpoint (`POST /tools/invoke`) are in that same trusted-operator bucket. Passing Gateway bearer auth there is equivalent to operator access for that gateway; they do not implement a narrower `operator.write` vs `operator.admin` trust split.
|
||||
- Concretely, on the OpenAI-compatible HTTP surface:
|
||||
- shared-secret bearer auth (`token` / `password`) authenticates possession of the gateway operator secret
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Shared iOS version defaults.
|
||||
// Generated overrides live in build/Version.xcconfig (git-ignored).
|
||||
|
||||
OPENCLAW_GATEWAY_VERSION = 2026.4.4
|
||||
OPENCLAW_MARKETING_VERSION = 2026.4.4
|
||||
OPENCLAW_BUILD_VERSION = 2026040401
|
||||
OPENCLAW_GATEWAY_VERSION = 2026.4.5
|
||||
OPENCLAW_MARKETING_VERSION = 2026.4.5
|
||||
OPENCLAW_BUILD_VERSION = 2026040501
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.4.4</string>
|
||||
<string>2026.4.5</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026040401</string>
|
||||
<string>2026040501</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
57a3b1cc7d573c3788a670d927eac947fb1685384804f5c3c926f702a27fe00b config-baseline.json
|
||||
82163136ff466db3caa61290fd65a8b8dd9487fc61f3871c177f96fcecf9e29b config-baseline.core.json
|
||||
0135fa04d71f209a54b076f41a3f6cb9795c9169fa631364fb3561eb5ff89891 config-baseline.json
|
||||
0e93c22a45545e13c74647f4945e9d8540d359640ed8c364b0f2514c9dc7a66c config-baseline.core.json
|
||||
ae67508350baf891b902348d55fada6c17e9c053adf53aaf3a8b92cd364ef3f1 config-baseline.channel.json
|
||||
d972a11d0f86080a722bddfe48990dd1b8fa16eb8e157e83f49bd46a5941c512 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
1a70d4d4f34ba5d0708a17540c0cbf1c98f50d37f25d2f71ad99b8bf6856cf9b plugin-sdk-api-baseline.json
|
||||
99cbe863efbed5ab42e0e7053d9486179aa689807696f0ebc4f4b89f1fe8cdfd plugin-sdk-api-baseline.jsonl
|
||||
97509287d728c8f5d1736f7ea07521451ada4b9d7ef56555dbe860a89e1b6e08 plugin-sdk-api-baseline.json
|
||||
a22b3d427953cc8394b28c87ef7a992d2eb4f2c9f6a76fa58b33079e2306661b plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -178,9 +178,9 @@ This is a practical baseline config with DM pairing, room allowlist, and E2EE en
|
||||
|
||||
Matrix reply streaming is opt-in.
|
||||
|
||||
Set `channels.matrix.streaming` to `"partial"` when you want OpenClaw to send a single draft reply,
|
||||
edit that draft in place while the model is generating text, and then finalize it when the reply is
|
||||
done:
|
||||
Set `channels.matrix.streaming` to `"partial"` when you want OpenClaw to send a single live preview
|
||||
reply, edit that preview in place while the model is generating text, and then finalize it when the
|
||||
reply is done:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -193,15 +193,164 @@ done:
|
||||
```
|
||||
|
||||
- `streaming: "off"` is the default. OpenClaw waits for the final reply and sends it once.
|
||||
- `streaming: "partial"` creates one editable preview message for the current assistant block instead of sending multiple partial messages.
|
||||
- `blockStreaming: true` enables separate Matrix progress messages. With `streaming: "partial"`, Matrix keeps the live draft for the current block and preserves completed blocks as separate messages.
|
||||
- When `streaming: "partial"` and `blockStreaming` is off, Matrix only edits the live draft and sends the completed reply once that block or turn finishes.
|
||||
- `streaming: "partial"` creates one editable preview message for the current assistant block using normal Matrix text messages. This preserves Matrix's legacy preview-first notification behavior, so stock clients may notify on the first streamed preview text instead of the finished block.
|
||||
- `streaming: "quiet"` creates one editable quiet preview notice for the current assistant block. Use this only when you also configure recipient push rules for finalized preview edits.
|
||||
- `blockStreaming: true` enables separate Matrix progress messages. With preview streaming enabled, Matrix keeps the live draft for the current block and preserves completed blocks as separate messages.
|
||||
- When preview streaming is on and `blockStreaming` is off, Matrix edits the live draft in place and finalizes that same event when the block or turn finishes.
|
||||
- If the preview no longer fits in one Matrix event, OpenClaw stops preview streaming and falls back to normal final delivery.
|
||||
- Media replies still send attachments normally. If a stale preview can no longer be reused safely, OpenClaw redacts it before sending the final media reply.
|
||||
- Preview edits cost extra Matrix API calls. Leave streaming off if you want the most conservative rate-limit behavior.
|
||||
|
||||
`blockStreaming` does not enable draft previews by itself.
|
||||
Use `streaming: "partial"` for preview edits; then add `blockStreaming: true` only if you also want completed assistant blocks to remain visible as separate progress messages.
|
||||
Use `streaming: "partial"` or `streaming: "quiet"` for preview edits; then add `blockStreaming: true` only if you also want completed assistant blocks to remain visible as separate progress messages.
|
||||
|
||||
If you need stock Matrix notifications without custom push rules, use `streaming: "partial"` for preview-first behavior or leave `streaming` off for final-only delivery. With `streaming: "off"`:
|
||||
|
||||
- `blockStreaming: true` sends each finished block as a normal notifying Matrix message.
|
||||
- `blockStreaming: false` sends only the final completed reply as a normal notifying Matrix message.
|
||||
|
||||
### Self-hosted push rules for quiet finalized previews
|
||||
|
||||
If you run your own Matrix infrastructure and want quiet previews to notify only when a block or
|
||||
final reply is done, set `streaming: "quiet"` and add a per-user push rule for finalized preview edits.
|
||||
|
||||
This is usually a recipient-user setup, not a homeserver-global config change:
|
||||
|
||||
Quick map before you start:
|
||||
|
||||
- recipient user = the person who should receive the notification
|
||||
- bot user = the OpenClaw Matrix account that sends the reply
|
||||
- use the recipient user's access token for the API calls below
|
||||
- match `sender` in the push rule against the bot user's full MXID
|
||||
|
||||
1. Configure OpenClaw to use quiet previews:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
matrix: {
|
||||
streaming: "quiet",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Make sure the recipient account already receives normal Matrix push notifications. Quiet preview
|
||||
rules only work if that user already has working pushers/devices.
|
||||
|
||||
3. Get the recipient user's access token.
|
||||
- Use the receiving user's token, not the bot's token.
|
||||
- Reusing an existing client session token is usually easiest.
|
||||
- If you need to mint a fresh token, you can log in through the standard Matrix Client-Server API:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
"https://matrix.example.org/_matrix/client/v3/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user",
|
||||
"user": "@alice:example.org"
|
||||
},
|
||||
"password": "REDACTED"
|
||||
}'
|
||||
```
|
||||
|
||||
4. Verify the recipient account already has pushers:
|
||||
|
||||
```bash
|
||||
curl -sS \
|
||||
-H "Authorization: Bearer $USER_ACCESS_TOKEN" \
|
||||
"https://matrix.example.org/_matrix/client/v3/pushers"
|
||||
```
|
||||
|
||||
If this returns no active pushers/devices, fix normal Matrix notifications first before adding the
|
||||
OpenClaw rule below.
|
||||
|
||||
OpenClaw marks finalized text-only preview edits with:
|
||||
|
||||
```json
|
||||
{
|
||||
"com.openclaw.finalized_preview": true
|
||||
}
|
||||
```
|
||||
|
||||
5. Create an override push rule for each recipient account which should receive these notifications:
|
||||
|
||||
```bash
|
||||
curl -sS -X PUT \
|
||||
"https://matrix.example.org/_matrix/client/v3/pushrules/global/override/openclaw-finalized-preview" \
|
||||
-H "Authorization: Bearer $USER_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{
|
||||
"conditions": [
|
||||
{ "kind": "event_match", "key": "type", "pattern": "m.room.message" },
|
||||
{
|
||||
"kind": "event_property_is",
|
||||
"key": "content.m\\.relates_to.rel_type",
|
||||
"value": "m.replace"
|
||||
},
|
||||
{
|
||||
"kind": "event_property_is",
|
||||
"key": "content.com\\.openclaw\\.finalized_preview",
|
||||
"value": true
|
||||
},
|
||||
{ "kind": "event_match", "key": "sender", "pattern": "@bot:example.org" }
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{ "set_tweak": "sound", "value": "default" },
|
||||
{ "set_tweak": "highlight", "value": false }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Replace these values before you run the command:
|
||||
|
||||
- `https://matrix.example.org`: your homeserver base URL
|
||||
- `$USER_ACCESS_TOKEN`: the receiving user's access token
|
||||
- `@bot:example.org`: your OpenClaw Matrix bot MXID, not the receiving user's MXID
|
||||
|
||||
The rule is evaluated against the event sender:
|
||||
|
||||
- authenticate with the receiving user's token
|
||||
- match `sender` against the OpenClaw bot MXID
|
||||
|
||||
6. Verify the rule exists:
|
||||
|
||||
```bash
|
||||
curl -sS \
|
||||
-H "Authorization: Bearer $USER_ACCESS_TOKEN" \
|
||||
"https://matrix.example.org/_matrix/client/v3/pushrules/global/override/openclaw-finalized-preview"
|
||||
```
|
||||
|
||||
7. Test a streamed reply. In quiet mode, the room should show a quiet draft preview and the final
|
||||
in-place edit should notify once the block or turn finishes.
|
||||
|
||||
Notes:
|
||||
|
||||
- Create the rule with the receiving user's access token, not the bot's.
|
||||
- New user-defined `override` rules are inserted ahead of default suppress rules, so no extra ordering parameter is needed.
|
||||
- This only affects text-only preview edits that OpenClaw can safely finalize in place. Media fallbacks and stale-preview fallbacks still use normal Matrix delivery.
|
||||
- If `GET /_matrix/client/v3/pushers` shows no pushers, the user does not yet have working Matrix push delivery for this account/device.
|
||||
|
||||
#### Synapse
|
||||
|
||||
For Synapse, the setup above is usually enough by itself:
|
||||
|
||||
- No special `homeserver.yaml` change is required for finalized OpenClaw preview notifications.
|
||||
- If your Synapse deployment already sends normal Matrix push notifications, the user token + `pushrules` call above is the main setup step.
|
||||
- If you run Synapse behind a reverse proxy or workers, make sure `/_matrix/client/.../pushrules/` reaches Synapse correctly.
|
||||
- If you run Synapse workers, make sure pushers are healthy. Push delivery is handled by the main process or `synapse.app.pusher` / configured pusher workers.
|
||||
|
||||
#### Tuwunel
|
||||
|
||||
For Tuwunel, use the same setup flow and push-rule API call shown above:
|
||||
|
||||
- No Tuwunel-specific config is required for the finalized preview marker itself.
|
||||
- If normal Matrix notifications already work for that user, the user token + `pushrules` call above is the main setup step.
|
||||
- If notifications seem to disappear while the user is active on another device, check whether `suppress_push_when_active` is enabled. Tuwunel added this option in Tuwunel 1.4.2 on September 12, 2025, and it can intentionally suppress pushes to other devices while one device is active.
|
||||
|
||||
## Encryption and verification
|
||||
|
||||
@@ -833,7 +982,7 @@ Live directory lookup uses the logged-in Matrix account:
|
||||
- `historyLimit`: max room messages to include as group history context. Falls back to `messages.groupChat.historyLimit`. Set `0` to disable.
|
||||
- `replyToMode`: `off`, `first`, or `all`.
|
||||
- `markdown`: optional Markdown rendering configuration for outbound Matrix text.
|
||||
- `streaming`: `off` (default), `partial`, `true`, or `false`. `partial` and `true` enable single-message draft previews with edit-in-place updates.
|
||||
- `streaming`: `off` (default), `partial`, `quiet`, `true`, or `false`. `partial` and `true` enable preview-first draft updates with normal Matrix text messages. `quiet` uses non-notifying preview notices for self-hosted push-rule setups.
|
||||
- `blockStreaming`: `true` enables separate progress messages for completed assistant blocks while draft preview streaming is active.
|
||||
- `threadReplies`: `off`, `inbound`, or `always`.
|
||||
- `threadBindings`: per-channel overrides for thread-bound session routing and lifecycle.
|
||||
|
||||
@@ -79,6 +79,12 @@ pnpm qa:lab:build
|
||||
pnpm openclaw qa ui
|
||||
```
|
||||
|
||||
Full repo-backed QA suite:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite
|
||||
```
|
||||
|
||||
That launches the private QA debugger at a local URL, separate from the
|
||||
shipped Control UI bundle.
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
|
||||
Reply threading controls:
|
||||
|
||||
- `channels.slack.replyToMode`: `off|first|all` (default `off`)
|
||||
- `channels.slack.replyToMode`: `off|first|all|batched` (default `off`)
|
||||
- `channels.slack.replyToModeByChatType`: per `direct|group|channel`
|
||||
- legacy fallback for direct chats: `channels.slack.dm.replyToMode`
|
||||
|
||||
|
||||
@@ -93,13 +93,14 @@ Full options:
|
||||
## Dreaming (experimental)
|
||||
|
||||
Dreaming is the background memory consolidation system with three cooperative
|
||||
phases: **light** (organize into daily note), **deep** (promote into
|
||||
`MEMORY.md`), and **REM** (reflect and find patterns in the daily note).
|
||||
phases: **light** (organize into `DREAMS.md` in inline mode), **deep**
|
||||
(promote into `MEMORY.md`), and **REM** (reflect and find patterns in
|
||||
`DREAMS.md` in inline mode).
|
||||
|
||||
- Enable with `plugins.entries.memory-core.config.dreaming.enabled: true`.
|
||||
- Toggle from chat with `/dreaming on|off` or `/dreaming enable|disable light|deep|rem`.
|
||||
- Each phase runs on its own cron schedule, managed automatically by `memory-core`.
|
||||
- Only the deep phase writes to `MEMORY.md`. Light and REM write to the daily note only.
|
||||
- Only the deep phase writes durable memory to `MEMORY.md`. With default inline storage, Light and REM write to `DREAMS.md`.
|
||||
- Ranking uses weighted signals: recall frequency, retrieval relevance, query diversity, temporal recency, cross-day consolidation, and derived concept richness.
|
||||
- Promotion re-reads the live daily note before writing to `MEMORY.md`, so edited or deleted short-term snippets do not get promoted from stale recall-store snapshots.
|
||||
- Scheduled and manual `memory promote` runs share the same deep phase defaults unless you pass CLI threshold overrides.
|
||||
|
||||
@@ -31,6 +31,8 @@ Current usage-window providers: Anthropic, GitHub Copilot, Gemini CLI, OpenAI
|
||||
Codex, MiniMax, Xiaomi, and z.ai. Usage auth comes from provider-specific hooks
|
||||
when available; otherwise OpenClaw falls back to matching OAuth/API-key
|
||||
credentials from auth profiles, env, or config.
|
||||
In `--json` output, `auth.providers` is the env/config/store-aware provider
|
||||
overview, while `auth.oauth` is auth-store profile health only.
|
||||
Add `--probe` to run live auth probes against each configured provider profile.
|
||||
Probes are real requests (may consume tokens and trigger rate limits).
|
||||
Use `--agent <id>` to inspect a configured agent’s model/auth state. When omitted,
|
||||
|
||||
@@ -22,7 +22,8 @@ a distinct job, writes to a distinct target, and runs on its own schedule.
|
||||
|
||||
Light dreaming sorts the recent mess. It scans recent memory traces, dedupes
|
||||
them by Jaccard similarity, clusters related entries, and stages candidate
|
||||
memories into the daily memory note (`memory/YYYY-MM-DD.md`).
|
||||
memories into the shared dreaming trail file (`DREAMS.md`) when inline storage
|
||||
is enabled.
|
||||
|
||||
Light does **not** write anything into `MEMORY.md`. It only organizes and
|
||||
stages. Think: "what from today might matter later?"
|
||||
@@ -41,18 +42,19 @@ threshold). Think: "what is true enough to keep?"
|
||||
|
||||
REM dreaming looks for patterns and reflection. It examines recent material,
|
||||
identifies recurring themes through concept tag clustering, and writes
|
||||
higher-order notes and reflections into the daily note.
|
||||
higher-order notes and reflections into `DREAMS.md` when inline storage is
|
||||
enabled.
|
||||
|
||||
REM writes to the daily note (`memory/YYYY-MM-DD.md`), **not** `MEMORY.md`.
|
||||
REM writes to `DREAMS.md` in inline mode, **not** `MEMORY.md`.
|
||||
Its output is interpretive, not canonical. Think: "what pattern am I noticing?"
|
||||
|
||||
## Hard boundaries
|
||||
|
||||
| Phase | Job | Writes to | Does NOT write to |
|
||||
| ----- | --------- | -------------------------- | ----------------- |
|
||||
| Light | Organize | Daily note (YYYY-MM-DD.md) | MEMORY.md |
|
||||
| Deep | Preserve | MEMORY.md | -- |
|
||||
| REM | Interpret | Daily note (YYYY-MM-DD.md) | MEMORY.md |
|
||||
| Phase | Job | Writes to | Does NOT write to |
|
||||
| ----- | --------- | ------------------------- | ----------------- |
|
||||
| Light | Organize | `DREAMS.md` (inline mode) | MEMORY.md |
|
||||
| Deep | Preserve | MEMORY.md | -- |
|
||||
| REM | Interpret | `DREAMS.md` (inline mode) | MEMORY.md |
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -105,12 +107,12 @@ for the full key list.
|
||||
|
||||
### Global settings
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ---------------- | --------- | ---------- | ------------------------------------------------ |
|
||||
| `enabled` | `boolean` | `true` | Master switch for all phases |
|
||||
| `timezone` | `string` | unset | Timezone for schedule evaluation and daily notes |
|
||||
| `verboseLogging` | `boolean` | `false` | Emit detailed per-run dreaming logs |
|
||||
| `storage.mode` | `string` | `"inline"` | `inline`, `separate`, or `both` |
|
||||
| Key | Type | Default | Description |
|
||||
| ---------------- | --------- | ---------- | ------------------------------------------------------------ |
|
||||
| `enabled` | `boolean` | `true` | Master switch for all phases |
|
||||
| `timezone` | `string` | unset | Timezone for schedule evaluation and dreaming date bucketing |
|
||||
| `verboseLogging` | `boolean` | `false` | Emit detailed per-run dreaming logs |
|
||||
| `storage.mode` | `string` | `"inline"` | Inline `DREAMS.md`, separate reports, or both |
|
||||
|
||||
### Light phase config
|
||||
|
||||
@@ -231,7 +233,8 @@ See [memory CLI](/cli/memory) for the full flag reference.
|
||||
2. Filter entries within `lookbackDays` of the current time.
|
||||
3. Deduplicate by Jaccard similarity (configurable threshold).
|
||||
4. Sort by average recall score, take up to `limit` entries.
|
||||
5. Write staged candidates into the daily note under a `## Light Sleep` block.
|
||||
5. Write staged candidates into `DREAMS.md` under a `## Light Sleep` block when
|
||||
inline storage is enabled.
|
||||
|
||||
### Deep phase pipeline
|
||||
|
||||
@@ -249,7 +252,8 @@ See [memory CLI](/cli/memory) for the full flag reference.
|
||||
1. Read recent memory traces within `lookbackDays`.
|
||||
2. Cluster concept tags by co-occurrence.
|
||||
3. Filter patterns by `minPatternStrength`.
|
||||
4. Write themes and reflections into the daily note under a `## REM Sleep` block.
|
||||
4. Write themes and reflections into `DREAMS.md` under a `## REM Sleep` block
|
||||
when inline storage is enabled.
|
||||
|
||||
## Scheduling
|
||||
|
||||
|
||||
@@ -162,10 +162,14 @@ Current bundled examples:
|
||||
OpenAI/Codex catalog rows, thinking/live-model policy, usage-token alias
|
||||
normalization (`input` / `output` and `prompt` / `completion` families), the
|
||||
shared `openai-responses-defaults` stream family for native OpenAI/Codex
|
||||
wrappers, and provider-family metadata
|
||||
wrappers, provider-family metadata, bundled image-generation provider
|
||||
registration for `gpt-image-1`, and bundled video-generation provider
|
||||
registration for `sora-2`
|
||||
- `google`: Gemini 3.1 forward-compat fallback, native Gemini replay
|
||||
validation, bootstrap replay sanitation, tagged reasoning-output mode, and
|
||||
modern-model matching
|
||||
validation, bootstrap replay sanitation, tagged reasoning-output mode,
|
||||
modern-model matching, bundled image-generation provider registration for
|
||||
Gemini image-preview models, and bundled video-generation provider
|
||||
registration for Veo models
|
||||
- `moonshot`: shared transport, plugin-owned thinking payload normalization
|
||||
- `kilocode`: shared transport, plugin-owned request headers, reasoning payload
|
||||
normalization, proxy-Gemini thought-signature sanitation, and cache-TTL
|
||||
@@ -174,20 +178,32 @@ Current bundled examples:
|
||||
policy, binary-thinking/live-model policy, and usage auth + quota fetching;
|
||||
unknown `glm-5*` ids synthesize from the bundled `glm-4.7` template
|
||||
- `xai`: native Responses transport normalization, `/fast` alias rewrites for
|
||||
Grok fast variants, default `tool_stream`, and xAI-specific tool-schema /
|
||||
reasoning-payload cleanup
|
||||
Grok fast variants, default `tool_stream`, xAI-specific tool-schema /
|
||||
reasoning-payload cleanup, and bundled video-generation provider
|
||||
registration for `grok-imagine-video`
|
||||
- `mistral`: plugin-owned capability metadata
|
||||
- `opencode` and `opencode-go`: plugin-owned capability metadata plus
|
||||
proxy-Gemini thought-signature sanitation
|
||||
- `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi`,
|
||||
`nvidia`, `qianfan`, `stepfun`, `synthetic`, `together`, `venice`,
|
||||
`vercel-ai-gateway`, and `volcengine`: plugin-owned catalogs only
|
||||
- `alibaba`: plugin-owned video-generation catalog for direct Wan model refs
|
||||
such as `alibaba/wan2.6-t2v`
|
||||
- `byteplus`: plugin-owned catalogs plus bundled video-generation provider
|
||||
registration for Seedance text-to-video/image-to-video models
|
||||
- `fal`: bundled video-generation provider registration for hosted third-party
|
||||
image-generation provider registration for FLUX image models plus bundled
|
||||
video-generation provider registration for hosted third-party video models
|
||||
- `cloudflare-ai-gateway`, `huggingface`, `kimi`, `nvidia`, `qianfan`,
|
||||
`stepfun`, `synthetic`, `venice`, `vercel-ai-gateway`, and `volcengine`:
|
||||
plugin-owned catalogs only
|
||||
- `qwen`: plugin-owned catalogs for text models plus shared
|
||||
media-understanding and video-generation provider registrations for its
|
||||
multimodal surfaces; Qwen video generation uses the Standard DashScope video
|
||||
endpoints with bundled Wan models such as `wan2.6-t2v` and `wan2.7-r2v`
|
||||
- `minimax`: plugin-owned catalogs, hybrid Anthropic/OpenAI replay-policy
|
||||
- `minimax`: plugin-owned catalogs, bundled video-generation provider
|
||||
registration for Hailuo video models, bundled image-generation provider
|
||||
registration for `image-01`, hybrid Anthropic/OpenAI replay-policy
|
||||
selection, and usage auth/snapshot logic
|
||||
- `together`: plugin-owned catalogs plus bundled video-generation provider
|
||||
registration for Wan video models
|
||||
- `xiaomi`: plugin-owned catalogs plus usage auth/snapshot logic
|
||||
|
||||
The bundled `openai` plugin now owns both provider ids: `openai` and
|
||||
|
||||
@@ -175,7 +175,8 @@ resolved primary model.
|
||||
OAuth status is always shown (and included in `--json` output). If a configured
|
||||
provider has no credentials, `models status` prints a **Missing auth** section.
|
||||
JSON includes `auth.oauth` (warn window + profiles) and `auth.providers`
|
||||
(effective auth per provider).
|
||||
(effective auth per provider, including env-backed credentials). `auth.oauth`
|
||||
is auth-store profile health only; env-only providers do not appear there.
|
||||
Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
|
||||
Use `--probe` for live auth checks; probe rows can come from auth profiles, env
|
||||
credentials, or `models.json`.
|
||||
|
||||
@@ -1259,7 +1259,6 @@
|
||||
"providers/openrouter",
|
||||
"providers/perplexity-provider",
|
||||
"providers/qianfan",
|
||||
"providers/qwen_modelstudio",
|
||||
"providers/qwen",
|
||||
"providers/sglang",
|
||||
"providers/stepfun",
|
||||
|
||||
@@ -179,7 +179,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
{ command: "generate", description: "Create an image" },
|
||||
],
|
||||
historyLimit: 50,
|
||||
replyToMode: "first", // off | first | all
|
||||
replyToMode: "first", // off | first | all | batched
|
||||
linkPreview: true,
|
||||
streaming: "partial", // off | partial | block | progress (default: off; opt in explicitly to avoid preview-edit rate limits)
|
||||
actions: { reactions: true, sendMessage: true },
|
||||
@@ -239,7 +239,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
events: true,
|
||||
moderation: false,
|
||||
},
|
||||
replyToMode: "off", // off | first | all
|
||||
replyToMode: "off", // off | first | all | batched
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["1234567890", "123456789012345678"],
|
||||
dm: { enabled: true, groupEnabled: false, groupChannels: ["openclaw-dm"] },
|
||||
@@ -405,7 +405,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
allowBots: false,
|
||||
reactionNotifications: "own",
|
||||
reactionAllowlist: ["U123"],
|
||||
replyToMode: "off", // off | first | all
|
||||
replyToMode: "off", // off | first | all | batched
|
||||
thread: {
|
||||
historyScope: "thread", // thread | channel
|
||||
inheritParent: false,
|
||||
|
||||
@@ -50,7 +50,8 @@ gateway without forcing a `tsdown` rebuild; source and config changes still
|
||||
rebuild `dist` first.
|
||||
|
||||
Add any gateway CLI flags after `gateway:watch` and they will be passed through on
|
||||
each restart.
|
||||
each restart. Re-running the same watch command for the same repo/flag set now
|
||||
replaces the older watcher instead of leaving duplicate watcher parents behind.
|
||||
|
||||
## Dev profile + dev gateway (--dev)
|
||||
|
||||
|
||||
@@ -442,6 +442,10 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
|
||||
The live-model Docker runners also bind-mount the current checkout read-only and
|
||||
stage it into a temporary workdir inside the container. This keeps the runtime
|
||||
image slim while still running Vitest against your exact local source/config.
|
||||
The staging step skips large local-only caches and app build outputs such as
|
||||
`.pnpm-store`, `.worktrees`, `__openclaw_vitest__`, and app-local `.build` or
|
||||
Gradle output directories so Docker live runs do not spend minutes copying
|
||||
machine-specific artifacts.
|
||||
They also set `OPENCLAW_SKIP_CHANNELS=1` so gateway live probes do not start
|
||||
real Telegram/Discord/etc. channel workers inside the container.
|
||||
`test:docker:live-models` still runs `pnpm test:live`, so pass through
|
||||
@@ -479,8 +483,8 @@ Useful env vars:
|
||||
- `OPENCLAW_PROFILE_FILE=...` (default: `~/.profile`) mounted to `/home/node/.profile` and sourced before running tests
|
||||
- `OPENCLAW_DOCKER_CLI_TOOLS_DIR=...` (default: `~/.cache/openclaw/docker-cli-tools`) mounted to `/home/node/.npm-global` for cached CLI installs inside Docker
|
||||
- External CLI auth dirs/files under `$HOME` are mounted read-only under `/host-auth...`, then copied into `/home/node/...` before tests start
|
||||
- Default dirs: `.codex`, `.minimax`
|
||||
- Default files: `.claude.json`, `~/.claude/.credentials.json`, `~/.claude/settings.json`, `~/.claude/settings.local.json`
|
||||
- Default dirs: `.minimax`
|
||||
- Default files: `~/.codex/auth.json`, `~/.codex/config.toml`, `.claude.json`, `~/.claude/.credentials.json`, `~/.claude/settings.json`, `~/.claude/settings.local.json`
|
||||
- Narrowed provider runs mount only the needed dirs/files inferred from `OPENCLAW_LIVE_PROVIDERS` / `OPENCLAW_LIVE_GATEWAY_PROVIDERS`
|
||||
- Override manually with `OPENCLAW_DOCKER_AUTH_DIRS=all`, `OPENCLAW_DOCKER_AUTH_DIRS=none`, or a comma list like `OPENCLAW_DOCKER_AUTH_DIRS=.claude,.codex`
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS=...` / `OPENCLAW_LIVE_MODELS=...` to narrow the run
|
||||
|
||||
@@ -103,12 +103,7 @@ docker build -t openclaw:local -f Dockerfile .
|
||||
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
|
||||
dist/index.js onboard --mode local --no-install-daemon
|
||||
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
|
||||
dist/index.js config set gateway.mode local
|
||||
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
|
||||
dist/index.js config set gateway.bind lan
|
||||
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
|
||||
dist/index.js config set gateway.controlUi.allowedOrigins \
|
||||
'["http://localhost:18789","http://127.0.0.1:18789"]' --strict-json
|
||||
dist/index.js config set --batch-json '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"lan"},{"path":"gateway.controlUi.allowedOrigins","value":["http://localhost:18789","http://127.0.0.1:18789"]}]'
|
||||
docker compose up -d openclaw-gateway
|
||||
```
|
||||
|
||||
@@ -395,8 +390,7 @@ scripts/sandbox-setup.sh
|
||||
Reset gateway mode and bind:
|
||||
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli config set gateway.mode local
|
||||
docker compose run --rm openclaw-cli config set gateway.bind lan
|
||||
docker compose run --rm openclaw-cli config set --batch-json '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"lan"}]'
|
||||
docker compose run --rm openclaw-cli devices list --url ws://127.0.0.1:18789
|
||||
```
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ Hook guard semantics to keep in mind:
|
||||
- `before_tool_call`: `{ requireApproval: true }` pauses agent execution and prompts the user for approval via the exec approval overlay, Telegram buttons, Discord interactions, or the `/approve` command on any channel.
|
||||
- `before_install`: `{ block: true }` is terminal and stops lower-priority handlers.
|
||||
- `before_install`: `{ block: false }` is treated as no decision.
|
||||
- `tool_result_persist`: must stay synchronous because it runs in the transcript persistence path; return an updated tool result payload or `undefined` to keep the original.
|
||||
- `message_sending`: `{ cancel: true }` is terminal and stops lower-priority handlers.
|
||||
- `message_sending`: `{ cancel: false }` is treated as no decision.
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ new plugin code.
|
||||
|
||||
The same rule applies to other bundled-helper families such as:
|
||||
|
||||
- browser support helpers: `plugin-sdk/browser-cdp`, `plugin-sdk/browser-config-support`, `plugin-sdk/browser-control-auth`, `plugin-sdk/browser-profiles`, `plugin-sdk/browser-support`
|
||||
- browser support helpers: `plugin-sdk/browser-cdp`, `plugin-sdk/browser-config-runtime`, `plugin-sdk/browser-config-support`, `plugin-sdk/browser-control-auth`, `plugin-sdk/browser-node-runtime`, `plugin-sdk/browser-profiles`, `plugin-sdk/browser-security-runtime`, `plugin-sdk/browser-setup-tools`, `plugin-sdk/browser-support`
|
||||
- Matrix: `plugin-sdk/matrix*`
|
||||
- LINE: `plugin-sdk/line*`
|
||||
- IRC: `plugin-sdk/irc*`
|
||||
|
||||
@@ -263,7 +263,7 @@ explicitly promotes one as public.
|
||||
<Accordion title="Reserved bundled-helper subpaths">
|
||||
| Family | Current subpaths | Intended use |
|
||||
| --- | --- | --- |
|
||||
| Browser | `plugin-sdk/browser-cdp`, `plugin-sdk/browser-config-support`, `plugin-sdk/browser-control-auth`, `plugin-sdk/browser-profiles`, `plugin-sdk/browser-support` | Bundled browser plugin support helpers |
|
||||
| Browser | `plugin-sdk/browser-cdp`, `plugin-sdk/browser-config-runtime`, `plugin-sdk/browser-config-support`, `plugin-sdk/browser-control-auth`, `plugin-sdk/browser-node-runtime`, `plugin-sdk/browser-profiles`, `plugin-sdk/browser-security-runtime`, `plugin-sdk/browser-setup-tools`, `plugin-sdk/browser-support` | Bundled browser plugin support helpers (`browser-support` remains the compatibility barrel) |
|
||||
| Matrix | `plugin-sdk/matrix`, `plugin-sdk/matrix-helper`, `plugin-sdk/matrix-runtime-heavy`, `plugin-sdk/matrix-runtime-shared`, `plugin-sdk/matrix-runtime-surface`, `plugin-sdk/matrix-surface`, `plugin-sdk/matrix-thread-bindings` | Bundled Matrix helper/runtime surface |
|
||||
| Line | `plugin-sdk/line`, `plugin-sdk/line-core`, `plugin-sdk/line-runtime`, `plugin-sdk/line-surface` | Bundled LINE helper/runtime surface |
|
||||
| IRC | `plugin-sdk/irc`, `plugin-sdk/irc-surface` | Bundled IRC helper surface |
|
||||
|
||||
72
docs/providers/alibaba.md
Normal file
72
docs/providers/alibaba.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: "Alibaba Model Studio"
|
||||
summary: "Alibaba Model Studio Wan video generation in OpenClaw"
|
||||
read_when:
|
||||
- You want to use Alibaba Wan video generation in OpenClaw
|
||||
- You need Model Studio or DashScope API key setup for video generation
|
||||
---
|
||||
|
||||
# Alibaba Model Studio
|
||||
|
||||
OpenClaw ships a bundled `alibaba` video-generation provider for Wan models on
|
||||
Alibaba Model Studio / DashScope.
|
||||
|
||||
- Provider: `alibaba`
|
||||
- Preferred auth: `MODELSTUDIO_API_KEY`
|
||||
- Also accepted: `DASHSCOPE_API_KEY`, `QWEN_API_KEY`
|
||||
- API: DashScope / Model Studio async video generation
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set an API key:
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice qwen-standard-api-key
|
||||
```
|
||||
|
||||
2. Set a default video model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "alibaba/wan2.6-t2v",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Wan models
|
||||
|
||||
The bundled `alibaba` provider currently registers:
|
||||
|
||||
- `alibaba/wan2.6-t2v`
|
||||
- `alibaba/wan2.6-i2v`
|
||||
- `alibaba/wan2.6-r2v`
|
||||
- `alibaba/wan2.6-r2v-flash`
|
||||
- `alibaba/wan2.7-r2v`
|
||||
|
||||
## Current limits
|
||||
|
||||
- Up to **1** output video per request
|
||||
- Up to **1** input image
|
||||
- Up to **4** input videos
|
||||
- Up to **10 seconds** duration
|
||||
- Supports `size`, `aspectRatio`, `resolution`, `audio`, and `watermark`
|
||||
- Reference image/video mode currently requires **remote http(s) URLs**
|
||||
|
||||
## Relationship to Qwen
|
||||
|
||||
The bundled `qwen` provider also uses Alibaba-hosted DashScope endpoints for
|
||||
Wan video generation. Use:
|
||||
|
||||
- `qwen/...` when you want the canonical Qwen provider surface
|
||||
- `alibaba/...` when you want the direct vendor-owned Wan video surface
|
||||
|
||||
## Related
|
||||
|
||||
- [Video Generation](/tools/video-generation)
|
||||
- [Qwen](/providers/qwen)
|
||||
- [Configuration Reference](/gateway/configuration-reference#agent-defaults)
|
||||
90
docs/providers/fal.md
Normal file
90
docs/providers/fal.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "fal"
|
||||
summary: "fal image and video generation setup in OpenClaw"
|
||||
read_when:
|
||||
- You want to use fal image generation in OpenClaw
|
||||
- You need the FAL_KEY auth flow
|
||||
- You want fal defaults for image_generate or video_generate
|
||||
---
|
||||
|
||||
# fal
|
||||
|
||||
OpenClaw ships a bundled `fal` provider for hosted image and video generation.
|
||||
|
||||
- Provider: `fal`
|
||||
- Auth: `FAL_KEY`
|
||||
- API: fal model endpoints
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set the API key:
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice fal-api-key
|
||||
```
|
||||
|
||||
2. Set a default image model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "fal/fal-ai/flux/dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Image generation
|
||||
|
||||
The bundled `fal` image-generation provider defaults to
|
||||
`fal/fal-ai/flux/dev`.
|
||||
|
||||
- Generate: up to 4 images per request
|
||||
- Edit mode: enabled, 1 reference image
|
||||
- Supports `size`, `aspectRatio`, and `resolution`
|
||||
- Current edit caveat: the fal image edit endpoint does **not** support
|
||||
`aspectRatio` overrides
|
||||
|
||||
To use fal as the default image provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "fal/fal-ai/flux/dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Video generation
|
||||
|
||||
The bundled `fal` video-generation provider defaults to
|
||||
`fal/fal-ai/minimax/video-01-live`.
|
||||
|
||||
- Modes: text-to-video and single-image reference flows
|
||||
|
||||
To use fal as the default video provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "fal/fal-ai/minimax/video-01-live",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Image Generation](/tools/image-generation)
|
||||
- [Video Generation](/tools/video-generation)
|
||||
- [Configuration Reference](/gateway/configuration-reference#agent-defaults)
|
||||
@@ -100,6 +100,50 @@ The bundled `google` image-generation provider defaults to
|
||||
Image generation, media understanding, and Gemini Grounding all stay on the
|
||||
`google` provider id.
|
||||
|
||||
To use Google as the default image provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "google/gemini-3.1-flash-image-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [Image Generation](/tools/image-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Video generation
|
||||
|
||||
The bundled `google` plugin also registers video generation through the shared
|
||||
`video_generate` tool.
|
||||
|
||||
- Default video model: `google/veo-3.1-fast-generate-preview`
|
||||
- Modes: text-to-video, image-to-video, and single-video reference flows
|
||||
- Supports `aspectRatio`, `resolution`, and `audio`
|
||||
- Current duration clamp: **4 to 8 seconds**
|
||||
|
||||
To use Google as the default video provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "google/veo-3.1-fast-generate-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [Video Generation](/tools/video-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure `GEMINI_API_KEY`
|
||||
|
||||
@@ -26,12 +26,14 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
||||
|
||||
## Provider docs
|
||||
|
||||
- [Alibaba Model Studio](/providers/alibaba)
|
||||
- [Amazon Bedrock](/providers/bedrock)
|
||||
- [Anthropic (API + Claude CLI)](/providers/anthropic)
|
||||
- [BytePlus (International)](/concepts/model-providers#byteplus-international)
|
||||
- [Chutes](/providers/chutes)
|
||||
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- [DeepSeek](/providers/deepseek)
|
||||
- [fal](/providers/fal)
|
||||
- [Fireworks](/providers/fireworks)
|
||||
- [GitHub Copilot](/providers/github-copilot)
|
||||
- [GLM models](/providers/glm)
|
||||
@@ -52,7 +54,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
||||
- [Perplexity (web search)](/providers/perplexity-provider)
|
||||
- [Qianfan](/providers/qianfan)
|
||||
- [Qwen Cloud](/providers/qwen)
|
||||
- [Qwen / Model Studio (endpoint detail; `qwen-*` canonical, `modelstudio-*` legacy)](/providers/qwen_modelstudio)
|
||||
- [SGLang (local models)](/providers/sglang)
|
||||
- [StepFun](/providers/stepfun)
|
||||
- [Synthetic](/providers/synthetic)
|
||||
@@ -68,6 +69,8 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
||||
## Shared overview pages
|
||||
|
||||
- [Additional bundled variants](/providers/models#additional-bundled-provider-variants) - Anthropic Vertex, Copilot Proxy, and Gemini CLI OAuth
|
||||
- [Image Generation](/tools/image-generation) - Shared `image_generate` tool, provider selection, and failover
|
||||
- [Video Generation](/tools/video-generation) - Shared `video_generate` tool, provider selection, and failover
|
||||
|
||||
## Transcription providers
|
||||
|
||||
|
||||
@@ -63,6 +63,35 @@ The built-in bundled MiniMax text catalog itself stays text-only metadata until
|
||||
that explicit provider config exists. Image understanding is exposed separately
|
||||
through the plugin-owned `MiniMax-VL-01` media provider.
|
||||
|
||||
See [Image Generation](/tools/image-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Video generation
|
||||
|
||||
The bundled `minimax` plugin also registers video generation through the shared
|
||||
`video_generate` tool.
|
||||
|
||||
- Default video model: `minimax/MiniMax-Hailuo-2.3`
|
||||
- Modes: text-to-video and single-image reference flows
|
||||
- Supports `aspectRatio` and `resolution`
|
||||
|
||||
To use MiniMax as the default video provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "minimax/MiniMax-Hailuo-2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [Video Generation](/tools/video-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Image understanding
|
||||
|
||||
The MiniMax plugin registers image understanding separately from the text
|
||||
|
||||
@@ -108,6 +108,63 @@ OpenClaw does **not** expose `openai/gpt-5.3-codex-spark` on the direct OpenAI
|
||||
API path. `pi-ai` still ships a built-in row for that model, but live OpenAI API
|
||||
requests currently reject it. Spark is treated as Codex-only in OpenClaw.
|
||||
|
||||
## Image generation
|
||||
|
||||
The bundled `openai` plugin also registers image generation through the shared
|
||||
`image_generate` tool.
|
||||
|
||||
- Default image model: `openai/gpt-image-1`
|
||||
- Generate: up to 4 images per request
|
||||
- Edit mode: enabled, up to 5 reference images
|
||||
- Supports `size`
|
||||
- Current OpenAI-specific caveat: OpenClaw does not forward `aspectRatio` or
|
||||
`resolution` overrides to the OpenAI Images API today
|
||||
|
||||
To use OpenAI as the default image provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "openai/gpt-image-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [Image Generation](/tools/image-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Video generation
|
||||
|
||||
The bundled `openai` plugin also registers video generation through the shared
|
||||
`video_generate` tool.
|
||||
|
||||
- Default video model: `openai/sora-2`
|
||||
- Modes: text-to-video, image-to-video, and single-video reference/edit flows
|
||||
- Current limits: 1 image or 1 video reference input
|
||||
- Current OpenAI-specific caveat: OpenClaw does not forward `aspectRatio` or
|
||||
`resolution` overrides to the native OpenAI video API today
|
||||
|
||||
To use OpenAI as the default video provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "openai/sora-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [Video Generation](/tools/video-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Option B: OpenAI Code (Codex) subscription
|
||||
|
||||
**Best for:** using ChatGPT/Codex subscription access instead of an API key.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
summary: "Use Qwen Cloud via OpenClaw's bundled qwen provider"
|
||||
read_when:
|
||||
|
||||
- You want to use Qwen with OpenClaw
|
||||
- You previously used Qwen OAuth
|
||||
title: "Qwen"
|
||||
|
||||
- You want to use Qwen with OpenClaw
|
||||
- You previously used Qwen OAuth
|
||||
title: "Qwen"
|
||||
---
|
||||
|
||||
# Qwen
|
||||
@@ -63,6 +62,69 @@ After onboarding, set a default model:
|
||||
}
|
||||
```
|
||||
|
||||
## Plan types and endpoints
|
||||
|
||||
| Plan | Region | Auth choice | Endpoint |
|
||||
| -------------------------- | ------ | -------------------------- | ------------------------------------------------ |
|
||||
| Standard (pay-as-you-go) | China | `qwen-standard-api-key-cn` | `dashscope.aliyuncs.com/compatible-mode/v1` |
|
||||
| Standard (pay-as-you-go) | Global | `qwen-standard-api-key` | `dashscope-intl.aliyuncs.com/compatible-mode/v1` |
|
||||
| Coding Plan (subscription) | China | `qwen-api-key-cn` | `coding.dashscope.aliyuncs.com/v1` |
|
||||
| Coding Plan (subscription) | Global | `qwen-api-key` | `coding-intl.dashscope.aliyuncs.com/v1` |
|
||||
|
||||
The provider auto-selects the endpoint based on your auth choice. Canonical
|
||||
choices use the `qwen-*` family; `modelstudio-*` remains compatibility-only.
|
||||
You can override with a custom `baseUrl` in config.
|
||||
|
||||
Native Model Studio endpoints advertise streaming usage compatibility on the
|
||||
shared `openai-completions` transport. OpenClaw keys that off endpoint
|
||||
capabilities now, so DashScope-compatible custom provider ids targeting the
|
||||
same native hosts inherit the same streaming-usage behavior instead of
|
||||
requiring the built-in `qwen` provider id specifically.
|
||||
|
||||
## Get your API key
|
||||
|
||||
- **Manage keys**: [home.qwencloud.com/api-keys](https://home.qwencloud.com/api-keys)
|
||||
- **Docs**: [docs.qwencloud.com](https://docs.qwencloud.com/developer-guides/getting-started/introduction)
|
||||
|
||||
## Built-in catalog
|
||||
|
||||
OpenClaw currently ships this bundled Qwen catalog:
|
||||
|
||||
| Model ref | Input | Context | Notes |
|
||||
| --------------------------- | ----------- | --------- | -------------------------------------------------- |
|
||||
| `qwen/qwen3.5-plus` | text, image | 1,000,000 | Default model |
|
||||
| `qwen/qwen3.6-plus` | text, image | 1,000,000 | Prefer Standard endpoints when you need this model |
|
||||
| `qwen/qwen3-max-2026-01-23` | text | 262,144 | Qwen Max line |
|
||||
| `qwen/qwen3-coder-next` | text | 262,144 | Coding |
|
||||
| `qwen/qwen3-coder-plus` | text | 1,000,000 | Coding |
|
||||
| `qwen/MiniMax-M2.5` | text | 1,000,000 | Reasoning enabled |
|
||||
| `qwen/glm-5` | text | 202,752 | GLM |
|
||||
| `qwen/glm-4.7` | text | 202,752 | GLM |
|
||||
| `qwen/kimi-k2.5` | text, image | 262,144 | Moonshot AI via Alibaba |
|
||||
|
||||
Availability can still vary by endpoint and billing plan even when a model is
|
||||
present in the bundled catalog.
|
||||
|
||||
Native-streaming usage compatibility applies to both the Coding Plan hosts and
|
||||
the Standard DashScope-compatible hosts:
|
||||
|
||||
- `https://coding.dashscope.aliyuncs.com/v1`
|
||||
- `https://coding-intl.dashscope.aliyuncs.com/v1`
|
||||
- `https://dashscope.aliyuncs.com/compatible-mode/v1`
|
||||
- `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`
|
||||
|
||||
## Qwen 3.6 Plus availability
|
||||
|
||||
`qwen3.6-plus` is available on the Standard (pay-as-you-go) Model Studio
|
||||
endpoints:
|
||||
|
||||
- China: `dashscope.aliyuncs.com/compatible-mode/v1`
|
||||
- Global: `dashscope-intl.aliyuncs.com/compatible-mode/v1`
|
||||
|
||||
If the Coding Plan endpoints return an "unsupported model" error for
|
||||
`qwen3.6-plus`, switch to Standard (pay-as-you-go) instead of the Coding Plan
|
||||
endpoint/key pair.
|
||||
|
||||
## Capability plan
|
||||
|
||||
The `qwen` extension is being positioned as the vendor home for the full Qwen
|
||||
@@ -127,5 +189,11 @@ Current bundled Qwen video-generation limits:
|
||||
file paths are rejected up front because the DashScope video endpoint does not
|
||||
accept uploaded local buffers for those references.
|
||||
|
||||
See [Qwen / Model Studio](/providers/qwen_modelstudio) for endpoint-level detail
|
||||
and compatibility notes.
|
||||
See [Video Generation](/tools/video-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure `QWEN_API_KEY` is
|
||||
available to that process (for example, in `~/.openclaw/.env` or via
|
||||
`env.shellEnv`).
|
||||
|
||||
@@ -1,137 +1,13 @@
|
||||
---
|
||||
title: "Qwen / Model Studio"
|
||||
summary: "Endpoint detail for the bundled qwen provider and its legacy modelstudio compatibility surface"
|
||||
summary: "Redirect to /providers/qwen"
|
||||
read_when:
|
||||
|
||||
- You want endpoint-level detail for Qwen Cloud / Alibaba DashScope
|
||||
- You need the env var compatibility story for the qwen provider
|
||||
- You want to use the Standard (pay-as-you-go) or Coding Plan endpoint
|
||||
|
||||
- You followed an older Model Studio link
|
||||
- You want the canonical Qwen provider page
|
||||
---
|
||||
|
||||
# Qwen / Model Studio (Alibaba Cloud)
|
||||
# Qwen / Model Studio
|
||||
|
||||
This page documents the endpoint mapping behind OpenClaw's bundled `qwen`
|
||||
provider. The provider keeps `modelstudio` provider ids, auth-choice ids, and
|
||||
model refs working as compatibility aliases while `qwen` becomes the canonical
|
||||
surface.
|
||||
|
||||
<Info>
|
||||
|
||||
If you need **`qwen3.6-plus`**, prefer **Standard (pay-as-you-go)**. Coding
|
||||
Plan availability can lag behind the public Model Studio catalog, and the
|
||||
Coding Plan API can reject a model until it appears in your plan's supported
|
||||
model list.
|
||||
|
||||
</Info>
|
||||
|
||||
- Provider: `qwen` (legacy alias: `modelstudio`)
|
||||
- Auth: `QWEN_API_KEY`
|
||||
- Also accepted: `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY`
|
||||
- API: OpenAI-compatible
|
||||
|
||||
## Quick start
|
||||
|
||||
### Standard (pay-as-you-go)
|
||||
|
||||
```bash
|
||||
# China endpoint
|
||||
openclaw onboard --auth-choice qwen-standard-api-key-cn
|
||||
|
||||
# Global/Intl endpoint
|
||||
openclaw onboard --auth-choice qwen-standard-api-key
|
||||
```
|
||||
|
||||
### Coding Plan (subscription)
|
||||
|
||||
```bash
|
||||
# China endpoint
|
||||
openclaw onboard --auth-choice qwen-api-key-cn
|
||||
|
||||
# Global/Intl endpoint
|
||||
openclaw onboard --auth-choice qwen-api-key
|
||||
```
|
||||
|
||||
Legacy `modelstudio-*` auth-choice ids still work as compatibility aliases, but
|
||||
the canonical onboarding ids are the `qwen-*` choices shown above.
|
||||
|
||||
After onboarding, set a default model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "qwen/qwen3.5-plus" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Plan types and endpoints
|
||||
|
||||
| Plan | Region | Auth choice | Endpoint |
|
||||
| -------------------------- | ------ | -------------------------- | ------------------------------------------------ |
|
||||
| Standard (pay-as-you-go) | China | `qwen-standard-api-key-cn` | `dashscope.aliyuncs.com/compatible-mode/v1` |
|
||||
| Standard (pay-as-you-go) | Global | `qwen-standard-api-key` | `dashscope-intl.aliyuncs.com/compatible-mode/v1` |
|
||||
| Coding Plan (subscription) | China | `qwen-api-key-cn` | `coding.dashscope.aliyuncs.com/v1` |
|
||||
| Coding Plan (subscription) | Global | `qwen-api-key` | `coding-intl.dashscope.aliyuncs.com/v1` |
|
||||
|
||||
The provider auto-selects the endpoint based on your auth choice. Canonical
|
||||
choices use the `qwen-*` family; `modelstudio-*` remains compatibility-only.
|
||||
You can
|
||||
override with a custom `baseUrl` in config.
|
||||
|
||||
Native Model Studio endpoints advertise streaming usage compatibility on the
|
||||
shared `openai-completions` transport. OpenClaw keys that off endpoint
|
||||
capabilities now, so DashScope-compatible custom provider ids targeting the
|
||||
same native hosts inherit the same streaming-usage behavior instead of
|
||||
requiring the built-in `qwen` provider id specifically.
|
||||
|
||||
## Get your API key
|
||||
|
||||
- **Manage keys**: [home.qwencloud.com/api-keys](https://home.qwencloud.com/api-keys)
|
||||
- **Docs**: [docs.qwencloud.com](https://docs.qwencloud.com/developer-guides/getting-started/introduction)
|
||||
|
||||
## Built-in catalog
|
||||
|
||||
OpenClaw currently ships this bundled Qwen catalog:
|
||||
|
||||
| Model ref | Input | Context | Notes |
|
||||
| --------------------------- | ----------- | --------- | -------------------------------------------------- |
|
||||
| `qwen/qwen3.5-plus` | text, image | 1,000,000 | Default model |
|
||||
| `qwen/qwen3.6-plus` | text, image | 1,000,000 | Prefer Standard endpoints when you need this model |
|
||||
| `qwen/qwen3-max-2026-01-23` | text | 262,144 | Qwen Max line |
|
||||
| `qwen/qwen3-coder-next` | text | 262,144 | Coding |
|
||||
| `qwen/qwen3-coder-plus` | text | 1,000,000 | Coding |
|
||||
| `qwen/MiniMax-M2.5` | text | 1,000,000 | Reasoning enabled |
|
||||
| `qwen/glm-5` | text | 202,752 | GLM |
|
||||
| `qwen/glm-4.7` | text | 202,752 | GLM |
|
||||
| `qwen/kimi-k2.5` | text, image | 262,144 | Moonshot AI via Alibaba |
|
||||
|
||||
Availability can still vary by endpoint and billing plan even when a model is
|
||||
present in the bundled catalog.
|
||||
|
||||
Native-streaming usage compatibility applies to both the Coding Plan hosts and
|
||||
the Standard DashScope-compatible hosts:
|
||||
|
||||
- `https://coding.dashscope.aliyuncs.com/v1`
|
||||
- `https://coding-intl.dashscope.aliyuncs.com/v1`
|
||||
- `https://dashscope.aliyuncs.com/compatible-mode/v1`
|
||||
- `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`
|
||||
|
||||
## Qwen 3.6 Plus availability
|
||||
|
||||
`qwen3.6-plus` is available on the Standard (pay-as-you-go) Model Studio
|
||||
endpoints:
|
||||
|
||||
- China: `dashscope.aliyuncs.com/compatible-mode/v1`
|
||||
- Global: `dashscope-intl.aliyuncs.com/compatible-mode/v1`
|
||||
|
||||
If the Coding Plan endpoints return an "unsupported model" error for
|
||||
`qwen3.6-plus`, switch to Standard (pay-as-you-go) instead of the Coding Plan
|
||||
endpoint/key pair.
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure
|
||||
`QWEN_API_KEY` is available to that process (for example, in
|
||||
`~/.openclaw/.env` or via `env.shellEnv`).
|
||||
This page moved to [Qwen](/providers/qwen). See [Qwen](/providers/qwen) for
|
||||
the canonical provider setup, endpoint details, compatibility aliases, and Wan
|
||||
video-generation notes.
|
||||
|
||||
@@ -68,3 +68,29 @@ OpenClaw currently ships this bundled Together catalog:
|
||||
| `together/moonshotai/Kimi-K2-Instruct-0905` | Kimi K2-Instruct 0905 | text | 262,144 | Secondary Kimi text model |
|
||||
|
||||
The onboarding preset sets `together/moonshotai/Kimi-K2.5` as the default model.
|
||||
|
||||
## Video generation
|
||||
|
||||
The bundled `together` plugin also registers video generation through the
|
||||
shared `video_generate` tool.
|
||||
|
||||
- Default video model: `together/Wan-AI/Wan2.2-T2V-A14B`
|
||||
- Modes: text-to-video and single-image reference flows
|
||||
- Supports `aspectRatio` and `resolution`
|
||||
|
||||
To use Together as the default video provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "together/Wan-AI/Wan2.2-T2V-A14B",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [Video Generation](/tools/video-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
@@ -75,6 +75,34 @@ The bundled `grok` web-search provider uses `XAI_API_KEY` too:
|
||||
openclaw config set tools.web.search.provider grok
|
||||
```
|
||||
|
||||
## Video generation
|
||||
|
||||
The bundled `xai` plugin also registers video generation through the shared
|
||||
`video_generate` tool.
|
||||
|
||||
- Default video model: `xai/grok-imagine-video`
|
||||
- Modes: text-to-video, image-to-video, and remote video edit/extend flows
|
||||
- Supports `aspectRatio` and `resolution`
|
||||
- Current limit: local video buffers are not accepted; use remote `http(s)`
|
||||
URLs for video-reference/edit inputs
|
||||
|
||||
To use xAI as the default video provider:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "xai/grok-imagine-video",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [Video Generation](/tools/video-generation) for the shared tool
|
||||
parameters, provider selection, and failover behavior.
|
||||
|
||||
## Known limits
|
||||
|
||||
- Auth is API-key only today. There is no xAI OAuth/device-code flow in OpenClaw yet.
|
||||
|
||||
@@ -382,17 +382,18 @@ conceptual details and chat commands, see [Dreaming](/concepts/dreaming).
|
||||
|
||||
### Global settings
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------------- | --------- | ---------- | ------------------------------------------------ |
|
||||
| `enabled` | `boolean` | `true` | Master switch for all phases |
|
||||
| `timezone` | `string` | unset | Timezone for schedule evaluation and daily notes |
|
||||
| `verboseLogging` | `boolean` | `false` | Emit detailed per-run dreaming logs |
|
||||
| `storage.mode` | `string` | `"inline"` | `inline`, `separate`, or `both` |
|
||||
| `storage.separateReports` | `boolean` | `false` | Write separate report files per phase |
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------------- | --------- | ---------- | ------------------------------------------------------------ |
|
||||
| `enabled` | `boolean` | `true` | Master switch for all phases |
|
||||
| `timezone` | `string` | unset | Timezone for schedule evaluation and dreaming date bucketing |
|
||||
| `verboseLogging` | `boolean` | `false` | Emit detailed per-run dreaming logs |
|
||||
| `storage.mode` | `string` | `"inline"` | Inline `DREAMS.md`, separate reports, or both |
|
||||
| `storage.separateReports` | `boolean` | `false` | Write separate report files per phase |
|
||||
|
||||
### Light phase (`phases.light`)
|
||||
|
||||
Scans recent traces, dedupes, and stages candidates into the daily note.
|
||||
Scans recent traces, dedupes, and stages candidates into `DREAMS.md` when
|
||||
inline storage is enabled.
|
||||
Does **not** write to `MEMORY.md`.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
@@ -434,7 +435,8 @@ writes durable facts. Also owns recovery when memory is thin.
|
||||
|
||||
### REM phase (`phases.rem`)
|
||||
|
||||
Writes themes, reflections, and pattern notes into the daily note.
|
||||
Writes themes, reflections, and pattern notes into `DREAMS.md` when inline
|
||||
storage is enabled.
|
||||
Does **not** write to `MEMORY.md`.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|
||||
@@ -24,7 +24,9 @@ The tool only appears when at least one image generation provider is available.
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: "openai/gpt-image-1",
|
||||
imageGenerationModel: {
|
||||
primary: "openai/gpt-image-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -74,10 +76,6 @@ Not all providers support all parameters. The tool passes what each provider sup
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
// String form: primary model only
|
||||
imageGenerationModel: "google/gemini-3.1-flash-image-preview",
|
||||
|
||||
// Object form: primary + ordered fallbacks
|
||||
imageGenerationModel: {
|
||||
primary: "openai/gpt-image-1",
|
||||
fallbacks: ["google/gemini-3.1-flash-image-preview", "fal/fal-ai/flux/dev"],
|
||||
@@ -135,5 +133,9 @@ MiniMax image generation is available through both bundled MiniMax auth paths:
|
||||
## Related
|
||||
|
||||
- [Tools Overview](/tools) — all available agent tools
|
||||
- [fal](/providers/fal) — fal image and video provider setup
|
||||
- [Google (Gemini)](/providers/google) — Gemini image provider setup
|
||||
- [MiniMax](/providers/minimax) — MiniMax image provider setup
|
||||
- [OpenAI](/providers/openai) — OpenAI Images provider setup
|
||||
- [Configuration Reference](/gateway/configuration-reference#agent-defaults) — `imageGenerationModel` config
|
||||
- [Models](/concepts/models) — model configuration and failover
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Generate videos using configured providers such as Qwen"
|
||||
summary: "Generate videos using configured providers such as Alibaba, OpenAI, Google, Qwen, and MiniMax"
|
||||
read_when:
|
||||
- Generating videos via the agent
|
||||
- Configuring video generation providers and models
|
||||
@@ -17,14 +17,16 @@ The tool only appears when at least one video-generation provider is available.
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set an API key for at least one provider (for example `QWEN_API_KEY`).
|
||||
1. Set an API key for at least one provider (for example `OPENAI_API_KEY`, `GEMINI_API_KEY`, `MODELSTUDIO_API_KEY`, or `QWEN_API_KEY`).
|
||||
2. Optionally set your preferred model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: "qwen/wan2.6-t2v",
|
||||
videoGenerationModel: {
|
||||
primary: "qwen/wan2.6-t2v",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -36,9 +38,17 @@ The agent calls `video_generate` automatically. No tool allow-listing needed —
|
||||
|
||||
## Supported providers
|
||||
|
||||
| Provider | Default model | Reference inputs | API key |
|
||||
| -------- | ------------- | ---------------- | ---------------------------------------------------------- |
|
||||
| Qwen | `wan2.6-t2v` | Yes, remote URLs | `QWEN_API_KEY`, `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
|
||||
| Provider | Default model | Reference inputs | API key |
|
||||
| -------- | ------------------------------- | ------------------ | ---------------------------------------------------------- |
|
||||
| Alibaba | `wan2.6-t2v` | Yes, remote URLs | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY`, `QWEN_API_KEY` |
|
||||
| BytePlus | `seedance-1-0-lite-t2v-250428` | 1 image | `BYTEPLUS_API_KEY` |
|
||||
| fal | `fal-ai/minimax/video-01-live` | 1 image | `FAL_KEY` |
|
||||
| Google | `veo-3.1-fast-generate-preview` | 1 image or 1 video | `GEMINI_API_KEY`, `GOOGLE_API_KEY` |
|
||||
| MiniMax | `MiniMax-Hailuo-2.3` | 1 image | `MINIMAX_API_KEY` |
|
||||
| OpenAI | `sora-2` | 1 image or 1 video | `OPENAI_API_KEY` |
|
||||
| Qwen | `wan2.6-t2v` | Yes, remote URLs | `QWEN_API_KEY`, `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
|
||||
| Together | `Wan-AI/Wan2.2-T2V-A14B` | 1 image | `TOGETHER_API_KEY` |
|
||||
| xAI | `grok-imagine-video` | 1 image or 1 video | `XAI_API_KEY` |
|
||||
|
||||
Use `action: "list"` to inspect available providers and models at runtime:
|
||||
|
||||
@@ -97,6 +107,15 @@ When generating a video, OpenClaw tries providers in this order:
|
||||
|
||||
If a provider fails, the next candidate is tried automatically. If all fail, the error includes details from each attempt.
|
||||
|
||||
## Provider notes
|
||||
|
||||
- Alibaba uses the DashScope / Model Studio async video endpoint and currently requires remote `http(s)` URLs for reference assets.
|
||||
- Google uses Gemini/Veo and supports a single image or video reference input.
|
||||
- MiniMax, Together, BytePlus, and fal currently support a single image reference input.
|
||||
- OpenAI uses the native video endpoint and currently defaults to `sora-2`.
|
||||
- Qwen supports image/video references, but the upstream DashScope video endpoint currently requires remote `http(s)` URLs for those references.
|
||||
- xAI uses the native xAI video API and supports text-to-video, image-to-video, and remote video edit/extend flows.
|
||||
|
||||
## Qwen reference inputs
|
||||
|
||||
The bundled Qwen provider supports text-to-video plus image/video reference modes, but the upstream DashScope video endpoint currently requires **remote http(s) URLs** for reference inputs. Local file paths and uploaded buffers are rejected up front instead of being silently ignored.
|
||||
@@ -104,6 +123,12 @@ The bundled Qwen provider supports text-to-video plus image/video reference mode
|
||||
## Related
|
||||
|
||||
- [Tools Overview](/tools) — all available agent tools
|
||||
- [Alibaba Model Studio](/providers/alibaba) — direct Wan provider setup
|
||||
- [Google (Gemini)](/providers/google) — Veo provider setup
|
||||
- [MiniMax](/providers/minimax) — Hailuo provider setup
|
||||
- [OpenAI](/providers/openai) — Sora provider setup
|
||||
- [Qwen](/providers/qwen) — Qwen-specific setup and limits
|
||||
- [Together AI](/providers/together) — Together Wan provider setup
|
||||
- [xAI](/providers/xai) — Grok video provider setup
|
||||
- [Configuration Reference](/gateway/configuration-reference#agent-defaults) — `videoGenerationModel` config
|
||||
- [Models](/concepts/models) — model configuration and failover
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"description": "OpenClaw ACP runtime backend",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
11
extensions/alibaba/index.ts
Normal file
11
extensions/alibaba/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { buildAlibabaVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "alibaba",
|
||||
name: "Alibaba Model Studio Plugin",
|
||||
description: "Bundled Alibaba Model Studio video provider plugin",
|
||||
register(api) {
|
||||
api.registerVideoGenerationProvider(buildAlibabaVideoGenerationProvider());
|
||||
},
|
||||
});
|
||||
30
extensions/alibaba/openclaw.plugin.json
Normal file
30
extensions/alibaba/openclaw.plugin.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "alibaba",
|
||||
"enabledByDefault": true,
|
||||
"providerAuthEnvVars": {
|
||||
"alibaba": ["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY", "QWEN_API_KEY"]
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "alibaba",
|
||||
"method": "api-key",
|
||||
"choiceId": "alibaba-model-studio-api-key",
|
||||
"choiceLabel": "Alibaba Model Studio API key",
|
||||
"groupId": "alibaba",
|
||||
"groupLabel": "Alibaba Model Studio",
|
||||
"groupHint": "DashScope / Model Studio API key",
|
||||
"optionKey": "alibabaModelStudioApiKey",
|
||||
"cliFlag": "--alibaba-model-studio-api-key",
|
||||
"cliOption": "--alibaba-model-studio-api-key <key>",
|
||||
"cliDescription": "Alibaba Model Studio API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"videoGenerationProviders": ["alibaba"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
12
extensions/alibaba/package.json
Normal file
12
extensions/alibaba/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Alibaba Model Studio video provider plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
extensions/alibaba/plugin-registration.contract.test.ts
Normal file
7
extensions/alibaba/plugin-registration.contract.test.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { describePluginRegistrationContract } from "../../test/helpers/plugins/plugin-registration-contract.js";
|
||||
|
||||
describePluginRegistrationContract({
|
||||
pluginId: "alibaba",
|
||||
videoGenerationProviderIds: ["alibaba"],
|
||||
requireGenerateVideo: true,
|
||||
});
|
||||
133
extensions/alibaba/video-generation-provider.test.ts
Normal file
133
extensions/alibaba/video-generation-provider.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildAlibabaVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
|
||||
const {
|
||||
resolveApiKeyForProviderMock,
|
||||
postJsonRequestMock,
|
||||
fetchWithTimeoutMock,
|
||||
assertOkOrThrowHttpErrorMock,
|
||||
resolveProviderHttpRequestConfigMock,
|
||||
} = vi.hoisted(() => ({
|
||||
resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "alibaba-key" })),
|
||||
postJsonRequestMock: vi.fn(),
|
||||
fetchWithTimeoutMock: vi.fn(),
|
||||
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
|
||||
resolveProviderHttpRequestConfigMock: vi.fn((params) => ({
|
||||
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
|
||||
allowPrivateNetwork: false,
|
||||
headers: new Headers(params.defaultHeaders),
|
||||
dispatcherPolicy: undefined,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
|
||||
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
|
||||
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
|
||||
fetchWithTimeout: fetchWithTimeoutMock,
|
||||
postJsonRequest: postJsonRequestMock,
|
||||
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
|
||||
}));
|
||||
|
||||
describe("alibaba video generation provider", () => {
|
||||
afterEach(() => {
|
||||
resolveApiKeyForProviderMock.mockClear();
|
||||
postJsonRequestMock.mockReset();
|
||||
fetchWithTimeoutMock.mockReset();
|
||||
assertOkOrThrowHttpErrorMock.mockClear();
|
||||
resolveProviderHttpRequestConfigMock.mockClear();
|
||||
});
|
||||
|
||||
it("submits async Wan generation, polls task status, and downloads the resulting video", async () => {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({
|
||||
request_id: "req-1",
|
||||
output: {
|
||||
task_id: "task-1",
|
||||
},
|
||||
}),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock
|
||||
.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
output: {
|
||||
task_status: "SUCCEEDED",
|
||||
results: [{ video_url: "https://example.com/out.mp4" }],
|
||||
},
|
||||
}),
|
||||
headers: new Headers(),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
arrayBuffer: async () => Buffer.from("mp4-bytes"),
|
||||
headers: new Headers({ "content-type": "video/mp4" }),
|
||||
});
|
||||
|
||||
const provider = buildAlibabaVideoGenerationProvider();
|
||||
const result = await provider.generateVideo({
|
||||
provider: "alibaba",
|
||||
model: "wan2.6-r2v-flash",
|
||||
prompt: "animate this shot",
|
||||
cfg: {},
|
||||
inputImages: [{ url: "https://example.com/ref.png" }],
|
||||
durationSeconds: 6,
|
||||
audio: true,
|
||||
watermark: false,
|
||||
});
|
||||
|
||||
expect(postJsonRequestMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
|
||||
body: expect.objectContaining({
|
||||
model: "wan2.6-r2v-flash",
|
||||
input: expect.objectContaining({
|
||||
prompt: "animate this shot",
|
||||
img_url: "https://example.com/ref.png",
|
||||
}),
|
||||
parameters: expect.objectContaining({
|
||||
duration: 6,
|
||||
enable_audio: true,
|
||||
watermark: false,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(fetchWithTimeoutMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"https://dashscope-intl.aliyuncs.com/api/v1/tasks/task-1",
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
120000,
|
||||
fetch,
|
||||
);
|
||||
expect(result.videos).toHaveLength(1);
|
||||
expect(result.videos[0]?.mimeType).toBe("video/mp4");
|
||||
expect(result.metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
requestId: "req-1",
|
||||
taskId: "task-1",
|
||||
taskStatus: "SUCCEEDED",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails fast when reference inputs are local buffers instead of remote URLs", async () => {
|
||||
const provider = buildAlibabaVideoGenerationProvider();
|
||||
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "alibaba",
|
||||
model: "wan2.6-i2v",
|
||||
prompt: "animate this local frame",
|
||||
cfg: {},
|
||||
inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Alibaba Wan video generation currently requires remote http(s) URLs for reference images/videos.",
|
||||
);
|
||||
expect(postJsonRequestMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
293
extensions/alibaba/video-generation-provider.ts
Normal file
293
extensions/alibaba/video-generation-provider.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
fetchWithTimeout,
|
||||
postJsonRequest,
|
||||
resolveProviderHttpRequestConfig,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import type {
|
||||
GeneratedVideoAsset,
|
||||
VideoGenerationProvider,
|
||||
VideoGenerationRequest,
|
||||
VideoGenerationResult,
|
||||
VideoGenerationSourceAsset,
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
|
||||
const DEFAULT_ALIBABA_VIDEO_BASE_URL = "https://dashscope-intl.aliyuncs.com";
|
||||
const DEFAULT_ALIBABA_VIDEO_MODEL = "wan2.6-t2v";
|
||||
const DEFAULT_DURATION_SECONDS = 5;
|
||||
const DEFAULT_TIMEOUT_MS = 120_000;
|
||||
const POLL_INTERVAL_MS = 2_500;
|
||||
const MAX_POLL_ATTEMPTS = 120;
|
||||
const RESOLUTION_TO_SIZE: Record<string, string> = {
|
||||
"480P": "832*480",
|
||||
"720P": "1280*720",
|
||||
"1080P": "1920*1080",
|
||||
};
|
||||
|
||||
type AlibabaVideoGenerationResponse = {
|
||||
output?: {
|
||||
task_id?: string;
|
||||
task_status?: string;
|
||||
submit_time?: string;
|
||||
results?: Array<{
|
||||
video_url?: string;
|
||||
orig_prompt?: string;
|
||||
actual_prompt?: string;
|
||||
}>;
|
||||
video_url?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
request_id?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function resolveAlibabaVideoBaseUrl(req: VideoGenerationRequest): string {
|
||||
return req.cfg?.models?.providers?.alibaba?.baseUrl?.trim() || DEFAULT_ALIBABA_VIDEO_BASE_URL;
|
||||
}
|
||||
|
||||
function resolveDashscopeAigcApiBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function resolveReferenceUrls(
|
||||
inputImages: VideoGenerationSourceAsset[] | undefined,
|
||||
inputVideos: VideoGenerationSourceAsset[] | undefined,
|
||||
): string[] {
|
||||
return [...(inputImages ?? []), ...(inputVideos ?? [])]
|
||||
.map((asset) => asset.url?.trim())
|
||||
.filter((value): value is string => Boolean(value));
|
||||
}
|
||||
|
||||
function assertAlibabaReferenceInputsSupported(
|
||||
inputImages: VideoGenerationSourceAsset[] | undefined,
|
||||
inputVideos: VideoGenerationSourceAsset[] | undefined,
|
||||
): void {
|
||||
const unsupported = [...(inputImages ?? []), ...(inputVideos ?? [])].some(
|
||||
(asset) => !asset.url?.trim() && asset.buffer,
|
||||
);
|
||||
if (unsupported) {
|
||||
throw new Error(
|
||||
"Alibaba Wan video generation currently requires remote http(s) URLs for reference images/videos.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildAlibabaVideoGenerationInput(req: VideoGenerationRequest): Record<string, unknown> {
|
||||
assertAlibabaReferenceInputsSupported(req.inputImages, req.inputVideos);
|
||||
const input: Record<string, unknown> = {
|
||||
prompt: req.prompt,
|
||||
};
|
||||
const referenceUrls = resolveReferenceUrls(req.inputImages, req.inputVideos);
|
||||
if (
|
||||
referenceUrls.length === 1 &&
|
||||
(req.inputImages?.length ?? 0) === 1 &&
|
||||
!req.inputVideos?.length
|
||||
) {
|
||||
input.img_url = referenceUrls[0];
|
||||
} else if (referenceUrls.length > 0) {
|
||||
input.reference_urls = referenceUrls;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function buildAlibabaVideoGenerationParameters(
|
||||
req: VideoGenerationRequest,
|
||||
): Record<string, unknown> | undefined {
|
||||
const parameters: Record<string, unknown> = {};
|
||||
const size =
|
||||
req.size?.trim() || (req.resolution ? RESOLUTION_TO_SIZE[req.resolution] : undefined);
|
||||
if (size) {
|
||||
parameters.size = size;
|
||||
}
|
||||
if (req.aspectRatio?.trim()) {
|
||||
parameters.aspect_ratio = req.aspectRatio.trim();
|
||||
}
|
||||
if (typeof req.durationSeconds === "number" && Number.isFinite(req.durationSeconds)) {
|
||||
parameters.duration = Math.max(1, Math.round(req.durationSeconds));
|
||||
}
|
||||
if (typeof req.audio === "boolean") {
|
||||
parameters.enable_audio = req.audio;
|
||||
}
|
||||
if (typeof req.watermark === "boolean") {
|
||||
parameters.watermark = req.watermark;
|
||||
}
|
||||
return Object.keys(parameters).length > 0 ? parameters : undefined;
|
||||
}
|
||||
|
||||
function extractVideoUrls(payload: AlibabaVideoGenerationResponse): string[] {
|
||||
const urls = [
|
||||
...(payload.output?.results?.map((entry) => entry.video_url).filter(Boolean) ?? []),
|
||||
payload.output?.video_url,
|
||||
].filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
||||
return [...new Set(urls)];
|
||||
}
|
||||
|
||||
async function pollTaskUntilComplete(params: {
|
||||
taskId: string;
|
||||
headers: Headers;
|
||||
timeoutMs?: number;
|
||||
fetchFn: typeof fetch;
|
||||
baseUrl: string;
|
||||
}): Promise<AlibabaVideoGenerationResponse> {
|
||||
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt += 1) {
|
||||
const response = await fetchWithTimeout(
|
||||
`${params.baseUrl}/api/v1/tasks/${params.taskId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: params.headers,
|
||||
},
|
||||
params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
params.fetchFn,
|
||||
);
|
||||
await assertOkOrThrowHttpError(response, "Alibaba Wan video-generation task poll failed");
|
||||
const payload = (await response.json()) as AlibabaVideoGenerationResponse;
|
||||
const status = payload.output?.task_status?.trim().toUpperCase();
|
||||
if (status === "SUCCEEDED") {
|
||||
return payload;
|
||||
}
|
||||
if (status === "FAILED" || status === "CANCELED") {
|
||||
throw new Error(
|
||||
payload.output?.message?.trim() ||
|
||||
payload.message?.trim() ||
|
||||
`Alibaba Wan video generation task ${params.taskId} ${status?.toLowerCase()}`,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
}
|
||||
throw new Error(`Alibaba Wan video generation task ${params.taskId} did not finish in time`);
|
||||
}
|
||||
|
||||
async function downloadGeneratedVideos(params: {
|
||||
urls: string[];
|
||||
timeoutMs?: number;
|
||||
fetchFn: typeof fetch;
|
||||
}): Promise<GeneratedVideoAsset[]> {
|
||||
const videos: GeneratedVideoAsset[] = [];
|
||||
for (const [index, url] of params.urls.entries()) {
|
||||
const response = await fetchWithTimeout(
|
||||
url,
|
||||
{ method: "GET" },
|
||||
params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
params.fetchFn,
|
||||
);
|
||||
await assertOkOrThrowHttpError(response, "Alibaba Wan generated video download failed");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
videos.push({
|
||||
buffer: Buffer.from(arrayBuffer),
|
||||
mimeType: response.headers.get("content-type")?.trim() || "video/mp4",
|
||||
fileName: `video-${index + 1}.mp4`,
|
||||
metadata: { sourceUrl: url },
|
||||
});
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
export function buildAlibabaVideoGenerationProvider(): VideoGenerationProvider {
|
||||
return {
|
||||
id: "alibaba",
|
||||
label: "Alibaba Model Studio",
|
||||
defaultModel: DEFAULT_ALIBABA_VIDEO_MODEL,
|
||||
models: ["wan2.6-t2v", "wan2.6-i2v", "wan2.6-r2v", "wan2.6-r2v-flash", "wan2.7-r2v"],
|
||||
isConfigured: ({ agentDir }) =>
|
||||
isProviderApiKeyConfigured({
|
||||
provider: "alibaba",
|
||||
agentDir,
|
||||
}),
|
||||
capabilities: {
|
||||
maxVideos: 1,
|
||||
maxInputImages: 1,
|
||||
maxInputVideos: 4,
|
||||
maxDurationSeconds: 10,
|
||||
supportsSize: true,
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
supportsAudio: true,
|
||||
supportsWatermark: true,
|
||||
},
|
||||
async generateVideo(req): Promise<VideoGenerationResult> {
|
||||
const fetchFn = fetch;
|
||||
const auth = await resolveApiKeyForProvider({
|
||||
provider: "alibaba",
|
||||
cfg: req.cfg,
|
||||
agentDir: req.agentDir,
|
||||
store: req.authStore,
|
||||
});
|
||||
if (!auth.apiKey) {
|
||||
throw new Error("Alibaba Model Studio API key missing");
|
||||
}
|
||||
|
||||
const requestBaseUrl = resolveAlibabaVideoBaseUrl(req);
|
||||
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
|
||||
resolveProviderHttpRequestConfig({
|
||||
baseUrl: requestBaseUrl,
|
||||
defaultBaseUrl: DEFAULT_ALIBABA_VIDEO_BASE_URL,
|
||||
defaultHeaders: {
|
||||
Authorization: `Bearer ${auth.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-DashScope-Async": "enable",
|
||||
},
|
||||
provider: "alibaba",
|
||||
capability: "video",
|
||||
transport: "http",
|
||||
});
|
||||
|
||||
const model = req.model?.trim() || DEFAULT_ALIBABA_VIDEO_MODEL;
|
||||
const { response, release } = await postJsonRequest({
|
||||
url: `${resolveDashscopeAigcApiBaseUrl(baseUrl)}/api/v1/services/aigc/video-generation/video-synthesis`,
|
||||
headers,
|
||||
body: {
|
||||
model,
|
||||
input: buildAlibabaVideoGenerationInput(req),
|
||||
parameters: buildAlibabaVideoGenerationParameters({
|
||||
...req,
|
||||
durationSeconds: req.durationSeconds ?? DEFAULT_DURATION_SECONDS,
|
||||
}),
|
||||
},
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
});
|
||||
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "Alibaba Wan video generation failed");
|
||||
const submitted = (await response.json()) as AlibabaVideoGenerationResponse;
|
||||
const taskId = submitted.output?.task_id?.trim();
|
||||
if (!taskId) {
|
||||
throw new Error("Alibaba Wan video generation response missing task_id");
|
||||
}
|
||||
const completed = await pollTaskUntilComplete({
|
||||
taskId,
|
||||
headers,
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
baseUrl: resolveDashscopeAigcApiBaseUrl(baseUrl),
|
||||
});
|
||||
const urls = extractVideoUrls(completed);
|
||||
if (urls.length === 0) {
|
||||
throw new Error("Alibaba Wan video generation completed without output video URLs");
|
||||
}
|
||||
const videos = await downloadGeneratedVideos({
|
||||
urls,
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
});
|
||||
return {
|
||||
videos,
|
||||
model,
|
||||
metadata: {
|
||||
requestId: submitted.request_id,
|
||||
taskId,
|
||||
taskStatus: completed.output?.task_status,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@openclaw/bluebubbles",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"description": "OpenClaw BlueBubbles channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"openclaw": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.4.4"
|
||||
"openclaw": ">=2026.4.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -39,13 +39,13 @@
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/bluebubbles",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.4"
|
||||
"minHostVersion": ">=2026.4.5"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.4.4"
|
||||
"pluginApi": ">=2026.4.5"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.4.4"
|
||||
"openclawVersion": "2026.4.5"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Brave plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { redactCdpUrl } from "./src/browser/cdp.helpers.js";
|
||||
export { parseBrowserHttpUrl, redactCdpUrl } from "./src/browser/cdp.helpers.js";
|
||||
|
||||
1
extensions/browser/browser-doctor.ts
Normal file
1
extensions/browser/browser-doctor.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { noteChromeMcpBrowserReadiness } from "./src/doctor-browser.js";
|
||||
6
extensions/browser/browser-host-inspection.ts
Normal file
6
extensions/browser/browser-host-inspection.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type { BrowserExecutable } from "./src/browser/chrome.executables.js";
|
||||
export {
|
||||
parseBrowserMajorVersion,
|
||||
readBrowserVersion,
|
||||
resolveGoogleChromeExecutableForPlatform,
|
||||
} from "./src/browser/chrome.executables.js";
|
||||
2
extensions/browser/browser-maintenance.ts
Normal file
2
extensions/browser/browser-maintenance.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { closeTrackedBrowserTabsForSessions } from "./src/browser/session-tab-registry.js";
|
||||
export { movePathToTrash } from "./src/browser/trash.js";
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
DEFAULT_UPLOAD_DIR,
|
||||
resolveBrowserConfig,
|
||||
resolveProfile,
|
||||
type ResolvedBrowserConfig,
|
||||
|
||||
@@ -48,6 +48,17 @@ function createApi() {
|
||||
}
|
||||
|
||||
describe("browser plugin", () => {
|
||||
it("exposes static browser metadata on the plugin definition", () => {
|
||||
expect(browserPlugin.reload).toEqual({ restartPrefixes: ["browser"] });
|
||||
expect(browserPlugin.nodeHostCommands).toEqual([
|
||||
expect.objectContaining({
|
||||
command: "browser.proxy",
|
||||
cap: "browser",
|
||||
}),
|
||||
]);
|
||||
expect(browserPlugin.securityAuditCollectors).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("forwards per-session browser options into the tool factory", async () => {
|
||||
const { api, registerTool } = createApi();
|
||||
await browserPlugin.register(api);
|
||||
|
||||
@@ -4,16 +4,27 @@ import {
|
||||
type OpenClawPluginToolFactory,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
collectBrowserSecurityAuditFindings,
|
||||
createBrowserPluginService,
|
||||
createBrowserTool,
|
||||
handleBrowserGatewayRequest,
|
||||
registerBrowserCli,
|
||||
runBrowserProxyCommand,
|
||||
} from "./register.runtime.js";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "browser",
|
||||
name: "Browser",
|
||||
description: "Default browser tool plugin",
|
||||
reload: { restartPrefixes: ["browser"] },
|
||||
nodeHostCommands: [
|
||||
{
|
||||
command: "browser.proxy",
|
||||
cap: "browser",
|
||||
handle: runBrowserProxyCommand,
|
||||
},
|
||||
],
|
||||
securityAuditCollectors: [collectBrowserSecurityAuditFindings],
|
||||
register(api) {
|
||||
api.registerTool(((ctx: OpenClawPluginToolContext) =>
|
||||
createBrowserTool({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/browser-plugin",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw browser tool plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export { createBrowserTool } from "./src/browser-tool.js";
|
||||
export { registerBrowserCli } from "./src/cli/browser-cli.js";
|
||||
export { handleBrowserGatewayRequest } from "./src/gateway/browser-request.js";
|
||||
export { runBrowserProxyCommand } from "./src/node-host/invoke-browser.js";
|
||||
export { createBrowserPluginService } from "./src/plugin-service.js";
|
||||
export { collectBrowserSecurityAuditFindings } from "./src/security-audit.js";
|
||||
|
||||
@@ -9,6 +9,33 @@ import { resolveBrowserRateLimitMessage } from "./client-fetch.js";
|
||||
|
||||
export { isLoopbackHost };
|
||||
|
||||
export function parseBrowserHttpUrl(raw: string, label: string) {
|
||||
const trimmed = raw.trim();
|
||||
const parsed = new URL(trimmed);
|
||||
const allowed = ["http:", "https:", "ws:", "wss:"];
|
||||
if (!allowed.includes(parsed.protocol)) {
|
||||
throw new Error(`${label} must be http(s) or ws(s), got: ${parsed.protocol.replace(":", "")}`);
|
||||
}
|
||||
|
||||
const isSecure = parsed.protocol === "https:" || parsed.protocol === "wss:";
|
||||
const port =
|
||||
parsed.port && Number.parseInt(parsed.port, 10) > 0
|
||||
? Number.parseInt(parsed.port, 10)
|
||||
: isSecure
|
||||
? 443
|
||||
: 80;
|
||||
|
||||
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
||||
throw new Error(`${label} has invalid port: ${parsed.port}`);
|
||||
}
|
||||
|
||||
return {
|
||||
parsed,
|
||||
port,
|
||||
normalized: parsed.toString().replace(/\/$/, ""),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the URL uses a WebSocket protocol (ws: or wss:).
|
||||
* Used to distinguish direct-WebSocket CDP endpoints
|
||||
|
||||
@@ -9,7 +9,7 @@ export type BrowserExecutable = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
const CHROME_VERSION_RE = /(\d+)(?:\.\d+){0,3}/;
|
||||
const CHROME_VERSION_RE = /\b(\d+)(?:\.\d+){1,3}\b/g;
|
||||
|
||||
const CHROMIUM_BUNDLE_IDS = new Set([
|
||||
"com.google.Chrome",
|
||||
@@ -464,9 +464,13 @@ function findFirstExecutable(candidates: Array<BrowserExecutable>): BrowserExecu
|
||||
function findFirstChromeExecutable(candidates: string[]): BrowserExecutable | null {
|
||||
for (const candidate of candidates) {
|
||||
if (exists(candidate)) {
|
||||
const normalizedPath = candidate.toLowerCase();
|
||||
return {
|
||||
kind:
|
||||
candidate.toLowerCase().includes("sxs") || candidate.toLowerCase().includes("canary")
|
||||
normalizedPath.includes("beta") ||
|
||||
normalizedPath.includes("canary") ||
|
||||
normalizedPath.includes("sxs") ||
|
||||
normalizedPath.includes("unstable")
|
||||
? "canary"
|
||||
: "chrome",
|
||||
path: candidate,
|
||||
@@ -683,7 +687,8 @@ export function readBrowserVersion(executablePath: string): string | null {
|
||||
}
|
||||
|
||||
export function parseBrowserMajorVersion(rawVersion: string | null | undefined): number | null {
|
||||
const match = String(rawVersion ?? "").match(CHROME_VERSION_RE);
|
||||
const matches = [...String(rawVersion ?? "").matchAll(CHROME_VERSION_RE)];
|
||||
const match = matches.at(-1);
|
||||
if (!match?.[1]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
import {
|
||||
type BrowserConfig,
|
||||
type BrowserProfileConfig,
|
||||
type OpenClawConfig,
|
||||
} from "../config/config.js";
|
||||
import { resolveGatewayPort } from "../config/paths.js";
|
||||
import {
|
||||
DEFAULT_BROWSER_CONTROL_PORT,
|
||||
deriveDefaultBrowserCdpPortRange,
|
||||
deriveDefaultBrowserControlPort,
|
||||
} from "../config/port-defaults.js";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { parseBrowserHttpUrl, redactCdpUrl, isLoopbackHost } from "./cdp.helpers.js";
|
||||
import {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
} from "./constants.js";
|
||||
import { resolveBrowserControlAuth, type BrowserControlAuth } from "./control-auth.js";
|
||||
import { DEFAULT_UPLOAD_DIR } from "./paths.js";
|
||||
|
||||
export {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
@@ -5,15 +30,329 @@ export {
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
resolveBrowserConfig,
|
||||
resolveProfile,
|
||||
type ResolvedBrowserConfig,
|
||||
type ResolvedBrowserProfile,
|
||||
} from "openclaw/plugin-sdk/browser-profiles";
|
||||
export { parseBrowserHttpUrl, redactCdpUrl } from "openclaw/plugin-sdk/browser-cdp";
|
||||
export type { BrowserControlAuth } from "openclaw/plugin-sdk/browser-control-auth";
|
||||
export { resolveBrowserControlAuth } from "openclaw/plugin-sdk/browser-control-auth";
|
||||
export { parseBrowserHttpUrl as parseHttpUrl } from "openclaw/plugin-sdk/browser-cdp";
|
||||
DEFAULT_UPLOAD_DIR,
|
||||
parseBrowserHttpUrl,
|
||||
redactCdpUrl,
|
||||
resolveBrowserControlAuth,
|
||||
};
|
||||
export type { BrowserControlAuth };
|
||||
export { parseBrowserHttpUrl as parseHttpUrl };
|
||||
|
||||
export type ResolvedBrowserConfig = {
|
||||
enabled: boolean;
|
||||
evaluateEnabled: boolean;
|
||||
controlPort: number;
|
||||
cdpPortRangeStart: number;
|
||||
cdpPortRangeEnd: number;
|
||||
cdpProtocol: "http" | "https";
|
||||
cdpHost: string;
|
||||
cdpIsLoopback: boolean;
|
||||
remoteCdpTimeoutMs: number;
|
||||
remoteCdpHandshakeTimeoutMs: number;
|
||||
color: string;
|
||||
executablePath?: string;
|
||||
headless: boolean;
|
||||
noSandbox: boolean;
|
||||
attachOnly: boolean;
|
||||
defaultProfile: string;
|
||||
profiles: Record<string, BrowserProfileConfig>;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
extraArgs: string[];
|
||||
};
|
||||
|
||||
export type ResolvedBrowserProfile = {
|
||||
name: string;
|
||||
cdpPort: number;
|
||||
cdpUrl: string;
|
||||
cdpHost: string;
|
||||
cdpIsLoopback: boolean;
|
||||
userDataDir?: string;
|
||||
color: string;
|
||||
driver: "openclaw" | "existing-session";
|
||||
attachOnly: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800;
|
||||
|
||||
function normalizeHexColor(raw: string | undefined): string {
|
||||
const value = (raw ?? "").trim();
|
||||
if (!value) {
|
||||
return DEFAULT_OPENCLAW_BROWSER_COLOR;
|
||||
}
|
||||
const normalized = value.startsWith("#") ? value : `#${value}`;
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) {
|
||||
return DEFAULT_OPENCLAW_BROWSER_COLOR;
|
||||
}
|
||||
return normalized.toUpperCase();
|
||||
}
|
||||
|
||||
function normalizeTimeoutMs(raw: number | undefined, fallback: number): number {
|
||||
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
|
||||
return value < 0 ? fallback : value;
|
||||
}
|
||||
|
||||
function resolveCdpPortRangeStart(
|
||||
rawStart: number | undefined,
|
||||
fallbackStart: number,
|
||||
rangeSpan: number,
|
||||
): number {
|
||||
const start =
|
||||
typeof rawStart === "number" && Number.isFinite(rawStart)
|
||||
? Math.floor(rawStart)
|
||||
: fallbackStart;
|
||||
if (start < 1 || start > 65535) {
|
||||
throw new Error(`browser.cdpPortRangeStart must be between 1 and 65535, got: ${start}`);
|
||||
}
|
||||
const maxStart = 65535 - rangeSpan;
|
||||
if (start > maxStart) {
|
||||
throw new Error(
|
||||
`browser.cdpPortRangeStart (${start}) is too high for a ${rangeSpan + 1}-port range; max is ${maxStart}.`,
|
||||
);
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
function normalizeStringList(raw: string[] | undefined): string[] | undefined {
|
||||
if (!Array.isArray(raw) || raw.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const values = raw
|
||||
.map((value) => value.trim())
|
||||
.filter((value): value is string => value.length > 0);
|
||||
return values.length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined {
|
||||
const rawPolicy = cfg?.ssrfPolicy as
|
||||
| (BrowserConfig["ssrfPolicy"] & { allowPrivateNetwork?: boolean })
|
||||
| undefined;
|
||||
const allowPrivateNetwork = rawPolicy?.allowPrivateNetwork;
|
||||
const dangerouslyAllowPrivateNetwork = rawPolicy?.dangerouslyAllowPrivateNetwork;
|
||||
const allowedHostnames = normalizeStringList(rawPolicy?.allowedHostnames);
|
||||
const hostnameAllowlist = normalizeStringList(rawPolicy?.hostnameAllowlist);
|
||||
const hasExplicitPrivateSetting =
|
||||
allowPrivateNetwork !== undefined || dangerouslyAllowPrivateNetwork !== undefined;
|
||||
const resolvedAllowPrivateNetwork =
|
||||
dangerouslyAllowPrivateNetwork === true ||
|
||||
allowPrivateNetwork === true ||
|
||||
!hasExplicitPrivateSetting;
|
||||
|
||||
if (
|
||||
!resolvedAllowPrivateNetwork &&
|
||||
!hasExplicitPrivateSetting &&
|
||||
!allowedHostnames &&
|
||||
!hostnameAllowlist
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...(resolvedAllowPrivateNetwork ? { dangerouslyAllowPrivateNetwork: true } : {}),
|
||||
...(allowedHostnames ? { allowedHostnames } : {}),
|
||||
...(hostnameAllowlist ? { hostnameAllowlist } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function ensureDefaultProfile(
|
||||
profiles: Record<string, BrowserProfileConfig> | undefined,
|
||||
defaultColor: string,
|
||||
legacyCdpPort?: number,
|
||||
derivedDefaultCdpPort?: number,
|
||||
legacyCdpUrl?: string,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
const result = { ...profiles };
|
||||
if (!result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]) {
|
||||
result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] = {
|
||||
cdpPort: legacyCdpPort ?? derivedDefaultCdpPort ?? DEFAULT_BROWSER_CDP_PORT_RANGE_START,
|
||||
color: defaultColor,
|
||||
...(legacyCdpUrl ? { cdpUrl: legacyCdpUrl } : {}),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function ensureDefaultUserBrowserProfile(
|
||||
profiles: Record<string, BrowserProfileConfig>,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
const result = { ...profiles };
|
||||
if (result.user) {
|
||||
return result;
|
||||
}
|
||||
result.user = {
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
color: "#00AA00",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveBrowserConfig(
|
||||
cfg: BrowserConfig | undefined,
|
||||
rootConfig?: OpenClawConfig,
|
||||
): ResolvedBrowserConfig {
|
||||
const enabled = cfg?.enabled ?? DEFAULT_OPENCLAW_BROWSER_ENABLED;
|
||||
const evaluateEnabled = cfg?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED;
|
||||
const gatewayPort = resolveGatewayPort(rootConfig);
|
||||
const controlPort = deriveDefaultBrowserControlPort(gatewayPort ?? DEFAULT_BROWSER_CONTROL_PORT);
|
||||
const defaultColor = normalizeHexColor(cfg?.color);
|
||||
const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500);
|
||||
const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs(
|
||||
cfg?.remoteCdpHandshakeTimeoutMs,
|
||||
Math.max(2000, remoteCdpTimeoutMs * 2),
|
||||
);
|
||||
|
||||
const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
|
||||
const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start;
|
||||
const cdpPortRangeStart = resolveCdpPortRangeStart(
|
||||
cfg?.cdpPortRangeStart,
|
||||
derivedCdpRange.start,
|
||||
cdpRangeSpan,
|
||||
);
|
||||
const cdpPortRangeEnd = cdpPortRangeStart + cdpRangeSpan;
|
||||
|
||||
const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
|
||||
let cdpInfo:
|
||||
| {
|
||||
parsed: URL;
|
||||
port: number;
|
||||
normalized: string;
|
||||
}
|
||||
| undefined;
|
||||
if (rawCdpUrl) {
|
||||
cdpInfo = parseBrowserHttpUrl(rawCdpUrl, "browser.cdpUrl");
|
||||
} else {
|
||||
const derivedPort = controlPort + 1;
|
||||
if (derivedPort > 65535) {
|
||||
throw new Error(
|
||||
`Derived CDP port (${derivedPort}) is too high; check gateway port configuration.`,
|
||||
);
|
||||
}
|
||||
const derived = new URL(`http://127.0.0.1:${derivedPort}`);
|
||||
cdpInfo = {
|
||||
parsed: derived,
|
||||
port: derivedPort,
|
||||
normalized: derived.toString().replace(/\/$/, ""),
|
||||
};
|
||||
}
|
||||
|
||||
const headless = cfg?.headless === true;
|
||||
const noSandbox = cfg?.noSandbox === true;
|
||||
const attachOnly = cfg?.attachOnly === true;
|
||||
const executablePath = cfg?.executablePath?.trim() || undefined;
|
||||
const defaultProfileFromConfig = cfg?.defaultProfile?.trim() || undefined;
|
||||
|
||||
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
|
||||
const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:";
|
||||
const legacyCdpUrl = rawCdpUrl && isWsUrl ? cdpInfo.normalized : undefined;
|
||||
const profiles = ensureDefaultUserBrowserProfile(
|
||||
ensureDefaultProfile(
|
||||
cfg?.profiles,
|
||||
defaultColor,
|
||||
legacyCdpPort,
|
||||
cdpPortRangeStart,
|
||||
legacyCdpUrl,
|
||||
),
|
||||
);
|
||||
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
|
||||
|
||||
const defaultProfile =
|
||||
defaultProfileFromConfig ??
|
||||
(profiles[DEFAULT_BROWSER_DEFAULT_PROFILE_NAME]
|
||||
? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME
|
||||
: profiles[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]
|
||||
? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME
|
||||
: "user");
|
||||
|
||||
const extraArgs = Array.isArray(cfg?.extraArgs)
|
||||
? cfg.extraArgs.filter(
|
||||
(value): value is string => typeof value === "string" && value.trim().length > 0,
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
enabled,
|
||||
evaluateEnabled,
|
||||
controlPort,
|
||||
cdpPortRangeStart,
|
||||
cdpPortRangeEnd,
|
||||
cdpProtocol,
|
||||
cdpHost: cdpInfo.parsed.hostname,
|
||||
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
|
||||
remoteCdpTimeoutMs,
|
||||
remoteCdpHandshakeTimeoutMs,
|
||||
color: defaultColor,
|
||||
executablePath,
|
||||
headless,
|
||||
noSandbox,
|
||||
attachOnly,
|
||||
defaultProfile,
|
||||
profiles,
|
||||
ssrfPolicy: resolveBrowserSsrFPolicy(cfg),
|
||||
extraArgs,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveProfile(
|
||||
resolved: ResolvedBrowserConfig,
|
||||
profileName: string,
|
||||
): ResolvedBrowserProfile | null {
|
||||
const profile = resolved.profiles[profileName];
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawProfileUrl = profile.cdpUrl?.trim() ?? "";
|
||||
let cdpHost = resolved.cdpHost;
|
||||
let cdpPort = profile.cdpPort ?? 0;
|
||||
let cdpUrl = "";
|
||||
const driver = profile.driver === "existing-session" ? "existing-session" : "openclaw";
|
||||
|
||||
if (driver === "existing-session") {
|
||||
return {
|
||||
name: profileName,
|
||||
cdpPort: 0,
|
||||
cdpUrl: "",
|
||||
cdpHost: "",
|
||||
cdpIsLoopback: true,
|
||||
userDataDir: resolveUserPath(profile.userDataDir?.trim() || "") || undefined,
|
||||
color: profile.color,
|
||||
driver,
|
||||
attachOnly: true,
|
||||
};
|
||||
}
|
||||
|
||||
const hasStaleWsPath =
|
||||
rawProfileUrl !== "" &&
|
||||
cdpPort > 0 &&
|
||||
/^wss?:\/\//i.test(rawProfileUrl) &&
|
||||
/\/devtools\/browser\//i.test(rawProfileUrl);
|
||||
|
||||
if (hasStaleWsPath) {
|
||||
const parsed = new URL(rawProfileUrl);
|
||||
cdpHost = parsed.hostname;
|
||||
cdpUrl = `${resolved.cdpProtocol}://${cdpHost}:${cdpPort}`;
|
||||
} else if (rawProfileUrl) {
|
||||
const parsed = parseBrowserHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`);
|
||||
cdpHost = parsed.parsed.hostname;
|
||||
cdpPort = parsed.port;
|
||||
cdpUrl = parsed.normalized;
|
||||
} else if (cdpPort) {
|
||||
cdpUrl = `${resolved.cdpProtocol}://${resolved.cdpHost}:${cdpPort}`;
|
||||
} else {
|
||||
throw new Error(`Profile "${profileName}" must define cdpPort or cdpUrl.`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: profileName,
|
||||
cdpPort,
|
||||
cdpUrl,
|
||||
cdpHost,
|
||||
cdpIsLoopback: isLoopbackHost(cdpHost),
|
||||
color: profile.color,
|
||||
driver,
|
||||
attachOnly: profile.attachOnly ?? resolved.attachOnly,
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldStartLocalBrowserServer(_resolved: unknown) {
|
||||
return true;
|
||||
|
||||
43
extensions/browser/src/browser/trash.test.ts
Normal file
43
extensions/browser/src/browser/trash.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runExec = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runExec,
|
||||
}));
|
||||
|
||||
describe("browser trash", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
runExec.mockReset();
|
||||
vi.spyOn(Date, "now").mockReturnValue(123);
|
||||
vi.spyOn(os, "homedir").mockReturnValue("/home/test");
|
||||
});
|
||||
|
||||
it("returns the target path when trash exits successfully", async () => {
|
||||
const { movePathToTrash } = await import("./trash.js");
|
||||
runExec.mockResolvedValue(undefined);
|
||||
const mkdirSync = vi.spyOn(fs, "mkdirSync");
|
||||
const renameSync = vi.spyOn(fs, "renameSync");
|
||||
|
||||
await expect(movePathToTrash("/tmp/demo")).resolves.toBe("/tmp/demo");
|
||||
expect(runExec).toHaveBeenCalledWith("trash", ["/tmp/demo"], { timeoutMs: 10_000 });
|
||||
expect(mkdirSync).not.toHaveBeenCalled();
|
||||
expect(renameSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to rename when trash exits non-zero", async () => {
|
||||
const { movePathToTrash } = await import("./trash.js");
|
||||
runExec.mockRejectedValue(new Error("permission denied"));
|
||||
const mkdirSync = vi.spyOn(fs, "mkdirSync").mockImplementation(() => undefined);
|
||||
const existsSync = vi.spyOn(fs, "existsSync").mockReturnValue(false);
|
||||
const renameSync = vi.spyOn(fs, "renameSync").mockImplementation(() => undefined);
|
||||
|
||||
await expect(movePathToTrash("/tmp/demo")).resolves.toBe("/home/test/.Trash/demo-123");
|
||||
expect(mkdirSync).toHaveBeenCalledWith("/home/test/.Trash", { recursive: true });
|
||||
expect(existsSync).toHaveBeenCalledWith("/home/test/.Trash/demo-123");
|
||||
expect(renameSync).toHaveBeenCalledWith("/tmp/demo", "/home/test/.Trash/demo-123");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Command } from "commander";
|
||||
import type { GatewayRpcOpts } from "openclaw/plugin-sdk/browser-support";
|
||||
import type { GatewayRpcOpts } from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
import { createCliRuntimeCapture } from "../../test-support.js";
|
||||
import type { CliRuntimeCapture } from "../../test-support.js";
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { formatCliCommand } from "openclaw/plugin-sdk/browser-support";
|
||||
export { formatCliCommand } from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
|
||||
@@ -6,4 +6,4 @@ export {
|
||||
type BrowserConfig,
|
||||
type BrowserProfileConfig,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-config-runtime";
|
||||
|
||||
@@ -64,47 +64,52 @@ export type {
|
||||
} from "./browser-runtime.js";
|
||||
export {
|
||||
callGatewayTool,
|
||||
createSubsystemLogger,
|
||||
danger,
|
||||
defaultRuntime,
|
||||
detectMime,
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatCliCommand,
|
||||
formatDocsLink,
|
||||
formatHelpExamples,
|
||||
addGatewayClientOptions,
|
||||
callGatewayFromCli,
|
||||
inheritOptionFromParent,
|
||||
info,
|
||||
imageResultFromFile,
|
||||
isNodeCommandAllowed,
|
||||
jsonResult,
|
||||
listNodes,
|
||||
loadConfig,
|
||||
normalizePluginsConfig,
|
||||
optionalStringEnum,
|
||||
parseBooleanValue,
|
||||
readStringParam,
|
||||
respondUnavailableOnNodeInvokeError,
|
||||
resolveEffectiveEnableState,
|
||||
resolveNodeIdFromList,
|
||||
resolveNodeCommandAllowlist,
|
||||
runCommandWithRuntime,
|
||||
selectDefaultNodeFromList,
|
||||
safeParseJson,
|
||||
shortenHomePath,
|
||||
stringEnum,
|
||||
theme,
|
||||
} from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
export {
|
||||
loadConfig,
|
||||
normalizePluginsConfig,
|
||||
parseBooleanValue,
|
||||
resolveEffectiveEnableState,
|
||||
shortenHomePath,
|
||||
} from "openclaw/plugin-sdk/browser-config-runtime";
|
||||
export {
|
||||
addGatewayClientOptions,
|
||||
callGatewayFromCli,
|
||||
defaultRuntime,
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
isNodeCommandAllowed,
|
||||
respondUnavailableOnNodeInvokeError,
|
||||
resolveNodeCommandAllowlist,
|
||||
runCommandWithRuntime,
|
||||
safeParseJson,
|
||||
withTimeout,
|
||||
} from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
export {
|
||||
createSubsystemLogger,
|
||||
wrapExternalContent,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
export type { AnyAgentTool, NodeListNode } from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
export type { OpenClawConfig } from "openclaw/plugin-sdk/browser-config-runtime";
|
||||
export type {
|
||||
AnyAgentTool,
|
||||
GatewayRequestHandlers,
|
||||
GatewayRpcOpts,
|
||||
NodeListNode,
|
||||
NodeSession,
|
||||
OpenClawConfig,
|
||||
OpenClawPluginService,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
|
||||
150
extensions/browser/src/doctor-browser.ts
Normal file
150
extensions/browser/src/doctor-browser.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { note } from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
import {
|
||||
parseBrowserMajorVersion,
|
||||
readBrowserVersion,
|
||||
resolveGoogleChromeExecutableForPlatform,
|
||||
} from "./browser/chrome.executables.js";
|
||||
import type { OpenClawConfig } from "./config/config.js";
|
||||
|
||||
const CHROME_MCP_MIN_MAJOR = 144;
|
||||
const REMOTE_DEBUGGING_PAGES = [
|
||||
"chrome://inspect/#remote-debugging",
|
||||
"brave://inspect/#remote-debugging",
|
||||
"edge://inspect/#remote-debugging",
|
||||
].join(", ");
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
type ExistingSessionProfile = {
|
||||
name: string;
|
||||
userDataDir?: string;
|
||||
};
|
||||
|
||||
function collectChromeMcpProfiles(cfg: OpenClawConfig): ExistingSessionProfile[] {
|
||||
const browser = asRecord(cfg.browser);
|
||||
if (!browser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const profiles = new Map<string, ExistingSessionProfile>();
|
||||
const defaultProfile =
|
||||
typeof browser.defaultProfile === "string" ? browser.defaultProfile.trim() : "";
|
||||
if (defaultProfile === "user") {
|
||||
profiles.set("user", { name: "user" });
|
||||
}
|
||||
|
||||
const configuredProfiles = asRecord(browser.profiles);
|
||||
if (!configuredProfiles) {
|
||||
return [...profiles.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
for (const [profileName, rawProfile] of Object.entries(configuredProfiles)) {
|
||||
const profile = asRecord(rawProfile);
|
||||
const driver = typeof profile?.driver === "string" ? profile.driver.trim() : "";
|
||||
if (driver === "existing-session") {
|
||||
const userDataDir =
|
||||
typeof profile?.userDataDir === "string" ? profile.userDataDir.trim() : undefined;
|
||||
profiles.set(profileName, { name: profileName, userDataDir: userDataDir || undefined });
|
||||
}
|
||||
}
|
||||
|
||||
return [...profiles.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export async function noteChromeMcpBrowserReadiness(
|
||||
cfg: OpenClawConfig,
|
||||
deps?: {
|
||||
platform?: NodeJS.Platform;
|
||||
noteFn?: typeof note;
|
||||
resolveChromeExecutable?: (platform: NodeJS.Platform) => { path: string } | null;
|
||||
readVersion?: (executablePath: string) => string | null;
|
||||
},
|
||||
) {
|
||||
const profiles = collectChromeMcpProfiles(cfg);
|
||||
if (profiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteFn = deps?.noteFn ?? note;
|
||||
const platform = deps?.platform ?? process.platform;
|
||||
const resolveChromeExecutable =
|
||||
deps?.resolveChromeExecutable ?? resolveGoogleChromeExecutableForPlatform;
|
||||
const readVersion = deps?.readVersion ?? readBrowserVersion;
|
||||
const explicitProfiles = profiles.filter((profile) => profile.userDataDir);
|
||||
const autoConnectProfiles = profiles.filter((profile) => !profile.userDataDir);
|
||||
const profileLabel = profiles.map((profile) => profile.name).join(", ");
|
||||
|
||||
if (autoConnectProfiles.length === 0) {
|
||||
noteFn(
|
||||
[
|
||||
`- Chrome MCP existing-session is configured for profile(s): ${profileLabel}.`,
|
||||
"- These profiles use an explicit Chromium user data directory instead of Chrome's default auto-connect path.",
|
||||
`- Verify the matching Chromium-based browser is version ${CHROME_MCP_MIN_MAJOR}+ on the same host as the Gateway or node.`,
|
||||
`- Enable remote debugging in that browser's inspect page (${REMOTE_DEBUGGING_PAGES}).`,
|
||||
"- Keep the browser running and accept the attach consent prompt the first time OpenClaw connects.",
|
||||
].join("\n"),
|
||||
"Browser",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const chrome = resolveChromeExecutable(platform);
|
||||
const autoProfileLabel = autoConnectProfiles.map((profile) => profile.name).join(", ");
|
||||
|
||||
if (!chrome) {
|
||||
const lines = [
|
||||
`- Chrome MCP existing-session is configured for profile(s): ${profileLabel}.`,
|
||||
`- Google Chrome was not found on this host for auto-connect profile(s): ${autoProfileLabel}. OpenClaw does not bundle Chrome.`,
|
||||
`- Install Google Chrome ${CHROME_MCP_MIN_MAJOR}+ on the same host as the Gateway or node, or set browser.profiles.<name>.userDataDir for a different Chromium-based browser.`,
|
||||
`- Enable remote debugging in the browser inspect page (${REMOTE_DEBUGGING_PAGES}).`,
|
||||
"- Keep the browser running and accept the attach consent prompt the first time OpenClaw connects.",
|
||||
"- Docker, headless, and sandbox browser flows stay on raw CDP; this check only applies to host-local Chrome MCP attach.",
|
||||
];
|
||||
if (explicitProfiles.length > 0) {
|
||||
lines.push(
|
||||
`- Profiles with explicit userDataDir skip Chrome auto-detection: ${explicitProfiles
|
||||
.map((profile) => profile.name)
|
||||
.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
noteFn(lines.join("\n"), "Browser");
|
||||
return;
|
||||
}
|
||||
|
||||
const versionRaw = readVersion(chrome.path);
|
||||
const major = parseBrowserMajorVersion(versionRaw);
|
||||
const lines = [
|
||||
`- Chrome MCP existing-session is configured for profile(s): ${profileLabel}.`,
|
||||
`- Chrome path: ${chrome.path}`,
|
||||
];
|
||||
|
||||
if (!versionRaw || major === null) {
|
||||
lines.push(
|
||||
`- Could not determine the installed Chrome version. Chrome MCP requires Google Chrome ${CHROME_MCP_MIN_MAJOR}+ on this host.`,
|
||||
);
|
||||
} else if (major < CHROME_MCP_MIN_MAJOR) {
|
||||
lines.push(
|
||||
`- Detected Chrome ${versionRaw}, which is too old for Chrome MCP existing-session attach. Upgrade to Chrome ${CHROME_MCP_MIN_MAJOR}+.`,
|
||||
);
|
||||
} else {
|
||||
lines.push(`- Detected Chrome ${versionRaw}.`);
|
||||
}
|
||||
|
||||
lines.push(`- Enable remote debugging in the browser inspect page (${REMOTE_DEBUGGING_PAGES}).`);
|
||||
lines.push(
|
||||
"- Keep the browser running and accept the attach consent prompt the first time OpenClaw connects.",
|
||||
);
|
||||
if (explicitProfiles.length > 0) {
|
||||
lines.push(
|
||||
`- Profiles with explicit userDataDir still need manual validation of the matching Chromium-based browser: ${explicitProfiles
|
||||
.map((profile) => profile.name)
|
||||
.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
|
||||
noteFn(lines.join("\n"), "Browser");
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export { resolveGatewayAuth } from "openclaw/plugin-sdk/browser-support";
|
||||
export { resolveGatewayAuth } from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { ensureGatewayStartupAuth } from "openclaw/plugin-sdk/browser-support";
|
||||
export { ensureGatewayStartupAuth } from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { extractErrorCode, formatErrorMessage } from "openclaw/plugin-sdk/browser-support";
|
||||
export { extractErrorCode, formatErrorMessage } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -2,4 +2,4 @@ export {
|
||||
SafeOpenError,
|
||||
openFileWithinRoot,
|
||||
writeFileFromPathWithinRoot,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { hasProxyEnvConfigured } from "openclaw/plugin-sdk/browser-support";
|
||||
export { hasProxyEnvConfigured } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -4,4 +4,4 @@ export {
|
||||
resolvePinnedHostnameWithPolicy,
|
||||
type LookupFn,
|
||||
type SsrFPolicy,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { isNotFoundPathError, isPathInside } from "openclaw/plugin-sdk/browser-support";
|
||||
export { isNotFoundPathError, isPathInside } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { ensurePortAvailable } from "openclaw/plugin-sdk/browser-support";
|
||||
export { ensurePortAvailable } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { generateSecureToken } from "openclaw/plugin-sdk/browser-support";
|
||||
export { generateSecureToken } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { rawDataToString } from "openclaw/plugin-sdk/browser-support";
|
||||
export { rawDataToString } from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { redactSensitiveText } from "openclaw/plugin-sdk/browser-support";
|
||||
export { redactSensitiveText } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -3,4 +3,4 @@ export {
|
||||
buildImageResizeSideGrid,
|
||||
getImageMetadata,
|
||||
resizeToJpeg,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { ensureMediaDir, saveMediaBuffer } from "openclaw/plugin-sdk/browser-support";
|
||||
export { ensureMediaDir, saveMediaBuffer } from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/browser-support";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/browser-config-runtime";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
resolveEffectiveEnableState,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-config-runtime";
|
||||
|
||||
export function isDefaultBrowserPluginEnabled(cfg: OpenClawConfig): boolean {
|
||||
return resolveEffectiveEnableState({
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
startLazyPluginServiceModule,
|
||||
type LazyPluginServiceHandle,
|
||||
type OpenClawPluginService,
|
||||
} from "openclaw/plugin-sdk/browser-support";
|
||||
} from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
|
||||
type BrowserControlHandle = LazyPluginServiceHandle | null;
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { runExec } from "openclaw/plugin-sdk/browser-support";
|
||||
export { runExec } from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
|
||||
86
extensions/browser/src/security-audit.test.ts
Normal file
86
extensions/browser/src/security-audit.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectBrowserSecurityAuditFindings } from "./security-audit.js";
|
||||
|
||||
function collectFindings(
|
||||
config: Parameters<typeof collectBrowserSecurityAuditFindings>[0]["config"],
|
||||
) {
|
||||
return collectBrowserSecurityAuditFindings({
|
||||
config,
|
||||
sourceConfig: config,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
stateDir: "/tmp/openclaw-state",
|
||||
configPath: "/tmp/openclaw.json",
|
||||
});
|
||||
}
|
||||
|
||||
describe("browser security audit collector", () => {
|
||||
it("flags browser control without auth", () => {
|
||||
const findings = collectFindings({
|
||||
gateway: {
|
||||
controlUi: { enabled: false },
|
||||
auth: {},
|
||||
},
|
||||
browser: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "browser.control_no_auth",
|
||||
severity: "critical",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns on remote http CDP profiles", () => {
|
||||
const findings = collectFindings({
|
||||
browser: {
|
||||
profiles: {
|
||||
remote: {
|
||||
cdpUrl: "http://example.com:9222",
|
||||
color: "#0066CC",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "browser.remote_cdp_http",
|
||||
severity: "warn",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts private-host CDP URLs in findings", () => {
|
||||
const findings = collectFindings({
|
||||
browser: {
|
||||
ssrfPolicy: {
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
},
|
||||
profiles: {
|
||||
remote: {
|
||||
cdpUrl:
|
||||
"http://169.254.169.254:9222/json/version?token=supersecrettokenvalue1234567890",
|
||||
color: "#0066CC",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "browser.remote_cdp_private_host",
|
||||
severity: "warn",
|
||||
detail: expect.stringContaining("token=supers…7890"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
122
extensions/browser/src/security-audit.ts
Normal file
122
extensions/browser/src/security-audit.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { OpenClawPluginSecurityAuditContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
import { formatCliCommand } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { isPrivateNetworkOptInEnabled, isPrivateIpAddress } from "openclaw/plugin-sdk/ssrf-policy";
|
||||
import { redactCdpUrl, resolveBrowserConfig, resolveProfile } from "./browser/config.js";
|
||||
import { resolveBrowserControlAuth } from "./browser/control-auth.js";
|
||||
|
||||
const BLOCKED_HOSTNAMES = new Set([
|
||||
"localhost",
|
||||
"localhost.localdomain",
|
||||
"metadata.google.internal",
|
||||
]);
|
||||
|
||||
function hasNonEmptyString(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isTrustedPrivateHostname(hostname: string): boolean {
|
||||
const normalized = hostname.trim().toLowerCase();
|
||||
return normalized.length > 0 && BLOCKED_HOSTNAMES.has(normalized);
|
||||
}
|
||||
|
||||
export function collectBrowserSecurityAuditFindings(ctx: OpenClawPluginSecurityAuditContext) {
|
||||
const findings: Array<{
|
||||
checkId: string;
|
||||
severity: "warn" | "critical";
|
||||
title: string;
|
||||
detail: string;
|
||||
remediation?: string;
|
||||
}> = [];
|
||||
|
||||
let resolved: ReturnType<typeof resolveBrowserConfig>;
|
||||
try {
|
||||
resolved = resolveBrowserConfig(ctx.config.browser, ctx.config);
|
||||
} catch (err) {
|
||||
findings.push({
|
||||
checkId: "browser.control_invalid_config",
|
||||
severity: "warn" as const,
|
||||
title: "Browser control config looks invalid",
|
||||
detail: String(err),
|
||||
remediation: `Fix browser.cdpUrl in ${ctx.configPath} and re-run "${formatCliCommand("openclaw security audit --deep")}".`,
|
||||
});
|
||||
return findings;
|
||||
}
|
||||
|
||||
if (!resolved.enabled) {
|
||||
return findings;
|
||||
}
|
||||
|
||||
const browserAuth = resolveBrowserControlAuth(ctx.config, ctx.env);
|
||||
const explicitAuthMode = ctx.config.gateway?.auth?.mode;
|
||||
const tokenConfigured =
|
||||
Boolean(browserAuth.token) ||
|
||||
hasNonEmptyString(ctx.env.OPENCLAW_GATEWAY_TOKEN) ||
|
||||
hasConfiguredSecretInput(ctx.config.gateway?.auth?.token, ctx.config.secrets?.defaults);
|
||||
const passwordCanWin =
|
||||
explicitAuthMode === "password" ||
|
||||
(explicitAuthMode !== "token" &&
|
||||
explicitAuthMode !== "none" &&
|
||||
explicitAuthMode !== "trusted-proxy" &&
|
||||
!tokenConfigured);
|
||||
const passwordConfigured =
|
||||
Boolean(browserAuth.password) ||
|
||||
(passwordCanWin &&
|
||||
(hasNonEmptyString(ctx.env.OPENCLAW_GATEWAY_PASSWORD) ||
|
||||
hasConfiguredSecretInput(
|
||||
ctx.config.gateway?.auth?.password,
|
||||
ctx.config.secrets?.defaults,
|
||||
)));
|
||||
if (!tokenConfigured && !passwordConfigured) {
|
||||
findings.push({
|
||||
checkId: "browser.control_no_auth",
|
||||
severity: "critical" as const,
|
||||
title: "Browser control has no auth",
|
||||
detail:
|
||||
"Browser control HTTP routes are enabled but no gateway.auth token/password is configured. " +
|
||||
"Any local process (or SSRF to loopback) can call browser control endpoints.",
|
||||
remediation:
|
||||
"Set gateway.auth.token (recommended) or gateway.auth.password so browser control HTTP routes require authentication. Restarting the gateway will auto-generate gateway.auth.token when browser control is enabled.",
|
||||
});
|
||||
}
|
||||
|
||||
for (const name of Object.keys(resolved.profiles)) {
|
||||
const profile = resolveProfile(resolved, name);
|
||||
if (!profile || profile.cdpIsLoopback) {
|
||||
continue;
|
||||
}
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(profile.cdpUrl);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const redactedCdpUrl = redactCdpUrl(profile.cdpUrl) ?? profile.cdpUrl;
|
||||
if (url.protocol === "http:") {
|
||||
findings.push({
|
||||
checkId: "browser.remote_cdp_http",
|
||||
severity: "warn" as const,
|
||||
title: "Remote CDP uses HTTP",
|
||||
detail: `browser profile "${name}" uses http CDP (${redactedCdpUrl}); this is OK only if it's tailnet-only or behind an encrypted tunnel.`,
|
||||
remediation: "Prefer HTTPS/TLS or a tailnet-only endpoint for remote CDP.",
|
||||
});
|
||||
}
|
||||
if (
|
||||
isPrivateNetworkOptInEnabled(resolved.ssrfPolicy) &&
|
||||
(isTrustedPrivateHostname(url.hostname) || isPrivateIpAddress(url.hostname))
|
||||
) {
|
||||
findings.push({
|
||||
checkId: "browser.remote_cdp_private_host",
|
||||
severity: "warn" as const,
|
||||
title: "Remote CDP targets a private/internal host",
|
||||
detail:
|
||||
`browser profile "${name}" points at a private/internal CDP host (${redactedCdpUrl}). ` +
|
||||
"This is expected for LAN/tailnet/WSL-style setups, but treat it as a trusted-network endpoint.",
|
||||
remediation:
|
||||
"Prefer a tailnet or tunnel for remote CDP. If you want strict blocking, set browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=false and allow only explicit hosts.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
export { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { withFetchPreconnect } from "openclaw/plugin-sdk/browser-support";
|
||||
export { withFetchPreconnect } from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type { MockFn } from "openclaw/plugin-sdk/browser-support";
|
||||
export type { MockFn } from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { parseBooleanValue } from "openclaw/plugin-sdk/browser-support";
|
||||
export { parseBooleanValue } from "openclaw/plugin-sdk/browser-config-runtime";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-aut
|
||||
import { ensureModelAllowlistEntry } from "openclaw/plugin-sdk/provider-onboard";
|
||||
import { BYTEPLUS_CODING_MODEL_CATALOG, BYTEPLUS_MODEL_CATALOG } from "./models.js";
|
||||
import { buildBytePlusCodingProvider, buildBytePlusProvider } from "./provider-catalog.js";
|
||||
import { buildBytePlusVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
|
||||
const PROVIDER_ID = "byteplus";
|
||||
const BYTEPLUS_DEFAULT_MODEL_REF = "byteplus-plan/ark-code-latest";
|
||||
@@ -78,5 +79,6 @@ export default definePluginEntry({
|
||||
return [...byteplusModels, ...byteplusPlanModels];
|
||||
},
|
||||
});
|
||||
api.registerVideoGenerationProvider(buildBytePlusVideoGenerationProvider());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"cliDescription": "BytePlus API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"videoGenerationProviders": ["byteplus"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/byteplus-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw BytePlus provider plugin",
|
||||
"type": "module",
|
||||
|
||||
8
extensions/byteplus/plugin-registration.contract.test.ts
Normal file
8
extensions/byteplus/plugin-registration.contract.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { describePluginRegistrationContract } from "../../test/helpers/plugins/plugin-registration-contract.js";
|
||||
|
||||
describePluginRegistrationContract({
|
||||
pluginId: "byteplus",
|
||||
providerIds: ["byteplus", "byteplus-plan"],
|
||||
videoGenerationProviderIds: ["byteplus"],
|
||||
requireGenerateVideo: true,
|
||||
});
|
||||
88
extensions/byteplus/video-generation-provider.test.ts
Normal file
88
extensions/byteplus/video-generation-provider.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildBytePlusVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
|
||||
const {
|
||||
resolveApiKeyForProviderMock,
|
||||
postJsonRequestMock,
|
||||
fetchWithTimeoutMock,
|
||||
assertOkOrThrowHttpErrorMock,
|
||||
resolveProviderHttpRequestConfigMock,
|
||||
} = vi.hoisted(() => ({
|
||||
resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "byteplus-key" })),
|
||||
postJsonRequestMock: vi.fn(),
|
||||
fetchWithTimeoutMock: vi.fn(),
|
||||
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
|
||||
resolveProviderHttpRequestConfigMock: vi.fn((params) => ({
|
||||
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
|
||||
allowPrivateNetwork: false,
|
||||
headers: new Headers(params.defaultHeaders),
|
||||
dispatcherPolicy: undefined,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
|
||||
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
|
||||
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
|
||||
fetchWithTimeout: fetchWithTimeoutMock,
|
||||
postJsonRequest: postJsonRequestMock,
|
||||
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
|
||||
}));
|
||||
|
||||
describe("byteplus video generation provider", () => {
|
||||
afterEach(() => {
|
||||
resolveApiKeyForProviderMock.mockClear();
|
||||
postJsonRequestMock.mockReset();
|
||||
fetchWithTimeoutMock.mockReset();
|
||||
assertOkOrThrowHttpErrorMock.mockClear();
|
||||
resolveProviderHttpRequestConfigMock.mockClear();
|
||||
});
|
||||
|
||||
it("creates a content-generation task, polls, and downloads the video", async () => {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({
|
||||
id: "task_123",
|
||||
}),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock
|
||||
.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
id: "task_123",
|
||||
status: "succeeded",
|
||||
content: {
|
||||
video_url: "https://example.com/byteplus.mp4",
|
||||
},
|
||||
model: "seedance-1-0-lite-t2v-250428",
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
headers: new Headers({ "content-type": "video/mp4" }),
|
||||
arrayBuffer: async () => Buffer.from("mp4-bytes"),
|
||||
});
|
||||
|
||||
const provider = buildBytePlusVideoGenerationProvider();
|
||||
const result = await provider.generateVideo({
|
||||
provider: "byteplus",
|
||||
model: "seedance-1-0-lite-t2v-250428",
|
||||
prompt: "A lantern floats upward into the night sky",
|
||||
cfg: {},
|
||||
});
|
||||
|
||||
expect(postJsonRequestMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks",
|
||||
}),
|
||||
);
|
||||
expect(result.videos).toHaveLength(1);
|
||||
expect(result.metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
taskId: "task_123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
253
extensions/byteplus/video-generation-provider.ts
Normal file
253
extensions/byteplus/video-generation-provider.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
fetchWithTimeout,
|
||||
postJsonRequest,
|
||||
resolveProviderHttpRequestConfig,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import type {
|
||||
GeneratedVideoAsset,
|
||||
VideoGenerationProvider,
|
||||
VideoGenerationRequest,
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
import { BYTEPLUS_BASE_URL } from "./models.js";
|
||||
|
||||
const DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-1-0-lite-t2v-250428";
|
||||
const DEFAULT_TIMEOUT_MS = 120_000;
|
||||
const POLL_INTERVAL_MS = 5_000;
|
||||
const MAX_POLL_ATTEMPTS = 120;
|
||||
|
||||
type BytePlusTaskCreateResponse = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
type BytePlusTaskResponse = {
|
||||
id?: string;
|
||||
model?: string;
|
||||
status?: "running" | "failed" | "queued" | "succeeded" | "cancelled";
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
content?: {
|
||||
video_url?: string;
|
||||
last_frame_url?: string;
|
||||
file_url?: string;
|
||||
};
|
||||
duration?: number;
|
||||
ratio?: string;
|
||||
resolution?: string;
|
||||
};
|
||||
|
||||
function resolveBytePlusVideoBaseUrl(req: VideoGenerationRequest): string {
|
||||
return req.cfg?.models?.providers?.byteplus?.baseUrl?.trim() || BYTEPLUS_BASE_URL;
|
||||
}
|
||||
|
||||
function toDataUrl(buffer: Buffer, mimeType: string): string {
|
||||
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
|
||||
function resolveBytePlusImageUrl(req: VideoGenerationRequest): string | undefined {
|
||||
const input = req.inputImages?.[0];
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
if (input.url?.trim()) {
|
||||
return input.url.trim();
|
||||
}
|
||||
if (!input.buffer) {
|
||||
throw new Error("BytePlus reference image is missing image data.");
|
||||
}
|
||||
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
|
||||
}
|
||||
|
||||
async function pollBytePlusTask(params: {
|
||||
taskId: string;
|
||||
headers: Headers;
|
||||
timeoutMs?: number;
|
||||
baseUrl: string;
|
||||
fetchFn: typeof fetch;
|
||||
}): Promise<BytePlusTaskResponse> {
|
||||
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt += 1) {
|
||||
const response = await fetchWithTimeout(
|
||||
`${params.baseUrl}/contents/generations/tasks/${params.taskId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: params.headers,
|
||||
},
|
||||
params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
params.fetchFn,
|
||||
);
|
||||
await assertOkOrThrowHttpError(response, "BytePlus video status request failed");
|
||||
const payload = (await response.json()) as BytePlusTaskResponse;
|
||||
switch (payload.status?.trim()) {
|
||||
case "succeeded":
|
||||
return payload;
|
||||
case "failed":
|
||||
case "cancelled":
|
||||
throw new Error(payload.error?.message?.trim() || "BytePlus video generation failed");
|
||||
case "queued":
|
||||
case "running":
|
||||
default:
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new Error(`BytePlus video generation task ${params.taskId} did not finish in time`);
|
||||
}
|
||||
|
||||
async function downloadBytePlusVideo(params: {
|
||||
url: string;
|
||||
timeoutMs?: number;
|
||||
fetchFn: typeof fetch;
|
||||
}): Promise<GeneratedVideoAsset> {
|
||||
const response = await fetchWithTimeout(
|
||||
params.url,
|
||||
{ method: "GET" },
|
||||
params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
params.fetchFn,
|
||||
);
|
||||
await assertOkOrThrowHttpError(response, "BytePlus generated video download failed");
|
||||
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return {
|
||||
buffer: Buffer.from(arrayBuffer),
|
||||
mimeType,
|
||||
fileName: `video-1.${mimeType.includes("webm") ? "webm" : "mp4"}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider {
|
||||
return {
|
||||
id: "byteplus",
|
||||
label: "BytePlus",
|
||||
defaultModel: DEFAULT_BYTEPLUS_VIDEO_MODEL,
|
||||
models: [
|
||||
DEFAULT_BYTEPLUS_VIDEO_MODEL,
|
||||
"seedance-1-0-lite-i2v-250428",
|
||||
"seedance-1-0-pro-250528",
|
||||
"seedance-1-5-pro-251215",
|
||||
],
|
||||
isConfigured: ({ agentDir }) =>
|
||||
isProviderApiKeyConfigured({
|
||||
provider: "byteplus",
|
||||
agentDir,
|
||||
}),
|
||||
capabilities: {
|
||||
maxVideos: 1,
|
||||
maxInputImages: 1,
|
||||
maxInputVideos: 0,
|
||||
maxDurationSeconds: 12,
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
supportsAudio: true,
|
||||
supportsWatermark: true,
|
||||
},
|
||||
async generateVideo(req) {
|
||||
if ((req.inputVideos?.length ?? 0) > 0) {
|
||||
throw new Error("BytePlus video generation does not support video reference inputs.");
|
||||
}
|
||||
const auth = await resolveApiKeyForProvider({
|
||||
provider: "byteplus",
|
||||
cfg: req.cfg,
|
||||
agentDir: req.agentDir,
|
||||
store: req.authStore,
|
||||
});
|
||||
if (!auth.apiKey) {
|
||||
throw new Error("BytePlus API key missing");
|
||||
}
|
||||
|
||||
const fetchFn = fetch;
|
||||
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
|
||||
resolveProviderHttpRequestConfig({
|
||||
baseUrl: resolveBytePlusVideoBaseUrl(req),
|
||||
defaultBaseUrl: BYTEPLUS_BASE_URL,
|
||||
allowPrivateNetwork: false,
|
||||
defaultHeaders: {
|
||||
Authorization: `Bearer ${auth.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
provider: "byteplus",
|
||||
capability: "video",
|
||||
transport: "http",
|
||||
});
|
||||
const content: Array<Record<string, unknown>> = [{ type: "text", text: req.prompt }];
|
||||
const imageUrl = resolveBytePlusImageUrl(req);
|
||||
if (imageUrl) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: imageUrl },
|
||||
role: "first_frame",
|
||||
});
|
||||
}
|
||||
const body: Record<string, unknown> = {
|
||||
model: req.model?.trim() || DEFAULT_BYTEPLUS_VIDEO_MODEL,
|
||||
content,
|
||||
};
|
||||
if (req.aspectRatio?.trim()) {
|
||||
body.ratio = req.aspectRatio.trim();
|
||||
}
|
||||
if (req.resolution) {
|
||||
body.resolution = req.resolution;
|
||||
}
|
||||
if (typeof req.durationSeconds === "number" && Number.isFinite(req.durationSeconds)) {
|
||||
body.duration = Math.max(1, Math.round(req.durationSeconds));
|
||||
}
|
||||
if (typeof req.audio === "boolean") {
|
||||
body.generate_audio = req.audio;
|
||||
}
|
||||
if (typeof req.watermark === "boolean") {
|
||||
body.watermark = req.watermark;
|
||||
}
|
||||
|
||||
const { response, release } = await postJsonRequest({
|
||||
url: `${baseUrl}/contents/generations/tasks`,
|
||||
headers,
|
||||
body,
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
});
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "BytePlus video generation failed");
|
||||
const submitted = (await response.json()) as BytePlusTaskCreateResponse;
|
||||
const taskId = submitted.id?.trim();
|
||||
if (!taskId) {
|
||||
throw new Error("BytePlus video generation response missing task id");
|
||||
}
|
||||
const completed = await pollBytePlusTask({
|
||||
taskId,
|
||||
headers,
|
||||
timeoutMs: req.timeoutMs,
|
||||
baseUrl,
|
||||
fetchFn,
|
||||
});
|
||||
const videoUrl = completed.content?.video_url?.trim();
|
||||
if (!videoUrl) {
|
||||
throw new Error("BytePlus video generation completed without a video URL");
|
||||
}
|
||||
const video = await downloadBytePlusVideo({
|
||||
url: videoUrl,
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
});
|
||||
return {
|
||||
videos: [video],
|
||||
model: completed.model ?? req.model ?? DEFAULT_BYTEPLUS_VIDEO_MODEL,
|
||||
metadata: {
|
||||
taskId,
|
||||
status: completed.status,
|
||||
videoUrl,
|
||||
ratio: completed.ratio,
|
||||
resolution: completed.resolution,
|
||||
duration: completed.duration,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/chutes-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Chutes.ai provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/cloudflare-ai-gateway-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot-proxy",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Copilot Proxy provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepgram-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.5",
|
||||
"private": true,
|
||||
"description": "OpenClaw Deepgram media-understanding provider",
|
||||
"type": "module",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user