Compare commits

...

12 Commits

Author SHA1 Message Date
Peter Steinberger
0e2bd4b3ee docs(release): clarify beta number reuse 2026-04-24 03:34:46 +01:00
Peter Steinberger
8c2235e873 fix(plugins): mirror sdk alias for external bundled deps 2026-04-24 03:33:42 +01:00
Peter Steinberger
99354fc1c9 fix(plugins): clean bundled runtime install stage 2026-04-24 03:33:42 +01:00
simonemacario
080ac622c1 fix(plugins): stage bundled-plugin runtime-dep install outside the plugin root
When a packaged bundled plugin's `pluginRoot` is used directly as the npm
execution cwd, `npm install <specs>` resolves the plugin's own
`package.json` as the project manifest and fails with
`EUNSUPPORTEDPROTOCOL: Unsupported URL Type "workspace:": workspace:*`
whenever that manifest declares a `workspace:` runtime dep (e.g.
`"@openclaw/plugin-sdk": "workspace:*"`). This takes out every plugin
with any runtime deps at gateway startup.

`ensureBundledPluginRuntimeDeps` already filters `workspace:` specs from
the CLI arguments, but npm's own resolver reads the cwd manifest
regardless, so the filter alone is not enough. The existing isolated
execution-root + `replaceNodeModulesDir` machinery handles this exact
problem for source-checkout + cache-hit installs. This change activates
the same staging path for the packaged case: when `installRoot ===
pluginRoot` and we are not in the source-checkout cache path, stage the
install inside `<pluginRoot>/.openclaw-install-stage` (which has a
minimal generated `package.json`) and move the produced `node_modules/`
back to the plugin root as before.

- Add regression test `stages plugin-root install when the plugin's own
  package.json declares workspace:* deps` covering the Docker scenario
  (mixed `workspace:*` + concrete runtime dep, e.g. anthropic-style
  `@openclaw/plugin-sdk` + `@anthropic-ai/sdk`).
- Update existing plugin-root-install expectations (`installs
  plugin-local runtime deps when one is missing`, `skips workspace-only
  runtime deps before npm install`, `installs deps that are only present
  in the package root`, `does not trust runtime deps that only resolve
  from the package root`, `does not treat sibling extension runtime deps
  as satisfying a plugin`) to assert the new `installExecutionRoot`.

Reported in #70844; same root cause as #70701, #70756, #70773, #70818,
#70839 which see the downstream "Cannot find package 'openclaw' from
plugin-runtime-deps" symptom because their
`resolveBundledRuntimeDependencyInstallRoot` resolves to an external
stage dir (clean manifest) so the install succeeds but the resulting
node_modules tree cannot satisfy the filtered-out workspace packages at
ESM import time.

## AI assistance

This PR was AI-assisted with Claude Code.

Testing degree: fully tested for the touched `bundled-runtime-deps`
install staging surface.

- `pnpm exec vitest run --config test/vitest/vitest.plugins.config.ts src/plugins/bundled-runtime-deps.test.ts` (31/31)
- `pnpm exec vitest run --config test/vitest/vitest.plugins.config.ts src/plugins/` (43/43 across 8 files)
- `pnpm exec tsgo --noEmit -p tsconfig.core.json`, `pnpm exec tsgo --noEmit -p tsconfig.core.test.json` (clean)
- `pnpm exec oxlint src/plugins/bundled-runtime-deps.ts src/plugins/bundled-runtime-deps.test.ts` (0 warnings, 0 errors)
- `node scripts/check-src-extension-import-boundary.mjs --json` and `node scripts/check-sdk-package-extension-import-boundary.mjs --json` (both `[]`)

I understand the code path changed here: packaged bundled plugins now
stage their runtime-dep install one directory below `pluginRoot` so npm
never reads the plugin's `workspace:*`-containing manifest during
install; after install completes, the produced `node_modules/` is moved
back to `pluginRoot` via the existing `replaceNodeModulesDir` helper.

Signed-off-by: Simone Macario <simone@sharly.ai>
2026-04-24 03:33:42 +01:00
Peter Steinberger
ef36ca9517 docs(changelog): credit bundled runtime deps fix 2026-04-24 03:31:41 +01:00
Peter Steinberger
c5620ddf9e fix(release): preserve plugin-local runtime deps in postpublish verify 2026-04-24 03:27:35 +01:00
Peter Steinberger
5ab5dc3900 fix(release): harden packed runtime smoke 2026-04-24 02:31:57 +01:00
Peter Steinberger
18c2531d16 chore(release): refresh plugin sdk api baseline 2026-04-24 02:12:38 +01:00
Peter Steinberger
fefc4b3d4e chore(release): refresh config docs baseline 2026-04-24 02:11:08 +01:00
Peter Steinberger
8da230a1b3 chore(release): refresh bundled channel metadata 2026-04-24 02:09:57 +01:00
Peter Steinberger
dc1ce0b2b1 docs(release): clarify beta validation flow 2026-04-24 01:59:49 +01:00
Peter Steinberger
e776922a15 chore(release): prepare 2026.4.23 beta 1 2026-04-24 01:56:25 +01:00
18 changed files with 529 additions and 146 deletions

View File

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

View File

@@ -2,36 +2,41 @@
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.
- 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.
@@ -43,8 +48,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 +57,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 +262,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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "openclaw",
"version": "2026.4.23",
"version": "2026.4.23-beta.2",
"description": "Multi-channel AI gateway with extensible messaging integrations",
"keywords": [],
"homepage": "https://github.com/openclaw/openclaw#readme",

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (128512 tokens) while keeping non-weight VRAM bounded. Lower to 10242048 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 40960 tokens can push VRAM from ~8.8GB to ~32GB).',
},
},
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 (128512 tokens) while keeping non-weight VRAM bounded. Lower to 10242048 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 40960 tokens can push VRAM from ~8.8GB to ~32GB).',
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.2",
generatedAt: "2026-03-22T21:17:33.302Z",
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1527,35 +1527,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", () => {

View File

@@ -2208,7 +2208,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 +2226,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)}`);

View File

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