mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
16 Commits
sqlite-str
...
v2026.4.23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4a92cff60 | ||
|
|
c41c212591 | ||
|
|
6926494f71 | ||
|
|
6f948d925e | ||
|
|
0e2bd4b3ee | ||
|
|
8c2235e873 | ||
|
|
99354fc1c9 | ||
|
|
080ac622c1 | ||
|
|
ef36ca9517 | ||
|
|
c5620ddf9e | ||
|
|
5ab5dc3900 | ||
|
|
18c2531d16 | ||
|
|
fefc4b3d4e | ||
|
|
8da230a1b3 | ||
|
|
dc1ce0b2b1 | ||
|
|
e776922a15 |
@@ -25,15 +25,25 @@ Use this skill for release and publish-time workflow. Keep ordinary development
|
||||
- Before release branching, commit any dirty files in coherent groups, push,
|
||||
pull/rebase, then run `/changelog` on `main` and commit/push/pull that
|
||||
changelog rewrite immediately before creating the release branch.
|
||||
- Do not delete or rewrite beta tags after they leave the machine. If a
|
||||
published or pushed beta needs a fix, commit the fix on the release branch and
|
||||
increment to the next `-beta.N`.
|
||||
- After the release branch is cut, do not keep rebasing it onto moving `main`.
|
||||
Treat the branch commit as the release base. If validation finds a concrete
|
||||
issue, inspect `main` and backport only the low-risk fix commits that directly
|
||||
address that failure.
|
||||
- Beta numbers are consumed by npm publication, not by a local tag or canceled
|
||||
preflight. If a beta tag was pushed but the matching npm package was not
|
||||
published, confirm `npm view openclaw@YYYY.M.D-beta.N` is missing and cancel
|
||||
any in-flight preflight/publish workflows before moving or recreating that
|
||||
beta tag. Once the npm package is published, do not delete or rewrite the
|
||||
beta tag; commit the fix on the release branch and increment to the next
|
||||
`-beta.N`.
|
||||
- For a beta release train, run the full pre-npm test roster before publishing
|
||||
each beta. After a beta is published, run the smaller published-install roster
|
||||
focused on install/update/Docker/Parallels. If anything fails, fix it on the
|
||||
release branch, commit/push/pull, increment beta number, and repeat. Operators
|
||||
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
|
||||
stop and report.
|
||||
focused on install/update plus all Docker and Parallels release checks. If
|
||||
those published-artifact checks all pass, proceed to the non-beta release only
|
||||
when the operator asked for the full beta-to-stable train. If anything fails,
|
||||
fix it on the release branch, commit/push/pull, increment beta number, and
|
||||
repeat. Operators may authorize up to 4 autonomous beta attempts; after 4
|
||||
failed published beta attempts, stop and report.
|
||||
- Use `/changelog` before version/tag preparation so the top changelog section
|
||||
is deduped and ordered by user impact.
|
||||
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
|
||||
|
||||
@@ -623,6 +623,9 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Setup Docker builder
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
|
||||
- name: Build and push shared Docker E2E image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -2,38 +2,44 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## Unreleased
|
||||
## 2026.4.23
|
||||
|
||||
### Changes
|
||||
|
||||
- Agents/tools: add optional per-call `timeoutMs` support for image, video, music, and TTS generation tools so agents can extend provider request timeouts only when a specific generation needs it.
|
||||
- Agents/subagents: add optional forked context for native `sessions_spawn` runs so agents can let a child inherit the requester transcript when needed, while keeping clean isolated sessions as the default; includes prompt guidance, context-engine hook metadata, docs, and QA coverage.
|
||||
- Codex harness: add structured debug logging for embedded harness selection decisions so `/status` stays simple while gateway logs explain auto-selection and Pi fallback reasons. (#70760) Thanks @100yenadmin.
|
||||
- Dependencies/Pi: update bundled Pi packages to `0.70.0`, use Pi's upstream `gpt-5.5` catalog metadata for OpenAI and OpenAI Codex, and keep only local `gpt-5.5-pro` forward-compat handling.
|
||||
- Providers/OpenAI: add image generation and reference-image editing through Codex OAuth, so `openai/gpt-image-2` works without an `OPENAI_API_KEY`. Fixes #70703.
|
||||
- Providers/OpenRouter: add image generation and reference-image editing through `image_generate`, so OpenRouter image models work with `OPENROUTER_API_KEY`. Fixes #55066 via #67668. Thanks @notamicrodose.
|
||||
- Image generation: let agents request provider-supported quality and output format hints, and pass OpenAI-specific background, moderation, compression, and user hints through the `image_generate` tool. (#70503) Thanks @ottodeng.
|
||||
- Agents/subagents: add optional forked context for native `sessions_spawn` runs so agents can let a child inherit the requester transcript when needed, while keeping clean isolated sessions as the default; includes prompt guidance, context-engine hook metadata, docs, and QA coverage.
|
||||
- Agents/tools: add optional per-call `timeoutMs` support for image, video, music, and TTS generation tools so agents can extend provider request timeouts only when a specific generation needs it.
|
||||
- Memory/local embeddings: add configurable `memorySearch.local.contextSize` with a 4096 default so local embedding contexts can be tuned for constrained hosts without patching the memory host. (#70544) Thanks @aalekh-sarvam.
|
||||
- Dependencies/Pi: update bundled Pi packages to `0.70.0`, use Pi's upstream `gpt-5.5` catalog metadata for OpenAI and OpenAI Codex, and keep only local `gpt-5.5-pro` forward-compat handling.
|
||||
- Codex harness: add structured debug logging for embedded harness selection decisions so `/status` stays simple while gateway logs explain auto-selection and Pi fallback reasons. (#70760) Thanks @100yenadmin.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Codex/media understanding: support `codex/*` image models through bounded Codex app-server image turns, while keeping `openai-codex/*` on the OpenAI Codex OAuth route and validating app-server responses against generated protocol contracts. Fixes #70201.
|
||||
- Providers/OpenAI Codex: synthesize the `openai-codex/gpt-5.5` OAuth model row when Codex catalog discovery omits it, so cron and subagent runs do not fail with `Unknown model` while the account is authenticated.
|
||||
- Providers/Google: honor the private-network SSRF opt-in for Gemini image generation requests, so trusted proxy setups that resolve Google API hosts to private addresses can use `image_generate`. Fixes #67216.
|
||||
- Agents/transport: stop embedded runs from lowering the process-wide undici stream timeouts, so slow Gemini image generation and other long-running provider requests no longer inherit short run-attempt headers timeouts. Fixes #70423. Thanks @giangthb.
|
||||
- Providers/OpenRouter: send image-understanding prompts as user text before image parts, restoring non-empty vision responses for OpenRouter multimodal models. Fixes #70410.
|
||||
- Memory/QMD: recreate stale managed QMD collections when startup repair finds the collection name already exists, so root memory narrows back to `MEMORY.md` instead of staying on broad workspace markdown indexing.
|
||||
- Agents/OpenAI: surface selected-model capacity failures from PI, Codex, and auto-reply harness paths with a model-switch hint instead of the generic empty-response error. Thanks @vincentkoc.
|
||||
- Models/Codex: preserve Codex provider metadata when adding models from chat or CLI commands, so manually added Codex models keep the right auth and routing behavior. (#70820) Thanks @Takhoffman.
|
||||
- Providers/OpenAI: route `openai/gpt-image-2` through configured Codex OAuth directly when an `openai-codex` profile is active, instead of probing `OPENAI_API_KEY` first.
|
||||
- Providers/OpenAI: harden image generation auth routing and Codex OAuth response parsing so fallback only applies to public OpenAI API routes and bounded SSE results. Thanks @Takhoffman.
|
||||
- OpenAI/image generation: send reference-image edits as guarded multipart uploads instead of JSON data URLs, restoring complex multi-reference `gpt-image-2` edits. Fixes #70642. Thanks @dashhuang.
|
||||
- Providers/OpenRouter: send image-understanding prompts as user text before image parts, restoring non-empty vision responses for OpenRouter multimodal models. Fixes #70410.
|
||||
- Providers/Google: honor the private-network SSRF opt-in for Gemini image generation requests, so trusted proxy setups that resolve Google API hosts to private addresses can use `image_generate`. Fixes #67216.
|
||||
- Agents/transport: stop embedded runs from lowering the process-wide undici stream timeouts, so slow Gemini image generation and other long-running provider requests no longer inherit short run-attempt headers timeouts. Fixes #70423. Thanks @giangthb.
|
||||
- Providers/OpenAI: honor the private-network SSRF opt-in for OpenAI-compatible image generation endpoints, so trusted LocalAI/LAN `image_generate` routes work without disabling SSRF checks globally. Fixes #62879. Thanks @seitzbg.
|
||||
- Providers/OpenAI: stop advertising the removed `gpt-5.3-codex-spark` Codex model through fallback catalogs, and suppress stale rows with a GPT-5.5 recovery hint.
|
||||
- Control UI/chat: persist assistant-generated images as authenticated managed media and accept paired-device tokens for assistant media fetches, so webchat history reloads keep showing generated images. (#70719, #70741) Thanks @Patrick-Erichsen.
|
||||
- Control UI/chat: queue Stop-button aborts across Gateway reconnects so a disconnected active run is canceled on reconnect instead of only clearing local UI state. (#70673) Thanks @chinar-amrutkar.
|
||||
- Memory/QMD: recreate stale managed QMD collections when startup repair finds the collection name already exists, so root memory narrows back to `MEMORY.md` instead of staying on broad workspace markdown indexing.
|
||||
- Agents/OpenAI: surface selected-model capacity failures from PI, Codex, and auto-reply harness paths with a model-switch hint instead of the generic empty-response error. Thanks @vincentkoc.
|
||||
- Plugins/QR: replace legacy `qrcode-terminal` QR rendering with bounded `qrcode-tui` helpers for plugin login/setup flows. (#65969) Thanks @vincentkoc.
|
||||
- Voice-call/realtime: wait for OpenAI session configuration before greeting or forwarding buffered audio, and reject non-allowlisted Twilio callers before stream setup. (#43501) Thanks @forrestblount.
|
||||
- ACPX/Codex: stop materializing `auth.json` bridge files for Codex ACP, Codex app-server, and Codex CLI runs; Codex-owned runtimes now use their normal `CODEX_HOME`/`~/.codex` auth path directly.
|
||||
- Auto-reply/system events: route async exec-event completion replies through the persisted session delivery context, so long-running command results return to the originating channel instead of being dropped when live origin metadata is missing. (#70258) Thanks @wzfukui.
|
||||
- OpenAI/image generation: send reference-image edits as guarded multipart uploads instead of JSON data URLs, restoring complex multi-reference `gpt-image-2` edits. Fixes #70642. Thanks @dashhuang.
|
||||
- Gateway/sessions: extend the webchat session-mutation guard to `sessions.compact` and `sessions.compaction.restore`, so `WEBCHAT_UI` clients are rejected from compaction-side session mutations consistently with the existing patch/delete guards. (#70716) Thanks @drobison00.
|
||||
- QA channel/security: reject non-HTTP(S) inbound attachment URLs before media fetch, and log rejected schemes so suspicious or misconfigured payloads are visible during debugging. (#70708) Thanks @vincentkoc.
|
||||
- Plugins/install: link the host OpenClaw package into external plugins that declare `openclaw` as a peer dependency, so peer-only plugin SDK imports resolve after install without bundling a duplicate host package. (#70462) Thanks @anishesg.
|
||||
- Plugins/Windows: refresh the packaged plugin SDK alias in place during bundled runtime dependency repair, so gateway and CLI plugin startup no longer race on `ENOTEMPTY`/`EPERM` after same-guest npm updates.
|
||||
- Teams/security: require shared Bot Framework audience tokens to name the configured Teams app via verified `appid` or `azp`, blocking cross-bot token replay on the global audience. (#70724) Thanks @vincentkoc.
|
||||
- Plugins/startup: resolve bundled plugin Jiti loads relative to the target plugin module instead of the central loader, so Bun global installs no longer hang while discovering bundled image providers. (#70073) Thanks @yidianyiko.
|
||||
- Anthropic/CLI security: derive Claude CLI `bypassPermissions` from OpenClaw's existing YOLO exec policy, preserve explicit raw Claude `--permission-mode` overrides, and strip malformed permission-mode args instead of silently falling back to a bypass. (#70723) Thanks @vincentkoc.
|
||||
@@ -43,8 +49,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Approvals/security: require explicit chat exec-approval enablement instead of auto-enabling approval clients just because approvers resolve from config or owner allowlists. (#70715) Thanks @vincentkoc.
|
||||
- Discord/security: keep native slash-command channel policy from bypassing configured owner or member restrictions, while preserving channel-policy fallback when no stricter access rule exists. (#70711) Thanks @vincentkoc.
|
||||
- Android/security: stop `ASK_OPENCLAW` intents from auto-sending injected prompts, so external app actions only prefill the draft instead of dispatching it immediately. (#70714) Thanks @vincentkoc.
|
||||
- Control UI/chat: persist assistant-generated images as authenticated managed media so webchat history reloads show the image instead of dropping it. (#70719)
|
||||
- Control UI/chat: queue Stop-button aborts across Gateway reconnects so a disconnected active run is canceled on reconnect instead of only clearing local UI state. (#70673) Thanks @chinar-amrutkar.
|
||||
- Secrets/Windows: strip UTF-8 BOMs from file-backed secrets and keep unavailable ACL checks fail-closed unless trusted file or exec providers explicitly opt into `allowInsecurePath`. (#70662) Thanks @zhanggpcsu.
|
||||
- Agents/image generation: escape ignored override values in tool warnings so parsed `MEDIA:` directives cannot be injected through unsupported model options. (#70710) Thanks @vincentkoc.
|
||||
- QQBot/security: require framework auth for `/bot-approve` so unauthorized QQ senders cannot change exec approval settings through the unauthenticated pre-dispatch slash-command path. (#70706) Thanks @vincentkoc.
|
||||
@@ -54,7 +58,8 @@ Docs: https://docs.openclaw.ai
|
||||
- WhatsApp/security: keep contact/vCard/location structured-object free text out of the inline message body and render it through fenced untrusted metadata JSON, limiting hidden prompt-injection payloads in names, phone fields, and location labels/comments.
|
||||
- Group-chat/security: keep channel-sourced group names and participant labels out of inline group system prompts and render them through fenced untrusted metadata JSON.
|
||||
- Agents/replay: preserve Kimi-style `functions.<name>:<index>` tool-call IDs during strict replay sanitization so custom OpenAI-compatible Kimi routes keep multi-turn tool use intact. (#70693) Thanks @geri4.
|
||||
- Plugins/startup: restore bundled plugin `openclaw/plugin-sdk/*` resolution from packaged installs and external runtime-deps stage roots, so Telegram/Discord no longer crash-loop with `Cannot find package 'openclaw'` after missing dependency repair.
|
||||
- Discord/replies: preserve final reply permission context through outbound delivery so Discord replies keep the same channel/member routing rules at send time.
|
||||
- Plugins/startup: restore bundled plugin `openclaw/plugin-sdk/*` resolution from packaged installs and external runtime-deps stage roots, so Telegram/Discord no longer crash-loop with `Cannot find package 'openclaw'` after missing dependency repair. (#70852) Thanks @simonemacario.
|
||||
- CLI/Claude: run the same prompt-build hooks and trigger/channel context on `claude-cli` turns as on direct embedded runs, keeping Claude Code sessions aligned with OpenClaw workspace identity, routing, and hook-driven prompt mutations. (#70625) Thanks @mbelinky.
|
||||
- Discord/plugin startup: keep subagent hooks lazy behind Discord's channel entry so packaged entry imports stay narrow and report import failures with the channel id and entry path.
|
||||
- Memory/doctor: keep root durable memory canonicalized on `MEMORY.md`, stop treating lowercase `memory.md` as a runtime fallback, and let `openclaw doctor --fix` merge true split-brain root files into `MEMORY.md` with a backup. (#70621) Thanks @mbelinky.
|
||||
@@ -258,7 +263,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Control UI: require authenticated Control UI read access before serving `/__openclaw/control-ui-config.json` when `gateway.auth` is enabled, so unauthenticated callers can no longer read bootstrap metadata. (#70247) Thanks @drobison00.
|
||||
- Gateway/restart: default session-scoped restart sentinels to a one-shot agent continuation, so chat-initiated Gateway restarts acknowledge successful boot automatically. (#70269) Thanks @obviyus.
|
||||
- Build/npm publish: fail postpublish verification when root `dist/*` files import bundled plugin runtime dependencies without mirroring them in the root package manifest, so Slack-style plugin deps cannot silently ship on the wrong module-resolution path again. (#60112) thanks @medns.
|
||||
- Gateway/sessions: extend the webchat session-mutation guard to `sessions.compact` and `sessions.compaction.restore`, so `WEBCHAT_UI` clients are rejected from compaction-side session mutations consistently with the existing patch/delete guards. (#70716) Thanks @drobison00.
|
||||
|
||||
## 2026.4.21
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
6b142e6a8aa513ccd8f9cfbf7e95fa4919fb6fca7aeaa841f57ad9e39e8901a9 config-baseline.json
|
||||
a4e167f169db58d71c385a31fa2b980772f9fee963e70dd9553f63536cae5aed config-baseline.core.json
|
||||
22d7cd6d8279146b2d79c9531a55b80b52a2c99c81338c508104729154fdd02d config-baseline.channel.json
|
||||
276eb2a0554c934b4c57c1734f6f2fafe0fed258b1a0c7e9393aa7081e6291bd config-baseline.json
|
||||
bf00f7910d8f0d8e12592e8a1c6bd0397f8e62fef2c11eb0cbd3b3a3e2a78ffe config-baseline.core.json
|
||||
8580cad7a65a9dc04a3e8f98b1e9252992aea2dedff16d5483934e4bc2841d57 config-baseline.channel.json
|
||||
a91304e3566ecc8906f199b88a2e38eaee86130aad799bf4d62921e2f0ddc1b5 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
1d2767b688414ac41305e88c830858c00947e2d7c713f1a25d86f38cd577620e plugin-sdk-api-baseline.json
|
||||
e5167477ab6aa2e67bd4361048cf5f6f8fd1cb7ee570544c634d14417f890674 plugin-sdk-api-baseline.jsonl
|
||||
793ed905cb0ba93b9a2f8c2c85c3cfb4d194dd9263353e74952bf9e382b03dc2 plugin-sdk-api-baseline.json
|
||||
032e7fd6f48344c9b3b98fd3e877e6d30cab92ed9a39dd309796cf1f0220820f plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -35,6 +35,12 @@ OpenClaw has three public release lanes:
|
||||
- Maintainers normally cut releases from a `release/YYYY.M.D` branch created
|
||||
from current `main`, so release validation and fixes do not block new
|
||||
development on `main`
|
||||
- After a release branch is cut, maintainers keep validating that branch instead
|
||||
of rebasing after every new `main` commit. If validation finds a concrete
|
||||
release issue, they may inspect `main` and backport only low-risk fixes that
|
||||
directly address the failure.
|
||||
- Stable follows a beta only after published-artifact validation passes,
|
||||
including Docker and Parallels release checks for install/update coverage.
|
||||
- If a beta tag has been pushed or published and needs a fix, maintainers cut
|
||||
the next `-beta.N` tag instead of deleting or recreating the old beta tag
|
||||
- Detailed release procedure, approvals, credentials, and recovery notes are
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.4.23",
|
||||
"version": "2026.4.23-beta.3",
|
||||
"description": "Multi-channel AI gateway with extensible messaging integrations",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
|
||||
@@ -641,6 +641,35 @@ function Stop-OpenClawUpdateProcesses {
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-FuturePluginEntries {
|
||||
$configPath = Join-Path $env:USERPROFILE '.openclaw\openclaw.json'
|
||||
if (-not (Test-Path $configPath)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
$config = Get-Content $configPath -Raw | ConvertFrom-Json -AsHashtable
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
$plugins = $config['plugins']
|
||||
if (-not ($plugins -is [hashtable])) {
|
||||
return
|
||||
}
|
||||
$entries = $plugins['entries']
|
||||
if ($entries -is [hashtable]) {
|
||||
foreach ($pluginId in @('feishu', 'whatsapp')) {
|
||||
if ($entries.ContainsKey($pluginId)) {
|
||||
$entries.Remove($pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
$allow = $plugins['allow']
|
||||
if ($allow -is [array]) {
|
||||
$plugins['allow'] = @($allow | Where-Object { $_ -notin @('feishu', 'whatsapp') })
|
||||
}
|
||||
$config | ConvertTo-Json -Depth 100 | Set-Content -Path $configPath -Encoding UTF8
|
||||
}
|
||||
|
||||
function Invoke-OpenClawUpdateWithTimeout {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$OpenClawPath,
|
||||
@@ -650,6 +679,7 @@ function Invoke-OpenClawUpdateWithTimeout {
|
||||
|
||||
$updateJob = Start-Job -ScriptBlock {
|
||||
param([string]$Path, [string]$Target)
|
||||
$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = '1'
|
||||
$output = & $Path update --tag $Target --yes --json *>&1
|
||||
[pscustomobject]@{
|
||||
ExitCode = $LASTEXITCODE
|
||||
@@ -785,6 +815,7 @@ try {
|
||||
}
|
||||
Set-Item -Path ('Env:' + $ProviderKeyEnv) -Value $ProviderKey
|
||||
$openclaw = Join-Path $env:APPDATA 'npm\openclaw.cmd'
|
||||
Remove-FuturePluginEntries
|
||||
Stop-OpenClawGatewayProcesses
|
||||
Write-ProgressLog 'update.openclaw-update'
|
||||
Invoke-OpenClawUpdateWithTimeout -OpenClawPath $openclaw -UpdateTarget $UpdateTarget
|
||||
@@ -1375,8 +1406,33 @@ if [ -z "\${$API_KEY_ENV:-}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
cd "\$HOME"
|
||||
scrub_future_plugin_entries() {
|
||||
/opt/homebrew/bin/python3 - <<'PY' || true
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path.home() / ".openclaw" / "openclaw.json"
|
||||
if not config_path.exists():
|
||||
raise SystemExit(0)
|
||||
try:
|
||||
config = json.loads(config_path.read_text())
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
plugins = config.get("plugins")
|
||||
if not isinstance(plugins, dict):
|
||||
raise SystemExit(0)
|
||||
entries = plugins.get("entries")
|
||||
if isinstance(entries, dict):
|
||||
for plugin_id in ("feishu", "whatsapp"):
|
||||
entries.pop(plugin_id, None)
|
||||
allow = plugins.get("allow")
|
||||
if isinstance(allow, list):
|
||||
plugins["allow"] = [plugin_id for plugin_id in allow if plugin_id not in {"feishu", "whatsapp"}]
|
||||
config_path.write_text(json.dumps(config, indent=2) + "\n")
|
||||
PY
|
||||
}
|
||||
stop_openclaw_gateway_processes() {
|
||||
/opt/homebrew/bin/openclaw gateway stop >/dev/null 2>&1 || true
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw gateway stop >/dev/null 2>&1 || true
|
||||
/usr/bin/pkill -9 -f openclaw-gateway || true
|
||||
/usr/bin/pkill -9 -f 'openclaw gateway run' || true
|
||||
/usr/bin/pkill -9 -f 'openclaw.mjs gateway' || true
|
||||
@@ -1386,8 +1442,9 @@ stop_openclaw_gateway_processes() {
|
||||
}
|
||||
# Stop the pre-update gateway before replacing the package. Otherwise the old
|
||||
# host can observe new plugin metadata mid-update and abort config validation.
|
||||
scrub_future_plugin_entries
|
||||
stop_openclaw_gateway_processes
|
||||
/opt/homebrew/bin/openclaw update --tag "$update_target" --yes --json
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw update --tag "$update_target" --yes --json
|
||||
# Same-guest npm upgrades can leave the old gateway process holding the old
|
||||
# bundled plugin host version. Stop it before post-update config commands.
|
||||
stop_openclaw_gateway_processes
|
||||
@@ -1473,8 +1530,33 @@ run_linux_update() {
|
||||
set -euo pipefail
|
||||
export HOME=/root
|
||||
cd "\$HOME"
|
||||
scrub_future_plugin_entries() {
|
||||
python3 - <<'PY' || true
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path.home() / ".openclaw" / "openclaw.json"
|
||||
if not config_path.exists():
|
||||
raise SystemExit(0)
|
||||
try:
|
||||
config = json.loads(config_path.read_text())
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
plugins = config.get("plugins")
|
||||
if not isinstance(plugins, dict):
|
||||
raise SystemExit(0)
|
||||
entries = plugins.get("entries")
|
||||
if isinstance(entries, dict):
|
||||
for plugin_id in ("feishu", "whatsapp"):
|
||||
entries.pop(plugin_id, None)
|
||||
allow = plugins.get("allow")
|
||||
if isinstance(allow, list):
|
||||
plugins["allow"] = [plugin_id for plugin_id in allow if plugin_id not in {"feishu", "whatsapp"}]
|
||||
config_path.write_text(json.dumps(config, indent=2) + "\n")
|
||||
PY
|
||||
}
|
||||
stop_openclaw_gateway_processes() {
|
||||
openclaw gateway stop >/dev/null 2>&1 || true
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw gateway stop >/dev/null 2>&1 || true
|
||||
pkill -9 -f openclaw-gateway || true
|
||||
pkill -9 -f 'openclaw gateway run' || true
|
||||
pkill -9 -f 'openclaw.mjs gateway' || true
|
||||
@@ -1489,8 +1571,9 @@ stop_openclaw_gateway_processes() {
|
||||
}
|
||||
# Stop the pre-update manual gateway before replacing the package. Otherwise
|
||||
# the old host can observe new plugin metadata mid-update and abort validation.
|
||||
scrub_future_plugin_entries
|
||||
stop_openclaw_gateway_processes
|
||||
openclaw update --tag "$update_target" --yes --json
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update --tag "$update_target" --yes --json
|
||||
# The fresh Linux lane starts a manual gateway; stop the old process before
|
||||
# post-update config validation sees mixed old-host/new-plugin metadata.
|
||||
stop_openclaw_gateway_processes
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { builtinModules } from "node:module";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join, relative } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
|
||||
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs";
|
||||
import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
type InstalledPackageJson = {
|
||||
version?: string;
|
||||
@@ -302,6 +302,8 @@ export function collectInstalledRootDependencyManifestErrors(packageRoot: string
|
||||
];
|
||||
}
|
||||
const missingImporters = new Map<string, Set<string>>();
|
||||
const bundledExtensionRuntimeDependencyOwners =
|
||||
collectBundledExtensionRuntimeDependencyOwners(packageRoot);
|
||||
|
||||
for (const filePath of distFiles) {
|
||||
const fileStat = lstatSync(filePath);
|
||||
@@ -324,7 +326,12 @@ export function collectInstalledRootDependencyManifestErrors(packageRoot: string
|
||||
if (
|
||||
!dependencyName ||
|
||||
NODE_BUILTIN_MODULES.has(dependencyName) ||
|
||||
declaredRuntimeDeps.has(dependencyName)
|
||||
declaredRuntimeDeps.has(dependencyName) ||
|
||||
isBundledExtensionOwnedRuntimeImport({
|
||||
dependencyName,
|
||||
ownersByDependency: bundledExtensionRuntimeDependencyOwners,
|
||||
source,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -342,6 +349,35 @@ export function collectInstalledRootDependencyManifestErrors(packageRoot: string
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function collectBundledExtensionRuntimeDependencyOwners(
|
||||
packageRoot: string,
|
||||
): Map<string, Set<string>> {
|
||||
const ownersByDependency = new Map<string, Set<string>>();
|
||||
const { manifests } = readBundledExtensionPackageJsons(packageRoot);
|
||||
for (const { id, manifest } of manifests) {
|
||||
for (const dependencyName of collectRuntimeDependencySpecs(manifest).keys()) {
|
||||
const owners = ownersByDependency.get(dependencyName) ?? new Set<string>();
|
||||
owners.add(id);
|
||||
ownersByDependency.set(dependencyName, owners);
|
||||
}
|
||||
}
|
||||
return ownersByDependency;
|
||||
}
|
||||
|
||||
function isBundledExtensionOwnedRuntimeImport(params: {
|
||||
dependencyName: string;
|
||||
ownersByDependency: Map<string, Set<string>>;
|
||||
source: string;
|
||||
}): boolean {
|
||||
const owners = params.ownersByDependency.get(params.dependencyName);
|
||||
if (!owners) {
|
||||
return false;
|
||||
}
|
||||
return [...owners].some((pluginId) =>
|
||||
params.source.includes(`//#region extensions/${pluginId}/`),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveInstalledBinaryPath(prefixDir: string, platform = process.platform): string {
|
||||
return platform === "win32"
|
||||
? join(prefixDir, "openclaw.cmd")
|
||||
|
||||
@@ -34,13 +34,15 @@ const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
|
||||
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
|
||||
const EAGER_BUNDLED_PLUGIN_DEPS_ENV = "OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS";
|
||||
const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
|
||||
const LEGACY_QA_CHANNEL_DIR = ["qa", "channel"].join("-");
|
||||
const LEGACY_QA_LAB_DIR = ["qa", "lab"].join("-");
|
||||
const LEGACY_UPDATE_COMPAT_SIDECARS = [
|
||||
{
|
||||
path: "dist/extensions/qa-channel/runtime-api.js",
|
||||
path: `dist/extensions/${LEGACY_QA_CHANNEL_DIR}/runtime-api.js`,
|
||||
content: "export {};\n",
|
||||
},
|
||||
{
|
||||
path: "dist/extensions/qa-lab/runtime-api.js",
|
||||
path: `dist/extensions/${LEGACY_QA_LAB_DIR}/runtime-api.js`,
|
||||
content: "export {};\n",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -423,14 +423,10 @@ function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: str
|
||||
execFileSync(process.execPath, [join(packageRoot, "openclaw.mjs"), "plugins", "doctor"], {
|
||||
cwd: packageRoot,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
env: createPackedCliSmokeEnv(process.env, {
|
||||
HOME: homeDir,
|
||||
OPENAI_API_KEY: "sk-openclaw-release-check",
|
||||
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
|
||||
OPENCLAW_NO_ONBOARD: "1",
|
||||
OPENCLAW_SUPPRESS_NOTES: "1",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
for (const dep of lazyDeps) {
|
||||
@@ -455,13 +451,17 @@ function runPackedCliSmoke(params: {
|
||||
|
||||
for (const args of PACKED_CLI_SMOKE_COMMANDS) {
|
||||
if (process.platform === "win32") {
|
||||
execFileSync(trustedCmdPath, ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, [...args])], {
|
||||
cwd: params.cwd,
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
});
|
||||
execFileSync(
|
||||
trustedCmdPath,
|
||||
["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, [...args])],
|
||||
{
|
||||
cwd: params.cwd,
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
execFileSync(binaryPath, [...args], {
|
||||
|
||||
@@ -893,6 +893,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -2066,6 +2069,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -3081,6 +3087,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Discord Draft Chunk Break Preference",
|
||||
help: "Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
},
|
||||
"streaming.preview.toolProgress": {
|
||||
label: "Discord Draft Tool Progress",
|
||||
help: "Show tool/progress activity in the live draft preview message (default: true). Set false to keep tool updates as separate messages.",
|
||||
},
|
||||
"retry.attempts": {
|
||||
label: "Discord Retry Attempts",
|
||||
help: "Max retry attempts for outbound Discord API calls (default: 3).",
|
||||
@@ -9417,6 +9427,27 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
],
|
||||
},
|
||||
},
|
||||
groupAllowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dmPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
groupPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
systemPrompt: {
|
||||
type: "string",
|
||||
},
|
||||
@@ -9479,42 +9510,41 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
],
|
||||
},
|
||||
tts: {
|
||||
execApprovals: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
anyOf: [
|
||||
{
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
},
|
||||
baseUrl: {
|
||||
type: "string",
|
||||
},
|
||||
apiKey: {
|
||||
type: "string",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
},
|
||||
voice: {
|
||||
type: "string",
|
||||
},
|
||||
authStyle: {
|
||||
type: "string",
|
||||
enum: ["bearer", "api-key"],
|
||||
},
|
||||
queryParams: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
approvers: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
speed: {
|
||||
type: "number",
|
||||
agentFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
sessionFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
target: {
|
||||
type: "string",
|
||||
enum: ["dm", "channel", "both"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
@@ -9637,6 +9667,27 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
],
|
||||
},
|
||||
},
|
||||
groupAllowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dmPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
groupPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
systemPrompt: {
|
||||
type: "string",
|
||||
},
|
||||
@@ -9699,6 +9750,45 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
],
|
||||
},
|
||||
execApprovals: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
approvers: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
agentFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
sessionFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
target: {
|
||||
type: "string",
|
||||
enum: ["dm", "channel", "both"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
@@ -10866,6 +10956,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -11775,6 +11868,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -12311,6 +12407,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Slack Native Streaming",
|
||||
help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Requires a reply thread target; top-level DMs stay on the non-thread fallback path.",
|
||||
},
|
||||
"streaming.preview.toolProgress": {
|
||||
label: "Slack Draft Tool Progress",
|
||||
help: "Show tool/progress activity in the live draft preview message (default: true). Set false to keep tool updates as separate messages.",
|
||||
},
|
||||
"thread.historyScope": {
|
||||
label: "Slack Thread History Scope",
|
||||
help: 'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
@@ -13058,6 +13158,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -14096,6 +14199,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -14498,6 +14604,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Telegram Draft Chunk Break Preference",
|
||||
help: "Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence).",
|
||||
},
|
||||
"streaming.preview.toolProgress": {
|
||||
label: "Telegram Draft Tool Progress",
|
||||
help: "Show tool/progress activity in the live draft preview message (default: true). Set false to keep tool updates as separate messages.",
|
||||
},
|
||||
"retry.attempts": {
|
||||
label: "Telegram Retry Attempts",
|
||||
help: "Max retry attempts for outbound Telegram API calls (default: 3).",
|
||||
|
||||
@@ -4176,12 +4176,19 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
},
|
||||
contextSize: {
|
||||
anyOf: [
|
||||
{ type: "integer", exclusiveMinimum: 0, maximum: 9007199254740991 },
|
||||
{ type: "string", const: "auto" },
|
||||
{
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "auto",
|
||||
},
|
||||
],
|
||||
title: "Local Embedding Context Size",
|
||||
description:
|
||||
'Context window size passed to node-llama-cpp when creating the embedding context (default: 4096). 4096 safely covers typical memory-search chunks (128\u2013512 tokens) while keeping non-weight VRAM bounded. Lower to 1024\u20132048 on resource-constrained hosts. Set to "auto" to let node-llama-cpp use the model\'s trained maximum \u2014 not recommended for large models (e.g. Qwen3-Embedding-8B trained on 40\u202f960 tokens can push VRAM from ~8.8\u202fGB to ~32\u202fGB).',
|
||||
'Context window size passed to node-llama-cpp when creating the embedding context (default: 4096). 4096 safely covers typical memory-search chunks (128–512 tokens) while keeping non-weight VRAM bounded. Lower to 1024–2048 on resource-constrained hosts. Set to "auto" to let node-llama-cpp use the model\'s trained maximum — not recommended for large models (e.g. Qwen3-Embedding-8B trained on 40 960 tokens can push VRAM from ~8.8 GB to ~32 GB).',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
@@ -6071,8 +6078,15 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
},
|
||||
contextSize: {
|
||||
anyOf: [
|
||||
{ type: "integer", exclusiveMinimum: 0, maximum: 9007199254740991 },
|
||||
{ type: "string", const: "auto" },
|
||||
{
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -25171,7 +25185,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
},
|
||||
"agents.defaults.memorySearch.local.contextSize": {
|
||||
label: "Local Embedding Context Size",
|
||||
help: 'Context window size passed to node-llama-cpp when creating the embedding context (default: 4096). 4096 safely covers typical memory-search chunks (128\u2013512 tokens) while keeping non-weight VRAM bounded. Lower to 1024\u20132048 on resource-constrained hosts. Set to "auto" to let node-llama-cpp use the model\'s trained maximum \u2014 not recommended for large models (e.g. Qwen3-Embedding-8B trained on 40\u202f960 tokens can push VRAM from ~8.8\u202fGB to ~32\u202fGB).',
|
||||
help: 'Context window size passed to node-llama-cpp when creating the embedding context (default: 4096). 4096 safely covers typical memory-search chunks (128–512 tokens) while keeping non-weight VRAM bounded. Lower to 1024–2048 on resource-constrained hosts. Set to "auto" to let node-llama-cpp use the model\'s trained maximum — not recommended for large models (e.g. Qwen3-Embedding-8B trained on 40 960 tokens can push VRAM from ~8.8 GB to ~32 GB).',
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"agents.defaults.memorySearch.store.path": {
|
||||
@@ -27755,6 +27769,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
tags: ["advanced", "url-secret"],
|
||||
},
|
||||
},
|
||||
version: "2026.4.23",
|
||||
version: "2026.4.23-beta.3",
|
||||
generatedAt: "2026-03-22T21:17:33.302Z",
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const LEGACY_QA_CHANNEL_DIR = ["qa", "channel"].join("-");
|
||||
const LEGACY_QA_LAB_DIR = ["qa", "lab"].join("-");
|
||||
|
||||
type NpmUpdateCompatSidecar = {
|
||||
@@ -9,7 +10,7 @@ const EMPTY_RUNTIME_SIDECAR = "export {};\n";
|
||||
|
||||
export const NPM_UPDATE_COMPAT_SIDECARS = [
|
||||
{
|
||||
path: "dist/extensions/qa-channel/runtime-api.js",
|
||||
path: `dist/extensions/${LEGACY_QA_CHANNEL_DIR}/runtime-api.js`,
|
||||
content: EMPTY_RUNTIME_SIDECAR,
|
||||
},
|
||||
{
|
||||
@@ -23,6 +24,7 @@ export const NPM_UPDATE_COMPAT_SIDECAR_PATHS = new Set<string>(
|
||||
);
|
||||
|
||||
export const NPM_UPDATE_OMITTED_BUNDLED_PLUGIN_ROOTS = new Set<string>([
|
||||
`dist/extensions/${LEGACY_QA_CHANNEL_DIR}`,
|
||||
`dist/extensions/${LEGACY_QA_LAB_DIR}`,
|
||||
"dist/extensions/qa-matrix",
|
||||
]);
|
||||
|
||||
@@ -3,10 +3,13 @@ import path from "node:path";
|
||||
import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "./npm-update-compat-sidecars.js";
|
||||
|
||||
export const PACKAGE_DIST_INVENTORY_RELATIVE_PATH = "dist/postinstall-inventory.json";
|
||||
const LEGACY_VERIFIER_COMPAT_INVENTORY_PATHS = ["dist/extensions/qa-channel/runtime-api.js"];
|
||||
const LEGACY_QA_CHANNEL_DIR = ["qa", "channel"].join("-");
|
||||
const LEGACY_QA_LAB_DIR = ["qa", "lab"].join("-");
|
||||
const LEGACY_VERIFIER_COMPAT_INVENTORY_PATHS = [
|
||||
`dist/extensions/${LEGACY_QA_CHANNEL_DIR}/runtime-api.js`,
|
||||
];
|
||||
const OMITTED_QA_EXTENSION_PREFIXES = [
|
||||
"dist/extensions/qa-channel/",
|
||||
`dist/extensions/${LEGACY_QA_CHANNEL_DIR}/`,
|
||||
`dist/extensions/${LEGACY_QA_LAB_DIR}/`,
|
||||
"dist/extensions/qa-matrix/",
|
||||
];
|
||||
|
||||
@@ -261,6 +261,47 @@ describe("installBundledRuntimeDeps", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("cleans an owned isolated execution root after copying node_modules back", () => {
|
||||
const installRoot = makeTempDir();
|
||||
const installExecutionRoot = path.join(installRoot, ".openclaw-install-stage");
|
||||
spawnSyncMock.mockImplementation((_command, _args, options) => {
|
||||
const cwd = String(options?.cwd ?? "");
|
||||
fs.mkdirSync(path.join(cwd, "node_modules", "tokenjuice"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(cwd, "node_modules", "tokenjuice", "package.json"),
|
||||
JSON.stringify({ name: "tokenjuice", version: "0.6.1" }),
|
||||
);
|
||||
return {
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
};
|
||||
});
|
||||
|
||||
installBundledRuntimeDeps({
|
||||
installRoot,
|
||||
installExecutionRoot,
|
||||
missingSpecs: ["tokenjuice@0.6.1"],
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(fs.existsSync(installExecutionRoot)).toBe(false);
|
||||
expect(
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(installRoot, "node_modules", "tokenjuice", "package.json"),
|
||||
"utf8",
|
||||
),
|
||||
),
|
||||
).toEqual({
|
||||
name: "tokenjuice",
|
||||
version: "0.6.1",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail an isolated runtime deps install when temp cleanup races", () => {
|
||||
const installRoot = makeTempDir();
|
||||
const installExecutionRoot = makeTempDir();
|
||||
@@ -370,6 +411,7 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
|
||||
const calls: Array<{
|
||||
installRoot: string;
|
||||
installExecutionRoot?: string;
|
||||
missingSpecs: string[];
|
||||
installSpecs?: string[];
|
||||
}> = [];
|
||||
@@ -391,6 +433,7 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
installRoot: pluginRoot,
|
||||
installExecutionRoot: path.join(pluginRoot, ".openclaw-install-stage"),
|
||||
missingSpecs: ["missing@2.0.0"],
|
||||
installSpecs: ["already-present@1.0.0", "missing@2.0.0", "previous@3.0.0"],
|
||||
},
|
||||
@@ -430,12 +473,62 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
installRoot: pluginRoot,
|
||||
installExecutionRoot: path.join(pluginRoot, ".openclaw-install-stage"),
|
||||
missingSpecs: ["external-runtime@^1.2.3"],
|
||||
installSpecs: ["external-runtime@^1.2.3"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("stages plugin-root install when the plugin's own package.json declares workspace:* deps", () => {
|
||||
// Regression guard for packaged/Docker bundled plugins whose `package.json`
|
||||
// still lists `"@openclaw/plugin-sdk": "workspace:*"` (and similar) alongside
|
||||
// concrete runtime deps. Without a distinct execution root, `npm install`
|
||||
// would resolve the plugin's own cwd manifest and fail with
|
||||
// EUNSUPPORTEDPROTOCOL on the `workspace:` protocol.
|
||||
const packageRoot = makeTempDir();
|
||||
const extensionsRoot = path.join(packageRoot, "dist", "extensions");
|
||||
const pluginRoot = path.join(extensionsRoot, "anthropic");
|
||||
fs.mkdirSync(pluginRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
dependencies: {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"@anthropic-ai/sdk": "^0.50.0",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const calls: BundledRuntimeDepsInstallParams[] = [];
|
||||
const result = ensureBundledPluginRuntimeDeps({
|
||||
env: {},
|
||||
installDeps: (params) => {
|
||||
calls.push(params);
|
||||
},
|
||||
pluginId: "anthropic",
|
||||
pluginRoot,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
installedSpecs: ["@anthropic-ai/sdk@^0.50.0"],
|
||||
retainSpecs: ["@anthropic-ai/sdk@^0.50.0"],
|
||||
});
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
installRoot: pluginRoot,
|
||||
installExecutionRoot: path.join(pluginRoot, ".openclaw-install-stage"),
|
||||
missingSpecs: ["@anthropic-ai/sdk@^0.50.0"],
|
||||
installSpecs: ["@anthropic-ai/sdk@^0.50.0"],
|
||||
},
|
||||
]);
|
||||
// The stage dir must be distinct from the plugin root so npm does not read
|
||||
// the plugin's cwd manifest during install.
|
||||
const installExecutionRoot = calls[0]?.installExecutionRoot;
|
||||
expect(installExecutionRoot).toBeDefined();
|
||||
expect(path.resolve(installExecutionRoot ?? "")).not.toEqual(path.resolve(pluginRoot));
|
||||
});
|
||||
|
||||
it("installs runtime deps into an external stage dir and exposes loader aliases", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
@@ -932,6 +1025,7 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
installRoot: pluginRoot,
|
||||
installExecutionRoot: path.join(pluginRoot, ".openclaw-install-stage"),
|
||||
missingSpecs: ["@mariozechner/pi-ai@0.68.1"],
|
||||
installSpecs: ["@mariozechner/pi-ai@0.68.1"],
|
||||
},
|
||||
@@ -974,6 +1068,7 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
installRoot: pluginRoot,
|
||||
installExecutionRoot: path.join(pluginRoot, ".openclaw-install-stage"),
|
||||
missingSpecs: ["ws@^8.20.0", "zod@^4.3.6"],
|
||||
installSpecs: ["ws@^8.20.0", "zod@^4.3.6"],
|
||||
},
|
||||
@@ -1018,6 +1113,7 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
installRoot: pluginRoot,
|
||||
installExecutionRoot: path.join(pluginRoot, ".openclaw-install-stage"),
|
||||
missingSpecs: ["zod@^4.3.6"],
|
||||
installSpecs: ["zod@^4.3.6"],
|
||||
},
|
||||
|
||||
@@ -42,6 +42,14 @@ export type BundledRuntimeDepsInstallRoot = {
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
const RETAINED_RUNTIME_DEPS_MANIFEST = ".openclaw-runtime-deps.json";
|
||||
// Packaged bundled plugins (Docker image, npm global install) keep their
|
||||
// `package.json` next to their entry point; running `npm install <specs>` with
|
||||
// `cwd: pluginRoot` would make npm resolve the plugin's own `workspace:*`
|
||||
// dependencies and fail with `EUNSUPPORTEDPROTOCOL`. To avoid that, stage the
|
||||
// install inside this sub-directory and move the produced `node_modules/` back
|
||||
// to the plugin root. Source-checkout installs already have their own cache
|
||||
// path and keep using it.
|
||||
const PLUGIN_ROOT_INSTALL_STAGE_DIR = ".openclaw-install-stage";
|
||||
|
||||
export type BundledRuntimeDepsNpmRunner = {
|
||||
command: string;
|
||||
@@ -817,6 +825,15 @@ export function createBundledRuntimeDependencyAliasMap(params: {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function shouldCleanBundledRuntimeDepsInstallExecutionRoot(params: {
|
||||
installRoot: string;
|
||||
installExecutionRoot: string;
|
||||
}): boolean {
|
||||
const installRoot = path.resolve(params.installRoot);
|
||||
const installExecutionRoot = path.resolve(params.installExecutionRoot);
|
||||
return installExecutionRoot.startsWith(`${installRoot}${path.sep}`);
|
||||
}
|
||||
|
||||
export function installBundledRuntimeDeps(params: {
|
||||
installRoot: string;
|
||||
installExecutionRoot?: string;
|
||||
@@ -824,39 +841,53 @@ export function installBundledRuntimeDeps(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const installExecutionRoot = params.installExecutionRoot ?? params.installRoot;
|
||||
fs.mkdirSync(params.installRoot, { recursive: true });
|
||||
fs.mkdirSync(installExecutionRoot, { recursive: true });
|
||||
if (path.resolve(installExecutionRoot) !== path.resolve(params.installRoot)) {
|
||||
fs.writeFileSync(
|
||||
path.join(installExecutionRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw-runtime-deps-install", private: true }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
const installEnv = createBundledRuntimeDepsInstallEnv(params.env);
|
||||
const npmRunner = resolveBundledRuntimeDepsNpmRunner({
|
||||
env: installEnv,
|
||||
npmArgs: createBundledRuntimeDepsInstallArgs(params.missingSpecs),
|
||||
});
|
||||
const result = spawnSync(npmRunner.command, npmRunner.args, {
|
||||
cwd: installExecutionRoot,
|
||||
encoding: "utf8",
|
||||
env: npmRunner.env ?? installEnv,
|
||||
stdio: "pipe",
|
||||
});
|
||||
if (result.status !== 0 || result.error) {
|
||||
const output = [result.error?.message, result.stderr, result.stdout]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.trim();
|
||||
throw new Error(output || "npm install failed");
|
||||
}
|
||||
if (path.resolve(installExecutionRoot) !== path.resolve(params.installRoot)) {
|
||||
const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
|
||||
if (!fs.existsSync(stagedNodeModulesDir)) {
|
||||
throw new Error("npm install did not produce node_modules");
|
||||
const isolatedExecutionRoot =
|
||||
path.resolve(installExecutionRoot) !== path.resolve(params.installRoot);
|
||||
const cleanInstallExecutionRoot =
|
||||
isolatedExecutionRoot &&
|
||||
shouldCleanBundledRuntimeDepsInstallExecutionRoot({
|
||||
installRoot: params.installRoot,
|
||||
installExecutionRoot,
|
||||
});
|
||||
try {
|
||||
fs.mkdirSync(params.installRoot, { recursive: true });
|
||||
fs.mkdirSync(installExecutionRoot, { recursive: true });
|
||||
if (isolatedExecutionRoot) {
|
||||
fs.writeFileSync(
|
||||
path.join(installExecutionRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw-runtime-deps-install", private: true }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
const installEnv = createBundledRuntimeDepsInstallEnv(params.env);
|
||||
const npmRunner = resolveBundledRuntimeDepsNpmRunner({
|
||||
env: installEnv,
|
||||
npmArgs: createBundledRuntimeDepsInstallArgs(params.missingSpecs),
|
||||
});
|
||||
const result = spawnSync(npmRunner.command, npmRunner.args, {
|
||||
cwd: installExecutionRoot,
|
||||
encoding: "utf8",
|
||||
env: npmRunner.env ?? installEnv,
|
||||
stdio: "pipe",
|
||||
});
|
||||
if (result.status !== 0 || result.error) {
|
||||
const output = [result.error?.message, result.stderr, result.stdout]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.trim();
|
||||
throw new Error(output || "npm install failed");
|
||||
}
|
||||
if (isolatedExecutionRoot) {
|
||||
const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
|
||||
if (!fs.existsSync(stagedNodeModulesDir)) {
|
||||
throw new Error("npm install did not produce node_modules");
|
||||
}
|
||||
replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
|
||||
}
|
||||
} finally {
|
||||
if (cleanInstallExecutionRoot) {
|
||||
fs.rmSync(installExecutionRoot, { recursive: true, force: true });
|
||||
}
|
||||
replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,12 +951,16 @@ export function ensureBundledPluginRuntimeDeps(params: {
|
||||
pluginRoot: params.pluginRoot,
|
||||
installSpecs,
|
||||
});
|
||||
const installExecutionRoot =
|
||||
const isPluginRootInstall = path.resolve(installRoot) === path.resolve(params.pluginRoot);
|
||||
const sourceCheckoutCacheStage =
|
||||
cacheDir &&
|
||||
path.resolve(installRoot) === path.resolve(params.pluginRoot) &&
|
||||
isPluginRootInstall &&
|
||||
resolveSourceCheckoutBundledPluginPackageRoot(params.pluginRoot)
|
||||
? cacheDir
|
||||
: undefined;
|
||||
const installExecutionRoot =
|
||||
sourceCheckoutCacheStage ??
|
||||
(isPluginRootInstall ? path.join(installRoot, PLUGIN_ROOT_INSTALL_STAGE_DIR) : undefined);
|
||||
if (
|
||||
restoreSourceCheckoutRuntimeDepsFromCache({
|
||||
cacheDir,
|
||||
|
||||
@@ -837,6 +837,24 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
describe("loadOpenClawPlugins", () => {
|
||||
it("refreshes bundled plugin-sdk aliases without deleting the shared alias directory", () => {
|
||||
const distRoot = makeTempDir();
|
||||
const pluginSdkDir = path.join(distRoot, "plugin-sdk");
|
||||
const aliasDir = path.join(distRoot, "extensions", "node_modules", "openclaw", "plugin-sdk");
|
||||
mkdirSafe(pluginSdkDir);
|
||||
mkdirSafe(aliasDir);
|
||||
fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const value = 1;\n", "utf8");
|
||||
fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 1;\n", "utf8");
|
||||
fs.writeFileSync(path.join(aliasDir, "sentinel.txt"), "keep\n", "utf8");
|
||||
|
||||
__testing.ensureOpenClawPluginSdkAlias(distRoot);
|
||||
fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 2;\n", "utf8");
|
||||
__testing.ensureOpenClawPluginSdkAlias(distRoot);
|
||||
|
||||
expect(fs.existsSync(path.join(aliasDir, "sentinel.txt"))).toBe(true);
|
||||
expect(fs.readFileSync(path.join(aliasDir, "core.js"), "utf8")).toContain("core.js");
|
||||
});
|
||||
|
||||
it("disables bundled plugins by default", () => {
|
||||
const bundledDir = makeTempDir();
|
||||
writePlugin({
|
||||
@@ -1527,35 +1545,42 @@ module.exports = {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = stageDir;
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
let registry: PluginRegistry | null = null;
|
||||
try {
|
||||
fs.chmodSync(bundledDir, 0o555);
|
||||
registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
bundledRuntimeDepsInstaller: ({ installRoot }) => {
|
||||
const depRoot = path.join(installRoot, "node_modules", "external-runtime");
|
||||
fs.mkdirSync(depRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "external-runtime",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: "./index.js",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "index.js"),
|
||||
"export default { marker: 'SDK-OK' };\n",
|
||||
"utf-8",
|
||||
);
|
||||
},
|
||||
});
|
||||
bundledRuntimeDepsInstaller: ({ installRoot }) => {
|
||||
const depRoot = path.join(installRoot, "node_modules", "external-runtime");
|
||||
fs.mkdirSync(depRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "external-runtime",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: "./index.js",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "index.js"),
|
||||
"export default { marker: 'SDK-OK' };\n",
|
||||
"utf-8",
|
||||
);
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
fs.chmodSync(bundledDir, 0o755);
|
||||
}
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "telegram")?.status).toBe("loaded");
|
||||
expect(registry?.plugins.find((entry) => entry.id === "telegram")?.status).toBe("loaded");
|
||||
expect(fs.existsSync(path.join(bundledDir, "node_modules", "openclaw"))).toBe(false);
|
||||
});
|
||||
|
||||
it("loads bundled plugins with plugin-sdk imports from a package dist root", () => {
|
||||
|
||||
@@ -689,7 +689,14 @@ function ensureOpenClawPluginSdkAlias(distRoot: string): void {
|
||||
"./plugin-sdk/*": "./plugin-sdk/*.js",
|
||||
},
|
||||
});
|
||||
fs.rmSync(pluginSdkAliasDir, { recursive: true, force: true });
|
||||
try {
|
||||
if (fs.existsSync(pluginSdkAliasDir) && !fs.lstatSync(pluginSdkAliasDir).isDirectory()) {
|
||||
fs.rmSync(pluginSdkAliasDir, { recursive: true, force: true });
|
||||
}
|
||||
} catch {
|
||||
// Another process may be creating the alias at the same time; mkdir/write
|
||||
// below will either converge or surface the real filesystem error.
|
||||
}
|
||||
fs.mkdirSync(pluginSdkAliasDir, { recursive: true });
|
||||
for (const entry of fs.readdirSync(pluginSdkDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile() || path.extname(entry.name) !== ".js") {
|
||||
@@ -727,6 +734,7 @@ export const __testing = {
|
||||
resolvePluginSdkAliasCandidateOrder,
|
||||
resolvePluginSdkAliasFile,
|
||||
resolvePluginRuntimeModulePath,
|
||||
ensureOpenClawPluginSdkAlias,
|
||||
shouldLoadChannelPluginInSetupRuntime,
|
||||
shouldPreferNativeJiti,
|
||||
toSafeImportPath,
|
||||
@@ -2208,7 +2216,6 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
);
|
||||
}
|
||||
}
|
||||
ensureOpenClawPluginSdkAlias(path.dirname(path.dirname(pluginRoot)));
|
||||
if (path.resolve(installRoot) !== path.resolve(pluginRoot)) {
|
||||
registerBundledRuntimeDependencyNodePath(installRoot);
|
||||
runtimePluginRoot = mirrorBundledPluginRuntimeRoot({
|
||||
@@ -2227,6 +2234,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
pluginRoot,
|
||||
mirroredRoot: runtimePluginRoot,
|
||||
});
|
||||
} else {
|
||||
ensureOpenClawPluginSdkAlias(path.dirname(path.dirname(pluginRoot)));
|
||||
}
|
||||
} catch (error) {
|
||||
pushPluginLoadError(`failed to install bundled runtime deps: ${String(error)}`);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { listBundledPluginPackArtifacts } from "../scripts/lib/bundled-plugin-build-entries.mjs";
|
||||
import { listPluginSdkDistArtifacts } from "../scripts/lib/plugin-sdk-entries.mjs";
|
||||
import { WORKSPACE_TEMPLATE_PACK_PATHS } from "../scripts/lib/workspace-bootstrap-smoke.mjs";
|
||||
import { collectInstalledRootDependencyManifestErrors } from "../scripts/openclaw-npm-postpublish-verify.ts";
|
||||
import {
|
||||
collectAppcastSparkleVersionErrors,
|
||||
collectBundledExtensionManifestErrors,
|
||||
@@ -77,6 +78,7 @@ describe("packed CLI smoke", () => {
|
||||
SystemRoot: "C:\\Windows",
|
||||
GITHUB_TOKEN: "redacted",
|
||||
OPENAI_API_KEY: "real-secret",
|
||||
OPENCLAW_CONFIG_PATH: "/tmp/leaky-config.json",
|
||||
},
|
||||
{ HOME: "/tmp/smoke-home", OPENCLAW_STATE_DIR: "/tmp/smoke-state" },
|
||||
),
|
||||
@@ -276,6 +278,62 @@ describe("bundled plugin root runtime mirrors", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not require root deps for root chunks sourced from the owning installed plugin", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-root-owned-installed-"));
|
||||
|
||||
try {
|
||||
mkdirSync(join(tempRoot, "dist", "extensions", "memory-lancedb"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(tempRoot, "package.json"),
|
||||
`{"name":"openclaw","dependencies":{}}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(tempRoot, "dist", "extensions", "memory-lancedb", "package.json"),
|
||||
`{"name":"@openclaw/memory-lancedb","dependencies":{"@lancedb/lancedb":"^0.27.2"}}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(tempRoot, "dist", "lancedb-runtime-7TYK-Pto.js"),
|
||||
`//#region extensions/memory-lancedb/lancedb-runtime.ts\nimport("@lancedb/lancedb");\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(tempRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("still requires root deps for root-owned installed chunks", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-root-owned-installed-missing-"));
|
||||
|
||||
try {
|
||||
mkdirSync(join(tempRoot, "dist", "extensions", "memory-lancedb"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(tempRoot, "package.json"),
|
||||
`{"name":"openclaw","dependencies":{}}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(tempRoot, "dist", "extensions", "memory-lancedb", "package.json"),
|
||||
`{"name":"@openclaw/memory-lancedb","dependencies":{"@lancedb/lancedb":"^0.27.2"}}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(tempRoot, "dist", "root-runtime.js"),
|
||||
`import("@lancedb/lancedb");\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.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not compare root mirror versions for plugin manifest deps", () => {
|
||||
expect(
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
|
||||
@@ -11,4 +11,19 @@ describe("parallels npm update smoke", () => {
|
||||
expect(script).toContain(") >&2 &");
|
||||
expect(script).toContain('wait "$pid" 2>/dev/null || true');
|
||||
});
|
||||
|
||||
it("scrubs future plugin entries before invoking old same-guest updaters", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain("Remove-FuturePluginEntries");
|
||||
expect(script).toContain("scrub_future_plugin_entries");
|
||||
expect(script).toContain('"feishu", "whatsapp"');
|
||||
expect(script).toContain("Remove-FuturePluginEntries\n Stop-OpenClawGatewayProcesses");
|
||||
expect(script).toContain("scrub_future_plugin_entries\nstop_openclaw_gateway_processes");
|
||||
expect(script).toContain("$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = '1'");
|
||||
expect(script).toContain(
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw update",
|
||||
);
|
||||
expect(script).toContain("OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user