Compare commits

...

40 Commits

Author SHA1 Message Date
Peter Steinberger
8b2a6e57fe docs: refresh plugin inventory for bundled channels 2026-05-03 01:02:42 +01:00
Vincent Koc
0accc7f745 fix(channels): keep matrix and mattermost bundled
(cherry picked from commit e3d76d2e1f)
2026-05-03 00:53:53 +01:00
Vincent Koc
8842a5bd43 test(e2e): allow npm configured plugin installs
(cherry picked from commit a1054fbe1b)
2026-05-03 00:26:08 +01:00
Vincent Koc
c975bff486 fix: trusted installs
(cherry picked from commit 5ed7f1fd26)
2026-05-03 00:26:08 +01:00
Peter Steinberger
90079f5790 test(tooling): align plugin prerelease expectations
(cherry picked from commit 9a899a29b8)
2026-05-03 00:26:00 +01:00
Peter Steinberger
d8f31a2bfb fix(plugins): allow Discord install repair
(cherry picked from commit e857c795a8)
2026-05-03 00:26:00 +01:00
Peter Steinberger
dd22838630 test(docker): expect discord onboard package lane
(cherry picked from commit 9ef7c024c8)
2026-05-03 00:24:41 +01:00
Vincent Koc
a121f98e55 test(plugins): avoid kitchen sink config drift
(cherry picked from commit 7a54076770)
2026-05-03 00:24:41 +01:00
Vincent Koc
13e8c49ee0 test(plugins): pin kitchen sink npm fixture
(cherry picked from commit a8f62725e5)
2026-05-03 00:24:41 +01:00
Vincent Koc
6cb548475e test(plugins): harden package plugin e2e lanes
(cherry picked from commit 1417008ff7)
2026-05-03 00:24:41 +01:00
Peter Steinberger
dfad38d153 test: align stable release expectations 2026-05-03 00:11:12 +01:00
Peter Steinberger
0126dbe171 chore: refresh stable config schema 2026-05-03 00:05:48 +01:00
Peter Steinberger
57b158ff90 chore: prepare stable release metadata 2026-05-02 23:57:56 +01:00
Peter Steinberger
f352caf07e fix: keep runtime model auth alias after build 2026-05-02 23:54:57 +01:00
Peter Steinberger
61349d6bdc test: isolate prerelease catalog fixture 2026-05-02 23:43:09 +01:00
Peter Steinberger
3d69cd0a1d test: align release assertions with beta metadata 2026-05-02 23:37:34 +01:00
Peter Steinberger
8ecc5fcc51 docs: prepare 2026.5.2 changelog 2026-05-02 23:35:11 +01:00
Peter Steinberger
b2c4c2daa4 docs: refresh release metadata after rebase 2026-05-02 23:28:35 +01:00
Peter Steinberger
793485a472 chore: bump beta release to 2026.5.2-beta.3 2026-05-02 23:19:53 +01:00
Peter Steinberger
8771cfb5b7 fix: resolve bundled public surfaces from packaged dist 2026-05-02 23:19:52 +01:00
Peter Steinberger
bf91494035 test: keep windows smoke compatible with old agent cli 2026-05-02 23:19:52 +01:00
Peter Steinberger
13424b9b3e test: accept externalized discord voice fallback 2026-05-02 23:19:52 +01:00
Peter Steinberger
c96e62d5ab build: avoid ambiguous runtime aliases 2026-05-02 23:19:52 +01:00
Peter Steinberger
beefac0564 build: restore postbuild config baseline 2026-05-02 23:19:52 +01:00
Peter Steinberger
de42d35441 build: use ci config baseline hash 2026-05-02 23:19:52 +01:00
Peter Steinberger
11932ccd92 build: refresh generated config schema 2026-05-02 23:19:52 +01:00
Peter Steinberger
d761910457 fix: backport gateway start repair 2026-05-02 23:19:52 +01:00
Peter Steinberger
0f1b1e0293 fix: backport release validation fixes 2026-05-02 23:19:52 +01:00
Peter Steinberger
4dc9c43df8 fix: stabilize beta update and OpenAI transport paths 2026-05-02 23:19:52 +01:00
Peter Steinberger
4b72d8e73c docs: clarify beta validation-only fixes 2026-05-02 23:19:52 +01:00
Vincent Koc
35af235755 fix(plugins): keep bare installs on npm for launch 2026-05-02 23:19:52 +01:00
Peter Steinberger
c22af827fd test: align release dependency fixture 2026-05-02 23:19:52 +01:00
Peter Steinberger
abe2b294ae fix: align postpublish verification with external plugins 2026-05-02 23:19:52 +01:00
Peter Steinberger
202b7fd597 ci: fix release publish repo context 2026-05-02 23:19:52 +01:00
Peter Steinberger
5305b172a3 fix: honor package excludes in channel pack smoke 2026-05-02 23:19:52 +01:00
Peter Steinberger
1bdf1378a3 docs: fold 2026.4.30 into 2026.5.2 changelog 2026-05-02 23:18:42 +01:00
Peter Steinberger
afd8ad14b2 chore: switch 2026.5.2 prerelease to beta 1 2026-05-02 23:18:42 +01:00
Peter Steinberger
e071c3321f chore: repair alpha release metadata 2026-05-02 23:18:42 +01:00
Peter Steinberger
8412f3369d chore: bump 2026.5.2 alpha 1 2026-05-02 23:18:41 +01:00
Peter Steinberger
64249819f5 docs: remove alpha release note 2026-05-02 23:18:41 +01:00
63 changed files with 1121 additions and 438 deletions

View File

@@ -41,11 +41,19 @@ Use this skill for release and publish-time workflow. Keep ordinary development
recommended replacement can shift as plugin ownership, externalization, and
config footprint move, so do not blindly copy stale replacement annotations
into release notes.
- Do not delete or rewrite beta tags after their matching npm package has been
published. If a pushed beta tag fails preflight before npm publish, delete and
- Do not delete or rewrite alpha or beta tags after their matching npm package
has been published. If a pushed alpha or beta tag fails preflight before npm
publish, first verify the matching npm package does not exist, then delete and
recreate the tag and prerelease at the fixed commit so npm prerelease versions
stay contiguous. If a published beta needs a fix, commit the fix on the
release branch and increment to the next `-beta.N`.
stay contiguous. If a published prerelease needs a fix, commit the fix on the
release branch and increment to the next matching `-alpha.N` or `-beta.N`.
- After a beta is published, distinguish validation-only fixes from product
fixes. Test, docs, workflow, release-script, changelog, or agent-skill fixes
that do not change the `openclaw` package runtime can be committed and
validated without cutting a new beta. Any fix that changes shipped OpenClaw
runtime, package contents, install/update behavior, public API, or user-facing
behavior after the beta was published usually requires a new `-beta.N` before
treating release validation as evidence for the published package.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on

View File

@@ -174,9 +174,9 @@ jobs:
shift
local before_json dispatch_output run_id status conclusion url
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
before_json="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
@@ -187,7 +187,7 @@ jobs:
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
BEFORE_IDS="$before_json" gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
@@ -207,13 +207,13 @@ jobs:
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
gh run cancel "$run_id" >/dev/null 2>&1 || true
gh run cancel --repo "$GITHUB_REPOSITORY" "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
status="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
@@ -221,14 +221,14 @@ jobs:
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
conclusion="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json url --jq '.url')"
echo "${workflow} finished with ${conclusion}: ${url}"
{
echo "- ${workflow}: ${conclusion} (${url})"
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
exit 1
fi
}

View File

@@ -2,113 +2,79 @@
Docs: https://docs.openclaw.ai
## Unreleased
### Changes
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
- Plugins/update: on the beta OpenClaw update channel, default-line npm and ClawHub plugin updates try `@beta` first and fall back to default/latest when no plugin beta release exists.
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
### Fixes
- CLI/plugins: stop treating the non-plugin `auth` command root as a bundled plugin id, so restrictive `plugins.allow` configs no longer tell users to add stale `auth` plugin entries.
- Doctor/plugins: update configured plugin installs whose stale manifests still declare channels without `channelConfigs`, so beta upgrades repair old Discord-style package payloads during `doctor --fix`.
- Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc.
- Upgrade/config: validate configured web-search providers and statically suppressed model/provider pairs against the active plugin set at config load, so stale plugin state fails loud before runtime fallback.
- Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses.
- Doctor/plugins: preserve unmanaged third-party plugin `node_modules` during `doctor --fix`, while still pruning OpenClaw-managed runtime dependency caches.
- Gateway/restart: add `openclaw gateway restart --force` and `--wait <duration>`, log active task run IDs before restart deferral timers, and report timeout restarts as explicit forced restarts.
- Discord: persist slash-command deploy hashes across process restarts so unchanged command sets skip redeploy and avoid restart-loop 429s.
- Providers/LM Studio: normalize binary `off`/`on` reasoning metadata from Gemma 4 and other local models to LM Studio's accepted OpenAI-compatible `reasoning_effort` values.
- Plugins/externalization: keep official external install docs, update examples, and live Codex npm checks on default npm tags instead of `@beta`. Thanks @vincentkoc.
- Plugins/externalization: keep ACPX, Google Chat, and LINE publishable plugin dist trees out of the core npm package file list.
- Plugins/ClawHub: fall back to version metadata when the artifact resolver route is missing and keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, avoiding false version-not-found failures during plugin install validation. Thanks @vincentkoc.
- Status/channels: show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, avoiding empty Channels tables on WSL and other no-Gateway paths. Thanks @vincentkoc.
- Plugins/ClawHub: explain unavailable explicit ClawHub ClawPack artifact downloads with a temporary npm install hint while ClawHub artifact routing rolls out. Thanks @vincentkoc.
- Media: accept home-relative `MEDIA:~/...` attachment paths while preserving existing file-read policy, traversal checks, and media type validation. Fixes #73796. Thanks @fabkury.
- Onboarding/search: install official external web-search plugins such as Brave before saving provider config, and make doctor repair reconcile selected external search providers whose npm payload is missing. Thanks @vincentkoc.
- Plugins/externalization: add official npm-first catalogs for externalized channel, provider, and generic plugins, keep unpublished ACPX/Google Chat/LINE bundled, and make missing-plugin repair honor npm-first metadata while ClawHub pack files roll out. Thanks @vincentkoc.
- Plugins/update: detect tracked plugin install records whose package directories disappeared during `openclaw update`, reinstall them before normal plugin updates, and fail the update if any install record still points at missing disk payloads.
- Plugins/registry: hash manifest and package metadata when validating persisted plugin registries so fast same-size rewrites cannot leave stale plugin metadata trusted.
- CLI/infer: reject local `codex/*` one-shot model probes before simple-completion dispatch and point operators at the Codex app-server runtime path instead of ending with an empty-output error.
- Agents/sessions: preserve terminal lifecycle state when final run metadata persists from a stale in-memory snapshot, preventing `main` sessions from staying stuck as running after completed or timed-out turns.
- Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting.
- Heartbeat/scheduler: make heartbeat phase scheduling active-hours-aware so the scheduler seeks forward to the first in-window phase slot instead of arming timers for quiet-hours slots and relying solely on the runtime guard. Non-UTC `activeHours.timezone` values (e.g. `Asia/Shanghai`) now correctly influence when the next heartbeat timer fires, avoiding wasted quiet-hours ticks and long dormant gaps after gateway restarts. Fixes #75487. Thanks @amknight.
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc.
- Setup/TUI: bound the Terminal hatch bootstrap run so a stalled provider request times out instead of leaving first-run hatching stuck behind the watchdog. (#76241) Thanks @joshavant.
- Plugins/Codex: allow the official npm Codex plugin to install without the unsafe-install override, keep `/codex` command ownership, and cover the real npm Docker live path through managed `.openclaw/npm` dependencies plus uninstall failure proof.
- Gateway/status: add concrete service, config, listener-owner, and log collection next steps when gateway probes fail and Bonjour finds no local gateway, so frozen or port-conflict reports include the data needed for root-cause triage. Refs #49012. Thanks @vincentkoc.
## 2026.5.2
### Highlights
- Alpha prerelease support adds the `vYYYY.M.D-alpha.N` tag shape, npm `alpha` dist-tag, release workflow inputs, package acceptance, Telegram package checks, and upgrade-survivor validation paths.
- External plugin installation now covers diagnostics, onboarding, doctor repair, channel setup, install/update records, and artifact metadata while keeping bare package installs on npm for the first cutover. Thanks @vincentkoc.
- Gateway startup, session listing, task maintenance, prompt prep, plugin loading, and filesystem hot paths get targeted cache and fanout reductions for large or plugin-heavy installs.
- Control UI and WebChat reliability improves across Sessions, Cron, long-running Gateway WebSockets, grouped-message width, slash-command feedback, iOS PWA bounds, selection contrast, and Talk diagnostics.
- Channel and provider fixes cover Telegram topic commands and networking, Discord delivery and startup edge cases, OpenAI-compatible TTS/Realtime, OpenRouter/DeepSeek replay, Anthropic-compatible streaming, Brave/SearXNG/Firecrawl web search, and voice-call routing.
- External plugin installation, update, doctor repair, dependency reporting, and artifact metadata now cover the npm-first cutover, stale configured installs, missing package payloads, and beta-channel plugin fallback. Thanks @vincentkoc.
- Gateway and agent hot paths are leaner across startup, session listing, task maintenance, prompt prep, plugin loading, tool descriptor planning, filesystem guards, and large runtime configs.
- Control UI and WebChat are more resilient across Sessions, Cron, long-running Gateway WebSockets, grouped-message width, slash-command feedback, iOS PWA bounds, selection contrast, and Talk diagnostics.
- Messaging fixes cover WhatsApp Channel/Newsletter targets, Telegram topic commands and networking, Discord delivery/startup edge cases, Slack threads, Signal groups/media, and visible reply routing.
- Provider and media fixes cover OpenAI-compatible TTS/Realtime, OpenRouter/DeepSeek replay, Anthropic-compatible streaming, LM Studio reasoning metadata, Brave/SearXNG/Firecrawl web search, media paths, music, and voice-call routing.
### Changes
- Release: add first-class alpha prerelease support across version parsing, release workflows, package specs, published-package validation, plugin publish planning, and release docs.
- Gateway/startup: skip plugin-backed auth-profile overlays during startup secrets preflight, reducing gateway readiness latency while keeping reload and OAuth recovery paths overlay-capable. (#68327) Thanks @JIRBOY.
- Gateway/startup and restart: skip plugin-backed auth-profile overlays during startup secrets preflight, reducing gateway readiness latency while keeping reload and OAuth recovery paths overlay-capable; add `openclaw gateway restart --force` and `--wait <duration>`, log active task run IDs before restart deferral timers, and report timeout restarts as explicit forced restarts. (#68327) Thanks @JIRBOY.
- Plugins/ClawHub: make diagnostics, onboarding, doctor repair, and channel setup carry ClawPack metadata through install records while keeping explicit `clawhub:` installs on ClawHub and bare package installs on npm for the launch cutover. Thanks @vincentkoc.
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
- Plugins/update: on the beta OpenClaw update channel, default-line npm and ClawHub plugin updates try `@beta` first and fall back to default/latest when no plugin beta release exists.
- Plugins/runtime: scope broad runtime preloads to the effective plugin ids derived from config, startup planning, configured channels, slots, and auto-enable rules instead of importing every discoverable plugin.
- Agents/runtime: reuse the startup-loaded plugin registry for request-time providers, tools, channel actions, web/capability/memory/migration helpers, and memoized provider extra-params so stable embedded-run inputs no longer repeat plugin registry resolution while model-specific transport hook patches stay isolated. Thanks @DmitryPogodaev.
- Agents/runtime: memoize transcript replay-policy resolution for stable config and process-env runs while preserving custom-env provider hook behavior. Thanks @DmitryPogodaev.
- Agents/runtime: reuse the startup-loaded plugin registry for request-time providers, tools, channel actions, web/capability/memory/migration helpers, and memoized provider extra-params, and memoize transcript replay-policy resolution for stable config and process-env runs while preserving model-specific transport hook patches and custom-env provider behavior. Thanks @DmitryPogodaev.
- Infra/path-guards: add a fast path for canonical absolute POSIX containment checks, avoiding repeated `path.resolve` and `path.relative` work in hot filesystem walkers. Refs #75895, #75575, and #68782. Thanks @Enderfga.
- Tools: add a platform-level tool descriptor planner for descriptor-first visibility, generic availability checks, and executor references. Thanks @shakkernerd.
- Plugins/tools: cache plugin tool descriptors captured from `api.registerTool(...)` so repeated prompt-time planning can skip plugin runtime loading while execution still loads the live plugin tool. (#76079) Thanks @shakkernerd.
- Tools/plugins: add a platform-level tool descriptor planner for descriptor-first visibility, generic availability checks, and executor references, and cache plugin tool descriptors captured from `api.registerTool(...)` so repeated prompt-time planning can skip plugin runtime loading while execution still loads the live plugin tool. (#76079) Thanks @shakkernerd.
- Docs/Codex: clarify that ChatGPT/Codex subscription setups should use `openai/gpt-*` with `agentRuntime.id: "codex"` for native Codex runtime, while `openai-codex/*` remains the PI OAuth route. Thanks @pashpashpash.
- Plugins/source checkout: load bundled plugins from the `extensions/*` pnpm workspace tree in source checkouts, so plugin-local dependencies and edits are used directly while packaged installs keep using the built runtime tree. Thanks @vincentkoc.
- Plugins/beta: externalize ACPX behind the official `@openclaw/acpx` package so packaged installs keep ACP harness adapter binaries out of core until the ACP backend is installed. Thanks @vincentkoc.
- Plugins/beta: externalize diagnostics OpenTelemetry behind the official `@openclaw/diagnostics-otel` package so packaged installs keep the OTEL dependency stack out of core until the plugin is installed. Thanks @vincentkoc.
- Plugins/beta: prepare Google Chat, LINE, Matrix, and Mattermost for `2026.5.1-beta.2` npm and ClawHub publishing, and keep publishable plugin dist trees out of the core npm package. Thanks @vincentkoc.
- Plugins/beta: prepare BlueBubbles, diagnostics Prometheus, Google Meet, Nextcloud Talk, Nostr, Zalo, and Zalo Personal for `2026.5.1-beta.2` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: prepare diagnostics OpenTelemetry, Discord, Diffs, Lobster, Memory LanceDB, Microsoft Teams, QQ Bot, Voice Call, and WhatsApp for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: prepare Brave, Codex, Feishu, Synology Chat, Tlon, and Twitch for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: externalize ACPX behind `@openclaw/acpx` and diagnostics OpenTelemetry behind `@openclaw/diagnostics-otel`, keeping their heavier runtime stacks out of the core package until installed; prepare Google Chat, LINE, Matrix, Mattermost, BlueBubbles, diagnostics Prometheus, Google Meet, Nextcloud Talk, Nostr, Zalo, Zalo Personal, diagnostics OpenTelemetry, Discord, Diffs, Lobster, Memory LanceDB, Microsoft Teams, QQ Bot, Voice Call, WhatsApp, Brave, Codex, Feishu, Synology Chat, Tlon, and Twitch for `2026.5.1-beta.1`/`2026.5.1-beta.2` npm and ClawHub publishing, and keep publishable plugin dist trees out of the core npm package. Thanks @vincentkoc.
- Providers/xAI: add Grok 4.3 to the bundled catalog and make it the default xAI chat model.
- Google Meet: let API-created rooms set `accessType` and `entryPointAccess`, and add `googlemeet end-active-conference` for closing managed spaces after a call. (#74824) Thanks @BsnizND.
- Google Meet: add `googlemeet test-listen` and the matching `google_meet` `test_listen` action so transcribe-mode joins wait for real caption or transcript movement before reporting listen-first health. Refs #72478. Thanks @DougButdorf.
- Plugins/ClawHub: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verifying the ClawPack response header and downloaded bytes before installing. Thanks @vincentkoc.
- Plugins/ClawHub: persist ClawPack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc.
- Plugins/ClawHub: allow official bundled-plugin cutovers to record ClawHub artifact metadata while preserving npm as the launch default for bare package specs. Thanks @vincentkoc.
- Plugins/onboarding: allow install-on-demand provider setup entries to persist ClawHub artifact metadata after explicit ClawHub installs while retaining npm/local fallback paths. Thanks @vincentkoc.
- Google Meet: let API-created rooms set `accessType` and `entryPointAccess`, add `googlemeet end-active-conference` for closing managed spaces after a call, and add `googlemeet test-listen` plus the matching `google_meet` `test_listen` action so transcribe-mode joins wait for real caption or transcript movement before reporting listen-first health. (#74824; refs #72478) Thanks @BsnizND and @DougButdorf.
- Plugins/ClawHub/onboarding: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verify ClawPack response headers and downloaded bytes, persist ClawPack digest/artifact metadata on install/update records and install-on-demand provider setup entries, and allow official bundled-plugin cutovers to record ClawHub artifact metadata while preserving npm as the launch default for bare package specs and retaining npm/local fallback paths. Thanks @vincentkoc.
- Plugins/Crestodian: add ClawHub plugin search plus Crestodian plugin list/search/install/uninstall operations, with approval and audit coverage for install and uninstall.
- Channels/thread bindings: replace split subagent/ACP thread-spawn toggles with `threadBindings.spawnSessions`, default thread-bound spawns on, and let `openclaw doctor --fix` migrate the legacy keys. (#75943)
- Providers/OpenAI: add `extraBody`/`extra_body` passthrough for OpenAI-compatible TTS endpoints, so custom speech servers can receive fields such as `lang` in `/audio/speech` requests. Fixes #39900. Thanks @R3NK0R.
- Dependencies: refresh workspace dependency pins, including TypeBox 1.1.37, AWS SDK 3.1041.0, Microsoft Teams 2.0.9, and Marked 18.0.3. Thanks @mariozechner, @aws, and @microsoft.
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
- Dependencies: refresh workspace, bundled runtime, and plugin dependency pins, including TypeBox 1.1.37, AWS SDK 3.1041.0, Microsoft Teams 2.0.9, Marked 18.0.3, Pi 0.71.1, OpenAI 6.35.0, Codex 0.128.0, Zod 4.4.1, and Matrix 41.4.0. Thanks @mariozechner, @aws, and @microsoft.
- Discord/channels: add reusable message-channel access groups plus Discord channel-audience DM authorization, so allowlists can reference `accessGroup:<name>` across channel auth paths. (#75813)
- Crabbox/scripts: print the selected Crabbox binary, version, and supported providers before `pnpm crabbox:*` commands, and reject stale binaries that lack `blacksmith-testbox` provider support.
- Agents/Codex: add committed happy-path prompt snapshots for Codex/message-tool Telegram direct, Discord group, and heartbeat turns so prompt drift can be reviewed. Thanks @pashpashpash.
- Agents/workspace: add `agents.defaults.skipOptionalBootstrapFiles` for skipping selected optional workspace files during bootstrap without disabling required workspace setup. (#62110) Thanks @mainstay22.
- Plugins/CLI: add first-class `git:` plugin installs with ref checkout, commit metadata, normal scanner/staging, and `plugins update` support for recorded git sources. Thanks @badlogic.
- Google Meet: add live caption health for Chrome transcribe mode, including caption observer state, transcript counters, last caption text, and recent transcript lines in status and doctor output. Refs #72478. Thanks @DougButdorf.
- Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP.
- macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti.
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.
- Discord: keep active buttons, selects, and forms working across Gateway restarts until they expire, so multi-step Discord interactions are less likely to break during upgrades or restarts. Thanks @amknight.
- Messages/docs: clarify that `BodyForAgent` is the primary inbound model text while `Body` is the legacy envelope fallback, and add Signal coverage so channel hardening patches target the real prompt path. Refs #66198. Thanks @defonota3box.
- Slack: publish a safe default App Home tab view on `app_home_opened`, include the Home tab event in setup manifests, and keep track of bot-participated threads across restarts so ongoing threaded conversations can continue auto-replying after the Gateway restarts. Fixes #11655; refs #52020. Thanks @TinyTb and @amknight.
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=…`/`?token=…` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.
- CLI/proxy: add `openclaw proxy validate` so operators can verify effective proxy configuration, proxy reachability, and expected allow/deny destination behavior before deploying proxy-routed OpenClaw commands. (#73438) Thanks @jesse-merhi.
- Agents/Codex: default Codex app-server dynamic tools to native-first, keeping OpenClaw integration tools while leaving file, patch, exec, and process ownership to the Codex harness; default Codex-harness direct source replies to the OpenClaw `message` tool when visible reply delivery is not explicitly configured, keeping channel-visible output as a deliberate tool call. (#75308, #75765) Thanks @pashpashpash.
- Heartbeats/agents: add a structured `heartbeat_respond` tool for tool-capable heartbeat runs so agents can record quiet outcomes or explicit notification text without relying only on `HEARTBEAT_OK` parsing. (#75765) Thanks @pashpashpash.
- Gateway/config: allow `$include` directives to read files from operator-approved `OPENCLAW_INCLUDE_ROOTS` directories while preserving default config-directory confinement. Thanks @ificator.
### Fixes
- Agents/OpenAI: default GPT-5 API-key sessions to the SSE Responses transport unless WebSocket is explicitly selected, restoring replies in fresh Control UI and WebChat beta installs where the auto WebSocket path connected but produced no model events.
- Agents/sessions: preserve terminal lifecycle state when final run metadata persists from a stale in-memory snapshot, preventing sessions from staying stuck as running after completed or timed-out turns.
- Gateway/CLI/status: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting; add concrete service, config, listener-owner, and log collection next steps when gateway probes fail and Bonjour finds no local gateway; avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. Refs #49012. (#75944) Thanks @vincentkoc and @joshavant.
- Plugins/update/config: stop treating the non-plugin `auth` command root as a bundled plugin id, keep packaged upgrades and beta external plugin installs on stable runtime aliases and matching prerelease npm specs, detect tracked plugin install records whose package directories disappeared during `openclaw update`, reinstall them before normal plugin updates, fail the update if install records still point at missing disk payloads, and validate configured web-search providers plus statically suppressed model/provider pairs against the active plugin set at config load. Thanks @vincentkoc.
- Codex/app-server: resolve managed binaries from bundled `dist` chunks and from the `@openai/codex` package bin when installs do not provide a nearby `.bin/codex` shim, avoiding false missing-binary startup failures.
- Plugins/ClawHub: use the ClawHub artifact resolver response as the install decision before downloading, keeping legacy ZIP fallback and future ClawPack npm-pack installs on the same explicit resolver path. Thanks @vincentkoc.
- Plugins/ClawHub: keep bare plugin package specs on npm for the launch cutover and reserve ClawHub resolution for explicit `clawhub:` specs until ClawHub pack readiness is deployed. Thanks @vincentkoc.
- Plugins/source checkout: discover source-only plugins such as Codex from the `extensions/*` workspace while using npm package excludes as the packaged-core boundary, removing the stale core-bundle metadata path.
- Plugins/ClawHub: install ClawPack artifacts from the explicit npm-pack `.tgz` resolver path and persist artifact kind, npm integrity, shasum, and tarball metadata for update and diagnostics flows. Thanks @vincentkoc.
- Control UI: allow deployments to configure grouped chat message max-width with a validated `gateway.controlUi.chatMessageMaxWidth` setting instead of patching bundled CSS after upgrades. Fixes #67935. Thanks @xiew4589-lang.
- Control UI/Cron: ignore malformed persisted cron rows without valid payloads before they enter UI state and guard stale cron render paths, preventing blank Control UI sections after a bad cron snapshot. Fixes #55047 and #54439; supersedes #54550 and #54552.
- Control UI/sessions: bound the default Sessions tab query to recent activity and fewer rows, avoiding expensive full-history loads while keeping filters editable. Fixes #76050. (#76051) Thanks @Neomail2.
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
- Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses. Thanks @vincentkoc.
- Plugins/externalization: add official npm-first catalogs for externalized channel, provider, and generic plugins; install official external web-search plugins before saving provider config; repair missing configured, selected-search, and env-selected plugin installs from npm by default; keep official install docs, update examples, live Codex checks, diagnostics ClawHub packages, and persisted bundled-plugin relocation on default npm tags; keep Matrix and Mattermost bundled until their npm packages cut over; and keep ACPX, Google Chat, and LINE publishable plugin dist trees out of the core package while ClawHub pack files roll out. Thanks @vincentkoc.
- Plugins/ClawHub/source/registry: use the ClawHub artifact resolver response as the install decision before downloading, keep bare plugin package specs on npm for the launch cutover and reserve ClawHub resolution for explicit `clawhub:` specs until ClawHub pack readiness is deployed, discover source-only plugins such as Codex from `extensions/*`, install ClawPack artifacts from the explicit npm-pack `.tgz` resolver path, persist artifact kind, npm integrity, shasum, and tarball metadata for update/diagnostics flows, fall back to version metadata when the artifact resolver route is missing, keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, explain unavailable explicit ClawHub ClawPack artifact downloads with a temporary npm install hint, and hash manifest/package metadata when validating persisted plugin registries so fast same-size rewrites cannot leave stale plugin metadata trusted. Thanks @vincentkoc.
- Control UI: add validated `gateway.controlUi.chatMessageMaxWidth` instead of patched bundled CSS, ignore malformed persisted cron rows before they enter UI state, guard stale cron render paths, and bound the default Sessions tab query to recent activity and fewer rows while keeping filters editable. Fixes #67935, #55047, #54439, and #76050; supersedes #54550 and #54552. (#76051) Thanks @xiew4589-lang and @Neomail2.
- Gateway/channels: cap startup fanout at four channel/account handoffs and recover from Bonjour ciao self-probe races, reducing Windows startup stalls with many Telegram accounts. Fixes #75687.
- Gateway/sessions: keep `sessions.list` polling responsive on large session stores by reusing list-safe session cache/indexes and returning a lightweight compaction checkpoint preview instead of heavyweight summaries. Thanks @rolandrscheel.
- Control UI/Gateway: keep long-running dashboard WebSocket sessions alive with protocol pings and keep Stop available after reconnect or reload by recovering session-scoped active-run abort state. Fixes #70991. Thanks @alexandre-leng.
- Control UI/Gateway: keep long-running dashboard WebSocket sessions alive with protocol pings, keep Stop available after reconnect or reload by recovering session-scoped active-run abort state, contain standalone iOS PWA viewports with safe-area-aware document locking, use high-contrast text selection colors, and show inline feedback when local slash-command dispatch is unavailable or fails unexpectedly. Fixes #70991, #60850, and #52105; supersedes #60854. Thanks @alexandre-leng, @kvncrw, @Badschaff, @efe-arv, and @MooreQiao.
- CLI/update: treat inherited Gateway service markers as origin hints and only block package replacement when the managed Gateway is still live, so self-updates can stop the service and continue safely. (#75729) Thanks @hxy91819.
- Agents/failover: exempt run-level timeouts that fire during tool execution from model fallback, timeout-triggered compaction, and generic timeout payload synthesis, avoiding misleading "LLM request timed out" errors after the primary model has already responded. Fixes #52147. (#75873) Thanks @simonusa.
- Docker: copy Bun 1.3.13 from a digest-pinned image and keep CI on the same version. Fixes #74356. Thanks @fede-kamel and @sallyom.
- Agents/compaction: keep prior context on consecutive turns against z.ai-style providers (z.ai direct, openrouter z-ai/\*, in-house GLM gateways), avoiding accidental Pi state reset after successful turns. (#76056) Thanks @openperf.
- Doctor/plugins: run a one-time 2026.5.2 configured-plugin install repair based on `meta.lastTouchedVersion`, installing actively used downloadable OpenClaw plugins through the configured external source before marking the config touched for the release.
- Doctor/plugins: run a one-time 2026.5.2 configured-plugin install repair based on `meta.lastTouchedVersion`, update stale configured plugin manifests that still declare channels without `channelConfigs`, install actively used downloadable OpenClaw plugins through the configured external source, preserve unmanaged third-party plugin `node_modules`, and then mark the config touched for the release.
- Sessions/transcripts: use one `session.writeLock.acquireTimeoutMs` policy for session transcript lock acquisitions and raise the default wait to 60 seconds, avoiding user-visible lock timeouts during legitimate slow prep, cleanup, compaction, and mirror work. Fixes #75894. Thanks @shandutta.
- Control UI: contain the standalone iOS PWA viewport with safe-area-aware document locking, so Add-to-Home-Screen launches cannot scroll past the device bounds. Refs #76072. Thanks @kvncrw.
- Agents/restart recovery: match cleaned transcript locks by exact transcript lock paths plus the canonical session fallback, so interrupted main sessions using topic-suffixed transcripts resume after gateway restart. Refs #76052. Thanks @anyech.
- Agents/runtime: cache the stable system-prompt prefix and reuse prompt-report tool schema stats during dispatch prep, reducing repeated CPU work before streaming starts. Fixes #75999; supersedes #76061. Thanks @zackchiutw and @STLI69.
- Control UI/WebChat: use high-contrast text selection colors so highlighted chat text stays visible across themes. Fixes #60850; supersedes #60854. Thanks @Badschaff and @efe-arv.
- Telegram/native commands: pass persisted session files into plugin commands for topic-bound sessions, so `/codex bind` works from Telegram forum topics. Refs #75845 and #76049. Thanks @MatthewSchleder.
- Security audit/plugins: ignore plugin install backup, disabled, and dependency debris directories when enumerating installed plugin roots, avoiding false-positive findings for `.openclaw-install-backups` after plugin updates. Fixes #75456.
- Telegram: honor runtime conversation bindings for native slash commands in bound top-level groups, so commands like `/status@bot` route to the active non-`main` session instead of falling back to the default route. Fixes #75405; supersedes #75558. Thanks @ziptbm and @yfge.
@@ -118,53 +84,37 @@ Docs: https://docs.openclaw.ai
- Gateway/macOS: write LaunchAgent services with a canonical system PATH and stop preserving old plist PATH entries, so Volta, asdf, fnm, and pnpm shell paths no longer affect gateway child-process Node resolution. Fixes #75233; supersedes #75246. Thanks @nphyde2.
- Slack/hooks: preserve bot alert attachment text in message-received hook content when command text is blank. Fixes #76035; refs #76036. Thanks @amsminn.
- Sessions/agents: route Gateway session-store writes, CLI cleanup maintenance, and agent-delete session purges through a dedicated in-process writer and borrow the validated mutable cache during the writer slot, avoiding runtime file locks plus repeated `sessions.json` rereads and JSON clones on hot metadata updates. Refs #68554. Thanks @henkterharmsel.
- Control UI/chat: show inline feedback when local slash-command dispatch is unavailable or fails unexpectedly instead of clearing the composer silently. Fixes #52105. Thanks @MooreQiao.
- Memory/markdown: replace CRLF managed blocks in place and collapse duplicate marker blocks without rewriting unmanaged markdown, so Dreaming and Memory Wiki files self-heal from repeated generated sections. Fixes #75491; supersedes #75495, #75810, and #76008. Thanks @asaenokkostya-coder, @ottodeng, @everettjf, and @lrg913427-dot.
- Agents/tools: return critical tool-loop circuit-breaker stops as blocked tool results instead of thrown tool failures, so models see the guardrail and stop retrying the same call. Thanks @rayraiser.
- Agents/sessions: preserve pre-existing runtime model and context window after heartbeat turns so a per-run heartbeat model override does not bleed into shared-session status. Fixes #75452. Thanks @zhangguiping-xydt.
- Model commands: clarify direct and inline `/model` acknowledgements for non-default selections as session-scoped. Thanks @addu2612.
- Doctor/gateway: stop warning that non-existent, unconfigured user-bin directories are required in the Gateway service PATH. Fixes #76017. Thanks @xiphis.
- TUI/chat: skip full provider model normalization during context-window warmup while preserving provider-owned context metadata, avoiding cold-start stalls with large model registries. Thanks @547895019.
- TUI/setup: skip full provider model normalization during context-window warmup and bound Terminal hatch bootstrap provider requests, avoiding cold-start stalls with large model registries and first-run hatching stuck behind the watchdog. (#76241) Thanks @547895019 and @joshavant.
- Agents: enable malformed tool-call argument repair for Codex and Azure OpenAI Responses transports while keeping generic OpenAI Responses paths out of the repair gate. Fixes #75154. Thanks @Nimraakram22.
- Memory Wiki: accept relative Markdown links that include the `.md` suffix during broken-wikilink validation, avoiding false positives for native render-mode links. Thanks @Kenneth8128.
- OpenAI Codex: show the device-pairing code in the interactive SSH/headless prompt while keeping the short-lived code out of persistent runtime logs. Fixes #74212. Thanks @da22le123.
- QA Lab: stop gateway children when the suite parent disappears, so interrupted local QA runs cannot leave hot orphaned gateways behind.
- Codex/app-server: tolerate a second connection close during startup recovery and include retry counts plus stringified errors in the restart warning, so concurrent lanes do not fail after one shared-client race.
- Codex/app-server/plugins: tolerate second connection closes during startup recovery, include retry counts plus stringified restart errors, and allow the official npm Codex plugin to install without the unsafe-install override while keeping `/codex` command ownership and covering the real npm Docker live path through managed `.openclaw/npm` dependencies plus uninstall failure proof.
- Plugins/CLI: cache plugin CLI registration entries per command program so completion state generation does not repeat the full plugin sweep in one invocation. Thanks @ScientificProgrammer.
- Plugins: reuse gateway-bindable plugin loader cache entries for later default-mode loads without serving default-built registries to gateway-bound requests, reducing repeated plugin registration during dispatch. Refs #61756. Thanks @DmitryPogodaev.
- Gateway/secrets: include the caught error message in `secrets.reload` and `secrets.resolve` warning logs while keeping RPC errors generic, so operators can diagnose reload and permission failures. Thanks @davidangularme.
- Providers/OpenRouter: fill DeepSeek V4 `reasoning_content` replay placeholders for `openrouter/deepseek/deepseek-v4-flash` and `openrouter/deepseek/deepseek-v4-pro`, so thinking/tool follow-up turns do not fail with DeepSeek's replay-shape error. Fixes #76018. Thanks @cloph-dsp.
- Anthropic-compatible streams: recover text deltas that arrive before their matching content block, so Kimi Code and similar providers do not finish as empty `incomplete_result` replies. Fixes #76007. Thanks @vliuyt.
- Providers/OpenRouter/LM Studio/Anthropic: fill DeepSeek V4 `reasoning_content` replay placeholders for `openrouter/deepseek/deepseek-v4-flash` and `openrouter/deepseek/deepseek-v4-pro`, normalize binary LM Studio reasoning metadata from Gemma 4 and other local models, and recover Anthropic-compatible stream text deltas that arrive before their matching content block. Fixes #76018 and #76007. Thanks @cloph-dsp and @vliuyt.
- fix(infra): block workspace state-directory env override [AI]. (#75940) Thanks @pgondhi987.
- MCP/OpenAI: normalize parameter-free tool schemas whose top-level object `properties` is missing, null, or invalid before sending tools to OpenAI, so MCP tools without params stay usable. Fixes #75362. Thanks @tolkonepiu and @SymbolStar.
- TTS: honor explicit short `[[tts:text]]...[[/tts:text]]` blocks while keeping untagged short auto-TTS suppressed, so tagged voice replies are synthesized instead of being dropped as empty voice-only payloads. Fixes #73758. Thanks @yfge.
- MCP/OpenAI and media: normalize parameter-free MCP tool schemas before OpenAI tool submission, honor explicit short `[[tts:text]]...[[/tts:text]]` blocks while keeping untagged short auto-TTS suppressed, and accept home-relative `MEDIA:~/...` attachment paths under the existing file-read policy. Fixes #75362, #73758, and #73796. Thanks @tolkonepiu, @SymbolStar, @yfge, and @fabkury.
- Hooks/doctor: warn when `hooks.transformsDir` points outside the canonical hooks transform directory, so invalid workspace skill paths get a direct recovery hint before the Gateway crash-loops. Fixes #75853. Thanks @midobk.
- Proxy/audio: convert standard `FormData` bodies before proxy-backed undici fetches, so audio transcription and multipart uploads no longer send `[object FormData]` when `HTTP_PROXY` or `HTTPS_PROXY` is configured. Fixes #48554. Thanks @dco5.
- Discord: allow explicitly configured ack reactions in tool-only guild channels while keeping automatic lifecycle/status reactions suppressed. Fixes #74922. Thanks @samvilian and @BlueBirdBack.
- Discord: enable session-backed A2A announce target lookup so `sessions_send` uses the target session's `deliveryContext.accountId` or `lastAccountId` instead of falling back to the default bot in multi-account setups. Fixes #42652; refs #51626 and #44773; supersedes #73975. Thanks @irchelper, @dpalfox, and @Lanfei.
- Discord/setup: write resolved guild/channel allowlist selections to the selected guild and channel instead of falling back to the wildcard guild during setup. Supersedes #47788. Thanks @Eldersonar.
- Discord: treat abort-time Carbon reconnect-exhausted events as expected shutdown during stale-socket restarts, so health-monitor restarts no longer reject the monitor lifecycle. Carries forward #58216; supersedes #73949. Thanks @Perttulands.
- Discord/native commands: return an explicit warning when slash command dispatch or direct plugin execution produces no visible reply instead of a success-style completion ack. Fixes #58986; supersedes #62057. Thanks @jb510.
- Discord: keep typing indicators alive during long tool runs and auto-compaction while keepalive ticks continue, so active sessions do not appear stalled before the final reply. Thanks @Squirbie.
- Discord: preserve multipart Content-Type headers for attachment uploads across REST fetch paths, so generated images and other media no longer fail delivery with `CONTENT_TYPE_INVALID`. Thanks @FunJim.
- Discord: preserve attachment and sticker filenames when saving inbound media, so agents can see human-readable file names instead of only UUID-based paths. Fixes #59744. Thanks @xela92 and @rockcent.
- Discord: preserve non-ASCII channel names in session display labels while keeping allowlist matching on the existing ASCII slug contract. Thanks @swjeong9.
- Discord/PluralKit: canonicalize proxied webhook turns to the original Discord message id for inbound dedupe, while preserving the proxy message id for reply routing. Thanks @acgh213.
- Discord: only inject thread starter context on the first turn of the effective thread session, so follow-up thread replies do not repeat the starter block. Fixes #41355; supersedes #44447 and #44449. Thanks @p3nchan.
- Discord: resolve thread `ownerId` and `parentId` from Discord API-style snake_case payload fields, so bot-owned autoThreads do not require unnecessary mentions. Thanks @mgh3326.
- Discord/setup/startup/native commands: write resolved guild/channel allowlist selections to the selected guild and channel, persist slash-command deploy hashes across process restarts, treat abort-time Carbon reconnect-exhausted events as expected shutdown during stale-socket restarts, allow explicit ack reactions in tool-only guild channels, and warn when slash dispatch or direct plugin execution produces no visible reply. Fixes #74922 and #58986; carries forward #58216; supersedes #47788, #73949, and #62057. Thanks @samvilian, @BlueBirdBack, @Eldersonar, @Perttulands, and @jb510.
- Discord/delivery/media: use session-backed A2A announce target lookup for multi-account `sessions_send`, keep typing indicators alive during long tool runs and auto-compaction, preserve multipart Content-Type headers for uploads, preserve attachment and sticker filenames, and keep non-ASCII channel names in session labels while preserving ASCII-slug allowlists. Fixes #42652 and #59744; refs #51626 and #44773; supersedes #73975. Thanks @irchelper, @dpalfox, @Lanfei, @Squirbie, @FunJim, @xela92, @rockcent, and @swjeong9.
- Discord/threads/PluralKit: canonicalize proxied webhook turns to the original message id for dedupe, inject thread starter context only on the first effective thread turn, and resolve thread `ownerId`/`parentId` from Discord API-style snake_case payload fields so bot-owned autoThreads do not require unnecessary mentions. Fixes #41355; supersedes #44447 and #44449. Thanks @acgh213, @p3nchan, and @mgh3326.
- Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma.
- Gateway/pricing: defer optional model pricing catalog refresh until after sidecars and channels reach the ready path, so slow OpenRouter or LiteLLM pricing fetches cannot block Gateway readiness. Fixes #74128; supersedes #73486. Thanks @ctbritt and @alprclbi.
- Gateway/pricing: abort in-flight model pricing catalog fetches when Gateway shutdown stops the refresh loop, and avoid post-stop cache writes or refresh timers. Fixes #72208. Thanks @rzcq.
- Codex/app-server: make startup retry cleanup ownership-aware so concurrent Codex lanes cannot close another lane's freshly restarted shared app-server client. Thanks @vincentkoc.
- Google Meet/Twilio: report missing dial-in details during setup and explain that Twilio cannot join Meet URLs without a phone dial plan.
- Google Meet/Twilio: start the phone leg before sending Meet PIN DTMF, delay intro speech until after the post-connect dial sequence, and log each stage so operators can tell Twilio-leg audio from Meet-room audio.
- Voice Call: accept provider call IDs for gateway speak/continue requests and report ended-call state from history instead of returning a generic "Call not found" for stale calls.
- Google Meet/Twilio/Voice Call: report missing dial-in details during setup, explain that Twilio needs a phone dial plan for Meet URLs, start the phone leg before Meet PIN DTMF, delay intro speech until after post-connect dialing, log each stage, and accept provider call IDs for gateway speak/continue while reporting ended-call state from history.
- Control UI/Talk: allow the OpenAI Realtime WebRTC offer endpoint through the Control UI CSP, configure browser sessions with explicit VAD/transcription input settings, and surface OpenAI realtime error/lifecycle events instead of leaving Talk stuck as live with no diagnostic. Fixes #73427.
- Plugins: clarify config-selected duplicate plugin override diagnostics and document manifest schema updates for bundled-plugin forks. Fixes #8582. Thanks @sachah.
- CLI backends/Claude: make live-session JSONL turn caps bounded and configurable via `reliability.outputLimits`, raising the default guard for tool-heavy Claude CLI turns while preserving memory limits. Fixes #75838. Thanks @hcordoba840.
- Telegram/DMs: keep incidental `message_thread_id` reply-with-quote metadata on the flat DM session by default while preserving opt-in DM topic isolation for configured topics, `dm.threadReplies`, and `direct.<chatId>.threadReplies`. Fixes #75975. Thanks @ProjectEvolutionEVE.
- Telegram/network: raise outbound text and typing Bot API request guards to 60 seconds, keep low grammY client timeouts from preempting those guards, let higher `timeoutSeconds` configs extend safe method guards, and retry timed-out typing indicators through the transport fallback without risking duplicate messages. Fixes #76013. Thanks @iaki1206.
- Telegram/native commands: register and clear command menus in both default and group-chat scopes, so `/status` and plugin commands stay available in forum topics. Fixes #74032; updates #6457. Thanks @dae-sun and @WouldenShyp.
- Telegram/DMs/network/commands: keep incidental `message_thread_id` reply-with-quote metadata on flat DM sessions unless topic isolation is configured, raise outbound text and typing Bot API guards to 60 seconds with safe timeout overrides and typing fallback retries, and register/clear command menus in default and group-chat scopes so `/status` and plugin commands stay available in forum topics. Fixes #75975, #76013, and #74032; updates #6457. Thanks @ProjectEvolutionEVE, @iaki1206, @dae-sun, and @WouldenShyp.
- Providers/OpenAI: resolve `keychain:<service>:<account>` `OPENAI_API_KEY` refs before creating OpenAI Realtime browser sessions or voice bridges, with a bounded cached Keychain lookup. Fixes #72120. Thanks @ctbritt.
- Discord/gateway: reconnect when the gateway socket closes while waiting for the shared IDENTIFY concurrency window, instead of silently skipping IDENTIFY and leaving the bot online but unresponsive. Fixes #74617. Thanks @zeeskdr-ai.
- Voice Call: add `sessionScope: "per-call"` for fresh per-call agent memory while preserving the default per-phone caller history. Fixes #45280. Thanks @pondcountry.
@@ -172,12 +122,13 @@ Docs: https://docs.openclaw.ai
- Memory-core/dreaming: include the primary runtime workspace in multi-agent dreaming sweeps without mixing main-agent session transcripts into configured subagent workspaces. Fixes #70014. Thanks @ttomiczek.
- Control UI: add tab/RPC timing attribution and decouple slow Overview/Cron secondary refreshes so Sessions navigation gets immediate visible feedback. Refs #64004. Thanks @WaMaSeDu.
- Memory: retry transient SQLite index file swaps during atomic reindex on Windows, so brief `EBUSY`, `EPERM`, or `EACCES` locks do not fail memory rebuilds. Fixes #64187. Thanks @kunpeng-ai-lab.
- Telegram/startup: use the existing `getMe` request guard for the gateway bot probe instead of a fixed 2.5-second budget, and honor higher `timeoutSeconds` configs for slow Telegram API paths. Fixes #75783. Thanks @tankotan.
- Telegram/models: make model picker confirmations say selections are session-scoped and do not change the agent's persistent default. Fixes #75965. Thanks @sd1114820.
- Telegram/startup/models: use the existing `getMe` request guard and higher `timeoutSeconds` configs for slow Bot API paths, and make model picker confirmations say selections are session-scoped. Fixes #75783 and #75965. Thanks @tankotan and @sd1114820.
- Control UI/slash commands: keep fallback command metadata on a browser-safe registry path, so provider thinking runtime imports cannot blank the Web UI with `process is not defined`. Fixes #75987. Thanks @novkien.
- Heartbeat/Discord: keep async exec completion events out of the generic `System (untrusted)` prompt block and let the dedicated exec heartbeat prompt handle them, so Discord no longer receives raw exec failure tails as separate system-style messages. Fixes #66366. Thanks @Promee-ThaBossHoss.
- Heartbeat/scheduler: make heartbeat phase scheduling active-hours-aware so the scheduler seeks forward to the first in-window phase slot instead of arming timers for quiet-hours slots and relying solely on the runtime guard. Non-UTC `activeHours.timezone` values (e.g. `Asia/Shanghai`) now correctly influence when the next heartbeat timer fires, avoiding wasted quiet-hours ticks and long dormant gaps after gateway restarts. Fixes #75487. Thanks @amknight.
- Channels: strip plain-text MiniMax and XML tool-call scaffolding from shared user-facing reply sanitization, so messaging channels do not deliver raw model tool syntax when a provider emits it as text instead of structured tool calls. Fixes #62820. Thanks @canh0chua.
- Infer/media: report missing image-understanding and audio-transcription provider configuration for `image describe`, `image describe-many`, and `audio transcribe` instead of blaming the input path when no provider is available. Fixes #73569 and supersedes #73593, #74288, and #74495. Thanks @bittoby, @tmimmanuel, @Linux2010, and @vyctorbrzezowski.
- CLI/infer: reject local `codex/*` one-shot model probes before simple-completion dispatch and point operators at the Codex app-server runtime path instead of ending with an empty-output error.
- Docs/health: clarify that session listing surfaces stored conversation rows rather than Discord/channel socket liveness, and point connectivity checks at channel status and health probes. Fixes #70420. Thanks @ashersoutherncities-art and @martingarramon.
- WhatsApp/Cron: keep DM pairing-store approvals out of implicit cron and heartbeat recipient fallback, so scheduled automation only uses explicit targets, active configured recipients, or configured `allowFrom` entries. Fixes #62339. Thanks @kelvinisly-collab.
- Google Meet: keep the agent-facing `google_meet` tool visible on non-macOS hosts but block local Chrome realtime actions with guidance, so Linux agents can still use transcribe, Twilio, chrome-node, and artifact flows without choosing the macOS-only BlackHole path. Refs #75950. Thanks @actual-software-inc.
@@ -204,19 +155,13 @@ Docs: https://docs.openclaw.ai
- Discord/message actions: advertise `upload-file` and route it through Discord's send runtime with agent-scoped media reads, so agents can discover and send file attachments. Fixes #60652 and supersedes #60808, #61087, and #61100. Thanks @claw-io, @efe-arv, @joelnishanth, and @sjhddh.
- Sessions: suppress exact inter-session control replies such as `NO_REPLY` and keep agent-to-agent announce bookkeeping out of visible transcripts. Fixes #53145. Thanks @TarahAssistant.
- CLI/directory: report unsupported directory operations for installed channel plugins instead of prompting to reinstall the plugin when it lacks a directory adapter. Fixes #75770. Thanks @lawong888.
- Web search/SearXNG: show the JSON API `search.formats` prerequisite during SearXNG setup before prompting for the base URL. Supersedes #65592. Thanks @evanpaul14.
- Web search/SearXNG: pass through `img_src` image URLs from SearXNG image-category results. Supersedes #61416. Thanks @sghael.
- Web search/Kimi: fail explicitly when Moonshot returns an ungrounded chat answer instead of native web-search evidence, so Kimi no longer reports generic fallback text as a successful search. Fixes #52573. Thanks @wangwllu.
- Web search: keep public provider requests on the strict SSRF guard and reserve private-network access for explicit self-hosted SearXNG/Firecrawl endpoints. Fixes #74357 and supersedes #74360. Thanks @fede-kamel.
- Firecrawl: reject private, loopback, metadata, and non-HTTP(S) `firecrawl_scrape` target URLs before forwarding them to Firecrawl. Supersedes #48133. Thanks @kn1ghtc.
- Web search/Firecrawl: allow self-hosted private/internal Firecrawl `baseUrl` endpoints, including HTTP for private targets, while keeping hosted Firecrawl on the strict official endpoint. Fixes #63877 and supersedes #59666, #63941, and #74013. Thanks @jhthompson12, @jzakirov, @Mlightsnow, and @shad0wca7.
- Web search/SearXNG/Firecrawl/Kimi: show the SearXNG JSON API `search.formats` prerequisite, pass through `img_src` image URLs, fail explicitly when Kimi returns ungrounded answers, keep public provider requests on strict SSRF guards, reject private/loopback/metadata/non-HTTP(S) hosted Firecrawl scrape targets, and allow explicit self-hosted private Firecrawl endpoints. Fixes #52573, #74357, and #63877; supersedes #65592, #61416, #74360, #48133, #59666, #63941, and #74013. Thanks @evanpaul14, @sghael, @wangwllu, @fede-kamel, @kn1ghtc, @jhthompson12, @jzakirov, @Mlightsnow, and @shad0wca7.
- CLI/models: report gateway model fallback attempts in `infer model run --json` and avoid double-prefixing provider-qualified defaults such as `openrouter/auto` in `models status`. Partially fixes #69527. Thanks @alexifra.
- Providers/OpenRouter: strip trailing assistant prefill turns from verified OpenRouter Anthropic model requests when reasoning is enabled, so Claude 4.6 routes no longer fail with Anthropic's prefill rejection through the OpenAI-compatible adapter. Fixes #75395. Thanks @sbmilburn.
- Voice Call: add per-number inbound routing for dialed-number greetings, response agents/models/prompts, and TTS voice overrides. Fixes #56604. Thanks @healthstatus.
- Feishu: preserve Feishu/Lark HTTP error bodies for message sends, media sends, and chat member lookups, so HTTP 400 failures include vendor code, message, log id, and troubleshooter details. Fixes #73860. Thanks @desksk.
- Agents/transcripts: avoid reopening large Pi transcript files through the synchronous session manager for maintenance rewrites, persisted tool-result truncation, manual compaction boundary hardening, and queued compaction rotation. Thanks @mariozechner.
- Web search/Exa: accept `plugins.entries.exa.config.webSearch.baseUrl`, normalize it to the Exa `/search` endpoint, and partition cached results by endpoint. Fixes #54928 and supersedes #54939. Thanks @mrpl327 and @lyfuci.
- Web search/MiniMax: include MiniMax Search in the web-search setup flow and let `MINIMAX_API_KEY` participate in MiniMax Search auto-detection. Supersedes #65828. Thanks @Jah-yee.
- Web search/Exa/MiniMax: accept Exa `webSearch.baseUrl` overrides with endpoint-partitioned caches, include MiniMax Search in setup, and let `MINIMAX_API_KEY` participate in MiniMax Search auto-detection. Fixes #54928; supersedes #54939 and #65828. Thanks @mrpl327, @lyfuci, and @Jah-yee.
- Plugins/ClawHub: preserve official source-linked trust through archive installs, so OpenClaw can install trusted ClawHub plugin packages that trigger the built-in dangerous-pattern scanner. Thanks @vincentkoc.
- Plugins/ClawHub: install package runtime dependencies for archive-backed plugin installs, so ClawHub packages such as WhatsApp load declared dependencies after download. Thanks @vincentkoc.
- Plugins/tools: cache repeated plugin tool factory results only for matching request context, reducing per-turn tool prep without leaking sandbox, session, browser, delivery, or runtime config state. Fixes #75956. Thanks @Linux2010.
@@ -224,8 +169,7 @@ Docs: https://docs.openclaw.ai
- Agents/transcripts: keep chat history, restart recovery, fork token checks, and stale-token compaction checks on bounded async transcript reads or cached async indexes instead of reparsing large session files. Thanks @mariozechner.
- Telegram: inherit the process DNS result order for Bot API transport and downgrade recovered sticky IPv4 fallback promotions to debug logs, while keeping pinned-IP escalation warnings visible. Fixes #75904. Thanks @highfly-hi and @neeravmakwana.
- Sessions: keep durable external conversation pointers, including group and thread-scoped chat sessions, out of age, count, and disk-budget maintenance eviction while still allowing synthetic runtime entries to age out. Fixes #58088. Thanks @drinkflav.
- Web search/MiniMax: allow `MINIMAX_OAUTH_TOKEN` to satisfy MiniMax Search credentials, so OAuth-authorized MiniMax Token Plan setups do not need a separate web-search key. Fixes #65768. Thanks @kikibrian and @zhouhe-xydt.
- Providers/MiniMax: derive Coding Plan usage polling from the configured MiniMax base URL, so global setups no longer query the CN usage host. Fixes #65054. Thanks @sixone74 and @Yanhu007.
- Web search/Providers MiniMax: allow `MINIMAX_OAUTH_TOKEN` to satisfy MiniMax Search credentials and derive Coding Plan usage polling from the configured MiniMax base URL, so OAuth-authorized and global setups use the right endpoint. Fixes #65768 and #65054. Thanks @kikibrian, @zhouhe-xydt, @sixone74, and @Yanhu007.
- Control UI/WebChat: skip assistant-media transcript supplements when stale media refs resolve to no playable media, so text-only final replies are not stored a second time as gateway-injected assistant messages. Fixes #73956. Thanks @HemantSudarshan.
- Sessions: reject `sessions_send` targets that resolve to thread-scoped chat sessions, so inter-agent coordination cannot be injected into active human-facing Slack or Discord threads. Fixes #52496. Thanks @barry-p5cc.
- Subagents: honor `sessions_spawn` with `expectsCompletionMessage: false` by skipping parent completion handoff delivery while still running child cleanup. Fixes #75848. Thanks @alfredjbclaw.
@@ -264,33 +208,11 @@ Docs: https://docs.openclaw.ai
- macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc.
- Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129.
- Providers/configure: preserve the existing default model when adding or reauthing a provider whose plugin returns a default-model config patch. Fixes #50268. Thanks @rixcorp-oc.
- Slack/message actions: send media before the follow-up Block Kit message when Slack `send` includes a file plus presentation or interactive controls, so file attachments are no longer rejected. Fixes #51458. Thanks @HirokiKobayashi-R.
- Slack/DMs: honor `dmHistoryLimit` for fresh 1:1 Slack DM sessions by backfilling recent conversation history before the current reply. Fixes #64427. Thanks @brantley-creator.
- Slack/DMs: keep top-level direct messages on the stable DM session even when `replyToMode` targets Slack thread replies, preserving context across DM turns. Fixes #58832. Thanks @daye-jjeong.
- Slack/delivery: preserve Slack Web API missing-scope details in outbound delivery errors, so queued retry state identifies the OAuth scope to add. Fixes #62391. Thanks @alexey-pelykh.
- Slack/capabilities: read granted scopes from `auth.test` response metadata before trying legacy scope APIs, so modern bot tokens no longer report `unknown_method` for channel capabilities. Fixes #44625. Thanks @Qquanwei and @martingarramon.
- Slack/DMs: send text/block-only proactive DMs directly with `chat.postMessage(channel=<user id>)` while keeping conversation resolution for uploads and threaded sends. Fixes #62042. Thanks @MarkMolina.
- Slack/routing: match route bindings written with Slack target syntax such as `channel:C...`, `user:U...`, or `<@U...>`, so bound Slack peers route to the configured agent instead of `main`. Fixes #41608. Thanks @Winnsolutionsadmin.
- Slack/routing: match public-channel allowlist entries written as `channel:C...` against bare Slack runtime channel IDs, so allowed channel mentions do not fail as `channel-not-allowed`. Fixes #41264 and supersedes #56530. Thanks @babutree and @Realworld404.
- Slack/message actions: prefer the account bound to the outbound target peer before falling back to the agent's first channel account, so multi-workspace sends use the intended Slack account. Supersedes #66807. Thanks @rijhsinghani.
- Slack/delivery: retry Slack Web API writes only when the SDK wraps a DNS request failure such as `EAI_AGAIN`, so transient resolver hiccups can recover without retrying platform errors that may duplicate messages. Fixes #68789. Thanks @sonnyb9.
- Slack/message actions: forward agent-scoped media roots through the bundled upload-file action path, so workspace files can be attached without failing the local-media guard. Fixes #64625. Thanks @benpchandler.
- Slack/mentions: resolve `<!subteam^...>` user-group mentions through Slack `usergroups.users.list` and treat them as explicit mentions only when the bot user is a member, so mention-gated agent channels wake for real user-group mentions without config-only allowlists. Fixes #73827. Thanks @CG-Intelligence-Agent-Jack.
- Slack/message tool: let `read` fetch an exact Slack message timestamp, including a specific thread reply when paired with `threadId`, instead of returning only the parent thread or recent channel history. Fixes #53943. Thanks @zomars.
- Slack/DMs/routing: honor `dmHistoryLimit` for fresh 1:1 DMs, keep top-level DMs on stable DM sessions even when `replyToMode` targets thread replies, send text/block-only proactive DMs directly with `chat.postMessage(channel=<user id>)`, match Slack target route syntax such as `channel:C...`, `user:U...`, or `<@U...>`, and match public-channel allowlists against bare runtime channel IDs. Fixes #64427, #58832, #62042, #41608, and #41264; supersedes #56530. Thanks @brantley-creator, @daye-jjeong, @MarkMolina, @Winnsolutionsadmin, @babutree, and @Realworld404.
- Slack/delivery/capabilities: preserve missing-scope details in outbound errors, read granted scopes from `auth.test` metadata before legacy APIs, retry Slack writes only for wrapped DNS request failures such as `EAI_AGAIN`, and prefer the account bound to the outbound target peer in multi-workspace sends. Fixes #62391, #44625, and #68789; supersedes #66807. Thanks @alexey-pelykh, @Qquanwei, @martingarramon, @sonnyb9, and @rijhsinghani.
- Slack/message actions/tools: send media before follow-up Block Kit messages for file sends, forward agent-scoped media roots through the bundled upload-file path, resolve `<!subteam^...>` user-group mentions before waking mention-gated channels, and let `read` fetch an exact Slack message timestamp or thread reply. Fixes #51458, #64625, #73827, and #53943. Thanks @HirokiKobayashi-R, @benpchandler, @CG-Intelligence-Agent-Jack, and @zomars.
- PDF/Gemini: send native PDF analysis API keys in the `x-goog-api-key` header instead of the request URL, keeping secrets out of proxy and access logs. Supersedes #60600. Thanks @garagon.
- Web search/Gemini: route agent abort signals into provider fetches and log provider-side abort failures as normal tool errors instead of silently aborting the run. Fixes #72995. Thanks @RoseKongPS.
- Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97.
- Web search: late-bind managed agent `web_search` calls to the current runtime config snapshot, so existing sessions do not keep stale unresolved SecretRefs after secrets reload. Fixes #75420. Thanks @richardmqq.
- Web search/Gemini: reuse `models.providers.google.apiKey` and `models.providers.google.baseUrl` as lower-priority fallbacks for Gemini web search after dedicated search config and `GEMINI_API_KEY`. Supersedes #57496. Thanks @Aoiujz.
- Web search/Gemini: pass `freshness` and `date_after`/`date_before` filters through Google Search grounding time ranges. Fixes #66498. Thanks @ismael-81.
- Web search/DuckDuckGo: include the keyless DuckDuckGo provider in the web search setup wizard. Fixes #65862 and supersedes #65940. Thanks @Jah-yee.
- Web search: honor `baseUrl` overrides for Gemini, Grok, and x_search provider-owned config, so proxy-backed search tools no longer dial hardcoded public endpoints. Supersedes #61972. Thanks @Lanfei.
- Web search/Brave: point Brave provider metadata at the canonical `/tools/brave-search` docs page and make the legacy `/brave-search` docs page a redirect stub. Fixes #65870 and supersedes #65892. Thanks @Magicray1217 and @Jah-yee.
- Web search/Brave: allow `freshness` and bounded date ranges in `llm-context` mode, matching Brave's documented LLM Context API support. Supersedes #51005. Thanks @remusao.
- Web fetch: resolve external plugin `webFetchProviders` for non-sandboxed `web_fetch`, while keeping sandboxed fetches limited to bundled providers. Fixes #74915. Thanks @ultrahighsuper and @mingmingtsao.
- Heartbeat: strip legacy `[TOOL_CALL]...[/TOOL_CALL]` and `[TOOL_RESULT]...[/TOOL_RESULT]` pseudo-call blocks from heartbeat replies before channel delivery. Fixes #54138. Thanks @Deniable9570.
- macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc.
- Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129.
- Web search/Gemini/DuckDuckGo/Brave/fetch: route abort signals into Gemini provider fetches, late-bind managed agent `web_search` calls to the current runtime config snapshot, reuse Google provider API key/base URL as lower-priority Gemini search fallbacks, pass Gemini freshness/date filters through grounding, include DuckDuckGo in setup, honor Gemini/Grok/x_search `baseUrl` overrides, point Brave metadata at canonical docs, support Brave LLM Context freshness/date ranges, resolve external `webFetchProviders` for non-sandboxed fetches, and point missing-key errors to `web_fetch` or browser where appropriate. Fixes #72995, #75420, #66498, #65862, #65870, and #74915; supersedes #57496, #65940, #61972, #65892, and #51005. Thanks @RoseKongPS, @richardmqq, @Aoiujz, @ismael-81, @Jah-yee, @Lanfei, @Magicray1217, @remusao, @ultrahighsuper, @mingmingtsao, and @zhaoyang97.
- Slack/directory: make `openclaw directory peers/groups list --channel slack` prefer token-backed live readers and return the connected Slack account from `directory self`, so valid Slack tokens no longer produce empty directory CLI results. Fixes #50776. Thanks @pjaillon.
- Slack: keep assistant typing status, temporary typing reactions, and status reactions active for group/channel turns that use message-tool-only visible replies, while still suppressing automatic source replies. Fixes #75877. Thanks @teosborne.
- Slack: recover full inbound DM text from top-level rich-text blocks when Slack sends a shortened message preview, so long direct messages still reach the agent intact. Fixes #55358. Thanks @tonyjwinter.
@@ -341,44 +263,7 @@ Docs: https://docs.openclaw.ai
- Cron: retry recurring wake-now main-session jobs through temporary heartbeat busy skips before recording success, so queued cron events no longer appear as ok ghost runs while the main lane is still busy. Fixes #75964. (#76083) Thanks @kshetrajna12 and @xuruiray.
- Providers/Google: keep Gemini thinking-signature-only stream chunks active during reasoning, so Gemini 3.1 Pro Preview replies no longer hit idle timeouts before visible text. Fixes #76071. (#76080) Thanks @marcoschierhorn and @zhangguiping-xydt.
- CLI/skills: show per-agent model and command visibility in `openclaw skills check --agent`, and let doctor report or disable unavailable skills allowed for the default agent. (#75983) Thanks @mbelinky.
## 2026.4.30
### Changes
- Dependencies: refresh bundled runtime and plugin dependency pins, including Pi 0.71.1, OpenAI 6.35.0, Codex 0.128.0, Zod 4.4.1, and Matrix 41.4.0. Thanks @mariozechner.
- Agents/workspace: add `agents.defaults.skipOptionalBootstrapFiles` for skipping selected optional workspace files during bootstrap without disabling required workspace setup. (#62110) Thanks @mainstay22.
- Plugins/CLI: add first-class `git:` plugin installs with ref checkout, commit metadata, normal scanner/staging, and `plugins update` support for recorded git sources. Thanks @badlogic.
- Google Meet: add live caption health for Chrome transcribe mode, including caption observer state, transcript counters, last caption text, and recent transcript lines in status and doctor output. Refs #72478. Thanks @DougButdorf.
- Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP.
- macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti.
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.
- Discord: keep active buttons, selects, and forms working across Gateway restarts until they expire, so multi-step Discord interactions are less likely to break during upgrades or restarts. Thanks @amknight.
- Messages/docs: clarify that `BodyForAgent` is the primary inbound model text while `Body` is the legacy envelope fallback, and add Signal coverage so channel hardening patches target the real prompt path. Refs #66198. Thanks @defonota3box.
- Slack: publish a safe default App Home tab view on `app_home_opened` and include the Home tab event in setup manifests. Fixes #11655; refs #52020. Thanks @TinyTb.
- Slack: keep track of bot-participated threads across restarts, so ongoing threaded conversations can continue auto-replying after the Gateway is restarted. Thanks @amknight.
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=…`/`?token=…` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.
- CLI/proxy: add `openclaw proxy validate` so operators can verify effective proxy configuration, proxy reachability, and expected allow/deny destination behavior before deploying proxy-routed OpenClaw commands. (#73438) Thanks @jesse-merhi.
- Agents/Codex: default Codex app-server dynamic tools to native-first, keeping OpenClaw integration tools while leaving file, patch, exec, and process ownership to the Codex harness. (#75308) Thanks @pashpashpash.
- Agents/Codex: default Codex-harness direct source replies to the OpenClaw `message` tool when visible reply delivery is not explicitly configured, keeping channel-visible output as a deliberate tool call. (#75765) Thanks @pashpashpash.
- Heartbeats/agents: add a structured `heartbeat_respond` tool for tool-capable heartbeat runs so agents can record quiet outcomes or explicit notification text without relying only on `HEARTBEAT_OK` parsing. (#75765) Thanks @pashpashpash.
- Gateway/config: allow `$include` directives to read files from operator-approved `OPENCLAW_INCLUDE_ROOTS` directories while preserving default config-directory confinement. Thanks @ificator.
### Fixes
- Agents/tools: skip unavailable media generation and PDF tool factories from the live reply path when Gateway metadata and the active auth store prove no configured provider can back them, while keeping explicit config and auth-backed providers on the normal factory path. Thanks @shakkernerd.
- Agents/runtime: reuse the Gateway metadata startup plan when ensuring reply runtime plugins are loaded, so live agent turns do not broad-load plugin runtimes after the Gateway already scoped startup activation. Thanks @shakkernerd.
- Agents/runtime: delegate scoped reply runtime registry reuse to the plugin loader cache-key compatibility checks, so config changes with the same startup plugin ids cannot keep stale runtime hooks or tools active. Thanks @shakkernerd.
- Agents/runtime: let compatible wider plugin registries satisfy scoped reply runtime requests when they already contain the requested plugins, avoiding redundant runtime loading without bypassing loader cache-key freshness checks. Thanks @shakkernerd.
- Agents/runtime: validate agent model allowlists against manifest model catalog metadata during reply startup, avoiding broad provider runtime catalog loading before the agent run lane starts. Thanks @shakkernerd.
- Agents/runtime: keep allowlisted configured model thinking metadata available when manifest catalog rows are absent, so explicit high-reasoning levels remain valid for custom configured models. Thanks @shakkernerd.
- Agents/tools: preserve plugin-declared config-only generation providers such as local Comfy workflows during reply tool pre-gating, and share manifest auth/config availability checks between the planner and final tool factories. Thanks @shakkernerd.
- Agents/tools: keep Comfy generation tools visible from legacy local workflow config and cloud API-key config when no Gateway metadata snapshot is active, using plugin-declared manifest signals instead of loading provider runtimes. Thanks @shakkernerd.
- Agents/tools: route media and generation capability lookups through the Gateway plugin metadata snapshot during reply tool registration, avoiding repeated manifest registry reloads on the live reply path. Thanks @shakkernerd.
- Agents/tools: let plugins declare media generation auth aliases and base-url guards in manifests, preserving OpenAI Codex OAuth image generation availability without core-owned provider special cases. Thanks @shakkernerd.
- Agents/tools: reuse the auth profile store already loaded for the active run when deciding media and generation tool availability, avoiding repeated provider-auth runtime discovery during reply startup. Thanks @shakkernerd.
- Agents/tools: keep image, video, and music generation tool registration on manifest/auth control-plane checks instead of loading runtime provider registries during reply startup, reducing live-path tool-prep blocking while leaving provider runtime resolution for execution and list actions. Thanks @shakkernerd.
- Agents/runtime/tools: keep reply startup on Gateway metadata, manifest catalog rows, auth-store state, and plugin loader cache-key compatibility checks so scoped runtime registries, model allowlists, thinking metadata, media/PDF/generation tools, Comfy workflows, OpenAI Codex OAuth image generation, and image/video/music tool registration avoid broad provider/runtime loads while preserving explicit config and auth-backed providers. Thanks @shakkernerd.
- Discord: document canonical mention formatting in agent prompt hints and channel docs so outbound replies use `<@USER_ID>`, `<#CHANNEL_ID>`, and `<@&ROLE_ID>` instead of legacy nickname mentions. (#75173)
- Heartbeat scheduler: gate exec-event/notification/spawn/retry wakes through a centralized cooldown so backgrounded `process.start` exit notifications can no longer self-feed runaway heartbeat runs (configured `every: "30m"` was firing every ~10s in production, pegging the gateway event loop with `eventLoopDelayMaxMs >6s` spikes that stalled control-UI asset serving and TUI handshakes). Documented wake-now paths (`manual`, `wake`, task completion, blocked-task follow-up, `/hooks/wake mode=now`, and cron `--wake now`) remain immediate; retryable busy skips no longer poison the cooldown for the next retry; per-agent flood guard caps any unexpected feedback loop at 5 runs/60s. (#64016, refs #17797 and #75436) Thanks @hexsprite.
- fix: block workspace CLOUDSDK_PYTHON override and always set trusted interpreter for gcloud. (#74492) Thanks @pgondhi987.
@@ -397,20 +282,13 @@ Docs: https://docs.openclaw.ai
- Plugins/loader: scope plugin-tool registry reuse to the enabled plugin plan and stored Gateway method keys, so embedded runner tool lookup can reuse compatible startup registries without hiding enabled non-startup plugin tools. Fixes #75520. Thanks @whtoo.
- Voice Call/Twilio: send notify-mode initial TwiML directly in the outbound create-call request while keeping conversation and pre-connect DTMF calls webhook-driven, so one-shot notify calls do not depend on a first-answer webhook fetch. Supersedes #72758. Thanks @tyshepps.
- Discord/Slack: defer status-reaction cleanup until run finalization so queued, thinking, tool, and terminal reactions no longer flicker during normal progress updates. (#75582)
- Discord/voice: leave Discord voice off for text-only configs unless `channels.discord.voice` is explicitly configured, avoiding default `GuildVoiceStates` traffic and idle gateway CPU pressure for bots that do not use `/vc`. Fixes #73753; refs #74044. Thanks @sanchezm86 and @SecureCloudProjO.
- Discord/voice: rerun configured voice auto-join after Discord gateway RESUMED events and ignore already-destroyed stale voice connections during reconnect cleanup, so health-monitor account restarts can rejoin configured channels. Fixes #40665. Thanks @liz709.
- Discord/voice: leave voice off for text-only configs unless explicitly configured, rerun configured voice auto-join after gateway RESUMED events, ignore already-destroyed stale voice connections during reconnect cleanup, lengthen the default voice join Ready wait with configurable timeouts, merge configured media-understanding providers such as Deepgram into partial active registries, apply per-channel `systemPrompt` overrides to voice transcript turns, and run voice-channel turns under a voice-output policy that hides the agent `tts` tool. Fixes #73753, #40665, #63098, #65687, #47095, and #61536; refs #74044, #39825, and #65039. Thanks @sanchezm86, @SecureCloudProjO, @liz709, @darealgege, @kzicherman, @ayochim, @OneMintJulep, @qearlyao, and @aounakram.
- Plugins/CLI: reuse the cold manifest registry while building plugin status and inspect reports, so large configured plugin sets no longer rediscover the bundled/plugin registry once per inspect row. Thanks @vincentkoc.
- Discord/voice: lengthen the default voice join Ready wait, add configurable `voice.connectTimeoutMs`/`voice.reconnectGraceMs`, and warn before destroying unrecovered disconnected sessions so slow Discord voice handshakes and reconnects no longer fail silently. Fixes #63098; refs #39825 and #65039. Thanks @darealgege, @kzicherman, and @ayochim.
- Gateway/health: refresh cached health RPC snapshots when channel runtime state diverges, so Discord and other channel status reads no longer report stale running or connected values until the cache TTL expires. (#75423)
- Gateway/sessions: keep session-store reads from running stale prune and entry-count cap maintenance during startup, so oversized stores no longer block chat history readiness after updates while writes and `sessions cleanup --enforce` still preserve the cleanup safeguards. Fixes #70050. Thanks @tangda18.
- Security/audit: keep plain `security audit` on the cold config/filesystem path and reserve plugin runtime security collectors for `--deep`, so large plugin installs cannot execute every plugin runtime during routine audits. Thanks @vincentkoc.
- Discord/voice: merge configured media-understanding providers such as Deepgram into partial active provider registries, so follow-up voice turns keep transcribing after another media plugin is already active. Fixes #65687. Thanks @OneMintJulep.
- WhatsApp: stage `qrcode` through root mirrored runtime dependencies so packaged QR pairing can render from staged plugin-runtime-deps installs. Fixes #75394. Thanks @FelipeX2001.
- Discord/voice: apply per-channel Discord `systemPrompt` overrides to voice transcript turns by forwarding the trusted channel prompt through the voice agent run. Fixes #47095. Thanks @qearlyao.
- Discord/native commands: send component-only interaction replies from slash command and status handlers instead of treating renderable Discord components as an empty response. Thanks @vincentkoc.
- Slack/slash commands: send block-only slash command replies instead of dropping Slack block payloads with no plain-text fallback. Thanks @vincentkoc.
- Telegram/messages: derive fallback text from interactive button/select labels before sending button-only payloads, so Telegram replies are not rejected as empty messages. Thanks @vincentkoc.
- LINE/messages: send quick-reply-only payloads with fallback option text instead of accepting the payload and returning an empty delivery. Thanks @vincentkoc.
- Interactive channel payloads: send Discord component-only interaction replies, Slack block-only slash replies, Telegram button/select fallback labels, and LINE quick-reply fallback option text instead of accepting empty renderable payloads. Thanks @vincentkoc.
- Auto-reply/docking: require `/dock-*` route switches to start from direct chats, so group or channel participants cannot reroute a shared session's future replies into a linked DM. Thanks @vincentkoc.
- Discord: keep text-DM main-session route updates pinned to the configured DM owner, matching component interactions so another direct-message sender cannot redirect future main-session replies. Thanks @vincentkoc.
- Mattermost/Matrix: keep direct-message main-session route updates pinned to the configured DM owner so paired or temporarily allowed senders cannot redirect future shared-session replies. Thanks @vincentkoc.
@@ -420,7 +298,6 @@ Docs: https://docs.openclaw.ai
- CLI/update: verify managed gateway restarts against the installed service port instead of the caller shell port, so package updates do not report a healthy daemon as failed when profiles use different gateway ports. Thanks @vincentkoc.
- Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc.
- Setup/import: honor non-interactive `--import-from` onboarding flags by running the migration import path instead of silently completing normal setup without importing anything. Thanks @vincentkoc.
- Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram.
- Doctor/plugins: keep plain `doctor --non-interactive` from installing bundled plugin runtime dependencies, so headless health checks report missing deps while `doctor --fix` remains the explicit repair path. Thanks @vincentkoc.
- Doctor/gateway: require an interactive confirmation before installing or rewriting the Gateway service, so `doctor --fix --non-interactive` can repair plugin/config drift without replacing the operator's launchd/systemd service from a temporary environment. Thanks @vincentkoc.
- Plugins/runtime-deps: include packaged OpenClaw identity in bundled plugin loader cache keys, so same-path package upgrades stop reusing stale versioned runtime-deps mirrors. Fixes #75045. Thanks @sahilsatralkar.
@@ -433,7 +310,6 @@ Docs: https://docs.openclaw.ai
- MCP/stdio: settle MCP stdio transport send() from the write callback instead of resolving immediately on buffer acceptance, so async write errors reject the promise instead of being lost. Refs #75438.
- Process/exec: add stdin error listener in runCommandWithTimeout so EPIPE from a prematurely-exited child is swallowed instead of escaping to uncaughtException. Refs #75438.
- Voice Call/realtime: add default-off fast memory/session context for `openclaw_agent_consult`, giving live calls a bounded answer-or-miss path before the full agent consult. Fixes #71849. Thanks @amzzzzzzz.
- Google Meet: interrupt Realtime provider output when local barge-in clears playback, so command-pair audio stops model speech instead of only restarting Chrome playback. Fixes #73850. (#73834) Thanks @shhtheonlyperson.
- Gateway/config: cap oversized plugin-owned schemas in the full `config.schema` response so large installed plugin sets cannot balloon Gateway RSS or crash schema clients. Thanks @vincentkoc.
- Plugins/update: skip ClawHub and marketplace plugin updates when the bundled version is newer than the recorded installed version, so `openclaw update` no longer overwrites working bundled plugins with older external packages. Fixes #75447. Thanks @amknight.

View File

@@ -1,2 +1,2 @@
5f5db635a11240bee5e6eec95d13ff5ab388f3a5477c2f17fd762b5ed5b3dbae plugin-sdk-api-baseline.json
463c3bc12bf78ec6fc9350909fb3076967d276944da14343015f0dfae6ea48ed plugin-sdk-api-baseline.jsonl
29ede89cae1f9ff212cc491e67eea71d8af0fd1cfb85c345e85e6f106d0a6e4a plugin-sdk-api-baseline.json
2e3a828eccf54d9b436acad495869cde77cb22fc0588c25ffead09e86d510563 plugin-sdk-api-baseline.jsonl

View File

@@ -92,9 +92,9 @@ OpenClaw ships with the piai catalog. These providers require **no** `models.
- Example models: `openai/gpt-5.5`, `openai/gpt-5.4-mini`
- Verify account/model availability with `openclaw models list --provider openai` if a specific install or API key behaves differently.
- CLI: `openclaw onboard --auth-choice openai-api-key`
- Default transport is `auto` (WebSocket-first, SSE fallback)
- Default transport is `sse` for GPT-5 API-key models while the native WebSocket path is under investigation; set `transport: "auto"` or `"websocket"` explicitly to opt back in
- Override per model via `agents.defaults.models["openai/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
- OpenAI Responses WebSocket warm-up defaults to enabled via `params.openaiWsWarmup` (`true`/`false`)
- OpenAI Responses WebSocket warm-up is used only when WebSocket transport is selected and can be controlled via `params.openaiWsWarmup` (`true`/`false`)
- OpenAI priority processing can be enabled via `agents.defaults.models["openai/<model>"].params.serviceTier`
- `/fast` and `params.fastMode` map direct `openai/*` Responses requests to `service_tier=priority` on `api.openai.com`
- Use `params.serviceTier` when you want an explicit tier instead of the shared `/fast` toggle

View File

@@ -30,7 +30,6 @@ dependencies are available.
| Plugin | Description | Distribution | Surface |
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />included in OpenClaw | skills |
| [alibaba](/plugins/reference/alibaba) | Adds video generation provider support. | `@openclaw/alibaba-provider`<br />included in OpenClaw | contracts: videoGenerationProviders |
| [amazon-bedrock](/plugins/reference/amazon-bedrock) | Adds Amazon Bedrock model provider support to OpenClaw. | `@openclaw/amazon-bedrock-provider`<br />included in OpenClaw | providers: amazon-bedrock; contracts: memoryEmbeddingProviders |
| [amazon-bedrock-mantle](/plugins/reference/amazon-bedrock-mantle) | Adds Amazon Bedrock Mantle model provider support to OpenClaw. | `@openclaw/amazon-bedrock-mantle-provider`<br />included in OpenClaw | providers: amazon-bedrock-mantle |
@@ -59,7 +58,6 @@ dependencies are available.
| [fireworks](/plugins/reference/fireworks) | Adds Fireworks model provider support to OpenClaw. | `@openclaw/fireworks-provider`<br />included in OpenClaw | providers: fireworks |
| [github-copilot](/plugins/reference/github-copilot) | Adds GitHub Copilot model provider support to OpenClaw. | `@openclaw/github-copilot-provider`<br />included in OpenClaw | providers: github-copilot; contracts: memoryEmbeddingProviders |
| [google](/plugins/reference/google) | Adds Google, Google Gemini CLI, Google Vertex model provider support to OpenClaw. | `@openclaw/google-plugin`<br />included in OpenClaw | providers: google, google-gemini-cli, google-vertex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, musicGenerationProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders, webSearchProviders |
| [googlechat](/plugins/reference/googlechat) | Adds the Google Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/googlechat`<br />included in OpenClaw | channels: googlechat |
| [gradium](/plugins/reference/gradium) | Adds text-to-speech provider support. | `@openclaw/gradium-speech`<br />included in OpenClaw | contracts: speechProviders |
| [groq](/plugins/reference/groq) | Adds Groq model provider support to OpenClaw. | `@openclaw/groq-provider`<br />included in OpenClaw | providers: groq; contracts: mediaUnderstandingProviders |
| [huggingface](/plugins/reference/huggingface) | Adds Hugging Face model provider support to OpenClaw. | `@openclaw/huggingface-provider`<br />included in OpenClaw | providers: huggingface |
@@ -68,10 +66,11 @@ dependencies are available.
| [irc](/plugins/reference/irc) | Adds the IRC channel surface for sending and receiving OpenClaw messages. | `@openclaw/irc`<br />included in OpenClaw | channels: irc |
| [kilocode](/plugins/reference/kilocode) | Adds Kilocode model provider support to OpenClaw. | `@openclaw/kilocode-provider`<br />included in OpenClaw | providers: kilocode |
| [kimi](/plugins/reference/kimi) | Adds Kimi, Kimi Coding model provider support to OpenClaw. | `@openclaw/kimi-provider`<br />included in OpenClaw | providers: kimi, kimi-coding |
| [line](/plugins/reference/line) | Adds the LINE channel surface for sending and receiving OpenClaw messages. | `@openclaw/line`<br />included in OpenClaw | channels: line |
| [litellm](/plugins/reference/litellm) | Adds LiteLLM model provider support to OpenClaw. | `@openclaw/litellm-provider`<br />included in OpenClaw | providers: litellm; contracts: imageGenerationProviders |
| [llm-task](/plugins/reference/llm-task) | Generic JSON-only LLM tool for structured tasks callable from workflows. | `@openclaw/llm-task`<br />included in OpenClaw | contracts: tools |
| [lmstudio](/plugins/reference/lmstudio) | Adds LM Studio model provider support to OpenClaw. | `@openclaw/lmstudio-provider`<br />included in OpenClaw | providers: lmstudio; contracts: memoryEmbeddingProviders |
| [matrix](/plugins/reference/matrix) | Adds the Matrix channel surface for sending and receiving OpenClaw messages. | `@openclaw/matrix`<br />included in OpenClaw | channels: matrix |
| [mattermost](/plugins/reference/mattermost) | Adds the Mattermost channel surface for sending and receiving OpenClaw messages. | `@openclaw/mattermost`<br />included in OpenClaw | channels: mattermost |
| [memory-core](/plugins/reference/memory-core) | Adds memory embedding provider support. Adds agent-callable tools. | `@openclaw/memory-core`<br />included in OpenClaw | contracts: memoryEmbeddingProviders, tools |
| [memory-wiki](/plugins/reference/memory-wiki) | Persistent wiki compiler and Obsidian-friendly knowledge vault for OpenClaw. | `@openclaw/memory-wiki`<br />included in OpenClaw | contracts: tools; skills |
| [microsoft](/plugins/reference/microsoft) | Adds text-to-speech provider support. | `@openclaw/microsoft-speech`<br />included in OpenClaw | contracts: speechProviders |
@@ -123,6 +122,7 @@ dependencies are available.
| Plugin | Description | Distribution | Surface |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />ClawHub + npm | skills |
| [bluebubbles](/plugins/reference/bluebubbles) | Adds the BlueBubbles channel surface for sending and receiving OpenClaw messages. | `@openclaw/bluebubbles`<br />ClawHub + npm | channels: bluebubbles |
| [brave](/plugins/reference/brave) | Adds web search provider support. | `@openclaw/brave-plugin`<br />ClawHub + npm | contracts: webSearchProviders |
| [codex](/plugins/reference/codex) | Codex app-server harness and Codex-managed GPT model catalog. | `@openclaw/codex`<br />ClawHub + npm | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
@@ -132,9 +132,9 @@ dependencies are available.
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />ClawHub + npm | channels: discord |
| [feishu](/plugins/reference/feishu) | Adds the Feishu channel surface for sending and receiving OpenClaw messages. | `@openclaw/feishu`<br />ClawHub + npm | channels: feishu; contracts: tools; skills |
| [google-meet](/plugins/reference/google-meet) | Join Google Meet calls through Chrome or Twilio transports. | `@openclaw/google-meet`<br />ClawHub + npm | contracts: tools |
| [googlechat](/plugins/reference/googlechat) | Adds the Google Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/googlechat`<br />ClawHub + npm | channels: googlechat |
| [line](/plugins/reference/line) | Adds the LINE channel surface for sending and receiving OpenClaw messages. | `@openclaw/line`<br />ClawHub + npm | channels: line |
| [lobster](/plugins/reference/lobster) | Typed workflow tool with resumable approvals. | `@openclaw/lobster`<br />ClawHub + npm | contracts: tools |
| [matrix](/plugins/reference/matrix) | Adds the Matrix channel surface for sending and receiving OpenClaw messages. | `@openclaw/matrix`<br />ClawHub + npm | channels: matrix |
| [mattermost](/plugins/reference/mattermost) | Adds the Mattermost channel surface for sending and receiving OpenClaw messages. | `@openclaw/mattermost`<br />ClawHub + npm | channels: mattermost |
| [memory-lancedb](/plugins/reference/memory-lancedb) | Adds agent-callable tools. | `@openclaw/memory-lancedb`<br />ClawHub + npm | contracts: tools |
| [msteams](/plugins/reference/msteams) | Adds the Microsoft Teams channel surface for sending and receiving OpenClaw messages. | `@openclaw/msteams`<br />ClawHub + npm | channels: msteams |
| [nextcloud-talk](/plugins/reference/nextcloud-talk) | Adds the Nextcloud Talk channel surface for sending and receiving OpenClaw messages. | `@openclaw/nextcloud-talk`<br />ClawHub + npm | channels: nextcloud-talk |

View File

@@ -17,7 +17,7 @@ pnpm plugins:inventory:gen
| Plugin | Description | Distribution | Surface |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />included in OpenClaw | skills |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />ClawHub + npm | skills |
| [alibaba](/plugins/reference/alibaba) | Adds video generation provider support. | `@openclaw/alibaba-provider`<br />included in OpenClaw | contracts: videoGenerationProviders |
| [amazon-bedrock](/plugins/reference/amazon-bedrock) | Adds Amazon Bedrock model provider support to OpenClaw. | `@openclaw/amazon-bedrock-provider`<br />included in OpenClaw | providers: amazon-bedrock; contracts: memoryEmbeddingProviders |
| [amazon-bedrock-mantle](/plugins/reference/amazon-bedrock-mantle) | Adds Amazon Bedrock Mantle model provider support to OpenClaw. | `@openclaw/amazon-bedrock-mantle-provider`<br />included in OpenClaw | providers: amazon-bedrock-mantle |
@@ -55,7 +55,7 @@ pnpm plugins:inventory:gen
| [github-copilot](/plugins/reference/github-copilot) | Adds GitHub Copilot model provider support to OpenClaw. | `@openclaw/github-copilot-provider`<br />included in OpenClaw | providers: github-copilot; contracts: memoryEmbeddingProviders |
| [google](/plugins/reference/google) | Adds Google, Google Gemini CLI, Google Vertex model provider support to OpenClaw. | `@openclaw/google-plugin`<br />included in OpenClaw | providers: google, google-gemini-cli, google-vertex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, musicGenerationProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders, webSearchProviders |
| [google-meet](/plugins/reference/google-meet) | Join Google Meet calls through Chrome or Twilio transports. | `@openclaw/google-meet`<br />ClawHub + npm | contracts: tools |
| [googlechat](/plugins/reference/googlechat) | Adds the Google Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/googlechat`<br />included in OpenClaw | channels: googlechat |
| [googlechat](/plugins/reference/googlechat) | Adds the Google Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/googlechat`<br />ClawHub + npm | channels: googlechat |
| [gradium](/plugins/reference/gradium) | Adds text-to-speech provider support. | `@openclaw/gradium-speech`<br />included in OpenClaw | contracts: speechProviders |
| [groq](/plugins/reference/groq) | Adds Groq model provider support to OpenClaw. | `@openclaw/groq-provider`<br />included in OpenClaw | providers: groq; contracts: mediaUnderstandingProviders |
| [huggingface](/plugins/reference/huggingface) | Adds Hugging Face model provider support to OpenClaw. | `@openclaw/huggingface-provider`<br />included in OpenClaw | providers: huggingface |
@@ -64,13 +64,13 @@ pnpm plugins:inventory:gen
| [irc](/plugins/reference/irc) | Adds the IRC channel surface for sending and receiving OpenClaw messages. | `@openclaw/irc`<br />included in OpenClaw | channels: irc |
| [kilocode](/plugins/reference/kilocode) | Adds Kilocode model provider support to OpenClaw. | `@openclaw/kilocode-provider`<br />included in OpenClaw | providers: kilocode |
| [kimi](/plugins/reference/kimi) | Adds Kimi, Kimi Coding model provider support to OpenClaw. | `@openclaw/kimi-provider`<br />included in OpenClaw | providers: kimi, kimi-coding |
| [line](/plugins/reference/line) | Adds the LINE channel surface for sending and receiving OpenClaw messages. | `@openclaw/line`<br />included in OpenClaw | channels: line |
| [line](/plugins/reference/line) | Adds the LINE channel surface for sending and receiving OpenClaw messages. | `@openclaw/line`<br />ClawHub + npm | channels: line |
| [litellm](/plugins/reference/litellm) | Adds LiteLLM model provider support to OpenClaw. | `@openclaw/litellm-provider`<br />included in OpenClaw | providers: litellm; contracts: imageGenerationProviders |
| [llm-task](/plugins/reference/llm-task) | Generic JSON-only LLM tool for structured tasks callable from workflows. | `@openclaw/llm-task`<br />included in OpenClaw | contracts: tools |
| [lmstudio](/plugins/reference/lmstudio) | Adds LM Studio model provider support to OpenClaw. | `@openclaw/lmstudio-provider`<br />included in OpenClaw | providers: lmstudio; contracts: memoryEmbeddingProviders |
| [lobster](/plugins/reference/lobster) | Typed workflow tool with resumable approvals. | `@openclaw/lobster`<br />ClawHub + npm | contracts: tools |
| [matrix](/plugins/reference/matrix) | Adds the Matrix channel surface for sending and receiving OpenClaw messages. | `@openclaw/matrix`<br />ClawHub + npm | channels: matrix |
| [mattermost](/plugins/reference/mattermost) | Adds the Mattermost channel surface for sending and receiving OpenClaw messages. | `@openclaw/mattermost`<br />ClawHub + npm | channels: mattermost |
| [matrix](/plugins/reference/matrix) | Adds the Matrix channel surface for sending and receiving OpenClaw messages. | `@openclaw/matrix`<br />included in OpenClaw | channels: matrix |
| [mattermost](/plugins/reference/mattermost) | Adds the Mattermost channel surface for sending and receiving OpenClaw messages. | `@openclaw/mattermost`<br />included in OpenClaw | channels: mattermost |
| [memory-core](/plugins/reference/memory-core) | Adds memory embedding provider support. Adds agent-callable tools. | `@openclaw/memory-core`<br />included in OpenClaw | contracts: memoryEmbeddingProviders, tools |
| [memory-lancedb](/plugins/reference/memory-lancedb) | Adds agent-callable tools. | `@openclaw/memory-lancedb`<br />ClawHub + npm | contracts: tools |
| [memory-wiki](/plugins/reference/memory-wiki) | Persistent wiki compiler and Obsidian-friendly knowledge vault for OpenClaw. | `@openclaw/memory-wiki`<br />included in OpenClaw | contracts: tools; skills |

View File

@@ -12,7 +12,7 @@ Embedded ACP runtime backend with plugin-owned session and transport management.
## Distribution
- Package: `@openclaw/acpx`
- Install route: included in OpenClaw
- Install route: ClawHub + npm
## Surface

View File

@@ -12,7 +12,7 @@ Adds the Google Chat channel surface for sending and receiving OpenClaw messages
## Distribution
- Package: `@openclaw/googlechat`
- Install route: included in OpenClaw
- Install route: ClawHub + npm
## Surface

View File

@@ -12,7 +12,7 @@ Adds the LINE channel surface for sending and receiving OpenClaw messages.
## Distribution
- Package: `@openclaw/line`
- Install route: included in OpenClaw
- Install route: ClawHub + npm
## Surface

View File

@@ -12,7 +12,7 @@ Adds the Matrix channel surface for sending and receiving OpenClaw messages.
## Distribution
- Package: `@openclaw/matrix`
- Install route: ClawHub + npm
- Install route: included in OpenClaw
## Surface

View File

@@ -12,7 +12,7 @@ Adds the Mattermost channel surface for sending and receiving OpenClaw messages.
## Distribution
- Package: `@openclaw/mattermost`
- Install route: ClawHub + npm
- Install route: included in OpenClaw
## Surface

View File

@@ -61,7 +61,8 @@
"install": {
"npmSpec": "@openclaw/discord",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
"minHostVersion": ">=2026.4.10",
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.2"

View File

@@ -1,5 +1,23 @@
# Changelog
## 2026.5.2-beta.3
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.5.2-beta.2
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.5.2-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.5.2
### Changes

View File

@@ -79,20 +79,7 @@
}
},
"install": {
"npmSpec": "@openclaw/matrix",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10",
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.2"
},
"build": {
"openclawVersion": "2026.5.2"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
"minHostVersion": ">=2026.4.10"
}
}
}

View File

@@ -37,19 +37,7 @@
"order": 65
},
"install": {
"npmSpec": "@openclaw/mattermost",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.2"
},
"build": {
"openclawVersion": "2026.5.2"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -2,15 +2,16 @@ import { describe, expect, it } from "vitest";
import { buildQaImageGenerationConfigPatch } from "./image-generation.js";
describe("QA provider image generation config", () => {
it("uses the selected mock provider for mock-openai image generation", () => {
it("routes mock-openai image generation through the OpenAI image provider", () => {
const patch = buildQaImageGenerationConfigPatch({
providerMode: "mock-openai",
providerBaseUrl: "http://127.0.0.1:44080/v1",
requiredPluginIds: ["qa-channel"],
});
expect(patch.plugins.allow).toEqual(["acpx", "memory-core", "qa-channel"]);
expect(patch.agents.defaults.imageGenerationModel.primary).toBe("mock-openai/gpt-image-1");
expect(patch.plugins.allow).toEqual(["acpx", "memory-core", "openai", "qa-channel"]);
expect(patch.plugins.entries).toEqual({ openai: { enabled: true } });
expect(patch.agents.defaults.imageGenerationModel.primary).toBe("openai/gpt-image-1");
expect(patch.models?.providers["mock-openai"]?.baseUrl).toBe("http://127.0.0.1:44080/v1");
});
@@ -30,14 +31,16 @@ describe("QA provider image generation config", () => {
"qa-channel",
]);
});
it("uses the selected mock provider for AIMock image generation", () => {
it("routes AIMock image generation through the OpenAI image provider", () => {
const patch = buildQaImageGenerationConfigPatch({
providerMode: "aimock",
providerBaseUrl: "http://127.0.0.1:45080/v1",
requiredPluginIds: [],
});
expect(patch.agents.defaults.imageGenerationModel.primary).toBe("aimock/gpt-image-1");
expect(patch.plugins.allow).toEqual(["acpx", "memory-core", "openai"]);
expect(patch.plugins.entries).toEqual({ openai: { enabled: true } });
expect(patch.agents.defaults.imageGenerationModel.primary).toBe("openai/gpt-image-1");
expect(patch.models?.providers.aimock?.baseUrl).toBe("http://127.0.0.1:45080/v1");
expect(patch.models?.providers["mock-openai"]).toBeUndefined();
});

View File

@@ -42,7 +42,7 @@ export function buildQaImageGenerationConfigPatch(input: QaImageGenerationPatchI
providerBaseUrl: input.providerBaseUrl,
});
})();
const providerPluginIds = provider.usesModelProviderPlugins ? [imageProviderId] : [];
const providerPluginIds = imageProviderId ? [imageProviderId] : [];
const enabledPluginIds = uniqueNonEmpty(providerPluginIds);
return {

View File

@@ -25,8 +25,9 @@ export function createMockQaProviderDefinition(
serverLabel: params.serverLabel,
},
defaultModel: (options) => mockModelRef(params.mode, options?.alternate),
defaultImageGenerationProviderIds: [],
defaultImageGenerationModel: () => `${params.mode}/gpt-image-1`,
defaultImageGenerationProviderIds: ["openai"],
defaultImageGenerationModel: ({ modelProviderIds }) =>
modelProviderIds.includes("openai") ? "openai/gpt-image-1" : null,
usesFastModeByDefault: () => false,
resolveModelParams: () => ({
transport: "sse",

View File

@@ -138,9 +138,7 @@ describe("buildQaGatewayConfig", () => {
});
expect(getPrimaryModel(cfg.agents?.defaults?.model)).toBe("aimock/gpt-5.5");
expect(cfg.agents?.defaults?.imageGenerationModel).toEqual({
primary: "aimock/gpt-image-1",
});
expect(cfg.agents?.defaults?.imageGenerationModel).toBeUndefined();
expect(cfg.models?.providers?.aimock?.baseUrl).toBe("http://127.0.0.1:45080/v1");
expect(cfg.models?.providers?.aimock?.api).toBe("openai-responses");
expect(cfg.models?.providers?.openai?.baseUrl).toBe("http://127.0.0.1:45080/v1");

View File

@@ -34,7 +34,6 @@
"!dist/extensions/acpx/**",
"!dist/extensions/node_modules/**",
"!dist/extensions/*/node_modules/**",
"!dist/extensions/acpx/**",
"!dist/extensions/bluebubbles/**",
"!dist/extensions/brave/**",
"!dist/extensions/codex/**",
@@ -47,8 +46,6 @@
"!dist/extensions/googlechat/**",
"!dist/extensions/line/**",
"!dist/extensions/lobster/**",
"!dist/extensions/matrix/**",
"!dist/extensions/mattermost/**",
"!dist/extensions/memory-lancedb/**",
"!dist/extensions/msteams/**",
"!dist/extensions/nextcloud-talk/**",

View File

@@ -7,16 +7,18 @@ IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-kitchen-sink-plugin-e2e" OPENCL
docker_e2e_build_or_reuse "$IMAGE_NAME" kitchen-sink-plugin
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 kitchen-sink-plugin empty)"
KITCHEN_SINK_NPM_SPEC="${OPENCLAW_KITCHEN_SINK_NPM_SPEC:-npm:@openclaw/kitchen-sink@0.1.5}"
KITCHEN_SINK_NPM_MISSING_SPEC="${OPENCLAW_KITCHEN_SINK_NPM_MISSING_SPEC:-npm:@openclaw/kitchen-sink@beta}"
DEFAULT_KITCHEN_SINK_SCENARIOS="$(
cat <<'SCENARIOS'
npm-latest-full|npm:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|npm|success|full
npm-latest-conformance|npm:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|npm|success|conformance|conformance
npm-latest-adversarial|npm:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|npm|success|adversarial|adversarial
npm-beta|npm:@openclaw/kitchen-sink@beta|openclaw-kitchen-sink-fixture|npm|failure|none
cat <<SCENARIOS
npm-pinned-full|${KITCHEN_SINK_NPM_SPEC}|openclaw-kitchen-sink-fixture|npm|success|full
npm-pinned-conformance|${KITCHEN_SINK_NPM_SPEC}|openclaw-kitchen-sink-fixture|npm|success|conformance|conformance
npm-pinned-adversarial|${KITCHEN_SINK_NPM_SPEC}|openclaw-kitchen-sink-fixture|npm|success|adversarial|adversarial
npm-beta|${KITCHEN_SINK_NPM_MISSING_SPEC}|openclaw-kitchen-sink-fixture|npm|failure|none
clawhub-latest|clawhub:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|clawhub|success|basic
clawhub-beta|clawhub:@openclaw/kitchen-sink@beta|openclaw-kitchen-sink-fixture|clawhub|failure|none
npm-to-clawhub|clawhub:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|clawhub|success|basic||npm:@openclaw/kitchen-sink@latest
npm-to-clawhub|clawhub:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|clawhub|success|basic||${KITCHEN_SINK_NPM_SPEC}
SCENARIOS
)"
KITCHEN_SINK_SCENARIOS="${OPENCLAW_KITCHEN_SINK_PLUGIN_SCENARIOS:-$DEFAULT_KITCHEN_SINK_SCENARIOS}"
@@ -36,18 +38,20 @@ DOCKER_ENV_ARGS=(
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64"
-e "KITCHEN_SINK_SCENARIOS=$KITCHEN_SINK_SCENARIOS"
)
for env_name in \
OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB \
OPENCLAW_CLAWHUB_URL \
CLAWHUB_URL \
OPENCLAW_CLAWHUB_TOKEN \
CLAWHUB_TOKEN \
CLAWHUB_AUTH_TOKEN; do
env_value="${!env_name:-}"
if [[ -n "$env_value" && "$env_value" != "undefined" && "$env_value" != "null" ]]; then
DOCKER_ENV_ARGS+=(-e "$env_name")
fi
done
if [[ "${OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB:-0}" = "1" ]]; then
for env_name in \
OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB \
OPENCLAW_CLAWHUB_URL \
CLAWHUB_URL \
OPENCLAW_CLAWHUB_TOKEN \
CLAWHUB_TOKEN \
CLAWHUB_AUTH_TOKEN; do
env_value="${!env_name:-}"
if [[ -n "$env_value" && "$env_value" != "undefined" && "$env_value" != "null" ]]; then
DOCKER_ENV_ARGS+=(-e "$env_name")
fi
done
fi
echo "Running kitchen-sink plugin Docker E2E..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true

View File

@@ -87,20 +87,11 @@ function readConfig() {
function configureRuntime() {
const pluginId = process.env.KITCHEN_SINK_ID;
const personality = process.env.KITCHEN_SINK_PERSONALITY;
const { configPath, config } = readConfig();
config.plugins = config.plugins || {};
config.plugins.entries = config.plugins.entries || {};
config.plugins.entries[pluginId] = {
...config.plugins.entries[pluginId],
...(personality
? {
config: {
...config.plugins.entries[pluginId]?.config,
personality,
},
}
: {}),
hooks: {
...config.plugins.entries[pluginId]?.hooks,
allowConversationAccess: true,

View File

@@ -112,11 +112,17 @@ run_failure_scenario() {
assert_kitchen_sink_removed
}
if [[ "$KITCHEN_SINK_SCENARIOS" == *"clawhub:"* ]] &&
[[ "${OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB:-0}" != "1" ]] &&
[[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]; then
clawhub_fixture_dir="$(mktemp -d "/tmp/openclaw-kitchen-sink-clawhub.XXXXXX")"
start_kitchen_sink_clawhub_fixture_server "$clawhub_fixture_dir"
if [[ "$KITCHEN_SINK_SCENARIOS" == *"clawhub:"* ]]; then
if [[ "${OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB:-0}" = "1" ]]; then
export OPENCLAW_CLAWHUB_URL="${OPENCLAW_CLAWHUB_URL:-${CLAWHUB_URL:-https://clawhub.ai}}"
else
if [[ -n "${OPENCLAW_CLAWHUB_URL:-}" || -n "${CLAWHUB_URL:-}" ]]; then
echo "Ignoring ambient ClawHub URL for fixture-mode kitchen-sink E2E; set OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB=1 for live ClawHub."
fi
unset OPENCLAW_CLAWHUB_URL CLAWHUB_URL
clawhub_fixture_dir="$(mktemp -d "/tmp/openclaw-kitchen-sink-clawhub.XXXXXX")"
start_kitchen_sink_clawhub_fixture_server "$clawhub_fixture_dir"
fi
fi
scenario_count=0

View File

@@ -38,8 +38,12 @@ run_plugins_clawhub_scenario() {
if [[ "${OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB:-0}" = "1" ]]; then
export OPENCLAW_CLAWHUB_URL="${OPENCLAW_CLAWHUB_URL:-${CLAWHUB_URL:-https://clawhub.ai}}"
export NPM_CONFIG_REGISTRY="${OPENCLAW_PLUGINS_E2E_LIVE_NPM_REGISTRY:-https://registry.npmjs.org/}"
elif [[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]; then
else
# Keep the release-path smoke hermetic; live ClawHub can rate-limit CI.
if [[ -n "${OPENCLAW_CLAWHUB_URL:-}" || -n "${CLAWHUB_URL:-}" ]]; then
echo "Ignoring ambient ClawHub URL for fixture-mode plugin E2E; set OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB=1 for live ClawHub."
fi
unset OPENCLAW_CLAWHUB_URL CLAWHUB_URL
clawhub_fixture_dir="$(mktemp -d "/tmp/openclaw-clawhub-fixture.XXXXXX")"
start_clawhub_fixture_server "$clawhub_fixture_dir"
fi

View File

@@ -336,10 +336,9 @@ assert_configured_plugin_installs_clawhub_attempted() {
return 0
fi
local requests_file="$ARTIFACT_ROOT/clawhub-not-found-requests.jsonl"
if ! grep -q '/api/v1/packages/%40openclaw%2Fmatrix' "$requests_file" 2>/dev/null; then
echo "configured plugin install scenario did not attempt ClawHub for @openclaw/matrix" >&2
cat "$requests_file" >&2 2>/dev/null || true
return 1
# The install catalog may prefer npm; assertions.mjs validates the installed source.
if grep -q '/api/v1/packages/%40openclaw%2Fmatrix' "$requests_file" 2>/dev/null; then
echo "configured plugin install scenario attempted ClawHub for @openclaw/matrix"
fi
}

View File

@@ -105,7 +105,12 @@ openclaw_e2e_install_package /tmp/openclaw-install.log
command -v openclaw >/dev/null
package_root="$(openclaw_e2e_package_root)"
openclaw_e2e_assert_package_extensions "$package_root" telegram discord
if [ -d "$package_root/dist/extensions/$CHANNEL" ]; then
CHANNEL_PACKAGE_MODE="bundled"
else
CHANNEL_PACKAGE_MODE="external"
echo "$CHANNEL is not packaged with core OpenClaw; expecting channel selection to install it on demand."
fi
mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" /tmp/openclaw-mock-openai.log)"
openclaw_e2e_wait_mock_openai "$MOCK_PORT"
@@ -134,7 +139,11 @@ node scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs assert-channel-con
echo "Running doctor after channel activation..."
openclaw doctor --repair --non-interactive >/tmp/openclaw-doctor.log 2>&1
openclaw_e2e_assert_dep_absent "$DEP_SENTINEL" "$HOME/.openclaw"
if [ "$CHANNEL_PACKAGE_MODE" = "external" ]; then
openclaw_e2e_assert_dep_present "$DEP_SENTINEL" "$HOME/.openclaw"
else
openclaw_e2e_assert_dep_absent "$DEP_SENTINEL" "$HOME/.openclaw"
fi
echo "Running local agent turn against mocked OpenAI..."
openclaw agent --local \

View File

@@ -909,8 +909,6 @@ for ($attempt = 1; $attempt -le 2; $attempt++) {
'main',
'--session-id',
$sessionId,
'--model',
${psSingleQuote(this.auth.modelId)},
'--message',
'Reply with exact ASCII text OK only.',
'--thinking',

View File

@@ -16,17 +16,25 @@ for env_name in \
OPENCLAW_PLUGINS_E2E_CLAWHUB \
OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB \
OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC \
OPENCLAW_PLUGINS_E2E_CLAWHUB_ID \
OPENCLAW_CLAWHUB_URL \
CLAWHUB_URL \
OPENCLAW_CLAWHUB_TOKEN \
CLAWHUB_TOKEN \
CLAWHUB_AUTH_TOKEN; do
OPENCLAW_PLUGINS_E2E_CLAWHUB_ID; do
env_value="${!env_name:-}"
if [[ -n "$env_value" && "$env_value" != "undefined" && "$env_value" != "null" ]]; then
DOCKER_ENV_ARGS+=(-e "$env_name")
fi
done
if [[ "${OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB:-0}" = "1" ]]; then
for env_name in \
OPENCLAW_CLAWHUB_URL \
CLAWHUB_URL \
OPENCLAW_CLAWHUB_TOKEN \
CLAWHUB_TOKEN \
CLAWHUB_AUTH_TOKEN; do
env_value="${!env_name:-}"
if [[ -n "$env_value" && "$env_value" != "undefined" && "$env_value" != "null" ]]; then
DOCKER_ENV_ARGS+=(-e "$env_name")
fi
done
fi
echo "Running plugins Docker E2E..."
docker_e2e_run_logged_with_harness plugins-run "${DOCKER_ENV_ARGS[@]}" "$IMAGE_NAME" bash scripts/e2e/lib/plugins/sweep.sh

View File

@@ -170,6 +170,11 @@ export const mainLanes = [
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
{ resources: ["service"], stateScenario: "empty", weight: 3 },
),
npmLane(
"npm-onboard-discord-channel-agent",
"OPENCLAW_NPM_ONBOARD_CHANNEL=discord OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
{ resources: ["service"], stateScenario: "empty", weight: 3 },
),
serviceLane("gateway-network", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:gateway-network"),
serviceLane(
"agents-delete-shared-workspace",
@@ -475,6 +480,11 @@ const releasePathPackageUpdateCoreLanes = [
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
{ resources: ["service"], stateScenario: "empty", weight: 3 },
),
npmLane(
"npm-onboard-discord-channel-agent",
"OPENCLAW_NPM_ONBOARD_CHANNEL=discord OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
{ resources: ["service"], stateScenario: "empty", weight: 3 },
),
npmLane("doctor-switch", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:doctor-switch", {
stateScenario: "empty",
weight: 3,

View File

@@ -95,7 +95,8 @@
"install": {
"npmSpec": "@openclaw/discord",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
"minHostVersion": ">=2026.4.10",
"allowInvalidConfigRecovery": true
}
}
},
@@ -198,52 +199,6 @@
}
}
},
{
"name": "@openclaw/matrix",
"description": "OpenClaw Matrix channel plugin",
"source": "official",
"kind": "channel",
"openclaw": {
"channel": {
"id": "matrix",
"label": "Matrix",
"selectionLabel": "Matrix (plugin)",
"docsPath": "/channels/matrix",
"docsLabel": "matrix",
"blurb": "open protocol; install the plugin to enable.",
"order": 70,
"quickstartAllowFrom": true
},
"install": {
"npmSpec": "@openclaw/matrix",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10",
"allowInvalidConfigRecovery": true
}
}
},
{
"name": "@openclaw/mattermost",
"description": "OpenClaw Mattermost channel plugin",
"source": "official",
"kind": "channel",
"openclaw": {
"channel": {
"id": "mattermost",
"label": "Mattermost",
"selectionLabel": "Mattermost (plugin)",
"docsPath": "/channels/mattermost",
"docsLabel": "mattermost",
"blurb": "self-hosted Slack-style chat; install the plugin to enable.",
"order": 65
},
"install": {
"npmSpec": "@openclaw/mattermost",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
}
}
},
{
"name": "@openclaw/msteams",
"description": "OpenClaw Microsoft Teams channel plugin",

View File

@@ -3,7 +3,6 @@ const optionalBundledClusters = [
"diagnostics-otel",
"diffs",
"googlechat",
"matrix",
"memory-lancedb",
"msteams",
"nostr",

View File

@@ -23,6 +23,16 @@ const pluginPrereleaseDockerLanes = Object.freeze([
lane: "npm-onboard-channel-agent",
surfaces: ["package-artifact", "gateway-bootstrap", "status-diagnostics"],
},
{
lane: "npm-onboard-discord-channel-agent",
surfaces: [
"package-artifact",
"external-plugins",
"installed-plugin-deps",
"gateway-bootstrap",
"status-diagnostics",
],
},
{
lane: "doctor-switch",
surfaces: ["package-artifact", "doctor-fix"],

View File

@@ -50,9 +50,19 @@ const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = BUNDLED_RUNTIME_SIDECAR_PATHS.fi
);
const NODE_BUILTIN_MODULES = new Set(builtinModules.map((name) => name.replace(/^node:/u, "")));
const MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES = 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_BYTES = 2 * 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_BYTES = 4 * 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_FILES = 5000;
const ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE = /\.(?:c|m)?js$/u;
const OPTIONAL_OR_EXTERNALIZED_RUNTIME_IMPORTS = new Set([
"@discordjs/opus",
"@lancedb/lancedb",
"@matrix-org/matrix-sdk-crypto-nodejs",
"link-preview-js",
"matrix-js-sdk",
// Discord voice decoder fallback. The root chunk catches missing decoders and the owning
// Discord plugin remains externalized from the root package.
"opusscript",
]);
const require = createRequire(import.meta.url);
const acorn = require("acorn") as typeof import("acorn");
@@ -102,7 +112,7 @@ export function collectInstalledPackageErrors(params: {
);
}
for (const relativePath of PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS) {
for (const relativePath of collectInstalledBundledRuntimeSidecarPaths(params.packageRoot)) {
if (!existsSync(join(params.packageRoot, relativePath))) {
errors.push(`installed package is missing required bundled runtime sidecar: ${relativePath}`);
}
@@ -114,6 +124,31 @@ export function collectInstalledPackageErrors(params: {
return errors;
}
function collectInstalledBundledExtensionIds(packageRoot: string): Set<string> {
const extensionsDir = join(packageRoot, "dist", "extensions");
if (!existsSync(extensionsDir)) {
return new Set();
}
const ids = new Set<string>();
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
if (existsSync(join(extensionsDir, entry.name, "package.json"))) {
ids.add(entry.name);
}
}
return ids;
}
export function collectInstalledBundledRuntimeSidecarPaths(packageRoot: string): string[] {
const installedExtensionIds = collectInstalledBundledExtensionIds(packageRoot);
return PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS.filter((relativePath) => {
const match = /^dist\/extensions\/([^/]+)\//u.exec(relativePath);
return match !== null && installedExtensionIds.has(match[1]);
});
}
export function normalizeInstalledBinaryVersion(output: string): string {
const trimmed = output.trim();
const versionMatch = /\b\d{4}\.\d{1,2}\.\d{1,2}(?:-\d+|-beta\.\d+)?\b/u.exec(trimmed);
@@ -304,6 +339,7 @@ export function collectInstalledRootDependencyManifestErrors(packageRoot: string
if (
!dependencyName ||
NODE_BUILTIN_MODULES.has(dependencyName) ||
OPTIONAL_OR_EXTERNALIZED_RUNTIME_IMPORTS.has(dependencyName) ||
declaredRuntimeDeps.has(dependencyName) ||
isBundledExtensionOwnedRuntimeImport({
dependencyName,

View File

@@ -10,6 +10,27 @@ import { writeOfficialChannelCatalog } from "./write-official-channel-catalog.mj
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const ROOT_RUNTIME_ALIAS_PATTERN = /^(?<base>.+\.(?:runtime|contract))-[A-Za-z0-9_-]+\.js$/u;
const ROOT_STABLE_RUNTIME_ALIAS_PATTERN = /^.+\.(?:runtime|contract)\.js$/u;
const ROOT_RUNTIME_IMPORT_SPECIFIER_PATTERN =
/(["'])\.\/([^"']+\.(?:runtime|contract)-[A-Za-z0-9_-]+\.js)\1/gu;
const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [
// v2026.4.29 dispatch lazy chunks. Package updates used to replace the
// dist tree before the live gateway had restarted, so an already-loaded old
// dispatch chunk could still resolve these names after the swap.
["abort.runtime-DX6vo4yJ.js", "abort.runtime.js"],
["get-reply-from-config.runtime-uABrvCZ-.js", "get-reply-from-config.runtime.js"],
["reply-media-paths.runtime-C5UnVaLF.js", "reply-media-paths.runtime.js"],
["route-reply.runtime-D4PGzijU.js", "route-reply.runtime.js"],
["runtime-plugins.runtime-fLHuT7Vs.js", "runtime-plugins.runtime.js"],
["tts.runtime-66taD50M.js", "tts.runtime.js"],
// v2026.5.2-beta.1 dispatch lazy chunks.
["abort.runtime-CKviLU0L.js", "abort.runtime.js"],
["get-reply-from-config.runtime-BzFAggVK.js", "get-reply-from-config.runtime.js"],
["reply-media-paths.runtime-ZpULeITb.js", "reply-media-paths.runtime.js"],
["route-reply.runtime-uzaOjbd1.js", "route-reply.runtime.js"],
["runtime-plugins.runtime-CNAfmQRG.js", "runtime-plugins.runtime.js"],
["tts.runtime-D-THXDsp.js", "tts.runtime.js"],
];
const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [
{
dest: "dist/memory-state-CcqRgDZU.js",
@@ -86,7 +107,8 @@ export function writeStableRootRuntimeAliases(params = {}) {
return;
}
for (const entry of entries) {
const candidatesByAlias = new Map();
for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
if (!entry.isFile()) {
continue;
}
@@ -94,8 +116,122 @@ export function writeStableRootRuntimeAliases(params = {}) {
if (!match?.groups?.base) {
continue;
}
const aliasPath = path.join(distDir, `${match.groups.base}.js`);
writeTextFileIfChanged(aliasPath, `export * from "./${entry.name}";\n`);
const aliasFileName = `${match.groups.base}.js`;
const candidates = candidatesByAlias.get(aliasFileName) ?? [];
candidates.push(entry.name);
candidatesByAlias.set(aliasFileName, candidates);
}
const resolveAliasCandidate = (candidates) => {
if (candidates.length === 1) {
return candidates[0];
}
const candidateSet = new Set(candidates);
const wrappers = candidates.filter((candidate) => {
const filePath = path.join(distDir, candidate);
let source;
try {
source = fsImpl.readFileSync(filePath, "utf8");
} catch {
return false;
}
return candidates.some(
(target) =>
target !== candidate &&
candidateSet.has(target) &&
source.includes(`"./${target}"`) &&
!source.includes("\n//#region "),
);
});
return wrappers.length === 1 ? wrappers[0] : null;
};
for (const [aliasFileName, candidates] of candidatesByAlias) {
const aliasPath = path.join(distDir, aliasFileName);
const candidate = resolveAliasCandidate(candidates);
if (!candidate) {
fsImpl.rmSync?.(aliasPath, { force: true });
continue;
}
writeTextFileIfChanged(aliasPath, `export * from "./${candidate}";\n`);
}
}
export function rewriteRootRuntimeImportsToStableAliases(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
const fsImpl = params.fs ?? fs;
let entries = [];
try {
entries = fsImpl.readdirSync(distDir, { withFileTypes: true });
} catch {
return;
}
const candidatesByAlias = new Map();
for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
if (!entry.isFile()) {
continue;
}
const match = entry.name.match(ROOT_RUNTIME_ALIAS_PATTERN);
if (match?.groups?.base) {
const aliasFileName = `${match.groups.base}.js`;
const candidates = candidatesByAlias.get(aliasFileName) ?? [];
candidates.push(entry.name);
candidatesByAlias.set(aliasFileName, candidates);
}
}
const runtimeAliasFiles = new Map();
for (const [aliasFileName, candidates] of candidatesByAlias) {
if (candidates.length !== 1) {
continue;
}
runtimeAliasFiles.set(candidates[0], aliasFileName);
}
if (runtimeAliasFiles.size === 0) {
return;
}
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".js")) {
continue;
}
if (ROOT_STABLE_RUNTIME_ALIAS_PATTERN.test(entry.name)) {
continue;
}
const filePath = path.join(distDir, entry.name);
let source;
try {
source = fsImpl.readFileSync(filePath, "utf8");
} catch {
continue;
}
const rewritten = source.replace(
ROOT_RUNTIME_IMPORT_SPECIFIER_PATTERN,
(specifier, quote, fileName) => {
const aliasFileName = runtimeAliasFiles.get(fileName);
return aliasFileName ? `${quote}./${aliasFileName}${quote}` : specifier;
},
);
if (rewritten !== source) {
writeTextFileIfChanged(filePath, rewritten);
}
}
}
export function writeLegacyRootRuntimeCompatAliases(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
const fsImpl = params.fs ?? fs;
for (const [legacyFileName, aliasFileName] of LEGACY_ROOT_RUNTIME_COMPAT_ALIASES) {
const legacyPath = path.join(distDir, legacyFileName);
if (fsImpl.existsSync(legacyPath)) {
continue;
}
if (!fsImpl.existsSync(path.join(distDir, aliasFileName))) {
continue;
}
writeTextFileIfChanged(legacyPath, `export * from "./${aliasFileName}";\n`);
}
}
@@ -124,7 +260,9 @@ export function runRuntimePostBuild(params = {}) {
runPhase("bundled plugin metadata", () => copyBundledPluginMetadata(params));
runPhase("official channel catalog", () => writeOfficialChannelCatalog(params));
runPhase("bundled plugin runtime overlay", () => stageBundledPluginRuntime(params));
runPhase("stable root runtime imports", () => rewriteRootRuntimeImportsToStableAliases(params));
runPhase("stable root runtime aliases", () => writeStableRootRuntimeAliases(params));
runPhase("legacy root runtime compat aliases", () => writeLegacyRootRuntimeCompatAliases(params));
runPhase("legacy CLI exit compat chunks", () => writeLegacyCliExitCompatChunks(params));
runPhase("static extension assets", () => copyStaticExtensionAssets(params));
}

View File

@@ -18,6 +18,26 @@ const { packageRoot } = parsePackageRootArg(
const distExtensionsRoot = path.join(packageRoot, "dist", "extensions");
const installedLayoutEnv = "OPENCLAW_BUNDLED_CHANNEL_SMOKE_INSTALLED_LAYOUT";
function collectExcludedDistExtensionIds() {
const packageJsonPath = path.join(packageRoot, "package.json");
if (!fs.existsSync(packageJsonPath)) {
return new Set();
}
const packageJson = readJson(packageJsonPath);
const files = Array.isArray(packageJson.files) ? packageJson.files : [];
const excludedIds = new Set();
for (const entry of files) {
if (typeof entry !== "string") {
continue;
}
const match = /^!dist\/extensions\/([^/*]+)\/\*\*$/u.exec(entry.replaceAll("\\", "/"));
if (match) {
excludedIds.add(match[1]);
}
}
return excludedIds;
}
function packageRootLooksInstalled(root) {
return root.replaceAll("\\", "/").endsWith("/node_modules/openclaw");
}
@@ -69,10 +89,14 @@ function extensionEntryToDistFilename(entry) {
function collectBundledChannelEntryFiles() {
const files = [];
const excludedDistExtensionIds = collectExcludedDistExtensionIds();
for (const dirent of fs.readdirSync(distExtensionsRoot, { withFileTypes: true })) {
if (!dirent.isDirectory()) {
continue;
}
if (excludedDistExtensionIds.has(dirent.name)) {
continue;
}
const extensionRoot = path.join(distExtensionsRoot, dirent.name);
const packageJsonPath = path.join(extensionRoot, "package.json");
if (!fs.existsSync(packageJsonPath)) {

View File

@@ -22,6 +22,7 @@ describe("resolveExtraParams", () => {
expect(result).toEqual({
parallel_tool_calls: true,
text_verbosity: "low",
transport: "sse",
openaiWsWarmup: false,
});
});
@@ -192,6 +193,7 @@ describe("resolveExtraParams", () => {
openaiWsWarmup: false,
parallel_tool_calls: true,
text_verbosity: "low",
transport: "sse",
});
});

View File

@@ -1882,7 +1882,7 @@ describe("applyExtraParamsToAgent", () => {
expect(calls[0]?.transport).toBe("auto");
});
it("defaults OpenAI transport to auto without websocket warm-up", () => {
it("defaults OpenAI API-key GPT-5 transport to SSE without websocket warm-up", () => {
const { calls, agent } = createOptionsCaptureAgent();
applyExtraParamsToAgent(agent, undefined, "openai", "gpt-5");
@@ -1896,7 +1896,7 @@ describe("applyExtraParamsToAgent", () => {
void agent.streamFn?.(model, context, {});
expect(calls).toHaveLength(1);
expect(calls[0]?.transport).toBe("auto");
expect(calls[0]?.transport).toBe("sse");
expect(calls[0]?.openaiWsWarmup).toBe(false);
});

View File

@@ -8,6 +8,11 @@ import {
} from "./extra-params.js";
import { runExtraParamsCase } from "./extra-params.test-support.js";
type OpenAIResponseRuntimeOptions = {
transport?: string;
openaiWsWarmup?: boolean;
};
vi.mock("@mariozechner/pi-ai", () => createPiAiStreamSimpleMock());
beforeEach(() => {
@@ -130,4 +135,58 @@ describe("extra-params: provider runtime handoff", () => {
expect(payload.think).toBeUndefined();
});
it("defaults OpenAI GPT-5 API-key runs to SSE transport", () => {
const result = runExtraParamsCase({
applyProvider: "openai",
applyModelId: "gpt-5.4",
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4",
} as unknown as Model<"openai-responses">,
payload: {
model: "gpt-5.4",
input: [],
},
});
const options = result.options as OpenAIResponseRuntimeOptions | undefined;
expect(options?.transport).toBe("sse");
expect(options?.openaiWsWarmup).toBe(false);
});
it("preserves explicit OpenAI GPT-5 transport overrides", () => {
const result = runExtraParamsCase({
applyProvider: "openai",
applyModelId: "gpt-5.4",
cfg: {
agents: {
defaults: {
models: {
"openai/gpt-5.4": {
params: {
transport: "websocket",
openaiWsWarmup: true,
},
},
},
},
},
},
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4",
} as unknown as Model<"openai-responses">,
payload: {
model: "gpt-5.4",
input: [],
},
});
const options = result.options as OpenAIResponseRuntimeOptions | undefined;
expect(options?.transport).toBe("websocket");
expect(options?.openaiWsWarmup).toBe(true);
});
});

View File

@@ -323,6 +323,9 @@ function applyDefaultOpenAIGptRuntimeParams(
if (!Object.hasOwn(merged, "text_verbosity") && !Object.hasOwn(merged, "textVerbosity")) {
merged.text_verbosity = "low";
}
if (params.provider === "openai" && !Object.hasOwn(merged, "transport")) {
merged.transport = "sse";
}
if (!Object.hasOwn(merged, "openaiWsWarmup")) {
merged.openaiWsWarmup = false;
}

View File

@@ -3,6 +3,7 @@ import path from "node:path";
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
import { resolveChannelAwareNpmSpec } from "../../plugins/channel-npm-spec.js";
import {
describePluginInstallSource,
type PluginInstallSourceInfo,
@@ -239,12 +240,16 @@ function toChannelMeta(params: {
function resolveInstallInfo(params: {
install?: PluginPackageInstall;
packageName?: string;
packageVersion?: string;
packageDir?: string;
workspaceDir?: string;
}): ChannelPluginCatalogEntry["install"] | null {
const clawhubSpec = normalizeOptionalString(params.install?.clawhubSpec);
const npmSpec =
normalizeOptionalString(params.install?.npmSpec) ?? normalizeOptionalString(params.packageName);
const npmSpec = resolveChannelAwareNpmSpec({
npmSpec: params.install?.npmSpec,
packageName: params.packageName,
packageVersion: params.packageVersion,
});
if (!clawhubSpec && !npmSpec) {
return null;
}
@@ -295,6 +300,7 @@ function resolveInstallInfo(params: {
function buildCatalogEntryFromManifest(params: {
pluginId?: string;
packageName?: string;
packageVersion?: string;
packageDir?: string;
origin?: PluginOrigin;
workspaceDir?: string;
@@ -315,6 +321,7 @@ function buildCatalogEntryFromManifest(params: {
const install = resolveInstallInfo({
install: params.install,
packageName: params.packageName,
packageVersion: params.packageVersion,
packageDir: params.packageDir,
workspaceDir: params.workspaceDir,
});
@@ -338,6 +345,7 @@ function buildExternalCatalogEntry(entry: ExternalCatalogEntry): ChannelPluginCa
const manifest = entry[MANIFEST_KEY];
return buildCatalogEntryFromManifest({
packageName: entry.name,
packageVersion: entry.version,
channel: manifest?.channel,
install: manifest?.install,
});
@@ -387,6 +395,7 @@ export function listChannelPluginCatalogEntries(
const entry = buildCatalogEntryFromManifest({
pluginId: candidate.pluginId,
packageName: candidate.packageName,
packageVersion: candidate.packageVersion,
packageDir: candidate.rootDir,
origin: candidate.origin,
workspaceDir: candidate.workspaceDir ?? options.workspaceDir,

View File

@@ -1,12 +1,25 @@
import fs from "node:fs";
import path from "node:path";
import {
describeBundledMetadataOnlyChannelCatalogContract,
describeChannelCatalogEntryContract,
describeOfficialFallbackChannelCatalogContract,
} from "./test-helpers/channel-catalog-contract.js";
function resolveWorkspacePrereleaseNpmSpec(pluginDir: string): string {
const packageJson = JSON.parse(
fs.readFileSync(path.join(process.cwd(), "extensions", pluginDir, "package.json"), "utf8"),
) as { name?: string; version?: string; openclaw?: { install?: { npmSpec?: string } } };
const npmSpec = packageJson.openclaw?.install?.npmSpec ?? packageJson.name;
if (!npmSpec || !packageJson.version) {
throw new Error(`missing package metadata for ${pluginDir}`);
}
return packageJson.version.includes("-") ? `${npmSpec}@${packageJson.version}` : npmSpec;
}
describeChannelCatalogEntryContract({
channelId: "msteams",
npmSpec: "@openclaw/msteams",
npmSpec: resolveWorkspacePrereleaseNpmSpec("msteams"),
alias: "teams",
});

View File

@@ -287,6 +287,39 @@ export function describeChannelPluginCatalogEntriesContract() {
};
},
},
{
name: "pins bare external prerelease package specs to the entry version",
setup: () => {
const dir = fs.mkdtempSync(
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-catalog-prerelease-"),
);
const catalogPath = path.join(dir, "catalog.json");
writeCatalogFile(catalogPath, {
...createCatalogEntry({
packageName: "@openclaw/prerelease-demo-channel",
channelId: "prerelease-demo",
label: "Prerelease Demo",
blurb: "Prerelease package pinning fixture",
}),
version: "2026.5.2-beta.2",
});
return {
channelId: "prerelease-demo",
catalogPaths: [catalogPath],
expected: {
install: { npmSpec: "@openclaw/prerelease-demo-channel@2026.5.2-beta.2" },
installSource: {
npm: {
spec: "@openclaw/prerelease-demo-channel@2026.5.2-beta.2",
packageName: "@openclaw/prerelease-demo-channel",
selector: "2026.5.2-beta.2",
selectorKind: "exact-version",
},
},
},
};
},
},
{
name: "accepts external manifest entries with ClawHub-only install metadata",
setup: () => {

View File

@@ -3,6 +3,11 @@ import path from "node:path";
import type { Command } from "commander";
import { findBundledPluginSource } from "../plugins/bundled-sources.js";
import { loadPluginManifest } from "../plugins/manifest.js";
import {
listOfficialExternalPluginCatalogEntries,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
} from "../plugins/official-external-plugin-catalog.js";
import { resolveUserPath } from "../utils.js";
import { parseNpmPrefixSpec, resolveFileNpmSpecToLocalPath } from "./plugins-command-helpers.js";
@@ -99,6 +104,40 @@ function resolveBundledInstallRecoveryMetadata(
return {};
}
function resolveOfficialExternalInstallRecoveryMetadata(
request: Pick<PluginInstallRequestContext, "rawSpec" | "normalizedSpec" | "marketplace">,
): {
pluginId?: string;
allowInvalidConfigRecovery?: boolean;
} {
if (request.marketplace) {
return {};
}
const rawNpmPrefixSpec = parseNpmPrefixSpec(request.rawSpec);
const normalizedNpmPrefixSpec = parseNpmPrefixSpec(request.normalizedSpec);
const values = new Set(
[request.rawSpec, request.normalizedSpec, rawNpmPrefixSpec ?? "", normalizedNpmPrefixSpec ?? ""]
.map((value) => value.trim())
.filter(Boolean),
);
if (values.size === 0) {
return {};
}
for (const entry of listOfficialExternalPluginCatalogEntries()) {
const install = resolveOfficialExternalPluginInstall(entry);
const npmSpec = install?.npmSpec?.trim() || entry.name?.trim();
if (!npmSpec || !values.has(npmSpec)) {
continue;
}
const pluginId = resolveOfficialExternalPluginId(entry);
return {
...(pluginId ? { pluginId } : {}),
allowInvalidConfigRecovery: install?.allowInvalidConfigRecovery === true,
};
}
return {};
}
function resolvePluginInstallArgvTokens(commandPath: string[], argv: string[]): string[] {
const args = argv.slice(2);
let cursor = 0;
@@ -165,12 +204,21 @@ export function resolvePluginInstallRequestContext(params: {
};
}
const normalizedSpec = fileSpec && fileSpec.ok ? fileSpec.path : params.rawSpec;
const recovered = resolveBundledInstallRecoveryMetadata({
const bundledRecovered = resolveBundledInstallRecoveryMetadata({
rawSpec: params.rawSpec,
normalizedSpec,
resolvedPath: resolveUserPath(normalizedSpec),
marketplace: params.marketplace,
});
const officialRecovered = resolveOfficialExternalInstallRecoveryMetadata({
rawSpec: params.rawSpec,
normalizedSpec,
marketplace: params.marketplace,
});
const recovered =
officialRecovered.pluginId || officialRecovered.allowInvalidConfigRecovery !== undefined
? officialRecovered
: bundledRecovered;
return {
ok: true,
request: {

View File

@@ -26,7 +26,7 @@ vi.mock("../commands/doctor/shared/channel-doctor.js", () => ({
collectChannelDoctorStaleConfigMutationsMock(cfg),
}));
const MATRIX_REPO_INSTALL_SPEC = repoInstallSpec("matrix");
const DISCORD_REPO_INSTALL_SPEC = repoInstallSpec("discord");
function makeSnapshot(overrides: Partial<ConfigFileSnapshot> = {}): ConfigFileSnapshot {
return {
@@ -40,7 +40,7 @@ function makeSnapshot(overrides: Partial<ConfigFileSnapshot> = {}): ConfigFileSn
runtimeConfig: { plugins: {} } as ConfigFileSnapshot["runtimeConfig"],
config: { plugins: {} } as OpenClawConfig,
hash: "abc",
issues: [{ path: "plugins.installs.matrix", message: "stale path" }],
issues: [{ path: "plugins.installs.discord", message: "stale path" }],
warnings: [],
legacyIssues: [],
...overrides,
@@ -48,10 +48,10 @@ function makeSnapshot(overrides: Partial<ConfigFileSnapshot> = {}): ConfigFileSn
}
describe("loadConfigForInstall", () => {
const matrixNpmRequest = {
rawSpec: "@openclaw/matrix",
normalizedSpec: "@openclaw/matrix",
bundledPluginId: "matrix",
const discordNpmRequest = {
rawSpec: "@openclaw/discord",
normalizedSpec: "@openclaw/discord",
bundledPluginId: "discord",
allowInvalidConfigRecovery: true,
} satisfies PluginInstallRequestContext;
@@ -68,22 +68,22 @@ describe("loadConfigForInstall", () => {
});
it("returns the source config and base hash when the snapshot is valid", async () => {
const cfg = { plugins: { entries: { matrix: { enabled: true } } } } as OpenClawConfig;
const cfg = { plugins: { entries: { discord: { enabled: true } } } } as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValue(
makeSnapshot({
valid: true,
sourceConfig: cfg,
config: { plugins: { entries: { matrix: { enabled: true } }, enabled: true } },
config: { plugins: { entries: { discord: { enabled: true } }, enabled: true } },
hash: "config-1",
issues: [],
}),
);
const result = await loadConfigForInstall(matrixNpmRequest);
const result = await loadConfigForInstall(discordNpmRequest);
expect(result).toEqual({ config: cfg, baseHash: "config-1" });
});
it("does not run stale Matrix cleanup on the happy path", async () => {
it("does not run stale Discord cleanup on the happy path", async () => {
const cfg = { plugins: {} } as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValue(
makeSnapshot({
@@ -94,27 +94,27 @@ describe("loadConfigForInstall", () => {
}),
);
const result = await loadConfigForInstall(matrixNpmRequest);
const result = await loadConfigForInstall(discordNpmRequest);
expect(collectChannelDoctorStaleConfigMutationsMock).not.toHaveBeenCalled();
expect(result.config).toBe(cfg);
});
it("falls back to snapshot config for explicit bundled-plugin reinstall when issues match the known upgrade failure", async () => {
const snapshotCfg = {
plugins: { installs: { matrix: { source: "path", installPath: "/gone" } } },
plugins: { installs: { discord: { source: "path", installPath: "/gone" } } },
} as unknown as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValue(
makeSnapshot({
parsed: { plugins: { installs: { matrix: {} } } },
parsed: { plugins: { installs: { discord: {} } } },
config: snapshotCfg,
issues: [
{ path: "channels.matrix", message: "unknown channel id: matrix" },
{ path: "channels.discord", message: "unknown channel id: discord" },
{ path: "plugins.load.paths", message: "plugin: plugin path not found: /gone" },
],
}),
);
const result = await loadConfigForInstall(matrixNpmRequest);
const result = await loadConfigForInstall(discordNpmRequest);
expect(readConfigFileSnapshotMock).toHaveBeenCalled();
expect(collectChannelDoctorStaleConfigMutationsMock).toHaveBeenCalledWith(snapshotCfg);
expect(result).toEqual({ config: snapshotCfg, baseHash: "abc" });
@@ -122,28 +122,28 @@ describe("loadConfigForInstall", () => {
it("allows npm:-prefixed bundled-plugin reinstall recovery", async () => {
const snapshotCfg = {
plugins: { installs: { matrix: { source: "path", installPath: "/gone" } } },
plugins: { installs: { discord: { source: "path", installPath: "/gone" } } },
} as unknown as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValue(
makeSnapshot({
parsed: { plugins: { installs: { matrix: {} } } },
parsed: { plugins: { installs: { discord: {} } } },
config: snapshotCfg,
issues: [
{ path: "channels.matrix", message: "unknown channel id: matrix" },
{ path: "channels.discord", message: "unknown channel id: discord" },
{ path: "plugins.load.paths", message: "plugin: plugin path not found: /gone" },
],
}),
);
const request = resolvePluginInstallRequestContext({
rawSpec: "npm:@openclaw/matrix",
rawSpec: "npm:@openclaw/discord",
});
if (!request.ok) {
throw new Error(request.error);
}
expect(request.request).toMatchObject({
bundledPluginId: "matrix",
bundledPluginId: "discord",
allowInvalidConfigRecovery: true,
});
const result = await loadConfigForInstall(request.request);
@@ -156,12 +156,12 @@ describe("loadConfigForInstall", () => {
readConfigFileSnapshotMock.mockResolvedValue(
makeSnapshot({
config: snapshotCfg,
issues: [{ path: "channels.matrix", message: "unknown channel id: matrix" }],
issues: [{ path: "channels.discord", message: "unknown channel id: discord" }],
}),
);
const repoRequest = resolvePluginInstallRequestContext({
rawSpec: MATRIX_REPO_INSTALL_SPEC,
rawSpec: DISCORD_REPO_INSTALL_SPEC,
});
if (!repoRequest.ok) {
throw new Error(repoRequest.error);
@@ -169,7 +169,7 @@ describe("loadConfigForInstall", () => {
const result = await loadConfigForInstall({
...repoRequest.request,
resolvedPath: bundledPluginRootAt("/tmp/repo", "matrix"),
resolvedPath: bundledPluginRootAt("/tmp/repo", "discord"),
});
expect(result.config).toBe(snapshotCfg);
});
@@ -181,12 +181,12 @@ describe("loadConfigForInstall", () => {
}),
);
await expect(loadConfigForInstall(matrixNpmRequest)).rejects.toThrow(
"Config invalid outside the bundled recovery path for matrix",
await expect(loadConfigForInstall(discordNpmRequest)).rejects.toThrow(
"Config invalid outside the bundled recovery path for discord",
);
});
it("rejects non-Matrix install requests when config is invalid", async () => {
it("rejects non-Discord install requests when config is invalid", async () => {
readConfigFileSnapshotMock.mockResolvedValue(makeSnapshot());
await expect(
@@ -205,7 +205,7 @@ describe("loadConfigForInstall", () => {
}),
);
await expect(loadConfigForInstall(matrixNpmRequest)).rejects.toThrow(
await expect(loadConfigForInstall(discordNpmRequest)).rejects.toThrow(
"Config file could not be parsed; run `openclaw doctor` to repair it.",
);
});
@@ -213,7 +213,7 @@ describe("loadConfigForInstall", () => {
it("throws when invalid snapshot config file does not exist", async () => {
readConfigFileSnapshotMock.mockResolvedValue(makeSnapshot({ exists: false, parsed: {} }));
await expect(loadConfigForInstall(matrixNpmRequest)).rejects.toThrow(
await expect(loadConfigForInstall(discordNpmRequest)).rejects.toThrow(
"Config file could not be parsed; run `openclaw doctor` to repair it.",
);
});

View File

@@ -43,6 +43,7 @@ function buildBridgeFromPersistedBundledRecord(
officialInstall?.defaultChoice === "clawhub" && clawhubSpec ? "clawhub" : "npm",
...(npmSpec ? { npmSpec } : {}),
...(clawhubSpec ? { clawhubSpec } : {}),
...(record.packageVersion ? { packageVersion: record.packageVersion } : {}),
...(record.enabledByDefault ? { enabledByDefault: true } : {}),
...(channelIds.length ? { channelIds } : {}),
};

View File

@@ -4,7 +4,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
import { loggingState } from "../../logging/state.js";
import { setCommandJsonMode } from "./json-mode.js";
const MATRIX_REPO_INSTALL_SPEC = repoInstallSpec("matrix");
const DISCORD_REPO_INSTALL_SPEC = repoInstallSpec("discord");
const setVerboseMock = vi.fn();
const emitCliBannerMock = vi.fn();
@@ -299,10 +299,10 @@ describe("registerPreActionHooks", () => {
expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled();
});
it("only allows invalid config for explicit Matrix reinstall requests", async () => {
it("only allows invalid config for explicit Discord reinstall requests", async () => {
await runPreAction({
parseArgv: ["plugins", "install", "@openclaw/matrix"],
processArgv: ["node", "openclaw", "plugins", "install", "@openclaw/matrix"],
parseArgv: ["plugins", "install", "@openclaw/discord"],
processArgv: ["node", "openclaw", "plugins", "install", "@openclaw/discord"],
});
expect(ensureConfigReadyMock).toHaveBeenCalledWith({
@@ -324,8 +324,8 @@ describe("registerPreActionHooks", () => {
vi.clearAllMocks();
await runPreAction({
parseArgv: ["plugins", "install", MATRIX_REPO_INSTALL_SPEC],
processArgv: ["node", "openclaw", "plugins", "install", MATRIX_REPO_INSTALL_SPEC],
parseArgv: ["plugins", "install", DISCORD_REPO_INSTALL_SPEC],
processArgv: ["node", "openclaw", "plugins", "install", DISCORD_REPO_INSTALL_SPEC],
});
expect(ensureConfigReadyMock).toHaveBeenCalledWith({
@@ -336,13 +336,13 @@ describe("registerPreActionHooks", () => {
vi.clearAllMocks();
await runPreAction({
parseArgv: ["plugins", "install", "@openclaw/matrix", "--marketplace", "local/repo"],
parseArgv: ["plugins", "install", "@openclaw/discord", "--marketplace", "local/repo"],
processArgv: [
"node",
"openclaw",
"plugins",
"install",
"@openclaw/matrix",
"@openclaw/discord",
"--marketplace",
"local/repo",
],

View File

@@ -10,6 +10,7 @@ export type PluginChannelCatalogEntry = {
pluginId: string;
origin: PluginOrigin;
packageName?: string;
packageVersion?: string;
workspaceDir?: string;
rootDir: string;
channel: PluginPackageChannel;
@@ -43,6 +44,7 @@ export function listChannelCatalogEntries(
pluginId: manifest.manifest.id,
origin: candidate.origin,
packageName: candidate.packageName,
packageVersion: candidate.packageVersion,
workspaceDir: candidate.workspaceDir,
rootDir: candidate.rootDir,
channel,

View File

@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { resolveChannelAwareNpmSpec } from "./channel-npm-spec.js";
describe("resolveChannelAwareNpmSpec", () => {
it("pins bare npm specs to the package prerelease version", () => {
expect(
resolveChannelAwareNpmSpec({
npmSpec: "@openclaw/twitch",
packageName: "@openclaw/twitch",
packageVersion: "2026.5.2-beta.2",
}),
).toBe("@openclaw/twitch@2026.5.2-beta.2");
});
it("targets the beta dist-tag for bare plugin specs on beta channel", () => {
expect(
resolveChannelAwareNpmSpec({
npmSpec: "@openclaw/twitch",
channel: "beta",
}),
).toBe("@openclaw/twitch@beta");
});
it("preserves explicit versions and tags", () => {
expect(
resolveChannelAwareNpmSpec({
npmSpec: "@openclaw/twitch@2026.5.2-beta.2",
channel: "beta",
}),
).toBe("@openclaw/twitch@2026.5.2-beta.2");
expect(
resolveChannelAwareNpmSpec({
npmSpec: "@openclaw/twitch@latest",
packageVersion: "2026.5.2-beta.2",
}),
).toBe("@openclaw/twitch@latest");
});
});

View File

@@ -0,0 +1,35 @@
import { isPrereleaseSemverVersion, parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import type { UpdateChannel } from "../infra/update-channels.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function resolveChannelAwareNpmSpec(params: {
npmSpec?: string;
packageName?: string;
packageVersion?: string;
channel?: UpdateChannel;
}): string | undefined {
const npmSpec =
normalizeOptionalString(params.npmSpec) ?? normalizeOptionalString(params.packageName);
if (!npmSpec) {
return undefined;
}
const parsed = parseRegistryNpmSpec(npmSpec);
if (!parsed || parsed.selectorKind !== "none") {
return npmSpec;
}
const packageName = normalizeOptionalString(params.packageName);
const expectedName = packageName
? (parseRegistryNpmSpec(packageName)?.name ?? packageName)
: undefined;
if (expectedName && parsed.name !== expectedName) {
return npmSpec;
}
const packageVersion = normalizeOptionalString(params.packageVersion);
if (packageVersion && isPrereleaseSemverVersion(packageVersion)) {
return `${parsed.name}@${packageVersion}`;
}
if (params.channel === "beta") {
return `${parsed.name}@beta`;
}
return npmSpec;
}

View File

@@ -9,6 +9,8 @@ export type ExternalizedBundledPluginBridge = {
preferredSource?: ExternalizedBundledPluginPreferredSource;
/** npm spec OpenClaw can install when migrating the bundled plugin out. */
npmSpec?: string;
/** Version of the bundled package that authored the install hint. */
packageVersion?: string;
/** ClawHub spec OpenClaw can install when migrating the bundled plugin out. */
clawhubSpec?: string;
/** Optional ClawHub base URL for non-default registries. */

View File

@@ -298,6 +298,61 @@ describe("installPluginFromNpmSpec", () => {
});
});
it("allows official catalog-matched npm plugins through the trusted scanner path", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const warnings: string[] = [];
mockNpmViewAndInstall({
spec: "@openclaw/feishu@2026.5.2",
packageName: "@openclaw/feishu",
version: "2026.5.2",
pluginId: "feishu",
npmRoot,
indexJs: `const token = process.env.FEISHU_BOT_TOKEN;\nfetch("https://open.feishu.cn/open-apis/bot/v2/hook", { headers: { authorization: token } });`,
});
const result = await installPluginFromNpmSpec({
spec: "@openclaw/feishu@2026.5.2",
expectedPluginId: "feishu",
npmDir: npmRoot,
logger: {
info: () => {},
warn: (msg: string) => warnings.push(msg),
},
});
expect(result.ok).toBe(true);
expect(
warnings.some((warning) =>
warning.includes("allowed because it is an official OpenClaw package"),
),
).toBe(true);
});
it("keeps blocking dangerous npm installs that do not match the official catalog", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
mockNpmViewAndInstall({
spec: "@openclaw/feishu-spoof@2026.5.2",
packageName: "@openclaw/feishu-spoof",
version: "2026.5.2",
pluginId: "feishu",
npmRoot,
indexJs: `const token = process.env.FEISHU_BOT_TOKEN;\nfetch("https://open.feishu.cn/open-apis/bot/v2/hook", { headers: { authorization: token } });`,
});
const result = await installPluginFromNpmSpec({
spec: "@openclaw/feishu-spoof@2026.5.2",
expectedPluginId: "feishu",
npmDir: npmRoot,
logger: { info: () => {}, warn: () => {} },
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
expect(result.error).toContain("dangerous code patterns detected");
}
});
it("rejects non-registry npm specs", async () => {
const result = await installPluginFromNpmSpec({ spec: "github:evil/evil" });
expect(result.ok).toBe(false);

View File

@@ -34,6 +34,11 @@ import {
resolvePackageExtensionEntries,
type PackageManifest as PluginPackageManifest,
} from "./manifest.js";
import {
listOfficialExternalPluginCatalogEntries,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
} from "./official-external-plugin-catalog.js";
import { validatePackageExtensionEntriesForInstall } from "./package-entry-resolution.js";
import { linkOpenClawPeerDependencies } from "./plugin-peer-link.js";
@@ -110,7 +115,23 @@ type PluginInstallPolicyRequest = {
};
const defaultLogger: PluginInstallLogger = {};
const TRUSTED_OFFICIAL_NPM_PLUGIN_PACKAGES = new Map([["@openclaw/codex", "codex"]]);
function listTrustedOfficialNpmPluginPackages(): Map<string, string> {
const packages = new Map<string, string>();
for (const entry of listOfficialExternalPluginCatalogEntries()) {
if (entry.source !== "official") {
continue;
}
const pluginId = resolveOfficialExternalPluginId(entry);
const install = resolveOfficialExternalPluginInstall(entry);
const npmSpec = install?.npmSpec ? parseRegistryNpmSpec(install.npmSpec) : null;
if (!pluginId || !npmSpec) {
continue;
}
packages.set(npmSpec.name, pluginId);
}
return packages;
}
function ensureOpenClawExtensions(params: { manifest: PackageManifest }):
| {
@@ -198,7 +219,7 @@ function isTrustedOfficialNpmPluginInstall(params: {
if (!requested) {
return false;
}
const expectedPluginId = TRUSTED_OFFICIAL_NPM_PLUGIN_PACKAGES.get(requested.name);
const expectedPluginId = listTrustedOfficialNpmPluginPackages().get(requested.name);
return (
expectedPluginId !== undefined &&
params.packageName === requested.name &&

View File

@@ -70,6 +70,25 @@ describe("bundled plugin public surface runtime", () => {
).toBe(sourceModulePath);
});
it("falls back from an incomplete package dist-runtime override to packaged dist", () => {
const packageRoot = createTempDir();
const distModulePath = path.join(packageRoot, "dist", "extensions", "demo", "api.js");
fs.mkdirSync(path.dirname(distModulePath), { recursive: true });
fs.writeFileSync(distModulePath, "export const marker = 'dist';\n", "utf8");
const runtimeBundledPluginsDir = path.join(packageRoot, "dist-runtime", "extensions");
fs.mkdirSync(path.join(runtimeBundledPluginsDir, "demo"), { recursive: true });
expect(
resolveBundledPluginPublicSurfacePath({
rootDir: packageRoot,
bundledPluginsDir: runtimeBundledPluginsDir,
dirName: "demo",
artifactBasename: "api.js",
}),
).toBe(distModulePath);
});
it("allows plugin-local nested artifact paths", () => {
expect(normalizeBundledPluginArtifactSubpath("src/outbound-adapter.js")).toBe(
"src/outbound-adapter.js",

View File

@@ -70,7 +70,7 @@ export function resolveBundledPluginSourcePublicSurfacePath(params: {
return null;
}
function resolvePackageSourceFallbackForBundledDir(params: {
function resolvePackageFallbackForBundledDir(params: {
rootDir: string;
bundledPluginsDir: string;
dirName: string;
@@ -85,6 +85,15 @@ function resolvePackageSourceFallbackForBundledDir(params: {
if (!packageBundledDirs.includes(normalizedBundledDir)) {
return null;
}
for (const packageBundledDir of packageBundledDirs) {
if (packageBundledDir === normalizedBundledDir) {
continue;
}
const builtCandidate = path.join(packageBundledDir, params.dirName, params.artifactBasename);
if (fs.existsSync(builtCandidate)) {
return builtCandidate;
}
}
return resolveBundledPluginSourcePublicSurfacePath({
sourceRoot: path.join(normalizedRootDir, "extensions"),
dirName: params.dirName,
@@ -116,7 +125,7 @@ export function resolveBundledPluginPublicSurfacePath(params: {
dirName,
artifactBasename,
}) ??
resolvePackageSourceFallbackForBundledDir({
resolvePackageFallbackForBundledDir({
rootDir: params.rootDir,
bundledPluginsDir: explicitBundledPluginsDir,
dirName,

View File

@@ -1947,6 +1947,52 @@ describe("syncPluginsForUpdateChannel", () => {
});
});
it("uses the beta dist-tag when beta restores an externalized bundled npm plugin", async () => {
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
installPluginFromNpmSpecMock.mockResolvedValue(
createSuccessfulNpmUpdateResult({
pluginId: "legacy-chat",
targetDir: "/tmp/openclaw-plugins/legacy-chat",
version: "2026.5.2-beta.2",
npmResolution: {
name: "@openclaw/legacy-chat",
version: "2026.5.2-beta.2",
resolvedSpec: "@openclaw/legacy-chat@2026.5.2-beta.2",
},
}),
);
const result = await syncPluginsForUpdateChannel({
channel: "beta",
externalizedBundledPluginBridges: [
{
bundledPluginId: "legacy-chat",
npmSpec: "@openclaw/legacy-chat",
channelIds: ["legacy-chat"],
},
],
config: {
channels: {
"legacy-chat": {
enabled: true,
},
},
},
});
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/legacy-chat@beta",
mode: "update",
expectedPluginId: "legacy-chat",
}),
);
expect(result.config.plugins?.installs?.["legacy-chat"]?.spec).toBe(
"@openclaw/legacy-chat@beta",
);
expect(result.summary.switchedToNpm).toEqual(["legacy-chat"]);
});
it("installs a ClawHub-preferred externalized bundled plugin", async () => {
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
installPluginFromClawHubMock.mockResolvedValue(

View File

@@ -13,6 +13,7 @@ import { compareComparableSemver, parseComparableSemver } from "../infra/semver-
import type { UpdateChannel } from "../infra/update-channels.js";
import { resolveUserPath } from "../utils.js";
import { resolveBundledPluginSources } from "./bundled-sources.js";
import { resolveChannelAwareNpmSpec } from "./channel-npm-spec.js";
import { buildClawHubPluginInstallRecordFields } from "./clawhub-install-records.js";
import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "./clawhub.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
@@ -507,8 +508,9 @@ function resolveClawHubUpdateSpecs(params: {
function isBridgeAlreadyInstalledFromPreferredSource(params: {
bridge: ExternalizedBundledPluginBridge;
record: PluginInstallRecord;
channel: UpdateChannel;
}): boolean {
const npmSpec = getExternalizedBundledPluginNpmSpec(params.bridge);
const npmSpec = resolveBridgeNpmSpec(params.bridge, params.channel);
if (npmSpec && params.record.source === "npm" && params.record.spec === npmSpec) {
return true;
}
@@ -518,6 +520,19 @@ function isBridgeAlreadyInstalledFromPreferredSource(params: {
);
}
function resolveBridgeNpmSpec(
bridge: ExternalizedBundledPluginBridge,
channel: UpdateChannel,
): string {
return (
resolveChannelAwareNpmSpec({
npmSpec: getExternalizedBundledPluginNpmSpec(bridge),
packageVersion: bridge.packageVersion,
channel,
}) ?? ""
);
}
function replacePluginIdInList(
entries: string[] | undefined,
fromId: string,
@@ -1335,6 +1350,7 @@ export async function syncPluginsForUpdateChannel(params: {
isBridgeAlreadyInstalledFromPreferredSource({
bridge,
record: existing.record,
channel: params.channel,
})
) {
if (existing.pluginId !== targetPluginId) {
@@ -1358,7 +1374,7 @@ export async function syncPluginsForUpdateChannel(params: {
}
const preferredSource = getExternalizedBundledPluginPreferredSource(bridge);
const npmSpec = getExternalizedBundledPluginNpmSpec(bridge);
const npmSpec = resolveBridgeNpmSpec(bridge, params.channel);
const clawhubSpec = getExternalizedBundledPluginClawHubSpec(bridge);
let installSource = preferredSource;
let installSpec = preferredSource === "clawhub" ? clawhubSpec : npmSpec;

View File

@@ -2,24 +2,16 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
import { listBundledPluginPackArtifacts } from "../scripts/lib/bundled-plugin-build-entries.mjs";
import {
buildPublishedInstallCommandArgs,
buildPublishedInstallScenarios,
collectInstalledBundledRuntimeSidecarPaths,
collectInstalledContextEngineRuntimeErrors,
collectInstalledRootDependencyManifestErrors,
collectInstalledPackageErrors,
normalizeInstalledBinaryVersion,
resolveInstalledBinaryPath,
} from "../scripts/openclaw-npm-postpublish-verify.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = BUNDLED_RUNTIME_SIDECAR_PATHS.filter(
(relativePath) => listBundledPluginPackArtifacts().includes(relativePath),
);
const REQUIRED_INSTALLED_RUNTIME_SIDECAR_PATHS = [
...PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS,
] as const;
describe("buildPublishedInstallScenarios", () => {
it("uses a single fresh scenario for plain stable releases", () => {
@@ -66,7 +58,11 @@ describe("buildPublishedInstallCommandArgs", () => {
});
describe("collectInstalledPackageErrors", () => {
it("flags version mismatches and missing runtime sidecars", () => {
function makeInstalledPackageRoot(): string {
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-package-"));
}
it("flags version mismatches", () => {
const errors = collectInstalledPackageErrors({
expectedVersion: "2026.3.23-2",
installedVersion: "2026.3.23",
@@ -76,17 +72,35 @@ describe("collectInstalledPackageErrors", () => {
expect(errors[0]).toBe(
"installed package version mismatch: expected 2026.3.23-2, found 2026.3.23.",
);
expect(errors).toEqual(
expect.arrayContaining(
REQUIRED_INSTALLED_RUNTIME_SIDECAR_PATHS.map(
(relativePath) =>
`installed package is missing required bundled runtime sidecar: ${relativePath}`,
),
),
);
expect(errors.length).toBeGreaterThanOrEqual(
1 + REQUIRED_INSTALLED_RUNTIME_SIDECAR_PATHS.length,
);
});
it("requires runtime sidecars for bundled extensions included in the package", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writeFileSync(join(packageRoot, "package.json"), '{"version":"2026.3.23"}\n', "utf8");
mkdirSync(join(packageRoot, "dist", "extensions", "slack"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "extensions", "slack", "package.json"),
"{}\n",
"utf8",
);
expect(collectInstalledBundledRuntimeSidecarPaths(packageRoot)).toContain(
"dist/extensions/slack/runtime-api.js",
);
expect(
collectInstalledPackageErrors({
expectedVersion: "2026.3.23",
installedVersion: "2026.3.23",
packageRoot,
}),
).toContain(
"installed package is missing required bundled runtime sidecar: dist/extensions/slack/runtime-api.js",
);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
});
@@ -212,6 +226,32 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
}
});
it("accepts optional or externalized runtime imports", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "optional-runtime.js"),
'await import("@lancedb/lancedb");\n',
"utf8",
);
writeFileSync(
join(packageRoot, "dist", "discord-voice-runtime.js"),
'const OpusScript = require("opusscript");\nexport { OpusScript };\n',
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("flags undeclared imports from mjs and cjs root dist files", () => {
const packageRoot = makeInstalledPackageRoot();
@@ -318,12 +358,12 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "oversized.js"),
"x".repeat(2 * 1024 * 1024 + 1),
"x".repeat(4 * 1024 * 1024 + 1),
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package root dist file 'oversized.js' is invalid or exceeds 2097152 bytes.",
"installed package root dist file 'oversized.js' is invalid or exceeds 4194304 bytes.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });

View File

@@ -319,17 +319,17 @@ describe("bundled plugin package dependency checks", () => {
);
writeFileSync(
join(tempRoot, "dist", "extensions", "memory-lancedb", "package.json"),
`{"name":"@openclaw/memory-lancedb","dependencies":{"@lancedb/lancedb":"^0.27.2"}}\n`,
`{"name":"@openclaw/memory-lancedb","dependencies":{"root-owned-only":"^1.0.0"}}\n`,
"utf8",
);
writeFileSync(
join(tempRoot, "dist", "root-runtime.js"),
`import("@lancedb/lancedb");\n`,
`import("root-owned-only");\n`,
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(tempRoot)).toEqual([
"installed package root is missing declared runtime dependency '@lancedb/lancedb' for dist importers: root-runtime.js. Add it to package.json dependencies/optionalDependencies.",
"installed package root is missing declared runtime dependency 'root-owned-only' for dist importers: root-runtime.js. Add it to package.json dependencies/optionalDependencies.",
]);
} finally {
rmSync(tempRoot, { recursive: true, force: true });

View File

@@ -284,7 +284,9 @@ describe("docker build helper", () => {
expect(clawhub).toContain("OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB");
expect(clawhub).toContain("OPENCLAW_PLUGINS_E2E_LIVE_NPM_REGISTRY");
expect(clawhub).toContain("live ClawHub can rate-limit CI");
expect(clawhub).toContain('[[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]');
expect(clawhub).toContain('[[ -n "${OPENCLAW_CLAWHUB_URL:-}" || -n "${CLAWHUB_URL:-}" ]]');
expect(clawhub).toContain("Ignoring ambient ClawHub URL for fixture-mode plugin E2E");
expect(clawhub).toContain("unset OPENCLAW_CLAWHUB_URL CLAWHUB_URL");
});
it("covers plugin install/update sources in the Docker plugin sweep", () => {

View File

@@ -146,6 +146,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
]);
expect(packageUpdateCore.lanes.map((lane) => lane.name)).toEqual([
"npm-onboard-channel-agent",
"npm-onboard-discord-channel-agent",
"doctor-switch",
"update-channel-switch",
"upgrade-survivor",
@@ -157,6 +158,10 @@ describe("scripts/lib/docker-e2e-plan", () => {
name: "npm-onboard-channel-agent",
stateScenario: "empty",
}),
expect.objectContaining({
name: "npm-onboard-discord-channel-agent",
stateScenario: "empty",
}),
expect.objectContaining({
name: "doctor-switch",
stateScenario: "empty",

View File

@@ -36,6 +36,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
expect(plan.dockerLanes).toEqual([
"npm-onboard-channel-agent",
"npm-onboard-discord-channel-agent",
"doctor-switch",
"update-channel-switch",
"plugins-offline",
@@ -98,14 +99,14 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
stateScenario: "empty",
}),
);
expect(script).toContain("npm:@openclaw/kitchen-sink@latest");
expect(script).toContain("npm-latest-conformance");
expect(script).toContain("npm-latest-adversarial");
expect(script).toContain("npm:@openclaw/kitchen-sink@0.1.5");
expect(script).toContain("npm-pinned-conformance");
expect(script).toContain("npm-pinned-adversarial");
expect(script).toContain("npm:@openclaw/kitchen-sink@beta");
expect(script).toContain("clawhub:@openclaw/kitchen-sink@latest");
expect(script).toContain("clawhub:@openclaw/kitchen-sink@beta");
expect(script).toContain(
"npm-to-clawhub|clawhub:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|clawhub|success|basic||npm:@openclaw/kitchen-sink@latest",
"npm-to-clawhub|clawhub:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|clawhub|success|basic||${KITCHEN_SINK_NPM_SPEC}",
);
expect(script).toContain("scripts/e2e/lib/kitchen-sink-plugin/sweep.sh");
expect(sweepScript).toContain('plugins install "$KITCHEN_SINK_SPEC"');

View File

@@ -4,7 +4,9 @@ import { describe, expect, it, vi } from "vitest";
import {
copyStaticExtensionAssets,
listStaticExtensionAssetOutputs,
rewriteRootRuntimeImportsToStableAliases,
writeLegacyCliExitCompatChunks,
writeLegacyRootRuntimeCompatAliases,
writeStableRootRuntimeAliases,
} from "../../scripts/runtime-postbuild.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
@@ -86,6 +88,160 @@ describe("runtime postbuild static assets", () => {
await expect(fs.stat(path.join(distDir, "library.js"))).rejects.toThrow();
});
it("does not write ambiguous stable aliases for colliding root runtime chunks", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "install.runtime-Aaa111.js"),
"export const pluginInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime-Bbb222.js"),
"export const daemonInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime.js"),
'export * from "./install.runtime-Stale.js";\n',
"utf8",
);
writeStableRootRuntimeAliases({ rootDir });
await expect(fs.stat(path.join(distDir, "install.runtime.js"))).rejects.toThrow();
});
it("keeps stable aliases when one colliding root runtime chunk re-exports the implementation", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-model-auth.runtime-Impl123.js"),
"export const auth = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "runtime-model-auth.runtime-Wrap456.js"),
'import { auth } from "./runtime-model-auth.runtime-Impl123.js";\nexport { auth };\n',
"utf8",
);
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "runtime-model-auth.runtime.js"), "utf8")).toBe(
'export * from "./runtime-model-auth.runtime-Wrap456.js";\n',
);
});
it("rewrites root runtime imports to stable aliases", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime-AbCd1234.js"),
"export const ready = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "dispatch-OldHash.js"),
[
'const lazy = () => import("./runtime-plugins.runtime-AbCd1234.js");',
'import "./missing.runtime-Nope.js";',
"",
].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "dispatch-OldHash.js"), "utf8")).toBe(
[
'const lazy = () => import("./runtime-plugins.runtime.js");',
'import "./missing.runtime-Nope.js";',
"",
].join("\n"),
);
});
it("keeps hashed imports when a stable runtime alias would collide", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "install.runtime-Aaa111.js"),
"export const pluginInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime-Bbb222.js"),
"export const daemonInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install-OldHash.js"),
[
'const pluginRuntime = () => import("./install.runtime-Aaa111.js");',
'const daemonRuntime = () => import("./install.runtime-Bbb222.js");',
"",
].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "install-OldHash.js"), "utf8")).toBe(
[
'const pluginRuntime = () => import("./install.runtime-Aaa111.js");',
'const daemonRuntime = () => import("./install.runtime-Bbb222.js");',
"",
].join("\n"),
);
});
it("leaves stable alias files pointing at their hashed runtime chunks", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime-AbCd1234.js"),
"export const ready = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime.js"),
'export * from "./runtime-plugins.runtime-AbCd1234.js";\n',
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "runtime-plugins.runtime.js"), "utf8")).toBe(
'export * from "./runtime-plugins.runtime-AbCd1234.js";\n',
);
});
it("writes compatibility aliases for previous release runtime chunk names", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime.js"),
'export * from "./runtime-plugins.runtime-NewHash.js";\n',
"utf8",
);
writeLegacyRootRuntimeCompatAliases({ rootDir });
expect(
await fs.readFile(path.join(distDir, "runtime-plugins.runtime-fLHuT7Vs.js"), "utf8"),
).toBe('export * from "./runtime-plugins.runtime.js";\n');
expect(
await fs.readFile(path.join(distDir, "runtime-plugins.runtime-CNAfmQRG.js"), "utf8"),
).toBe('export * from "./runtime-plugins.runtime.js";\n');
});
it("writes legacy CLI exit compatibility chunks", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");