refactor: simplify plugin dependency handling

Simplify plugin installation and runtime loading around package-manager-owned dependencies, with Jiti reserved for local/TS fallback paths.

Also scans npm plugin install roots so hoisted transitive dependencies are covered by dependency denylist and node_modules symlink checks.
This commit is contained in:
Peter Steinberger
2026-05-01 21:32:22 +01:00
committed by GitHub
parent 2e8e9cd6ca
commit ed8f50f240
294 changed files with 2562 additions and 25454 deletions

View File

@@ -20,8 +20,7 @@ paths:
- src/plugins/bundled-dir.ts
- src/plugins/bundled-plugin-metadata.ts
- src/plugins/bundled-public-surface-runtime-root.ts
- src/plugins/bundled-runtime-deps.ts
- src/plugins/bundled-runtime-root.ts
- src/plugins/plugin-sdk-dist-alias.ts
- src/plugins/captured-registration.ts
- src/plugins/config-activation-shared.ts
- src/plugins/config-contracts.ts

View File

@@ -25,8 +25,7 @@ paths:
- src/plugins/bundled-dir.ts
- src/plugins/bundled-plugin-metadata.ts
- src/plugins/bundled-plugin-scan.ts
- src/plugins/bundled-runtime-deps*.ts
- src/plugins/bundled-runtime-root.ts
- src/plugins/plugin-sdk-dist-alias.ts
- src/plugins/cli-registry-loader.ts
- src/plugins/config-activation-shared.ts
- src/plugins/config-contracts.ts

View File

@@ -564,9 +564,6 @@ jobs:
- name: Smoke test built bundled plugin singleton
run: pnpm test:build:singleton
- name: Smoke test built bundled runtime deps
run: pnpm test:build:bundled-runtime-deps
- name: Check CLI startup memory
run: pnpm test:startup:memory

View File

@@ -510,9 +510,3 @@ jobs:
with:
install-bun: "false"
install-deps: "true"
- name: Run fast bundled plugin Docker E2E
env:
OPENCLAW_BUNDLED_CHANNEL_DEPS_E2E_IMAGE: openclaw-bundled-channel-fast:local
OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT: 90s
run: timeout 480s pnpm test:docker:bundled-channel-deps:fast

View File

@@ -646,21 +646,6 @@ jobs:
- chunk_id: plugins-runtime-install-h
label: plugins/runtime install H
timeout_minutes: 120
- chunk_id: bundled-channels-core
label: bundled channels core
timeout_minutes: 90
- chunk_id: bundled-channels-update-a
label: bundled channels update A
timeout_minutes: 45
- chunk_id: bundled-channels-update-discord
label: bundled channels update Discord
timeout_minutes: 30
- chunk_id: bundled-channels-update-b
label: bundled channels update B
timeout_minutes: 45
- chunk_id: bundled-channels-contracts
label: bundled channels contracts
timeout_minutes: 90
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}

View File

@@ -440,7 +440,7 @@ jobs:
artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}
suite_profile: custom
docker_lanes: bundled-channel-deps-compat plugins-offline
docker_lanes: plugins-offline plugin-update
telegram_mode: mock-openai
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating
secrets:

View File

@@ -386,10 +386,10 @@ jobs:
docker_lanes="npm-onboard-channel-agent gateway-network config-reload"
;;
package)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor bundled-channel-deps-compat plugins-offline plugin-update"
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update"
;;
product)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor bundled-channel-deps-compat plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
include_openwebui=true
;;
full)

View File

@@ -63,7 +63,6 @@ COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts/lib/bundled-runtime-deps-install.mjs ./scripts/lib/bundled-runtime-deps-install.mjs
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
COPY --from=ext-deps /out/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/
@@ -268,12 +267,10 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
&& chmod 755 /app/openclaw.mjs
# Pre-create the default state and runtime-deps dirs so first-run Docker named
# volumes mounted here inherit node ownership instead of root-owned state.
# Pre-create the default state dir so first-run Docker named volumes mounted
# here inherit node ownership instead of root-owned state.
RUN install -d -m 0700 -o node -g node /home/node/.openclaw && \
install -d -m 0700 -o node -g node /var/lib/openclaw/plugin-runtime-deps && \
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /var/lib/openclaw/plugin-runtime-deps | grep -qx 'node:node 700'
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700'
ENV NODE_ENV=production

View File

@@ -23,12 +23,10 @@ services:
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-}
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-}
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
OPENCLAW_PLUGIN_STAGE_DIR: /var/lib/openclaw/plugin-runtime-deps
TZ: ${OPENCLAW_TZ:-UTC}
volumes:
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
- openclaw-plugin-runtime-deps:/var/lib/openclaw/plugin-runtime-deps
## Uncomment the lines below to enable sandbox isolation
## (agents.defaults.sandbox). Requires Docker CLI in the image
## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use
@@ -87,18 +85,13 @@ services:
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-}
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-}
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
OPENCLAW_PLUGIN_STAGE_DIR: /var/lib/openclaw/plugin-runtime-deps
TZ: ${OPENCLAW_TZ:-UTC}
volumes:
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
- openclaw-plugin-runtime-deps:/var/lib/openclaw/plugin-runtime-deps
stdin_open: true
tty: true
init: true
entrypoint: ["node", "dist/index.js"]
depends_on:
- openclaw-gateway
volumes:
openclaw-plugin-runtime-deps:

View File

@@ -11,13 +11,16 @@ QQ Bot connects to OpenClaw via the official QQ Bot API (WebSocket gateway). The
plugin supports C2C private chat, group @messages, and guild channel messages with
rich media (images, voice, video, files).
Status: bundled plugin. Direct messages, group chats, guild channels, and
Status: downloadable plugin. Direct messages, group chats, guild channels, and
media are supported. Reactions and threads are not supported.
## Bundled plugin
## Install
Current OpenClaw releases bundle QQ Bot, so normal packaged builds do not need
a separate `openclaw plugins install` step.
Install QQ Bot before setup:
```bash
openclaw plugins install @openclaw/qqbot
```
## Setup

View File

@@ -181,14 +181,14 @@ Keep `workflow_ref` and `package_ref` separate. `workflow_ref` is the trusted wo
### Suite profiles
- `smoke``npm-onboard-channel-agent`, `gateway-network`, `config-reload`
- `package``npm-onboard-channel-agent`, `doctor-switch`, `update-channel-switch`, `upgrade-survivor`, `published-upgrade-survivor`, `bundled-channel-deps-compat`, `plugins-offline`, `plugin-update`
- `package``npm-onboard-channel-agent`, `doctor-switch`, `update-channel-switch`, `upgrade-survivor`, `published-upgrade-survivor`, `plugins-offline`, `plugin-update`
- `product``package` plus `mcp-channels`, `cron-mcp-cleanup`, `openai-web-search-minimal`, `openwebui`
- `full` — full Docker release-path chunks with OpenWebUI
- `custom` — exact `docker_lanes`; required when `suite_profile=custom`
The `package` profile uses offline plugin coverage so published-package validation is not gated on live ClawHub availability. The optional Telegram lane reuses the `package-under-test` artifact in `NPM Telegram Beta E2E`, with the published npm spec path kept for standalone dispatches.
Release checks call Package Acceptance with `source=ref`, `package_ref=<release-ref>`, `workflow_ref=<release workflow ref>`, `suite_profile=custom`, `docker_lanes='bundled-channel-deps-compat plugins-offline'`, and `telegram_mode=mock-openai`. Release-path Docker chunks cover the overlapping package/update/plugin lanes; Package Acceptance keeps the artifact-native bundled-channel compat, offline plugin, and Telegram proof against the same resolved package tarball. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Set `published_upgrade_survivor_baselines=release-history` to expand the lane across a deduped history matrix: the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. Set `published_upgrade_survivor_scenarios=reported-issues` to expand the same baselines across issue-shaped fixtures for Feishu config/runtime-deps, preserved bootstrap/persona files, tilde log paths, and stale versioned runtime-deps roots. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.4-mini`, so the install and gateway proof stays fast and deterministic.
Release checks call Package Acceptance with `source=ref`, `package_ref=<release-ref>`, `workflow_ref=<release workflow ref>`, `suite_profile=custom`, `docker_lanes='plugins-offline plugin-update'`, and `telegram_mode=mock-openai`. Release-path Docker chunks cover the overlapping package/update/plugin lanes; Package Acceptance keeps offline plugin, update, and Telegram proof against the same resolved package tarball. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Set `published_upgrade_survivor_baselines=release-history` to expand the lane across a deduped history matrix: the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. Set `published_upgrade_survivor_scenarios=reported-issues` to expand the same baselines across issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, tilde log paths, and stale legacy plugin dependency roots. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.4-mini`, so the install and gateway proof stays fast and deterministic.
### Legacy compatibility windows
@@ -290,9 +290,9 @@ The reusable live/E2E workflow asks `scripts/test-docker-all.mjs --plan-json` wh
Release Docker coverage runs smaller chunked jobs with `OPENCLAW_SKIP_DOCKER_BUILD=1` so each chunk pulls only the image kind it needs and executes multiple lanes through the same weighted scheduler:
- `OPENCLAW_DOCKER_ALL_PROFILE=release-path`
- `OPENCLAW_DOCKER_ALL_CHUNK=core | package-update-openai | package-update-anthropic | package-update-core | plugins-runtime-plugins | plugins-runtime-services | plugins-runtime-install-a..h | bundled-channels`
- `OPENCLAW_DOCKER_ALL_CHUNK=core | package-update-openai | package-update-anthropic | package-update-core | plugins-runtime-plugins | plugins-runtime-services | plugins-runtime-install-a..h`
Current release Docker chunks are `core`, `package-update-openai`, `package-update-anthropic`, `package-update-core`, `plugins-runtime-plugins`, `plugins-runtime-services`, `plugins-runtime-install-a` through `plugins-runtime-install-h`, `bundled-channels-core`, `bundled-channels-update-a`, `bundled-channels-update-discord`, `bundled-channels-update-b`, and `bundled-channels-contracts`. The aggregate `bundled-channels` chunk remains available for manual one-shot reruns, and `plugins-runtime-core`, `plugins-runtime`, and `plugins-integrations` remain aggregate plugin/runtime aliases. The `install-e2e` lane alias remains the aggregate manual rerun alias for both provider installer lanes. The `bundled-channels` chunk runs split `bundled-channel-*` and `bundled-channel-update-*` lanes rather than the serial all-in-one `bundled-channel-deps` lane.
Current release Docker chunks are `core`, `package-update-openai`, `package-update-anthropic`, `package-update-core`, `plugins-runtime-plugins`, `plugins-runtime-services`, and `plugins-runtime-install-a` through `plugins-runtime-install-h`. `plugins-runtime-core`, `plugins-runtime`, and `plugins-integrations` remain aggregate plugin/runtime aliases. The `install-e2e` lane alias remains the aggregate manual rerun alias for both provider installer lanes.
OpenWebUI is folded into `plugins-runtime-services` when full release-path coverage requests it, and keeps a standalone `openwebui` chunk only for OpenWebUI-only dispatches. Bundled-channel update lanes retry once for transient npm network failures.
@@ -332,13 +332,13 @@ The pull request guard stays light: it only starts for changes under `.github/ac
### Security categories
| Category | Surface |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `/codeql-security-high/core-auth-secrets` | Auth, secrets, sandbox, cron, and gateway baseline |
| `/codeql-security-high/channel-runtime-boundary` | Core channel implementation contracts plus the channel plugin runtime, gateway, Plugin SDK, secrets, audit touchpoints |
| `/codeql-security-high/network-ssrf-boundary` | Core SSRF, IP parsing, network guard, web-fetch, and Plugin SDK SSRF policy surfaces |
| `/codeql-security-high/mcp-process-tool-boundary` | MCP servers, process execution helpers, outbound delivery, and agent tool-execution gates |
| `/codeql-security-high/plugin-trust-boundary` | Plugin install, loader, manifest, registry, runtime-dependency staging, source-loading, and Plugin SDK package contract trust surfaces |
| Category | Surface |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `/codeql-security-high/core-auth-secrets` | Auth, secrets, sandbox, cron, and gateway baseline |
| `/codeql-security-high/channel-runtime-boundary` | Core channel implementation contracts plus the channel plugin runtime, gateway, Plugin SDK, secrets, audit touchpoints |
| `/codeql-security-high/network-ssrf-boundary` | Core SSRF, IP parsing, network guard, web-fetch, and Plugin SDK SSRF policy surfaces |
| `/codeql-security-high/mcp-process-tool-boundary` | MCP servers, process execution helpers, outbound delivery, and agent tool-execution gates |
| `/codeql-security-high/plugin-trust-boundary` | Plugin install, loader, manifest, registry, package-manager install, source-loading, and Plugin SDK package contract trust surfaces |
### Platform-specific security shards

View File

@@ -52,7 +52,7 @@ Available sections:
Notes:
- Choosing where the Gateway runs always updates `gateway.mode`. You can select "Continue" without other sections if that is all you need.
- After local config writes, configure materializes newly required bundled plugin runtime dependencies. This is a narrow package-manager repair step, not a full `openclaw doctor` run. Remote gateway config does not install local plugin dependencies.
- After local config writes, configure installs selected downloadable plugins when the chosen setup path requires them. Remote gateway config does not install local plugin packages.
- Channel-oriented services (Slack/Discord/Matrix/Microsoft Teams) prompt for channel/room allowlists during setup. You can enter names or IDs; the wizard resolves names to IDs when possible.
- If you run the daemon install step, token auth requires a token, and `gateway.auth.token` is SecretRef-managed, configure validates the SecretRef but does not persist resolved plaintext token values into supervisor service environment metadata.
- If token auth requires a token and the configured token SecretRef is unresolved, configure blocks daemon install with actionable remediation guidance.

View File

@@ -44,7 +44,7 @@ Notes:
- `doctor --fix --non-interactive` reports missing or stale gateway service definitions but does not install or rewrite them outside update repair mode. Run `openclaw gateway install` for a missing service, or `openclaw gateway install --force` when you intentionally want to replace the launcher.
- State integrity checks now detect orphan transcript files in the sessions directory. Archiving them as `.deleted.<timestamp>` requires an interactive confirmation; `--fix`, `--yes`, and headless runs leave them in place.
- Doctor also scans `~/.openclaw/cron/jobs.json` (or `cron.store`) for legacy cron job shapes and can rewrite them in place before the scheduler has to auto-normalize them at runtime.
- Doctor repairs missing bundled plugin runtime dependencies without writing into packaged global installs. For root-owned npm installs or hardened systemd units, set `OPENCLAW_PLUGIN_STAGE_DIR` to a writable directory such as `/var/lib/openclaw/plugin-runtime-deps`; it can also be a path-list such as `/opt/openclaw/plugin-runtime-deps:/var/lib/openclaw/plugin-runtime-deps`, where earlier roots are read-only lookup layers and the final root is the repair target.
- Doctor cleans legacy plugin dependency staging state created by older OpenClaw versions. It also repairs missing configured downloadable plugins when the registry can resolve them.
- Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy.
- Doctor quarantines invalid plugin config by disabling the affected `plugins.entries.<id>` entry and removing its invalid `config` payload. Gateway startup already skips only that bad plugin so other plugins and channels can keep running.
- Set `OPENCLAW_SERVICE_REPAIR_POLICY=external` when another supervisor owns the gateway lifecycle. Doctor still reports gateway/service health and applies non-service repairs, but skips service install/start/restart/bootstrap and legacy service cleanup.

View File

@@ -146,7 +146,7 @@ When you set `--url`, the CLI does not fall back to config or environment creden
openclaw gateway health --url ws://127.0.0.1:18789
```
The HTTP `/healthz` endpoint is a liveness probe: it returns once the server can answer HTTP. The HTTP `/readyz` endpoint is stricter and stays red while startup plugin runtime dependencies, sidecars, channels, or configured hooks are still settling. Local or authenticated detailed readiness responses include an `eventLoop` diagnostic block with event-loop delay, event-loop utilization, CPU core ratio, and a `degraded` flag.
The HTTP `/healthz` endpoint is a liveness probe: it returns once the server can answer HTTP. The HTTP `/readyz` endpoint is stricter and stays red while startup plugin sidecars, channels, or configured hooks are still settling. Local or authenticated detailed readiness responses include an `eventLoop` diagnostic block with event-loop delay, event-loop utilization, CPU core ratio, and a `degraded` flag.
### `gateway usage-cost`

View File

@@ -49,8 +49,8 @@ Benefits:
For end-to-end provider checks, prefer `openclaw infer ...` once lower-level
provider tests are green. It exercises the shipped CLI, config loading,
default-agent resolution, bundled plugin activation, runtime-dependency repair,
and the shared capability runtime before the provider request is made.
default-agent resolution, bundled plugin activation, and the shared capability
runtime before the provider request is made.
## Command tree

View File

@@ -119,8 +119,8 @@ Gateway token options in non-interactive mode:
- With `--install-daemon`, if token mode requires a token and the configured token SecretRef is unresolved, onboarding fails closed with remediation guidance.
- With `--install-daemon`, if both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, onboarding blocks install until mode is set explicitly.
- Local onboarding writes `gateway.mode="local"` into the config. If a later config file is missing `gateway.mode`, treat that as config damage or an incomplete manual edit, not as a valid local-mode shortcut.
- Local onboarding materializes newly required bundled plugin runtime dependencies after writing config, before workspace/bootstrap, daemon install, or health checks continue. This is a narrow package-manager repair step, not a full `openclaw doctor` run.
- Remote onboarding only writes connection info for the remote Gateway and does not install local bundled plugin dependencies.
- Local onboarding installs selected downloadable plugins when the chosen setup path requires them.
- Remote onboarding only writes connection info for the remote Gateway and does not install local plugin packages.
- `--allow-unconfigured` is a separate gateway runtime escape hatch. It does not mean onboarding may omit `gateway.mode`.
Example:

View File

@@ -1,5 +1,5 @@
---
summary: "CLI reference for `openclaw plugins` (list, install, marketplace, uninstall, enable/disable, deps, doctor)"
summary: "CLI reference for `openclaw plugins` (list, install, marketplace, uninstall, enable/disable, doctor)"
read_when:
- You want to install or manage Gateway plugins or compatible bundles
- You want to debug plugin load failures
@@ -42,10 +42,6 @@ openclaw plugins disable <id>
openclaw plugins registry
openclaw plugins registry --refresh
openclaw plugins uninstall <id>
openclaw plugins deps
openclaw plugins deps --repair
openclaw plugins deps --prune
openclaw plugins deps --json
openclaw plugins doctor
openclaw plugins update <id-or-npm-spec>
openclaw plugins update --all
@@ -129,13 +125,13 @@ current OpenClaw or a local checkout until a newer npm package is published.
Bare specs and `@latest` stay on the stable track. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
If a bare install spec matches a bundled plugin id (for example `diffs`), OpenClaw installs the bundled plugin directly. To install an npm package with the same name, use an explicit scoped spec (for example `@scope/diffs`).
If a bare install spec matches an official plugin id (for example `diffs`), OpenClaw installs the catalog entry directly. To install an npm package with the same name, use an explicit scoped spec (for example `@scope/diffs`).
</Accordion>
<Accordion title="Git repositories">
Use `git:<repo>` to install directly from a git repository. Supported forms include `git:github.com/owner/repo`, `git:owner/repo`, full `https://`, `ssh://`, `git://`, `file://`, and `git@host:owner/repo.git` clone URLs. Add `@<ref>` or `#<ref>` to check out a branch, tag, or commit before install.
Git installs clone into a temporary directory, check out the requested ref when present, then use the normal plugin directory installer. That means manifest validation, dangerous-code scanning, runtime dependency staging, and install records behave like local-path installs. Recorded git installs include the source URL/ref plus the resolved commit so `openclaw plugins update` can re-resolve the source later.
Git installs clone into a temporary directory, check out the requested ref when present, then use the normal plugin directory installer. That means manifest validation, dangerous-code scanning, package-manager install work, and install records behave like npm installs. Recorded git installs include the source URL/ref plus the resolved commit so `openclaw plugins update` can re-resolve the source later.
After installing from git, use `openclaw plugins inspect <id> --runtime --json` to verify runtime registrations such as gateway methods and CLI commands. If the plugin registered a CLI root with `api.registerCli`, execute that command directly through the OpenClaw root CLI, for example `openclaw demo-plugin ping`.
@@ -245,7 +241,7 @@ directory remains inert so normal packaged installs still use compiled dist.
For runtime hook debugging:
- `openclaw plugins inspect <id> --runtime --json` shows registered hooks and diagnostics from a module-loaded inspection pass. Runtime inspection never downloads missing bundled runtime dependencies; use `openclaw plugins deps --repair` when repair is needed.
- `openclaw plugins inspect <id> --runtime --json` shows registered hooks and diagnostics from a module-loaded inspection pass. Runtime inspection never installs dependencies; use `openclaw doctor --fix` to clean legacy dependency state or install missing configured downloadable plugins.
- `openclaw gateway status --deep --require-rpc` confirms the reachable Gateway, service/process hints, config path, and RPC health.
- Non-bundled conversation hooks (`llm_input`, `llm_output`, `before_agent_finalize`, `agent_end`) require `plugins.entries.<id>.hooks.allowConversationAccess=true`.
@@ -267,21 +263,6 @@ Plugin install metadata is machine-managed state, not user config. Installs and
When OpenClaw sees shipped legacy `plugins.installs` records in config, it moves them into the plugin index and removes the config key; if either write fails, the config records are kept so the install metadata is not lost.
### Runtime deps
```bash
openclaw plugins deps
openclaw plugins deps --repair
openclaw plugins deps --prune
openclaw plugins deps --json
```
`plugins deps` inspects the packaged runtime dependency stage for OpenClaw-owned bundled plugins selected by plugin config, enabled/configured channels, configured model providers, or bundled manifest defaults. It is not the install/update path for third-party npm or ClawHub plugins.
Use `--repair` when a packaged install reports missing bundled runtime dependencies during Gateway startup or `plugins doctor`. Repair installs only missing enabled bundled-plugin deps with lifecycle scripts disabled. Use `--prune` to remove stale unknown external runtime-dependency roots left behind by older packaged layouts.
For the full plan, staging, and repair lifecycle, see [Plugin dependency resolution](/plugins/dependency-resolution).
### Uninstall
```bash
@@ -336,7 +317,7 @@ openclaw plugins inspect <id> --runtime
openclaw plugins inspect <id> --json
```
Inspect shows identity, load status, source, manifest capabilities, policy flags, diagnostics, install metadata, bundle capabilities, and any detected MCP or LSP server support without importing plugin runtime by default. Add `--runtime` to load the plugin module and include registered hooks, tools, commands, services, gateway methods, and HTTP routes. Runtime inspection fails with a repair hint when bundled runtime dependencies are missing; use `openclaw plugins deps --repair` to repair them explicitly.
Inspect shows identity, load status, source, manifest capabilities, policy flags, diagnostics, install metadata, bundle capabilities, and any detected MCP or LSP server support without importing plugin runtime by default. Add `--runtime` to load the plugin module and include registered hooks, tools, commands, services, gateway methods, and HTTP routes. Runtime inspection reports missing plugin dependencies directly; installs and repairs stay in `openclaw plugins install`, `openclaw plugins update`, and `openclaw doctor --fix`.
Plugin-owned CLI commands are installed as root `openclaw` command groups. After `inspect --runtime` shows a command under `cliCommands`, run it as `openclaw <command> ...`; for example a plugin that registers `demo-git` can be verified with `openclaw demo-git ping`.

View File

@@ -155,7 +155,7 @@ If an exact pinned npm plugin update resolves to an artifact whose integrity dif
<Note>
Post-update plugin sync failures fail the update result and stop restart follow-up work. Fix the plugin install or update error, then rerun `openclaw update`.
When the updated Gateway starts, enabled bundled plugin runtime dependencies are staged before plugin activation. Package-manager `update.run` restarts bypass the normal idle deferral and restart cooldown after the package tree has been swapped, so the old process cannot keep lazy-loading removed chunks. Service-manager restarts still drain runtime-dependency staging before closing the Gateway.
When the updated Gateway starts, plugin loading is verify-only: startup does not run package managers or mutate dependency trees. Package-manager `update.run` restarts bypass the normal idle deferral and restart cooldown after the package tree has been swapped, so the old process cannot keep lazy-loading removed chunks.
If pnpm bootstrap still fails, the updater stops early with a package-manager-specific error instead of trying `npm run build` inside the checkout.
</Note>

View File

@@ -33,9 +33,9 @@ For multi-endpoint setups, `provider` can also be a custom
`models.providers.<id>` entry, such as `ollama-5080`, when that provider sets
`api: "ollama"` or another embedding adapter owner.
For local embeddings with no API key, set `provider: "local"`. Packaged
installs retain the native `node-llama-cpp` runtime in OpenClaw's managed plugin
runtime-deps tree; run `openclaw doctor --fix` if that tree needs repair.
For local embeddings with no API key, set `provider: "local"`. Source checkouts
may still require native build approval: `pnpm approve-builds` then
`pnpm rebuild node-llama-cpp`.
Some OpenAI-compatible embedding endpoints require asymmetric labels such as
`input_type: "query"` for searches and `input_type: "document"` or `"passage"`

View File

@@ -339,10 +339,10 @@ That stages grounded durable candidates into the short-term dreaming store while
<Accordion title="7. Sandbox image repair">
When sandboxing is enabled, doctor checks Docker images and offers to build or switch to legacy names if the current image is missing.
</Accordion>
<Accordion title="7b. Bundled plugin runtime deps">
Doctor verifies runtime dependencies only for bundled plugins that are active in the current config or enabled by their bundled manifest default, for example `plugins.entries.discord.enabled: true`, legacy `channels.discord.enabled: true`, configured `models.providers.*` / agent model refs, or a default-enabled bundled plugin without provider ownership. If any are missing, doctor reports the packages and installs them in `openclaw doctor --fix` / `openclaw doctor --repair` mode. External plugins still use `openclaw plugins install` / `openclaw plugins update`; doctor does not install dependencies for arbitrary plugin paths.
<Accordion title="7b. Plugin install cleanup">
Doctor removes legacy OpenClaw-generated plugin dependency staging state in `openclaw doctor --fix` / `openclaw doctor --repair` mode. This covers stale generated dependency roots, old install-stage directories, and package-local debris from earlier bundled-plugin dependency repair code.
During doctor repair, bundled runtime-dependency npm installs report spinner progress in TTY sessions and periodic line progress in piped/headless output. Gateway startup and config reload enter plugin-plan mode before importing bundled plugin runtime modules; normal runtime imports are verify-only and do not spawn package-manager repair. These installs are scoped to the plugin runtime install root, run with scripts disabled, do not write a package lock, and are guarded by an install-root lock so concurrent CLI or Gateway starts do not mutate the same `node_modules` tree at the same time. Stale legacy locks from killed Docker/container starts are reclaimed when their owner metadata cannot prove a current process incarnation and the lock files are old.
Doctor can also reinstall configured downloadable plugins when the config references them but the local plugin registry cannot find them. Gateway startup and config reload do not run package managers; plugin installs remain explicit doctor/install/update work.
</Accordion>
<Accordion title="8. Gateway service migrations and cleanup hints">

View File

@@ -517,7 +517,7 @@ Look for:
- `browser.executablePath not found` → configured path is invalid.
- `browser.cdpUrl must be http(s) or ws(s)` → the configured CDP URL uses an unsupported scheme such as `file:` or `ftp:`.
- `browser.cdpUrl has invalid port` → the configured CDP URL has a bad or out-of-range port.
- `Playwright is not available in this gateway build; '<feature>' is unsupported.` → the current gateway install lacks the bundled browser plugin's `playwright-core` runtime dependency; run `openclaw doctor --fix`, then restart the gateway. ARIA snapshots and basic page screenshots can still work, but navigation, AI snapshots, CSS-selector element screenshots, and PDF export stay unavailable.
- `Playwright is not available in this gateway build; '<feature>' is unsupported.` → the current gateway install lacks the core browser runtime dependency; reinstall or update OpenClaw, then restart the gateway. ARIA snapshots and basic page screenshots can still work, but navigation, AI snapshots, CSS-selector element screenshots, and PDF export stay unavailable.
</Accordion>
<Accordion title="Chrome MCP / existing-session signatures">

View File

@@ -498,8 +498,8 @@ openclaw infer image generate \
```
This covers CLI argument parsing, config/default-agent resolution, bundled
plugin activation, on-demand bundled runtime-dependency repair, the shared
image-generation runtime, and the live provider request.
plugin activation, the shared image-generation runtime, and the live provider
request. Plugin dependencies are expected to be present before runtime load.
## Music generation live

View File

@@ -160,9 +160,9 @@ inside every shard.
- `pnpm test:docker:npm-onboard-channel-agent`
- Builds an npm tarball from the current checkout, installs it globally in
Docker, runs non-interactive OpenAI API-key onboarding, configures Telegram
by default, verifies enabling the plugin installs runtime dependencies on
demand, runs doctor, and runs one local agent turn against a mocked OpenAI
endpoint.
by default, verifies the packaged plugin runtime loads without startup
dependency repair, runs doctor, and runs one local agent turn against a
mocked OpenAI endpoint.
- Use `OPENCLAW_NPM_ONBOARD_CHANNEL=discord` to run the same packaged-install
lane with Discord.
- `pnpm test:docker:session-runtime-context`
@@ -227,17 +227,17 @@ gh workflow run package-acceptance.yml --ref main \
-f suite_profile=smoke
```
- `pnpm test:docker:bundled-channel-deps`
- `pnpm test:docker:plugins`
- Packs and installs the current OpenClaw build in Docker, starts the Gateway
with OpenAI configured, then enables bundled channel/plugins via config
edits.
- Verifies setup discovery leaves unconfigured plugin runtime dependencies
absent, the first configured Gateway or doctor run installs each bundled
plugin's runtime dependencies on demand, and a second restart does not
reinstall dependencies that were already activated.
- Verifies setup discovery leaves unconfigured downloadable plugins absent,
the first configured doctor repair installs each missing downloadable
plugin explicitly, and a second restart does not run hidden dependency
repair.
- Also installs a known older npm baseline, enables Telegram before running
`openclaw update --tag <candidate>`, and verifies the candidate's
post-update doctor repairs bundled channel runtime dependencies without a
post-update doctor cleans legacy plugin dependency debris without a
harness-side postinstall repair.
- `pnpm test:parallels:npm-update`
- Runs the native packaged-install update smoke across Parallels guests. Each
@@ -263,9 +263,9 @@ gh workflow run package-acceptance.yml --ref main \
- The script writes nested lane logs under `/tmp/openclaw-parallels-npm-update.*`.
Inspect `windows-update.log`, `macos-update.log`, or `linux-update.log`
before assuming the outer wrapper is hung.
- Windows update can spend 10 to 15 minutes in post-update doctor/runtime
dependency repair on a cold guest; that is still healthy when the nested
npm debug log is advancing.
- Windows update can spend 10 to 15 minutes in post-update doctor and package
update work on a cold guest; that is still healthy when the nested npm
debug log is advancing.
- Do not run this aggregate wrapper in parallel with individual Parallels
macOS, Windows, or Linux smoke lanes. They share VM state and can collide on
snapshot restore, package serving, or guest gateway state.
@@ -600,7 +600,7 @@ These Docker runners split into two buckets:
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
explicitly want the larger exhaustive scan.
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. If a single lane is heavier than the active caps, the scheduler can still start it when the pool is empty and then keeps it running alone until capacity is available again. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
- `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. `workflow_ref` selects the trusted workflow/harness scripts, while `package_ref` selects the source commit/branch/tag to pack when `source=ref`; this lets current acceptance logic validate older trusted commits. Profiles are ordered by breadth: `smoke` is quick install/channel/agent plus gateway/config, `package` is the package/update/plugin contract plus the keyless upgrade-survivor fixture, the published-baseline upgrade survivor lane, and the default native replacement for most Parallels package/update coverage, `product` adds MCP channels, cron/subagent cleanup, OpenAI web search, and OpenWebUI, and `full` runs the release-path Docker chunks with OpenWebUI. For `published-upgrade-survivor`, Package Acceptance always uses `package-under-test` as the candidate and `published_upgrade_survivor_baseline` as the fallback published baseline, defaulting to `openclaw@latest`; set `published_upgrade_survivor_baselines=release-history` to shard the lane across a deduped matrix of the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. The published lane configures its baseline with a baked `openclaw config set` command recipe, then records recipe steps in the lane summary. Release validation runs a custom package delta (`bundled-channel-deps-compat plugins-offline`) plus Telegram package QA because the release-path Docker chunks already cover the overlapping package/update/plugin lanes. Targeted GitHub Docker rerun commands generated from artifacts include prior package artifact, prepared image inputs, and the published upgrade-survivor baseline list when available, so failed lanes can avoid rebuilding the package and images.
- `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. `workflow_ref` selects the trusted workflow/harness scripts, while `package_ref` selects the source commit/branch/tag to pack when `source=ref`; this lets current acceptance logic validate older trusted commits. Profiles are ordered by breadth: `smoke` is quick install/channel/agent plus gateway/config, `package` is the package/update/plugin contract plus the keyless upgrade-survivor fixture, the published-baseline upgrade survivor lane, and the default native replacement for most Parallels package/update coverage, `product` adds MCP channels, cron/subagent cleanup, OpenAI web search, and OpenWebUI, and `full` runs the release-path Docker chunks with OpenWebUI. For `published-upgrade-survivor`, Package Acceptance always uses `package-under-test` as the candidate and `published_upgrade_survivor_baseline` as the fallback published baseline, defaulting to `openclaw@latest`; set `published_upgrade_survivor_baselines=release-history` to shard the lane across a deduped matrix of the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. The published lane configures its baseline with a baked `openclaw config set` command recipe, then records recipe steps in the lane summary. Release validation runs a custom package delta (`plugins-offline plugin-update`) plus Telegram package QA because the release-path Docker chunks already cover the overlapping package/update/plugin lanes. Targeted GitHub Docker rerun commands generated from artifacts include prior package artifact, prepared image inputs, and the published upgrade-survivor baseline list when available, so failed lanes can avoid rebuilding the package and images.
- Build and release checks run `scripts/check-cli-bootstrap-imports.mjs` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if pre-dispatch startup imports package dependencies such as Commander, prompt UI, undici, or logging before command dispatch; it also keeps the bundled gateway run chunk under budget and rejects static imports of known cold gateway paths. Packaged CLI smoke also covers root help, onboard help, doctor help, status, config schema, and a model-list command.
- Package Acceptance legacy compatibility is capped at `2026.4.25` (`2026.4.25-beta.*` included). Through that cutoff, the harness tolerates only shipped-package metadata gaps: omitted private QA inventory entries, missing `gateway install --wrapper`, missing patch files in the tarball-derived git fixture, missing persisted `update.channel`, legacy plugin install-record locations, missing marketplace install-record persistence, and config metadata migration during `plugins update`. For packages after `2026.4.25`, those paths are strict failures.
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
@@ -615,9 +615,9 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
- Observability smoke: `pnpm qa:otel:smoke` is a private QA source-checkout lane. It is intentionally not part of package Docker release lanes because the npm tarball omits QA Lab.
- Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`)
- Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`)
- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies doctor repairs activated plugin runtime deps, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`.
- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, runs doctor, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`.
- Update channel switch smoke: `pnpm test:docker:update-channel-switch` installs the packed OpenClaw tarball globally in Docker, switches from package `stable` to git `dev`, verifies the persisted channel and plugin post-update work, then switches back to package `stable` and checks update status.
- Upgrade survivor smoke: `pnpm test:docker:upgrade-survivor` installs the packed OpenClaw tarball over a dirty old-user fixture with agents, channel config, plugin allowlists, stale plugin runtime-deps state, and existing workspace/session files. It runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks config/state preservation plus startup/status budgets.
- Upgrade survivor smoke: `pnpm test:docker:upgrade-survivor` installs the packed OpenClaw tarball over a dirty old-user fixture with agents, channel config, plugin allowlists, stale plugin dependency state, and existing workspace/session files. It runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks config/state preservation plus startup/status budgets.
- Published upgrade survivor smoke: `pnpm test:docker:published-upgrade-survivor` installs `openclaw@latest` by default, seeds realistic existing-user files, configures that baseline with a baked command recipe, validates the resulting config, updates that published install to the candidate tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks configured intents, state preservation, startup, `/healthz`, `/readyz`, and RPC status budgets. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, ask the aggregate scheduler to expand exact baselines with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, and expand issue-shaped fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` such as `reported-issues`; Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
- Session runtime context smoke: `pnpm test:docker:session-runtime-context` verifies hidden runtime context transcript persistence plus doctor repair of affected duplicated prompt-rewrite branches.
- Bun global install smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g` in an isolated home, and verifies `openclaw infer image providers --json` returns bundled image providers instead of hanging. Reuse a prebuilt tarball with `OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host build with `OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0`, or copy `dist/` from a built Docker image with `OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE=openclaw-dockerfile-smoke:local`.
@@ -634,9 +634,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
Set `OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to skip the ClawHub block, or override the default kitchen-sink package/runtime pair with `OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC` and `OPENCLAW_PLUGINS_E2E_CLAWHUB_ID`. Without `OPENCLAW_CLAWHUB_URL`/`CLAWHUB_URL`, the test uses a hermetic local ClawHub fixture server.
- Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`)
- Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`)
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate and release-path bundled-channel chunks pre-pack this tarball once, then shard bundled channel checks into independent lanes, including separate update lanes for Telegram, Discord, Slack, Feishu, memory-lancedb, and ACPX. Release chunks split channel smokes, update targets, and setup/runtime contracts into `bundled-channels-core`, `bundled-channels-update-a`, `bundled-channels-update-b`, and `bundled-channels-contracts`; the aggregate `bundled-channels` chunk remains available for manual reruns. The release workflow also splits provider installer chunks and bundled plugin install/uninstall chunks; legacy `package-update`, `plugins-runtime`, and `plugins-integrations` chunks remain aggregate aliases for manual reruns. Use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly, or `OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=telegram,acpx` to narrow the update scenario. Per-scenario Docker runs default to `OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT=900s`; the multi-target update scenario defaults to `OPENCLAW_BUNDLED_CHANNEL_UPDATE_DOCKER_RUN_TIMEOUT=2400s`. The lane also verifies that `channels.<id>.enabled=false` and `plugins.entries.<id>.enabled=false` suppress doctor/runtime-dependency repair.
- Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example:
`OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 pnpm test:docker:bundled-channel-deps`.
- Plugins: `pnpm test:docker:plugins` covers install smoke, local ClawHub fixture installs, marketplace updates, npm package dependency installs, and Claude-bundle enable/inspect. `pnpm test:docker:plugin-update` covers unchanged update behavior for installed plugins.
To prebuild and reuse the shared functional image manually:

View File

@@ -122,19 +122,19 @@ Expected output:
OpenClaw runs in Docker, but Docker is not the source of truth.
All long-lived state must survive restarts, rebuilds, and reboots.
| Component | Location | Persistence mechanism | Notes |
| ------------------- | ---------------------------------------- | ---------------------- | ------------------------------------------------------------- |
| Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, `.env` |
| Model auth profiles | `/home/node/.openclaw/agents/` | Host volume mount | `agents/<agentId>/agent/auth-profiles.json` (OAuth, API keys) |
| Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state |
| Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts |
| WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login |
| Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
| Plugin runtime deps | `/var/lib/openclaw/plugin-runtime-deps/` | Docker named volume | Generated bundled plugin deps and runtime mirrors |
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
| OS packages | Container filesystem | Docker image | Do not install at runtime |
| Docker container | Ephemeral | Restartable | Safe to destroy |
| Component | Location | Persistence mechanism | Notes |
| ------------------- | ------------------------------------------------------ | ---------------------- | ------------------------------------------------------------- |
| Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, `.env` |
| Model auth profiles | `/home/node/.openclaw/agents/` | Host volume mount | `agents/<agentId>/agent/auth-profiles.json` (OAuth, API keys) |
| Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state |
| Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts |
| WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login |
| Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
| Plugin packages | `/home/node/.openclaw/npm`, `/home/node/.openclaw/git` | Host volume mount | Downloadable plugin package roots |
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
| OS packages | Container filesystem | Docker image | Do not install at runtime |
| Docker container | Ephemeral | Restartable | Safe to destroy |
## Updates

View File

@@ -126,10 +126,9 @@ The setup script accepts these optional environment variables:
| ------------------------------------------ | --------------------------------------------------------------- |
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
| `OPENCLAW_DOCKER_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
| `OPENCLAW_EXTENSIONS` | Pre-install plugin deps at build time (space-separated names) |
| `OPENCLAW_EXTENSIONS` | Include selected bundled plugin helpers at build time |
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
| `OPENCLAW_PLUGIN_STAGE_DIR` | Container path for generated bundled plugin deps and mirrors |
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_SKIP_ONBOARDING` | Skip the interactive onboarding step (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
@@ -163,11 +162,8 @@ export OTEL_SERVICE_NAME="openclaw-gateway"
```
The official OpenClaw Docker release image includes the bundled
`diagnostics-otel` plugin source. Depending on the image and cache state, the
Gateway may still stage plugin-local OpenTelemetry runtime dependencies the
first time the plugin is enabled, so allow that first boot to reach the package
registry or prewarm the image in your release lane. To enable export, allow and
enable the `diagnostics-otel` plugin in config, then set
`diagnostics-otel` plugin source. To enable export, allow and enable the
`diagnostics-otel` plugin in config, then set
`diagnostics.otel.enabled=true` or use the config example in
[OpenTelemetry export](/gateway/opentelemetry). Collector auth headers are
configured through `diagnostics.otel.headers`, not through Docker environment
@@ -273,24 +269,16 @@ That mounted config directory is where OpenClaw keeps:
- `agents/<agentId>/agent/auth-profiles.json` for stored provider OAuth/API-key auth
- `.env` for env-backed runtime secrets such as `OPENCLAW_GATEWAY_TOKEN`
Bundled plugin runtime dependencies and mirrored runtime files are generated
state, not user config. Compose stores them in the named Docker volume
`openclaw-plugin-runtime-deps` mounted at
`/var/lib/openclaw/plugin-runtime-deps`. Keeping that high-churn tree out of the
host config bind mount avoids slow Docker Desktop/WSL file operations and stale
Windows handles during cold Gateway startup.
The default Compose file sets `OPENCLAW_PLUGIN_STAGE_DIR` to that path for both
`openclaw-gateway` and `openclaw-cli`, so `openclaw doctor --fix`, channel
login/setup commands, and Gateway startup all use the same generated runtime
volume.
Installed downloadable plugins store their package state under the mounted
OpenClaw home, so plugin install records and package roots survive container
replacement. Gateway startup does not generate bundled-plugin dependency trees.
For full persistence details on VM deployments, see
[Docker VM Runtime - What persists where](/install/docker-vm-runtime#what-persists-where).
**Disk growth hotspots:** watch `media/`, session JSONL files, `cron/runs/*.jsonl`,
the `openclaw-plugin-runtime-deps` Docker volume, and rolling file logs under
`/tmp/openclaw/`.
**Disk growth hotspots:** watch `media/`, session JSONL files,
`cron/runs/*.jsonl`, installed plugin package roots, and rolling file logs
under `/tmp/openclaw/`.
### Shell helpers (optional)

View File

@@ -107,37 +107,21 @@ bun add -g openclaw@latest
<AccordionGroup>
<Accordion title="Read-only package tree">
OpenClaw treats packaged global installs as read-only at runtime, even when the global package directory is writable by the current user. Bundled plugin runtime dependencies are staged into a writable runtime directory instead of mutating the package tree. This keeps `openclaw update` from racing with a running gateway or local agent that is repairing plugin dependencies during the same install.
OpenClaw treats packaged global installs as read-only at runtime, even when the global package directory is writable by the current user. Plugin package installs live in OpenClaw-owned npm/git roots under the user config directory, and Gateway startup does not mutate the OpenClaw package tree.
Some Linux npm setups install global packages under root-owned directories such as `/usr/lib/node_modules/openclaw`. OpenClaw supports that layout through the same external staging path.
Some Linux npm setups install global packages under root-owned directories such as `/usr/lib/node_modules/openclaw`. OpenClaw supports that layout because plugin install/update commands write outside that global package directory.
</Accordion>
<Accordion title="Hardened systemd units">
Set a writable stage directory that is included in `ReadWritePaths`:
Give OpenClaw write access to its config/state roots so explicit plugin installs, plugin updates, and doctor cleanup can persist their changes:
```ini
Environment=OPENCLAW_PLUGIN_STAGE_DIR=/var/lib/openclaw/plugin-runtime-deps
ReadWritePaths=/var/lib/openclaw /home/openclaw/.openclaw /tmp
```
`OPENCLAW_PLUGIN_STAGE_DIR` also accepts a path list. OpenClaw resolves bundled plugin runtime dependencies left-to-right across the listed roots, treats earlier roots as read-only preinstalled layers, and installs or repairs only into the final writable root:
```ini
Environment=OPENCLAW_PLUGIN_STAGE_DIR=/opt/openclaw/plugin-runtime-deps:/var/lib/openclaw/plugin-runtime-deps
ReadWritePaths=/var/lib/openclaw /home/openclaw/.openclaw /tmp
```
If `OPENCLAW_PLUGIN_STAGE_DIR` is not set, OpenClaw uses `$STATE_DIRECTORY` when systemd provides it, then falls back to `~/.openclaw/plugin-runtime-deps`. The repair step treats that stage as an OpenClaw-owned local package root and ignores user npm prefix and global settings, so global-install npm config does not redirect bundled plugin dependencies into `~/node_modules` or the global package tree.
</Accordion>
<Accordion title="Disk-space preflight">
Before package updates and bundled runtime-dependency repairs, OpenClaw tries a best-effort disk-space check for the target volume. Low space produces a warning with the checked path, but does not block the update because filesystem quotas, snapshots, and network volumes can change after the check. The actual npm install, copy, and post-install verification remain authoritative.
</Accordion>
<Accordion title="Bundled plugin runtime dependencies">
Packaged installs keep bundled plugin runtime dependencies out of the read-only package tree. On startup and during `openclaw doctor --fix`, OpenClaw repairs runtime dependencies only for bundled plugins that are active in config, active through legacy channel config, or enabled by their bundled manifest default. Persisted channel auth state alone does not trigger Gateway startup runtime-dependency repair.
Explicit disablement wins. A disabled plugin or channel does not get its runtime dependencies repaired just because it exists in the package. External plugins and custom load paths still use `openclaw plugins install` or `openclaw plugins update`.
Before package updates and explicit plugin installs, OpenClaw tries a best-effort disk-space check for the target volume. Low space produces a warning with the checked path, but does not block the update because filesystem quotas, snapshots, and network volumes can change after the check. The actual package-manager install and post-install verification remain authoritative.
</Accordion>
</AccordionGroup>

View File

@@ -118,8 +118,7 @@ loader state when code or installed artifacts are actually loaded, such as:
- `PluginLoaderCacheState` and compatible active runtime registries
- jiti/module caches and public-surface loader caches used to avoid importing
the same runtime surface repeatedly
- runtime dependency mirrors and filesystem caches for installed plugin
artifacts
- filesystem caches for installed plugin artifacts
- short-lived per-call maps for path normalization or duplicate resolution
Those caches are data-plane implementation details. They must not answer

View File

@@ -258,13 +258,12 @@ dual-format packages from being partially installed as bundles.
- Third-party compatible bundles do not get startup `npm install` repair. They
should be installed through `openclaw plugins install` and ship everything
they need in the installed plugin directory.
- OpenClaw-owned packaged bundled plugins have a narrow exception: when one is
enabled, Gateway startup can repair missing declared runtime dependencies
before import. Operators can inspect or repair that stage with
`openclaw plugins deps`.
- The release pipeline is still responsible for shipping a complete bundled
dependency payload when possible (see the postpublish verification rule in
[Releasing](/reference/RELEASING)).
- OpenClaw-owned bundled plugins are either shipped lightweight in core or
downloadable through the plugin installer. Gateway startup never runs a
package manager for them.
- `openclaw doctor --fix` removes legacy staged dependency directories and can
install configured downloadable plugins that are missing from the local
plugin index.
## Security

View File

@@ -458,11 +458,10 @@ By default, the plugin starts OpenClaw's managed Codex binary locally with:
codex app-server --listen stdio://
```
The managed binary is declared as a bundled plugin runtime dependency and staged
with the rest of the `codex` plugin dependencies. This keeps the app-server
version tied to the bundled plugin instead of whichever separate Codex CLI
happens to be installed locally. Set `appServer.command` only when you
intentionally want to run a different executable.
The managed binary is shipped with the `codex` plugin package. This keeps the
app-server version tied to the bundled plugin instead of whichever separate
Codex CLI happens to be installed locally. Set `appServer.command` only when
you intentionally want to run a different executable.
By default, OpenClaw starts local Codex harness sessions in YOLO mode:
`approvalPolicy: "never"`, `approvalsReviewer: "user"`, and

View File

@@ -1,214 +1,103 @@
---
summary: "How OpenClaw plans, stages, and repairs bundled plugin runtime dependencies"
summary: "How OpenClaw installs plugin packages and resolves plugin dependencies"
read_when:
- You are debugging bundled plugin runtime dependency repair
- You are debugging plugin package installs
- You are changing plugin startup, doctor, or package-manager install behavior
- You are maintaining packaged OpenClaw installs or bundled plugin manifests
title: "Plugin dependency resolution"
sidebarTitle: "Dependencies"
---
OpenClaw does not install every bundled plugin dependency tree at package install
time. It first derives an effective plugin plan from config and plugin metadata,
then stages runtime dependencies only for bundled OpenClaw-owned plugins that
the plan can actually load.
# Plugin dependency resolution
This page covers packaged runtime dependencies for bundled OpenClaw plugins.
Third-party plugins and custom plugin paths still use explicit plugin
installation commands such as `openclaw plugins install` and
`openclaw plugins update`.
OpenClaw keeps plugin dependency work at install/update time. Runtime loading
does not run package managers, repair dependency trees, or mutate the OpenClaw
package directory.
## Responsibility split
OpenClaw owns the plan and policy:
Plugin packages own their dependency graph:
- which plugins are active for this config
- which dependency roots are writable or read-only
- when repair is allowed
- which plugin ids are staged for startup
- final checks before importing plugin runtime modules
- runtime dependencies live in the plugin package `dependencies` or
`optionalDependencies`
- SDK/core imports are peer or supplied OpenClaw imports
- local development plugins bring their own already-installed dependencies
- npm and git plugins are installed into OpenClaw-owned package roots
The package manager owns dependency convergence:
OpenClaw owns only the plugin lifecycle:
- package graph resolution
- production, optional, and peer dependency handling
- `node_modules` layout
- package integrity
- lock and install metadata
In practice, OpenClaw should decide what needs to exist. `pnpm` or `npm` should
make the filesystem match that decision.
OpenClaw also owns the per-install-root coordination lock. Package managers
protect their own install transaction, but they do not serialize OpenClaw's
manifest writes, isolated-stage copy/rename, final validation, or plugin import
against another Gateway, doctor, or CLI process touching the same runtime
dependency root.
## Effective plugin plan
The effective plugin plan is derived from config plus discovered plugin
metadata. These inputs can activate bundled plugin runtime dependencies:
- `plugins.entries.<id>.enabled`
- `plugins.allow`, `plugins.deny`, and `plugins.enabled`
- legacy channel config such as `channels.telegram.enabled`
- configured providers, models, or CLI backend references that require a plugin
- bundled manifest defaults such as `enabledByDefault`
- the installed plugin index and bundled manifest metadata
Explicit disablement wins. A disabled plugin, denied plugin id, disabled plugin
system, or disabled channel does not trigger runtime dependency repair. Persisted
auth state alone also does not activate a bundled channel or provider.
The plugin plan is the stable input. The generated dependency materialization is
an output of that plan.
## Startup flow
Gateway startup parses config and builds the startup plugin lookup table before
plugin runtime modules are loaded. Startup then stages runtime dependencies only
for the `startupPluginIds` selected by that plan.
For packaged installs, dependency staging is allowed before plugin import. After
staging, the runtime loader imports startup plugins with install repair disabled;
at that point missing dependency materialization is treated as a load failure,
not another repair loop.
When startup dependency staging is deferred behind the HTTP bind, Gateway
readiness stays blocked on the `plugin-runtime-deps` reason until the selected
startup plugin dependencies are materialized and the startup plugin runtime has
loaded.
## When repair runs
Runtime dependency repair should run when one of these is true:
- the effective plugin plan changed and adds bundled plugins that need runtime
dependencies
- the generated dependency manifest no longer matches the effective plan
- expected installed package sentinels are missing or incomplete
- `openclaw doctor --fix` or `openclaw plugins deps --repair` was requested
Runtime dependency repair should not run just because OpenClaw started. A normal
startup with an unchanged plan and complete dependency materialization should
skip package-manager work.
Commands that edit config, enable plugins, or repair doctor findings can enter
plugin plan mode once, materialize the newly required bundled dependencies, then
return to the normal command flow. Local `openclaw onboard` and
`openclaw configure` do this automatically after they successfully write config,
so the next Gateway run does not discover missing bundled plugin packages after
startup has already begun. Remote onboarding/configure stays read-only for local
runtime deps.
## Hot reload rule
Hot reload paths that can change active plugins must go back through plugin plan
mode before loading plugin runtime. The reload should compare the new effective
plugin plan with the previous one, stage missing dependencies for newly active
bundled plugins, then load or restart the affected runtime.
If a config reload does not change the effective plugin plan, it should not
repair bundled runtime dependencies.
## Package manager execution
OpenClaw writes a generated install manifest for the selected bundled runtime
dependencies and runs the package manager in the runtime dependency install
root. It prefers `pnpm` when available and falls back to the Node-bundled `npm`
runner.
The `pnpm` path uses production dependencies, disables lifecycle scripts, ignores
the workspace, and keeps the store inside the install root:
```bash
pnpm install \
--prod \
--ignore-scripts \
--ignore-workspace \
--config.frozen-lockfile=false \
--config.minimum-release-age=0 \
--config.store-dir=<install-root>/.openclaw-pnpm-store \
--config.node-linker=hoisted \
--config.virtual-store-dir=.pnpm
```
The `npm` fallback uses the safe npm install wrapper with production
dependencies, lifecycle scripts disabled, workspace mode disabled, audit
disabled, fund output disabled, legacy peer dependency behavior, and package-lock
output enabled for the generated install root.
After install, OpenClaw validates the staged dependency tree before making it
visible to the runtime dependency root. Isolated staging is copied into the
runtime dependency root and validated again.
The whole repair/materialization section is guarded by an install-root lock.
Current lock owners record PID, process start-time when available, and creation
time. Legacy locks without process start-time or creation-time evidence are only
reclaimed by filesystem age, so recycled Docker PID 1 locks recover without
expiring normal long-running current installs by age alone.
- discover the plugin source
- install or update the package when explicitly requested
- record the install metadata
- load the plugin entrypoint
- fail with an actionable error when dependencies are missing
## Install roots
Packaged installs must not mutate read-only package directories. OpenClaw can
read dependency roots from packaged layers, but writes generated runtime
dependencies to a writable stage such as:
OpenClaw uses stable per-source roots:
- `OPENCLAW_PLUGIN_STAGE_DIR`
- `$STATE_DIRECTORY`
- `~/.openclaw/plugin-runtime-deps`
- `/var/lib/openclaw/plugin-runtime-deps` in container-style installs
- npm packages install under `~/.openclaw/npm`
- git packages clone under `~/.openclaw/git`
- local/path/archive installs are copied or referenced without dependency repair
The writable root is the final materialization target. Older read-only roots are
kept as compatibility layers only when needed.
When a packaged OpenClaw update changes the versioned writable root but the
selected bundled-plugin dependency plan is still satisfied by a previous staged
root, repair reuses that previous `node_modules` tree instead of running the
package manager again. The new versioned root still gets its own current package
runtime mirror, so plugin code comes from the current OpenClaw package while
unchanged dependency trees are shared across updates. Reuse skips previous roots
with an active OpenClaw runtime-dependency lock, so a new root does not link to a
dependency tree that another Gateway, doctor, or CLI process is currently
repairing.
## Doctor and CLI commands
Use `plugins deps` to inspect or repair bundled plugin runtime dependency
materialization:
npm installs run in the npm root with:
```bash
openclaw plugins deps
openclaw plugins deps --json
openclaw plugins deps --repair
openclaw plugins deps --prune
npm install --prefix ~/.openclaw/npm <spec> --omit=dev --ignore-scripts --no-audit --no-fund
```
Use doctor when the dependency state is part of broader install health:
git installs clone or refresh the repository, then run:
```bash
openclaw doctor
npm install --omit=dev --ignore-scripts --no-audit --no-fund
```
The installed plugin then loads from that package directory, so package-local
`node_modules` resolution works the same way it does for a normal Node package.
## Local plugins
Local plugins are treated as developer-controlled directories. OpenClaw does not
run `npm install`, `pnpm install`, or dependency repair for them. If a local
plugin has dependencies, install them in that plugin before loading it.
TypeScript local plugins can use the emergency Jiti path. Packaged JavaScript
plugins load through native import/require instead of Jiti.
## Startup and reload
Gateway startup and config reload never install plugin dependencies. They read
the plugin install records, compute the entrypoint, and load it.
If a dependency is missing at runtime, the plugin fails to load and the error
should point the operator to an explicit fix:
```bash
openclaw plugins update <id>
openclaw plugins install <source>
openclaw doctor --fix
```
`plugins deps` and doctor operate on OpenClaw-owned bundled plugin runtime
dependencies selected by the effective plugin plan. They are not third-party
plugin install or update commands.
`doctor --fix` can clean legacy OpenClaw-generated dependency state and install
configured downloadable plugins that are missing from the local install records.
It does not repair dependencies for an already-installed local plugin.
## Troubleshooting
## Bundled plugins
If a packaged install reports missing bundled runtime dependencies:
Lightweight and core-critical bundled plugins are shipped as part of OpenClaw.
They should either have no heavy runtime dependency tree or be moved out to a
downloadable package on ClawHub/npm.
1. Run `openclaw plugins deps --json` to inspect the selected plan and missing
packages.
2. Run `openclaw plugins deps --repair` or `openclaw doctor --fix` to repair the
writable dependency stage.
3. If the install root is read-only, set `OPENCLAW_PLUGIN_STAGE_DIR` to a
writable path and rerun repair.
4. Restart Gateway after repair if the missing dependency blocked startup plugin
loading.
Bundled plugin manifests must not request dependency staging. Large or optional
plugin functionality should be packaged as a normal plugin and installed through
the same npm/git/ClawHub path as third-party plugins.
In source checkouts, the workspace install usually provides bundled plugin
dependencies. Run `pnpm install` for source dependency repair instead of using
packaged runtime dependency repair as the first step.
## Legacy cleanup
Older OpenClaw versions generated bundled-plugin dependency roots at startup or
during doctor repair. Current doctor cleanup removes those stale directories and
symlinks when `--fix` is used, including old `plugin-runtime-deps` roots,
`.openclaw-runtime-deps*` manifests, generated plugin `node_modules`, install
stage directories, and package-local pnpm stores.
These paths are legacy debris only. New installs should not create them.

View File

@@ -294,9 +294,9 @@ supports `${ENV_VAR}` expansion:
## Runtime dependencies
`memory-lancedb` depends on the native `@lancedb/lancedb` package. Packaged
OpenClaw installs first try the bundled runtime dependency and can repair the
plugin runtime dependency under OpenClaw state when the bundled import is not
available.
OpenClaw treats that package as part of the plugin package. Gateway startup
does not repair plugin dependencies; if the dependency is missing, reinstall or
update the plugin package and restart the Gateway.
If an older install logs a missing `dist/package.json` or missing
`@lancedb/lancedb` error during plugin load, upgrade OpenClaw and restart the

View File

@@ -355,8 +355,8 @@ Facade-loaded bundled plugin public surfaces (`api.ts`, `runtime-api.ts`,
active runtime config snapshot when OpenClaw is already running. If no runtime
snapshot exists yet, they fall back to the resolved config file on disk.
Packaged bundled plugin facades should be loaded through OpenClaw's plugin
facade loaders; direct imports from `dist/extensions/...` bypass staged runtime
dependency mirrors that packaged installs use for plugin-owned dependencies.
facade loaders; direct imports from `dist/extensions/...` bypass the manifest
and runtime sidecar checks that packaged installs use for plugin-owned code.
Provider plugins can expose a narrow plugin-local contract barrel when a
helper is intentionally provider-specific and does not belong in a generic SDK

View File

@@ -513,14 +513,14 @@ openclaw plugins install <package-name>
```
<Info>
For npm-sourced installs, `openclaw plugins install` runs project-local `npm install --ignore-scripts` (no lifecycle scripts), ignoring inherited global npm install settings. Keep plugin dependency trees pure JS/TS and avoid packages that require `postinstall` builds.
For npm-sourced installs, `openclaw plugins install` installs the package under `~/.openclaw/npm` with lifecycle scripts disabled. Keep plugin dependency trees pure JS/TS and avoid packages that require `postinstall` builds.
</Info>
<Note>
Bundled OpenClaw-owned plugins are the only startup repair exception: when a packaged install sees one enabled by plugin config, legacy channel config, or its bundled default-enabled manifest, startup installs that plugin's missing runtime dependencies before import. Operators can inspect or repair that stage with `openclaw plugins deps`. Third-party plugins should not rely on startup installs; keep using the explicit plugin installer.
Gateway startup does not install plugin dependencies. npm/git/ClawHub install flows own dependency convergence; local plugins must already have their dependencies installed.
</Note>
Bundled package-level runtime deps are explicit metadata, not inferred from built JavaScript at gateway startup. If a shared OpenClaw root dependency must be available inside the external bundled-plugin runtime mirror, declare it in `openclaw.bundle.mirroredRootRuntimeDependencies` in the root package manifest.
Bundled package metadata is explicit, not inferred from built JavaScript at gateway startup. Runtime dependencies belong in the plugin package that owns them; packaged OpenClaw startup never repairs or mirrors plugin dependencies.
## Related

View File

@@ -211,11 +211,10 @@ Validation` or from the `main`/release workflow ref so workflow logic and
- npm release preflight fails closed unless the tarball includes both
`dist/control-ui/index.html` and a non-empty `dist/control-ui/assets/` payload
so we do not ship an empty browser dashboard again
- Post-publish verification also checks that the published registry install
contains non-empty bundled plugin runtime deps under the root `dist/*`
layout. A release that ships with missing or empty bundled plugin
dependency payloads fails the postpublish verifier and cannot be promoted
to `latest`.
- Post-publish verification also checks that published plugin entrypoints and
package metadata are present in the installed registry layout. A release that
ships missing plugin runtime payloads fails the postpublish verifier and
cannot be promoted to `latest`.
- `pnpm test:install:smoke` also enforces the npm pack `unpackedSize` budget on
the candidate update tarball, so installer e2e catches accidental pack bloat
before the release publish path
@@ -370,13 +369,8 @@ Release Docker coverage includes:
`plugins-runtime-install-a`, `plugins-runtime-install-b`,
`plugins-runtime-install-c`, `plugins-runtime-install-d`,
`plugins-runtime-install-e`, `plugins-runtime-install-f`,
`plugins-runtime-install-g`, `plugins-runtime-install-h`,
`bundled-channels-core`, `bundled-channels-update-a`,
`bundled-channels-update-discord`, `bundled-channels-update-b`, and
`bundled-channels-contracts`
`plugins-runtime-install-g`, and `plugins-runtime-install-h`
- OpenWebUI coverage inside the `plugins-runtime-services` chunk when requested
- split bundled-channel dependency lanes across channel-smoke, update-target,
and setup/runtime contract chunks instead of one large bundled-channel job
- split bundled plugin install/uninstall lanes
`bundled-plugin-install-uninstall-0` through
`bundled-plugin-install-uninstall-23`
@@ -430,11 +424,11 @@ Supported candidate sources:
`OpenClaw Release Checks` runs Package Acceptance with `source=ref`,
`package_ref=<release-ref>`, `suite_profile=custom`,
`docker_lanes=bundled-channel-deps-compat plugins-offline`, and
`telegram_mode=mock-openai`. The release-path Docker chunks cover the
overlapping install, update, and plugin-update lanes; Package Acceptance keeps
artifact-native bundled-channel compat, offline plugin fixtures, and Telegram
package QA against the same resolved tarball. It is the GitHub-native
`docker_lanes=plugins-offline plugin-update`, and `telegram_mode=mock-openai`.
The release-path Docker chunks cover the overlapping install, update, and
plugin-update lanes; Package Acceptance keeps offline plugin fixtures, plugin
update, and Telegram package QA against the same resolved tarball. It is the
GitHub-native
replacement for most of the package/update coverage that previously required
Parallels. Cross-OS release checks still matter for OS-specific onboarding,
installer, and platform behavior, but package/update product validation should

View File

@@ -53,11 +53,11 @@ or Docker-facing stages need it.
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Release target | **Job:** `Resolve target ref`<br />**Backing workflow:** none<br />**Tests:** selected ref, optional expected SHA, profile, rerun group, and focused live suite filter.<br />**Rerun:** `rerun_group=release-checks`. |
| Package artifact | **Job:** `Prepare release package artifact`<br />**Backing workflow:** none<br />**Tests:** packs or resolves one candidate tarball and uploads `release-package-under-test` for downstream package-facing checks.<br />**Rerun:** the affected package, cross-OS, or live/E2E group. |
| Install smoke | **Job:** `Run install smoke`<br />**Backing workflow:** `Install Smoke`<br />**Tests:** full install path with root Dockerfile smoke image reuse, QR package install, root and gateway Docker smokes, installer Docker tests, Bun global install image-provider smoke, and fast bundled-plugin Docker E2E.<br />**Rerun:** `rerun_group=install-smoke`. |
| Install smoke | **Job:** `Run install smoke`<br />**Backing workflow:** `Install Smoke`<br />**Tests:** full install path with root Dockerfile smoke image reuse, QR package install, root and gateway Docker smokes, installer Docker tests, Bun global install image-provider smoke, and fast bundled-plugin install/uninstall E2E.<br />**Rerun:** `rerun_group=install-smoke`. |
| Cross-OS | **Job:** `cross_os_release_checks`<br />**Backing workflow:** `OpenClaw Cross-OS Release Checks (Reusable)`<br />**Tests:** fresh and upgrade lanes on Linux, Windows, and macOS for the selected provider and mode, using the candidate tarball plus a baseline package.<br />**Rerun:** `rerun_group=cross-os`. |
| Repo and live E2E | **Job:** `Run repo/live E2E validation`<br />**Backing workflow:** `OpenClaw Live And E2E Checks (Reusable)`<br />**Tests:** repository E2E, live cache, OpenAI websocket streaming, native live provider and plugin shards, and Docker-backed live model/backend/gateway harnesses selected by `release_profile`.<br />**Rerun:** `rerun_group=live-e2e`, optionally with `live_suite_filter`. |
| Docker release path | **Job:** `Run Docker release-path validation`<br />**Backing workflow:** `OpenClaw Live And E2E Checks (Reusable)`<br />**Tests:** release-path Docker chunks against the shared package artifact.<br />**Rerun:** `rerun_group=live-e2e`. |
| Package Acceptance | **Job:** `Run package acceptance`<br />**Backing workflow:** `Package Acceptance`<br />**Tests:** artifact-native bundled-channel dependency compatibility, offline plugin package fixtures, and mock-OpenAI Telegram package acceptance against the same tarball.<br />**Rerun:** `rerun_group=package`. |
| Package Acceptance | **Job:** `Run package acceptance`<br />**Backing workflow:** `Package Acceptance`<br />**Tests:** offline plugin package fixtures, plugin update, and mock-OpenAI Telegram package acceptance against the same tarball.<br />**Rerun:** `rerun_group=package`. |
| QA parity | **Job:** `Run QA Lab parity lane` and `Run QA Lab parity report`<br />**Backing workflow:** direct jobs<br />**Tests:** candidate and baseline agentic parity packs, then the parity report.<br />**Rerun:** `rerun_group=qa-parity` or `rerun_group=qa`. |
| QA live Matrix | **Job:** `Run QA Lab live Matrix lane`<br />**Backing workflow:** direct job<br />**Tests:** fast live Matrix QA profile in the `qa-live-shared` environment.<br />**Rerun:** `rerun_group=qa-live` or `rerun_group=qa`. |
| QA live Telegram | **Job:** `Run QA Lab live Telegram lane`<br />**Backing workflow:** direct job<br />**Tests:** live Telegram QA with Convex CI credential leases.<br />**Rerun:** `rerun_group=qa-live` or `rerun_group=qa`. |
@@ -68,18 +68,15 @@ or Docker-facing stages need it.
The Docker release-path stage runs these chunks when `live_suite_filter` is
empty:
| Chunk | Coverage |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `core` | Core Docker release-path smoke lanes. |
| `package-update-openai` | OpenAI package install and update behavior. |
| `package-update-anthropic` | Anthropic package install and update behavior. |
| `package-update-core` | Provider-neutral package and update behavior. |
| `plugins-runtime-plugins` | Plugin runtime lanes that exercise plugin behavior. |
| `plugins-runtime-services` | Service-backed plugin runtime lanes; includes OpenWebUI when requested. |
| `plugins-runtime-install-a` through `plugins-runtime-install-h` | Plugin install/runtime batches split for parallel release validation. |
| `bundled-channels-core` | Bundled channel Docker behavior. |
| `bundled-channels-update-a`, `bundled-channels-update-discord`, `bundled-channels-update-b` | Bundled channel update behavior. |
| `bundled-channels-contracts` | Bundled channel contract checks in the Docker release path. |
| Chunk | Coverage |
| --------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `core` | Core Docker release-path smoke lanes. |
| `package-update-openai` | OpenAI package install and update behavior. |
| `package-update-anthropic` | Anthropic package install and update behavior. |
| `package-update-core` | Provider-neutral package and update behavior. |
| `plugins-runtime-plugins` | Plugin runtime lanes that exercise plugin behavior. |
| `plugins-runtime-services` | Service-backed plugin runtime lanes; includes OpenWebUI when requested. |
| `plugins-runtime-install-a` through `plugins-runtime-install-h` | Plugin install/runtime batches split for parallel release validation. |
Use targeted `docker_lanes=<lane[,lane]>` on the reusable live/E2E workflow when
only one Docker lane failed. The release artifacts include per-lane rerun

View File

@@ -284,7 +284,7 @@ For custom OpenAI-compatible endpoints or overriding provider defaults:
| `local.modelCacheDir` | `string` | node-llama-cpp default | Cache dir for downloaded models |
| `local.contextSize` | `number \| "auto"` | `4096` | Context window size for the embedding context. 4096 covers typical chunks (128512 tokens) while bounding non-weight VRAM. Lower to 10242048 on constrained hosts. `"auto"` uses the model's trained maximum — not recommended for 8B+ models (Qwen3-Embedding-8B: 40 960 tokens → ~32 GB VRAM vs ~8.8 GB at 4096). |
Default model: `embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB, auto-downloaded). Packaged installs repair the native `node-llama-cpp` runtime through managed plugin runtime deps when `provider: "local"` is configured. Source checkouts still require native build approval: `pnpm approve-builds` then `pnpm rebuild node-llama-cpp`.
Default model: `embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB, auto-downloaded). Source checkouts still require native build approval: `pnpm approve-builds` then `pnpm rebuild node-llama-cpp`.
Use the standalone CLI to verify the same provider path the Gateway uses:

View File

@@ -42,8 +42,8 @@ title: "Tests"
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:codex`, `pnpm test:docker:live-cli-backend:codex:resume`, or `pnpm test:docker:live-cli-backend:codex:mcp`. Claude and Gemini have matching `:resume` and `:mcp` aliases.
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale plugin runtime-deps state, startup, and RPC status survive.
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config/runtime-deps state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale legacy plugin dependency state, startup, and RPC status survive.
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config and legacy dependency state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
## Local PR gate

View File

@@ -809,8 +809,8 @@ permission modes, see
| `ACP is disabled by policy (acp.enabled=false)` | ACP globally disabled. | Set `acp.enabled=true`. |
| `ACP dispatch is disabled by policy (acp.dispatch.enabled=false)` | Automatic dispatch from normal thread messages disabled. | Set `acp.dispatch.enabled=true` to resume automatic thread routing; explicit `sessions_spawn({ runtime: "acp" })` calls still work. |
| `ACP agent "<id>" is not allowed by policy` | Agent not in allowlist. | Use allowed `agentId` or update `acp.allowedAgents`. |
| `/acp doctor` reports backend not ready right after startup | Plugin dependency probe or self-repair is still running. | Wait briefly and rerun `/acp doctor`; if it stays unhealthy, inspect the backend install error and plugin allow/deny policy. |
| Harness command not found | Adapter CLI is not installed, staged plugin deps are missing, or first-run `npx` fetch failed for a non-Codex adapter. | Run `/acp doctor`, repair plugin dependencies, install/prewarm the adapter on the Gateway host, or configure the acpx agent command explicitly. |
| `/acp doctor` reports backend not ready right after startup | Backend plugin is missing, disabled, blocked by allow/deny policy, or its configured executable is unavailable. | Install/enable the backend plugin, rerun `/acp doctor`, and inspect the backend install or policy error if it stays unhealthy. |
| Harness command not found | Adapter CLI is not installed, the external plugin is missing, or first-run `npx` fetch failed for a non-Codex adapter. | Run `/acp doctor`, install/prewarm the adapter on the Gateway host, or configure the acpx agent command explicitly. |
| Model-not-found from the harness | Model id is valid for another provider/harness but not this ACP target. | Use a model listed by that harness, configure the model in the harness, or omit the override. |
| Vendor auth error from the harness | OpenClaw is healthy, but the target CLI/provider is not logged in. | Log in or provide the required provider key on the Gateway host environment. |
| `Unable to resolve session target: ...` | Bad key/id/label token. | Run `/acp sessions`, copy exact key/label, retry. |

View File

@@ -96,10 +96,10 @@ What still needs Playwright:
Element screenshots also reject `--full-page`; the route returns `fullPage is
not supported for element screenshots`.
If you see `Playwright is not available in this gateway build`, repair the
bundled browser plugin runtime dependencies so `playwright-core` is installed,
then restart the gateway. For packaged installs, run `openclaw doctor --fix`.
For Docker, also install the Chromium browser binaries as shown below.
If you see `Playwright is not available in this gateway build`, the packaged
Gateway is missing the core browser runtime dependency. Reinstall or update
OpenClaw, then restart the gateway. For Docker, also install the Chromium
browser binaries as shown below.
#### Docker Playwright install

View File

@@ -26,6 +26,11 @@ When enabled, the plugin prepends concise usage guidance into system-prompt spac
## Quick start
<Steps>
<Step title="Install the plugin">
```bash
openclaw plugins install diffs
```
</Step>
<Step title="Enable the plugin">
```json5
{

View File

@@ -96,22 +96,15 @@ Gateway startup skips plugin discovery/load work and `openclaw doctor` preserves
the disabled plugin config instead of auto-removing it. Re-enable plugins before
running doctor cleanup if you want stale plugin ids removed.
Packaged OpenClaw installs do not eagerly install every bundled plugin's
runtime dependency tree. When a bundled OpenClaw-owned plugin is active from
plugin config, legacy channel config, or a default-enabled manifest, startup
repairs only that plugin's declared runtime dependencies before importing it.
Persisted channel auth state alone does not activate a bundled channel for
Gateway startup runtime-dependency repair.
Explicit disablement still wins: `plugins.entries.<id>.enabled: false`,
`plugins.deny`, `plugins.enabled: false`, and `channels.<id>.enabled: false`
prevent automatic bundled runtime-dependency repair for that plugin/channel.
A non-empty `plugins.allow` also bounds default-enabled bundled runtime-dependency
repair; explicit bundled channel enablement (`channels.<id>.enabled: true`) can
still repair that channel's plugin dependencies.
External plugins and custom load paths must still be installed through
`openclaw plugins install`.
See [Plugin dependency resolution](/plugins/dependency-resolution) for the full
planning and staging lifecycle.
Plugin dependency installation happens only during explicit install/update or
doctor repair flows. Gateway startup, config reload, and runtime inspection do
not run package managers or repair dependency trees. Local plugins must already
have their dependencies installed, while npm, git, and ClawHub plugins are
installed under OpenClaw's managed plugin roots with package-local
dependencies. External plugins and custom load paths must still be installed
through `openclaw plugins install`.
See [Plugin dependency resolution](/plugins/dependency-resolution) for the
install-time lifecycle.
## Plugin types

View File

@@ -14,9 +14,6 @@
"openclaw": {
"extensions": [
"./index.ts"
],
"bundle": {
"stageRuntimeDependencies": true
}
]
}
}

View File

@@ -177,7 +177,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
});
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.1"');
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.4"');
expect(wrapper).toContain('"--", "claude-agent-acp"');
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.0");
@@ -379,7 +379,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
rawConfig: {
agents: {
claude: {
command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.1 --permission-mode bypass",
command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.4 --permission-mode bypass",
},
},
},
@@ -425,7 +425,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
const root = await makeTempDir();
const stateDir = path.join(root, "state");
const command =
"node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.1 --flag";
"node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.4 --flag";
const pluginConfig = resolveAcpxPluginConfig({
rawConfig: {
agents: {

View File

@@ -7,7 +7,7 @@ const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
const CODEX_ACP_PACKAGE_RANGE = "^0.12.0";
const CODEX_ACP_BIN = "codex-acp";
const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
const CLAUDE_ACP_PACKAGE_VERSION = "0.31.1";
const CLAUDE_ACP_PACKAGE_VERSION = "0.31.4";
const CLAUDE_ACP_BIN = "claude-agent-acp";
const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
const requireFromHere = createRequire(import.meta.url);

View File

@@ -4,15 +4,10 @@ import { describe, expect, it } from "vitest";
type AcpxPackageManifest = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
openclaw?: {
bundle?: {
stageRuntimeDependencies?: boolean;
};
};
};
describe("acpx package manifest", () => {
it("opts into staging bundled runtime dependencies", () => {
it("keeps runtime dependencies in the package manifest", () => {
const packageJson = JSON.parse(
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as AcpxPackageManifest;
@@ -21,6 +16,5 @@ describe("acpx package manifest", () => {
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.12.0");
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.31.4");
expect(packageJson.devDependencies?.["@agentclientprotocol/claude-agent-acp"]).toBeUndefined();
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
});
});

View File

@@ -13,9 +13,6 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]

View File

@@ -13,9 +13,6 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]

View File

@@ -13,9 +13,6 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]

View File

@@ -11,9 +11,6 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]

View File

@@ -12,9 +12,6 @@
"openclaw": {
"extensions": [
"./index.ts"
],
"bundle": {
"stageRuntimeDependencies": true
}
]
}
}

View File

@@ -84,7 +84,7 @@ export async function requirePwAi(
501,
[
`Playwright is not available in this gateway build; '${feature}' is unsupported.`,
"Repair the bundled browser plugin runtime dependencies so playwright-core is installed, then restart the gateway. In Docker, also install Chromium with the bundled playwright-core CLI.",
"Reinstall or update OpenClaw so the core browser runtime dependency is present, then restart the gateway. In Docker, also install Chromium with the bundled playwright-core CLI.",
"Docs: /tools/browser#playwright-requirement",
].join("\n"),
);

View File

@@ -16,9 +16,6 @@
"openclaw": {
"extensions": [
"./index.ts"
],
"bundle": {
"stageRuntimeDependencies": true
}
]
}
}

View File

@@ -105,7 +105,7 @@ async function findManagedCodexAppServerCommandPath(params: {
throw new Error(
[
`Managed Codex app-server binary was not found for ${MANAGED_CODEX_APP_SERVER_PACKAGE}.`,
"Run OpenClaw with bundled plugin runtime dependencies enabled, or run pnpm install in a source checkout.",
"Reinstall or update OpenClaw, or run pnpm install in a source checkout.",
"Set plugins.entries.codex.config.appServer.command or OPENCLAW_CODEX_APP_SERVER_BIN to use a custom Codex binary.",
].join(" "),
);

View File

@@ -4,15 +4,10 @@ import { MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION } from "./app-server/version.j
type CodexPackageManifest = {
dependencies?: Record<string, string>;
openclaw?: {
bundle?: {
stageRuntimeDependencies?: boolean;
};
};
};
describe("codex package manifest", () => {
it("opts into staging bundled runtime dependencies", () => {
it("keeps runtime dependencies in the package manifest", () => {
const packageJson = JSON.parse(
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as CodexPackageManifest;
@@ -21,6 +16,5 @@ describe("codex package manifest", () => {
expect(packageJson.dependencies?.["@openai/codex"]).toBe(
MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION,
);
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
});
});

View File

@@ -1,7 +1,7 @@
{
"id": "diffs",
"activation": {
"onStartup": true
"onStartup": false
},
"name": "Diffs",
"description": "Read-only diff viewer and file renderer for agents.",

View File

@@ -17,11 +17,27 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]
],
"install": {
"npmSpec": "@openclaw/diffs",
"localPath": "extensions/diffs",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.30"
},
"compat": {
"pluginApi": ">=2026.4.30"
},
"build": {
"openclawVersion": "2026.4.30"
},
"bundle": {
"includeInCore": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -3,20 +3,14 @@ import { describe, expect, it } from "vitest";
type DiffsPackageManifest = {
dependencies?: Record<string, string>;
openclaw?: {
bundle?: {
stageRuntimeDependencies?: boolean;
};
};
};
describe("diffs package manifest", () => {
it("opts into staging bundled runtime dependencies", () => {
it("keeps runtime dependencies in the package manifest", () => {
const packageJson = JSON.parse(
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as DiffsPackageManifest;
expect(packageJson.dependencies?.["@pierre/diffs"]).toBeDefined();
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
});
});

View File

@@ -59,9 +59,6 @@
"build": {
"openclawVersion": "2026.4.25"
},
"bundle": {
"stageRuntimeDependencies": true
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -48,9 +48,6 @@
"build": {
"openclawVersion": "2026.4.25"
},
"bundle": {
"stageRuntimeDependencies": true
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -13,9 +13,6 @@
"openclaw": {
"extensions": [
"./index.ts"
],
"bundle": {
"stageRuntimeDependencies": false
}
]
}
}

View File

@@ -12,9 +12,6 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]

View File

@@ -22,9 +22,6 @@
}
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
],

View File

@@ -80,9 +80,6 @@
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10",
"allowInvalidConfigRecovery": true
},
"bundle": {
"stageRuntimeDependencies": true
}
}
}

View File

@@ -3,20 +3,14 @@ import { describe, expect, it } from "vitest";
type MatrixPackageManifest = {
dependencies?: Record<string, string>;
openclaw?: {
bundle?: {
stageRuntimeDependencies?: boolean;
};
};
};
describe("matrix package manifest", () => {
it("opts into staging bundled runtime dependencies", () => {
it("keeps runtime dependencies in the package manifest", () => {
const packageJson = JSON.parse(
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as MatrixPackageManifest;
expect(packageJson.dependencies?.["fake-indexeddb"]).toBeDefined();
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
});
});

View File

@@ -10,9 +10,5 @@
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
}
}
"openclaw": {}
}

View File

@@ -7,9 +7,6 @@
"contracts": {
"memoryEmbeddingProviders": ["local"]
},
"runtimeDependencies": {
"localMemoryEmbedding": ["node-llama-cpp@3.18.1"]
},
"commandAliases": [
{
"name": "dreaming",

View File

@@ -59,9 +59,6 @@
"build": {
"openclawVersion": "2026.4.25"
},
"bundle": {
"stageRuntimeDependencies": true
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -55,9 +55,6 @@
"build": {
"openclawVersion": "2026.4.25"
},
"bundle": {
"stageRuntimeDependencies": true
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -65,7 +65,9 @@ function raiseMinimalReasoningForOpenAINativeWebSearch(payload: Record<string, u
reasoning.effort = "low";
}
function patchOpenAINativeWebSearchPayload(payload: unknown): OpenAINativeWebSearchPatchResult {
export function patchOpenAINativeWebSearchPayload(
payload: unknown,
): OpenAINativeWebSearchPatchResult {
if (!isRecord(payload)) {
return "payload_not_object";
}

View File

@@ -21,11 +21,6 @@ const packageJson = JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8"),
) as {
dependencies?: Record<string, string>;
openclaw?: {
bundle?: {
stageRuntimeDependencies?: boolean;
};
};
};
function manifestComparableWizardFields(choice: {
@@ -64,10 +59,9 @@ function providerWizardByKey() {
}
describe("OpenAI plugin manifest", () => {
it("opts into staging bundled runtime dependencies", () => {
it("keeps runtime dependencies in the package manifest", () => {
expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.71.1");
expect(packageJson.dependencies?.ws).toBe("^8.20.0");
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
});
it("keeps removed Codex CLI import auth choice as a deprecated browser-login alias", () => {

View File

@@ -12,9 +12,6 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]

View File

@@ -934,8 +934,8 @@ describe("qa bundled plugin dir", () => {
).resolves.toBeTruthy();
});
it("skips transient runtime dependency artifacts while staging built bundled plugins", async () => {
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-runtime-deps-"));
it("skips legacy dependency debris while staging built bundled plugins", async () => {
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-legacy-deps-"));
cleanups.push(async () => {
await rm(repoRoot, { recursive: true, force: true });
});
@@ -961,7 +961,7 @@ describe("qa bundled plugin dir", () => {
"export {};\n",
"utf8",
);
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-runtime-deps-target-"));
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-legacy-deps-target-"));
cleanups.push(async () => {
await rm(tempRoot, { recursive: true, force: true });
});

View File

@@ -52,7 +52,7 @@
"openclawVersion": "2026.4.27"
},
"bundle": {
"stageRuntimeDependencies": true
"includeInCore": false
},
"release": {
"publishToClawHub": true,

View File

@@ -36,9 +36,6 @@
"specifier": "./configured-state",
"exportName": "hasSlackConfiguredState"
}
},
"bundle": {
"stageRuntimeDependencies": true
}
}
}

View File

@@ -46,9 +46,6 @@
"specifier": "./configured-state",
"exportName": "hasTelegramConfiguredState"
}
},
"bundle": {
"stageRuntimeDependencies": true
}
}
}

View File

@@ -3,11 +3,6 @@ import { describe, expect, it } from "vitest";
type TokenjuicePackageManifest = {
dependencies?: Record<string, string>;
openclaw?: {
bundle?: {
stageRuntimeDependencies?: boolean;
};
};
};
type TokenjuicePluginManifest = {
@@ -17,13 +12,12 @@ type TokenjuicePluginManifest = {
};
describe("tokenjuice package manifest", () => {
it("opts into staging bundled runtime dependencies", () => {
it("keeps runtime dependencies in the package manifest", () => {
const packageJson = JSON.parse(
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8"),
) as TokenjuicePackageManifest;
expect(packageJson.dependencies?.tokenjuice).toBe("0.7.0");
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
});
it("declares runtime-neutral tool result middleware ownership in the manifest contract", () => {

View File

@@ -12,9 +12,6 @@
"openclaw": {
"extensions": [
"./index.ts"
],
"bundle": {
"stageRuntimeDependencies": true
}
]
}
}

View File

@@ -11,9 +11,6 @@
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"bundle": {
"stageRuntimeDependencies": true
},
"extensions": [
"./index.ts"
]

View File

@@ -59,9 +59,6 @@
"compat": {
"pluginApi": ">=2026.4.25"
},
"bundle": {
"stageRuntimeDependencies": true
},
"build": {
"openclawVersion": "2026.4.25"
},

View File

@@ -111,7 +111,7 @@ const config = {
workspaces: {
".": {
entry: rootEntries,
ignoreDependencies: ["@openclaw/*", "sqlite-vec"],
ignoreDependencies: ["@openclaw/*", "playwright-core", "sqlite-vec"],
project: [
"src/**/*.ts!",
"scripts/**/*.{js,mjs,cjs,ts,mts,cts}!",

View File

@@ -31,9 +31,6 @@
"!dist/.runtime-postbuildstamp",
"!dist/**/*.map",
"!dist/plugin-sdk/.tsbuildinfo",
"!dist/extensions/*/.openclaw-install-stage*/**",
"!dist/extensions/*/.openclaw-runtime-deps-*/**",
"!dist/extensions/*/.openclaw-runtime-deps-stamp.json",
"!dist/extensions/node_modules/**",
"!dist/extensions/*/node_modules/**",
"!dist/extensions/qa-channel/**",
@@ -57,7 +54,6 @@
"skills/",
"scripts/npm-runner.mjs",
"scripts/preinstall-package-manager-warning.mjs",
"scripts/lib/bundled-runtime-deps-install.mjs",
"scripts/lib/package-dist-imports.mjs",
"scripts/postinstall-bundled-plugins.mjs",
"scripts/windows-cmd-helpers.mjs"
@@ -1446,12 +1442,11 @@
"rtt": "node --import tsx scripts/rtt.ts",
"runtime-sidecars:check": "node --import tsx scripts/generate-runtime-sidecar-paths-baseline.ts --check",
"runtime-sidecars:gen": "node --import tsx scripts/generate-runtime-sidecar-paths-baseline.ts --write",
"stage:bundled-plugin-runtime-deps": "node scripts/stage-bundled-plugin-runtime-deps.mjs",
"start": "node scripts/run-node.mjs",
"test": "node scripts/test-projects.mjs",
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
"test:auth:compat": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts",
"test:build:bundled-runtime-deps": "node scripts/test-built-bundled-runtime-deps.mjs",
"test:build:bundled-runtime-deps": "node -e \"console.log('bundled plugin runtime dependency staging was removed; no-op')\"",
"test:build:singleton": "node scripts/test-built-plugin-singleton.mjs",
"test:build:status-message-runtime": "node scripts/test-built-status-message-runtime.mjs",
"test:bundled": "node scripts/run-vitest.mjs run --config test/vitest/vitest.bundled.config.ts",
@@ -1466,8 +1461,7 @@
"test:docker:agents-delete-shared-workspace": "bash scripts/e2e/agents-delete-shared-workspace-docker.sh",
"test:docker:all": "node scripts/test-docker-all.mjs",
"test:docker:browser-cdp-snapshot": "bash scripts/e2e/browser-cdp-snapshot-docker.sh",
"test:docker:bundled-channel-deps": "bash scripts/e2e/bundled-channel-runtime-deps-docker.sh",
"test:docker:bundled-channel-deps:fast": "OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=1 bash scripts/e2e/bundled-channel-runtime-deps-docker.sh",
"test:docker:bundled-channel-deps:fast": "node -e \"console.log('bundled channel dependency staging was removed; no-op')\"",
"test:docker:bundled-plugin-install-uninstall": "bash scripts/e2e/bundled-plugin-install-uninstall-docker.sh",
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
"test:docker:commitments-safety": "bash scripts/e2e/commitments-safety-docker.sh",
@@ -1641,9 +1635,9 @@
"jszip": "^3.10.1",
"markdown-it": "14.1.1",
"openai": "^6.35.0",
"playwright-core": "1.59.1",
"proxy-agent": "^8.0.1",
"qrcode": "1.5.4",
"semver": "7.7.4",
"sqlite-vec": "0.1.9",
"tar": "7.5.13",
"tslog": "^4.10.2",
@@ -1746,41 +1740,5 @@
"@whiskeysockets/baileys@7.0.0-rc.9": "patches/@whiskeysockets__baileys@7.0.0-rc.9.patch",
"@agentclientprotocol/claude-agent-acp@0.31.4": "patches/@agentclientprotocol__claude-agent-acp@0.31.4.patch"
}
},
"openclaw": {
"bundle": {
"mirroredRootRuntimeDependencies": [
"@agentclientprotocol/sdk",
"@clack/prompts",
"@lydell/node-pty",
"@mariozechner/pi-ai",
"@mariozechner/pi-coding-agent",
"@modelcontextprotocol/sdk",
"ajv",
"chalk",
"chokidar",
"commander",
"croner",
"dotenv",
"global-agent",
"https-proxy-agent",
"jiti",
"json5",
"jszip",
"markdown-it",
"openai",
"qrcode",
"semver",
"sqlite-vec",
"tar",
"tslog",
"typebox",
"undici",
"web-push",
"ws",
"yaml",
"zod"
]
}
}
}

6
pnpm-lock.yaml generated
View File

@@ -111,15 +111,15 @@ importers:
openai:
specifier: ^6.35.0
version: 6.35.0(ws@8.20.0)(zod@4.4.1)
playwright-core:
specifier: 1.59.1
version: 1.59.1
proxy-agent:
specifier: ^8.0.1
version: 8.0.1
qrcode:
specifier: 1.5.4
version: 1.5.4
semver:
specifier: 7.7.4
version: 7.7.4
sqlite-vec:
specifier: 0.1.9
version: 0.1.9

View File

@@ -46,7 +46,7 @@ const CONTROL_UI_I18N_SCOPE_RE =
const NATIVE_ONLY_RE =
/^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|Swabble\/|appcast\.xml$)/;
const FAST_INSTALL_SMOKE_SCOPE_RE =
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|agents-delete-shared-workspace-docker\.sh|gateway-network-docker\.sh|bundled-channel-runtime-deps-docker\.sh)$|src\/plugins\/bundled-runtime-deps\.ts$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|agents-delete-shared-workspace-docker\.sh|gateway-network-docker\.sh)$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const FULL_INSTALL_SMOKE_SCOPE_RE =
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install\.sh$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|qr-import-docker\.sh|bun-global-install-smoke\.sh)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE = /^src\/(?:channels|gateway|plugin-sdk|plugins)\//;

View File

@@ -192,14 +192,13 @@ The `Dockerfile` supports two optional build args:
volumes:
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
- openclaw-plugin-runtime-deps:/var/lib/openclaw/plugin-runtime-deps
```
This means:
- `~/.openclaw/.env` is available inside the container at `/home/node/.openclaw/.env` — OpenClaw loads it automatically as the global env fallback
- `~/.openclaw/openclaw.json` is available at `/home/node/.openclaw/openclaw.json` — the gateway watches it and hot-reloads most changes
- Generated bundled plugin runtime deps and mirrors live in the `openclaw-plugin-runtime-deps` Docker volume at `/var/lib/openclaw/plugin-runtime-deps`, not in the host config bind mount
- Downloadable plugin packages and install records live under the mounted OpenClaw home
- No need to add API keys to `docker-compose.yml` or configure anything inside the container
- Keys survive `clawdock-update`, `clawdock-rebuild`, and `clawdock-clean` because they live on the host

View File

@@ -363,8 +363,7 @@ export function copyBundledPluginMetadata(params = {}) {
manifest,
generatedChannelConfigsByPlugin.get(manifest.id),
);
// Generated skill assets live under a dedicated dist-owned directory. Runtime
// dependency staging owns dist plugin node_modules; do not remove it here.
// Generated skill assets live under a dedicated dist-owned directory.
removePathIfExists(path.join(distPluginDir, GENERATED_BUNDLED_SKILLS_DIR));
const copiedSkills = copyDeclaredPluginSkillPaths({
manifest: manifestWithGeneratedChannelConfigs,

View File

@@ -20,7 +20,6 @@ COPY packages ./packages
COPY extensions ./extensions
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts/lib/bundled-runtime-deps-install.mjs ./scripts/lib/bundled-runtime-deps-install.mjs
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
corepack enable \

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# Runs bundled plugin runtime-dependency Docker scenarios from a mounted OpenClaw
# npm tarball. The default image is a clean runner; each scenario installs the
# tarball so package install behavior is what gets tested.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel-runtime-deps-runner.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/channel.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/root-owned.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/setup-entry.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/disabled-config.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/update.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/load-failure.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-bundled-channel-deps-e2e" OPENCLAW_BUNDLED_CHANNEL_DEPS_E2E_IMAGE)"
UPDATE_BASELINE_VERSION="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_BASELINE_VERSION:-2026.4.20}"
DOCKER_TARGET="${OPENCLAW_BUNDLED_CHANNEL_DOCKER_TARGET:-bare}"
HOST_BUILD="${OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD:-1}"
PACKAGE_TGZ="${OPENCLAW_CURRENT_PACKAGE_TGZ:-}"
RUN_CHANNEL_SCENARIOS="${OPENCLAW_BUNDLED_CHANNEL_SCENARIOS:-1}"
RUN_UPDATE_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO:-1}"
RUN_ROOT_OWNED_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO:-1}"
RUN_SETUP_ENTRY_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO:-1}"
RUN_LOAD_FAILURE_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO:-1}"
RUN_DISABLED_CONFIG_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO:-1}"
CHANNEL_ONLY="${OPENCLAW_BUNDLED_CHANNEL_ONLY:-}"
DOCKER_RUN_TIMEOUT="${OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT:-900s}"
DOCKER_UPDATE_RUN_TIMEOUT="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_DOCKER_RUN_TIMEOUT:-${OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT:-2400s}}"
docker_e2e_build_or_reuse "$IMAGE_NAME" bundled-channel-deps "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET"
prepare_package_tgz() {
if [ -n "$PACKAGE_TGZ" ]; then
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz bundled-channel-deps "$PACKAGE_TGZ")"
return 0
fi
if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then
echo "OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz bundled-channel-deps)"
}
prepare_package_tgz
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_harness_mount_args
run_bundled_channel_runtime_dep_scenarios

View File

@@ -1,83 +0,0 @@
#!/usr/bin/env bash
#
# Scenario selection for bundled plugin runtime-dependency Docker tests.
# The large scenario bodies stay in the owning test script; this helper keeps
# env flag parsing and dispatch in one small, reviewable place.
bundled_channel_state_script_b64() {
docker_e2e_test_state_shell_b64 "$1" empty
}
run_bundled_channel_container() {
local label="$1"
local timeout_value="$2"
shift 2
run_logged_print "$label" timeout "$timeout_value" docker run --rm \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$@"
}
run_bundled_channel_container_with_state() {
local label="$1"
local timeout_value="$2"
local state_label="$3"
shift 3
local state_script_b64
state_script_b64="$(bundled_channel_state_script_b64 "$state_label")"
run_bundled_channel_container "$label" "$timeout_value" \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"$@"
}
run_bundled_channel_container_with_state_heartbeat() {
local label="$1"
local heartbeat="$2"
local timeout_value="$3"
local state_label="$4"
shift 4
local state_script_b64
state_script_b64="$(bundled_channel_state_script_b64 "$state_label")"
run_logged_print_heartbeat "$label" "$heartbeat" timeout "$timeout_value" docker run --rm \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"$@"
}
run_bundled_channel_runtime_dep_scenarios() {
if [ "$RUN_CHANNEL_SCENARIOS" != "0" ]; then
IFS=',' read -r -a CHANNEL_SCENARIOS <<<"${OPENCLAW_BUNDLED_CHANNELS:-${CHANNEL_ONLY:-telegram,discord,slack,feishu,memory-lancedb}}"
for channel_scenario in "${CHANNEL_SCENARIOS[@]}"; do
channel_scenario="${channel_scenario//[[:space:]]/}"
[ -n "$channel_scenario" ] || continue
case "$channel_scenario" in
telegram) run_channel_scenario telegram grammy ;;
discord) run_channel_scenario discord discord-api-types ;;
slack) run_channel_scenario slack @slack/web-api ;;
feishu) run_channel_scenario feishu @larksuiteoapi/node-sdk ;;
memory-lancedb) run_channel_scenario memory-lancedb @lancedb/lancedb ;;
*)
echo "Unsupported OPENCLAW_BUNDLED_CHANNELS entry: $channel_scenario" >&2
exit 1
;;
esac
done
fi
if [ "$RUN_UPDATE_SCENARIO" != "0" ]; then
run_update_scenario
fi
if [ "$RUN_ROOT_OWNED_SCENARIO" != "0" ]; then
run_root_owned_global_scenario
fi
if [ "$RUN_SETUP_ENTRY_SCENARIO" != "0" ]; then
run_setup_entry_scenario
fi
if [ "$RUN_DISABLED_CONFIG_SCENARIO" != "0" ]; then
run_disabled_config_scenario
fi
if [ "$RUN_LOAD_FAILURE_SCENARIO" != "0" ]; then
run_load_failure_scenario
fi
}

View File

@@ -1,22 +0,0 @@
import fs from "node:fs";
const raw = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const payload = raw.result ?? raw.data ?? raw;
const channel = process.argv[3];
const dump = () => JSON.stringify(raw, null, 2).slice(0, 4000);
const hasChannelMeta = Array.isArray(payload.channelMeta)
? payload.channelMeta.some((entry) => entry?.id === channel)
: Boolean(payload.channelMeta?.[channel]);
if (!hasChannelMeta) {
throw new Error(`missing channelMeta.${channel}\n${dump()}`);
}
if (!payload.channels || !payload.channels[channel]) {
throw new Error(`missing channels.${channel}\n${dump()}`);
}
const accounts = payload.channelAccounts?.[channel];
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error(`missing channelAccounts.${channel}\n${dump()}`);
}
console.log(`${channel} channel plugin visible`);

View File

@@ -1,44 +0,0 @@
import fs from "node:fs";
import path from "node:path";
const stageDir = process.argv[2];
const depName = process.argv[3];
const manifestName = ".openclaw-runtime-deps.json";
const matches = [];
function visit(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(fullPath);
continue;
}
if (entry.name !== manifestName) {
continue;
}
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
} catch {
continue;
}
const specs = Array.isArray(parsed.specs) ? parsed.specs : [];
for (const spec of specs) {
if (typeof spec === "string" && spec.startsWith(`${depName}@`)) {
matches.push(`${fullPath}: ${spec}`);
}
}
}
}
visit(stageDir);
if (matches.length > 0) {
process.stderr.write(`${matches.join("\n")}\n`);
process.exit(1);
}

View File

@@ -1,26 +0,0 @@
import fs from "node:fs";
const payload = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const expectedBefore = process.argv[3];
const expectedAfter = process.argv[4];
if (payload.status !== "ok") {
throw new Error(`expected update status ok, got ${JSON.stringify(payload.status)}`);
}
if (expectedBefore && (payload.before?.version ?? null) !== expectedBefore) {
throw new Error(
`expected before.version ${expectedBefore}, got ${JSON.stringify(payload.before?.version)}`,
);
}
if ((payload.after?.version ?? null) !== expectedAfter) {
throw new Error(
`expected after.version ${expectedAfter}, got ${JSON.stringify(payload.after?.version)}`,
);
}
const steps = Array.isArray(payload.steps) ? payload.steps : [];
const doctor = steps.find((step) => step?.name === "openclaw doctor");
if (!doctor) {
throw new Error("missing openclaw doctor step");
}
if (Number(doctor.exitCode ?? 1) !== 0) {
throw new Error(`openclaw doctor step failed: ${JSON.stringify(doctor)}`);
}

View File

@@ -1,224 +0,0 @@
#!/usr/bin/env bash
#
# Runs one bundled plugin channel runtime-dependency scenario.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_channel_scenario() {
local channel="$1"
local dep_sentinel="$2"
echo "Running bundled $channel runtime deps Docker E2E..."
run_bundled_channel_container_with_state \
"bundled-channel-deps-$channel" \
"$DOCKER_RUN_TIMEOUT" \
"bundled-channel-deps-$channel" \
-e OPENCLAW_CHANNEL_UNDER_TEST="$channel" \
-e OPENCLAW_DEP_SENTINEL="$dep_sentinel" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.sh
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENAI_API_KEY="sk-openclaw-bundled-channel-deps-e2e"
export OPENCLAW_NO_ONBOARD=1
TOKEN="bundled-channel-deps-token"
PORT="18789"
CHANNEL="${OPENCLAW_CHANNEL_UNDER_TEST:?missing OPENCLAW_CHANNEL_UNDER_TEST}"
DEP_SENTINEL="${OPENCLAW_DEP_SENTINEL:?missing OPENCLAW_DEP_SENTINEL}"
gateway_pid=""
terminate_gateways() {
openclaw_e2e_terminate_gateways "${gateway_pid:-}"
}
cleanup() {
terminate_gateways
}
trap cleanup EXIT
bundled_channel_install_package /tmp/openclaw-install.log
command -v openclaw >/dev/null
package_root="$(openclaw_e2e_package_root)"
openclaw_e2e_assert_package_extensions "$package_root" telegram discord slack feishu memory-lancedb
if [ -d "$package_root/dist/extensions/$CHANNEL/node_modules" ]; then
echo "$CHANNEL runtime deps should not be preinstalled in package" >&2
find "$package_root/dist/extensions/$CHANNEL/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
exit 1
fi
start_gateway() {
local log_file="$1"
local skip_sidecars="${2:-0}"
: >"$log_file"
if [ "$skip_sidecars" = "1" ]; then
OPENCLAW_SKIP_CHANNELS=1 OPENCLAW_SKIP_PROVIDERS=1 \
openclaw gateway --port "$PORT" --bind loopback --allow-unconfigured >"$log_file" 2>&1 &
else
openclaw gateway --port "$PORT" --bind loopback --allow-unconfigured >"$log_file" 2>&1 &
fi
gateway_pid="$!"
# Cold bundled dependency staging can exceed 60s under 10-way Docker aggregate load.
for _ in $(seq 1 1200); do
if grep -Eq "listening on ws://|\\[gateway\\] http server listening|\\[gateway\\] ready( \\(|$)" "$log_file"; then
return 0
fi
if ! kill -0 "$gateway_pid" 2>/dev/null; then
echo "gateway exited unexpectedly" >&2
cat "$log_file" >&2
exit 1
fi
sleep 0.25
done
echo "timed out waiting for gateway" >&2
cat "$log_file" >&2
exit 1
}
stop_gateway() {
terminate_gateways
gateway_pid=""
}
wait_for_gateway_health() {
local log_file="${1:-}"
if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then
return 0
fi
echo "gateway process exited after ready marker" >&2
if [ -n "$log_file" ]; then
cat "$log_file" >&2
fi
return 1
}
parse_channel_status_json() {
local out="$1"
local channel="$2"
node scripts/e2e/lib/bundled-channel/assert-channel-status.mjs "$out" "$channel"
}
assert_channel_status() {
local channel="$1"
if [ "$channel" = "memory-lancedb" ]; then
echo "memory-lancedb plugin activation verified by dependency sentinel"
return 0
fi
local out="/tmp/openclaw-channel-status-$channel.json"
local err="/tmp/openclaw-channel-status-$channel.err"
local parse_err="/tmp/openclaw-channel-status-$channel.parse.err"
local parse_out="/tmp/openclaw-channel-status-$channel.parse.out"
for _ in $(seq 1 30); do
if openclaw gateway call channels.status \
--url "ws://127.0.0.1:$PORT" \
--token "$TOKEN" \
--timeout 10000 \
--json \
--params '{"probe":false}' >"$out" 2>"$err"; then
if parse_channel_status_json "$out" "$channel" >"$parse_out" 2>"$parse_err"; then
cat "$parse_out"
return 0
fi
fi
if grep -Eq "\\[gateway\\] ready \\(.*\\b$channel\\b" /tmp/openclaw-"$channel"-*.log 2>/dev/null; then
echo "$channel channel plugin visible in gateway ready log"
return 0
fi
sleep 2
done
if [ ! -s "$out" ]; then
cat "$err" >&2 || true
else
cat "$parse_err" >&2 || true
cat "$out" >&2 || true
fi
cat /tmp/openclaw-"$channel"-*.log >&2 2>/dev/null || true
return 1
}
assert_installed_once() {
local log_file="$1"
local channel="$2"
local dep_path="$3"
local count
count="$(grep -Ec "\\[plugins\\] $channel installed bundled runtime deps( in [0-9]+ms)?:" "$log_file" || true)"
if [ "$count" -eq 1 ]; then
return 0
fi
if [ "$count" -eq 0 ] && [ -n "$(bundled_channel_find_external_dep_package "$dep_path")" ]; then
return 0
fi
echo "expected one runtime deps install log or staged dependency sentinel for $channel, got $count log lines" >&2
cat "$log_file" >&2
find "$(bundled_channel_stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
}
assert_not_installed() {
local log_file="$1"
local channel="$2"
if grep -Eq "\\[plugins\\] $channel installed bundled runtime deps( in [0-9]+ms)?:" "$log_file"; then
echo "expected no runtime deps reinstall for $channel" >&2
cat "$log_file" >&2
exit 1
fi
}
assert_dep_sentinel() {
local channel="$1"
local dep_path="$2"
bundled_channel_assert_dep_available "$channel" "$dep_path" "$package_root"
}
assert_no_dep_sentinel() {
local channel="$1"
local dep_path="$2"
bundled_channel_assert_no_dep_available "$channel" "$dep_path" "$package_root"
}
assert_no_install_stage() {
local channel="$1"
local stage="$package_root/dist/extensions/$channel/.openclaw-install-stage"
if [ -e "$stage" ]; then
echo "install stage should be cleaned after activation for $channel" >&2
find "$stage" -maxdepth 4 -type f | sort | head -80 >&2 || true
exit 1
fi
}
echo "Starting baseline gateway with OpenAI configured..."
bundled_channel_write_config baseline
start_gateway "/tmp/openclaw-$CHANNEL-baseline.log" 1
wait_for_gateway_health "/tmp/openclaw-$CHANNEL-baseline.log"
stop_gateway
assert_no_dep_sentinel "$CHANNEL" "$DEP_SENTINEL"
echo "Enabling $CHANNEL by config edit, then restarting gateway..."
bundled_channel_write_config "$CHANNEL"
start_gateway "/tmp/openclaw-$CHANNEL-first.log"
wait_for_gateway_health "/tmp/openclaw-$CHANNEL-first.log"
assert_installed_once "/tmp/openclaw-$CHANNEL-first.log" "$CHANNEL" "$DEP_SENTINEL"
assert_dep_sentinel "$CHANNEL" "$DEP_SENTINEL"
assert_no_install_stage "$CHANNEL"
assert_channel_status "$CHANNEL"
stop_gateway
echo "Restarting gateway again; $CHANNEL deps must stay installed..."
start_gateway "/tmp/openclaw-$CHANNEL-second.log"
wait_for_gateway_health "/tmp/openclaw-$CHANNEL-second.log"
assert_not_installed "/tmp/openclaw-$CHANNEL-second.log" "$CHANNEL"
assert_no_install_stage "$CHANNEL"
assert_channel_status "$CHANNEL"
stop_gateway
echo "bundled $CHANNEL runtime deps Docker E2E passed"
EOF
}

View File

@@ -1,137 +0,0 @@
#!/usr/bin/env bash
#
# Container-side helpers shared by bundled channel Docker E2E scenarios.
# These functions assume the OpenClaw package is installed globally inside the
# test container and the scenario has exported HOME/OPENAI_API_KEY as needed.
bundled_channel_package_root() {
printf "%s/openclaw" "$(npm root -g)"
}
bundled_channel_stage_root() {
printf "%s/.openclaw/plugin-runtime-deps" "$HOME"
}
bundled_channel_stage_dir() {
printf "%s" "${OPENCLAW_PLUGIN_STAGE_DIR:-$(bundled_channel_stage_root)}"
}
bundled_channel_install_package() {
openclaw_e2e_install_package "$@"
}
bundled_channel_find_external_dep_package() {
local dep_path="$1"
find "$(bundled_channel_stage_root)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
}
bundled_channel_find_staged_dep_package() {
local dep_path="$1"
find "$(bundled_channel_stage_dir)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
}
bundled_channel_dump_stage_dir() {
find "$(bundled_channel_stage_dir)" -maxdepth 12 -type f | sort | head -160 >&2 || true
}
bundled_channel_assert_no_package_dep_available() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
for candidate in \
"$root/dist/extensions/$channel/node_modules/$dep_path/package.json" \
"$root/dist/extensions/node_modules/$dep_path/package.json" \
"$root/node_modules/$dep_path/package.json"; do
if [ -f "$candidate" ]; then
echo "packaged install should not mutate package tree for $channel: $candidate" >&2
exit 1
fi
done
if [ -f "$HOME/node_modules/$dep_path/package.json" ]; then
echo "bundled runtime deps should not use HOME npm project for $channel: $HOME/node_modules/$dep_path/package.json" >&2
exit 1
fi
}
bundled_channel_assert_dep_available() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
if [ -n "$(bundled_channel_find_external_dep_package "$dep_path")" ]; then
bundled_channel_assert_no_package_dep_available "$channel" "$dep_path" "$root"
return 0
fi
echo "missing dependency sentinel for $channel: $dep_path" >&2
find "$root/dist/extensions/$channel" -maxdepth 3 -type f | sort | head -80 >&2 || true
find "$root/node_modules" -maxdepth 3 -path "*/$dep_path/package.json" -type f -print >&2 || true
find "$(bundled_channel_stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
}
bundled_channel_assert_no_dep_available() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_path" "$root"
if [ -n "$(bundled_channel_find_external_dep_package "$dep_path")" ]; then
echo "dependency sentinel should be absent before repair for $channel: $dep_path" >&2
exit 1
fi
}
bundled_channel_assert_no_staged_dep() {
local channel="$1"
local dep_path="$2"
local message="${3:-$channel unexpectedly staged $dep_path}"
if [ -n "$(bundled_channel_find_staged_dep_package "$dep_path")" ]; then
echo "$message" >&2
bundled_channel_dump_stage_dir
exit 1
fi
}
bundled_channel_assert_staged_dep() {
local channel="$1"
local dep_path="$2"
local log_file="${3:-}"
if [ -n "$(bundled_channel_find_staged_dep_package "$dep_path")" ]; then
return 0
fi
echo "missing external staged dependency sentinel for $channel: $dep_path" >&2
if [ -n "$log_file" ]; then
cat "$log_file" >&2 || true
fi
bundled_channel_dump_stage_dir
exit 1
}
bundled_channel_assert_no_staged_manifest_spec() {
local channel="$1"
local dep_path="$2"
local log_file="${3:-}"
if ! node scripts/e2e/lib/bundled-channel/assert-no-staged-manifest-spec.mjs "$(bundled_channel_stage_dir)" "$dep_path"; then
echo "$channel unexpectedly selected $dep_path for external runtime deps" >&2
if [ -n "$log_file" ]; then
cat "$log_file" >&2 || true
fi
exit 1
fi
}
bundled_channel_remove_runtime_dep() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
rm -rf "$root/dist/extensions/$channel/node_modules"
rm -rf "$root/dist/extensions/node_modules/$dep_path"
rm -rf "$root/node_modules/$dep_path"
rm -rf "$(bundled_channel_stage_root)"
}
bundled_channel_write_config() {
local mode="$1"
node scripts/e2e/lib/bundled-channel/write-config.mjs \
"$mode" \
"${TOKEN:-bundled-channel-config-token}" \
"${PORT:-18789}"
}

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env bash
#
# Runs disabled-config runtime-dependency isolation scenarios.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_disabled_config_scenario() {
echo "Running bundled channel disabled-config runtime deps Docker E2E..."
run_bundled_channel_container_with_state \
bundled-channel-disabled-config \
"$DOCKER_RUN_TIMEOUT" \
bundled-channel-disabled-config \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.sh
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_PLUGIN_STAGE_DIR="$HOME/.openclaw/plugin-runtime-deps"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR"
assert_dep_absent_everywhere() {
local channel="$1"
local dep_path="$2"
local root="$3"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_path" "$root"
bundled_channel_assert_no_staged_manifest_spec "$channel" "$dep_path" /tmp/openclaw-disabled-config-doctor.log
}
bundled_channel_install_package /tmp/openclaw-disabled-config-install.log
root="$(bundled_channel_package_root)"
test -d "$root/dist/extensions/telegram"
test -d "$root/dist/extensions/discord"
test -d "$root/dist/extensions/slack"
rm -rf "$root/dist/extensions/telegram/node_modules"
rm -rf "$root/dist/extensions/discord/node_modules"
rm -rf "$root/dist/extensions/slack/node_modules"
bundled_channel_write_config disabled-config
if ! openclaw doctor --non-interactive >/tmp/openclaw-disabled-config-doctor.log 2>&1; then
echo "doctor failed for disabled-config runtime deps smoke" >&2
cat /tmp/openclaw-disabled-config-doctor.log >&2
exit 1
fi
assert_dep_absent_everywhere telegram grammy "$root"
assert_dep_absent_everywhere slack @slack/web-api "$root"
assert_dep_absent_everywhere discord discord-api-types "$root"
if grep -Eq "(grammy|@slack/web-api|discord-api-types)" /tmp/openclaw-disabled-config-doctor.log; then
echo "doctor installed runtime deps for an explicitly disabled channel/plugin" >&2
cat /tmp/openclaw-disabled-config-doctor.log >&2
exit 1
fi
echo "bundled channel disabled-config runtime deps Docker E2E passed"
EOF
}

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env node
import { readdir } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const root = process.argv[2] || process.env.OPENCLAW_PACKAGE_ROOT;
if (!root) {
throw new Error("missing package root");
}
const distDir = path.join(root, "dist");
const onboardChannelFiles = (await readdir(distDir))
.filter((entry) => /^onboard-channels-.*\.js$/.test(entry))
.toSorted();
let setupChannels;
for (const entry of onboardChannelFiles) {
const module = await import(pathToFileURL(path.join(distDir, entry)));
if (typeof module.setupChannels === "function") {
setupChannels = module.setupChannels;
break;
}
}
if (!setupChannels) {
throw new Error(
`could not find packaged setupChannels export in ${JSON.stringify(onboardChannelFiles)}`,
);
}
let channelSelectCount = 0;
const notes = [];
const prompter = {
intro: async () => {},
outro: async () => {},
note: async (body, title) => {
notes.push({ title, body });
},
confirm: async ({ message, initialValue }) => {
if (message === "Link WhatsApp now (QR)?") {
return false;
}
return initialValue ?? true;
},
select: async ({ message, options }) => {
if (message === "Select a channel") {
channelSelectCount += 1;
return channelSelectCount === 1 ? "whatsapp" : "__done__";
}
if (message === "Install WhatsApp plugin?") {
if (!options?.some((option) => option.value === "local")) {
throw new Error(`missing bundled local install option: ${JSON.stringify(options)}`);
}
return "local";
}
if (message === "WhatsApp phone setup") {
return "separate";
}
if (message === "WhatsApp DM policy") {
return "disabled";
}
throw new Error(`unexpected select prompt: ${message}`);
},
multiselect: async ({ message }) => {
throw new Error(`unexpected multiselect prompt: ${message}`);
},
text: async ({ message }) => {
throw new Error(`unexpected text prompt: ${message}`);
},
};
const runtime = {
log: (message) => console.log(message),
error: (message) => console.error(message),
};
const result = await setupChannels({ plugins: { enabled: true } }, runtime, prompter, {
deferStatusUntilSelection: true,
skipConfirm: true,
skipStatusNote: true,
skipDmPolicyPrompt: true,
initialSelection: ["whatsapp"],
});
if (!result.channels?.whatsapp) {
throw new Error(`WhatsApp setup did not write channel config: ${JSON.stringify(result)}`);
}
console.log("packaged guided WhatsApp setup completed");

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
#
# Runs load-failure isolation scenarios.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_load_failure_scenario() {
echo "Running bundled channel load-failure isolation Docker E2E..."
run_bundled_channel_container_with_state \
bundled-channel-load-failure \
"$DOCKER_RUN_TIMEOUT" \
bundled-channel-load-failure \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.sh
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENCLAW_NO_ONBOARD=1
bundled_channel_install_package /tmp/openclaw-load-failure-install.log
root="$(bundled_channel_package_root)"
plugin_dir="$root/dist/extensions/load-failure-alpha"
node scripts/e2e/lib/bundled-channel/write-load-failure-fixture.mjs "$plugin_dir"
echo "Loading synthetic failing bundled channel through packaged loader..."
node scripts/e2e/lib/bundled-channel/loader-probe.mjs load-failure "$root" load-failure-alpha
echo "bundled channel load-failure isolation Docker E2E passed"
EOF
}

View File

@@ -1,121 +0,0 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
function usage() {
console.error("Usage: loader-probe.mjs <setup-entries|load-failure> <package-root> [channel...]");
process.exit(2);
}
function findBundledLoader(root) {
const distDir = path.join(root, "dist");
const bundledPath = fs
.readdirSync(distDir)
.filter((entry) => /^bundled-[A-Za-z0-9_-]+\.js$/.test(entry))
.map((entry) => path.join(distDir, entry))
.find((entry) => fs.readFileSync(entry, "utf8").includes("src/channels/plugins/bundled.ts"));
if (!bundledPath) {
throw new Error("missing packaged bundled channel loader artifact");
}
return bundledPath;
}
function namedExport(module, name) {
const fn = Object.values(module).find(
(value) => typeof value === "function" && value.name === name,
);
if (typeof fn !== "function") {
throw new Error(
`missing packaged bundled loader export ${name}; exports=${Object.keys(module).join(",")}`,
);
}
return fn;
}
async function importBundled(root) {
return import(pathToFileURL(findBundledLoader(root)));
}
function loadCounts() {
return {
plugin: globalThis.__loadFailurePlugin,
setup: globalThis.__loadFailureSetup,
secrets: globalThis.__loadFailureSecrets,
setupSecrets: globalThis.__loadFailureSetupSecrets,
};
}
function exerciseLoaders(loaders, id) {
for (const [name, fn] of loaders) {
try {
fn(id);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("synthetic")) {
throw new Error(`bundled export ${name} leaked synthetic load failure: ${message}`, {
cause: error,
});
}
}
}
}
const [command, root, ...args] = process.argv.slice(2);
if (!command || !root) {
usage();
}
if (command === "load-failure") {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(root, "dist/extensions");
}
const bundled = await importBundled(root);
if (command === "setup-entries") {
const channels = args.length > 0 ? args : ["feishu", "whatsapp"];
const setupPluginLoader = namedExport(bundled, "getBundledChannelSetupPlugin");
for (const channel of channels) {
const plugin = setupPluginLoader(channel);
if (!plugin) {
throw new Error(`${channel} setup plugin did not load pre-config`);
}
if (plugin.id !== channel) {
throw new Error(`${channel} setup plugin id mismatch: ${plugin.id}`);
}
console.log(`${channel} setup plugin loaded pre-config`);
}
} else if (command === "load-failure") {
const id = args[0] || "load-failure-alpha";
const loaderNames = [
"getBundledChannelPlugin",
"getBundledChannelSetupPlugin",
"getBundledChannelSecrets",
"getBundledChannelSetupSecrets",
];
const loaders = loaderNames.map((name) => [name, namedExport(bundled, name)]);
exerciseLoaders(loaders, id);
const firstCounts = loadCounts();
exerciseLoaders(loaders, id);
const secondCounts = loadCounts();
for (const key of ["plugin", "setup", "setupSecrets"]) {
const first = firstCounts[key];
if (!Number.isInteger(first) || first < 1) {
throw new Error(`expected ${key} failure to be exercised at least once, got ${first}`);
}
if (secondCounts[key] !== first) {
throw new Error(
`expected ${key} failure to be cached after first pass, got ${first} then ${secondCounts[key]}`,
);
}
}
if (firstCounts.secrets !== undefined && secondCounts.secrets !== firstCounts.secrets) {
throw new Error(
`expected secrets failure to be cached after first pass, got ${firstCounts.secrets} then ${secondCounts.secrets}`,
);
}
console.log("synthetic bundled channel load failures were isolated and cached");
} else {
usage();
}

View File

@@ -1,6 +0,0 @@
import { execFileSync } from "node:child_process";
const raw = execFileSync("tar", ["-xOf", process.argv[2], "package/package.json"], {
encoding: "utf8",
});
process.stdout.write(String(JSON.parse(raw).version));

View File

@@ -1,124 +0,0 @@
#!/usr/bin/env bash
#
# Runs the root-owned global install runtime-dependency scenario.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_root_owned_global_scenario() {
echo "Running bundled channel root-owned global install Docker E2E..."
run_bundled_channel_container bundled-channel-root-owned "$DOCKER_RUN_TIMEOUT" \
--user root \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.sh
export HOME="/root"
export OPENAI_API_KEY="sk-openclaw-bundled-channel-root-owned-e2e"
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_PLUGIN_STAGE_DIR="/var/lib/openclaw/plugin-runtime-deps"
TOKEN="bundled-channel-root-owned-token"
PORT="18791"
CHANNEL="slack"
DEP_SENTINEL="@slack/web-api"
gateway_pid=""
cleanup() {
if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then
kill "$gateway_pid" 2>/dev/null || true
wait "$gateway_pid" 2>/dev/null || true
fi
}
trap cleanup EXIT
bundled_channel_install_package /tmp/openclaw-root-owned-install.log "mounted OpenClaw package into root-owned global npm"
root="$(bundled_channel_package_root)"
test -d "$root/dist/extensions/$CHANNEL"
rm -rf "$root/dist/extensions/$CHANNEL/node_modules"
chmod -R a-w "$root"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR" /home/appuser/.openclaw
chown -R appuser:appuser /home/appuser/.openclaw /var/lib/openclaw
if runuser -u appuser -- test -w "$root"; then
echo "expected package root to be unwritable for appuser" >&2
exit 1
fi
OPENCLAW_BUNDLED_CHANNEL_CONFIG_PATH=/home/appuser/.openclaw/openclaw.json \
OPENCLAW_BUNDLED_CHANNEL_SLACK_BOT_TOKEN=xoxb-bundled-channel-root-owned-token \
OPENCLAW_BUNDLED_CHANNEL_SLACK_APP_TOKEN=xapp-bundled-channel-root-owned-token \
bundled_channel_write_config slack
chown appuser:appuser /home/appuser/.openclaw/openclaw.json
start_gateway() {
local log_file="$1"
: >"$log_file"
chown appuser:appuser "$log_file"
runuser -u appuser -- env \
HOME=/home/appuser \
OPENAI_API_KEY="$OPENAI_API_KEY" \
OPENCLAW_NO_ONBOARD=1 \
OPENCLAW_PLUGIN_STAGE_DIR="$OPENCLAW_PLUGIN_STAGE_DIR" \
npm_config_cache=/tmp/openclaw-root-owned-npm-cache \
bash -c 'openclaw gateway --port "$1" --bind loopback --allow-unconfigured >"$2" 2>&1' \
bash "$PORT" "$log_file" &
gateway_pid="$!"
# Cold bundled dependency staging can exceed 60s under 10-way Docker aggregate load.
for _ in $(seq 1 1200); do
if grep -Eq "listening on ws://|\\[gateway\\] http server listening|\\[gateway\\] ready( \\(|$)" "$log_file"; then
return 0
fi
if ! kill -0 "$gateway_pid" 2>/dev/null; then
echo "gateway exited unexpectedly" >&2
cat "$log_file" >&2
exit 1
fi
sleep 0.25
done
echo "timed out waiting for gateway" >&2
cat "$log_file" >&2
exit 1
}
wait_for_slack_provider_start() {
for _ in $(seq 1 180); do
if grep -Eq "\\[slack\\] \\[default\\] starting provider|An API error occurred: invalid_auth|\\[plugins\\] slack installed bundled runtime deps|\\[gateway\\] ready \\(.*\\bslack\\b" /tmp/openclaw-root-owned-gateway.log; then
return 0
fi
sleep 1
done
echo "timed out waiting for slack provider startup" >&2
cat /tmp/openclaw-root-owned-gateway.log >&2
exit 1
}
start_gateway /tmp/openclaw-root-owned-gateway.log
wait_for_slack_provider_start
bundled_channel_assert_no_package_dep_available "$CHANNEL" "$DEP_SENTINEL" "$root"
bundled_channel_assert_staged_dep "$CHANNEL" "$DEP_SENTINEL" /tmp/openclaw-root-owned-gateway.log
if [ -e "$root/dist/extensions/node_modules/openclaw/package.json" ]; then
echo "root-owned package tree was mutated with SDK alias" >&2
find "$root/dist/extensions/node_modules/openclaw" -maxdepth 4 -type f | sort | head -80 >&2 || true
exit 1
fi
if ! find "$(bundled_channel_stage_dir)" -maxdepth 12 -path "*/dist/extensions/node_modules/openclaw/package.json" -type f | grep -q .; then
echo "missing external staged openclaw/plugin-sdk alias" >&2
bundled_channel_dump_stage_dir
cat /tmp/openclaw-root-owned-gateway.log >&2
exit 1
fi
if grep -Eq "failed to install bundled runtime deps|Cannot find package 'openclaw'|Cannot find module 'openclaw/plugin-sdk'" /tmp/openclaw-root-owned-gateway.log; then
echo "root-owned gateway hit bundled runtime dependency errors" >&2
cat /tmp/openclaw-root-owned-gateway.log >&2
exit 1
fi
echo "root-owned global install Docker E2E passed"
EOF
}

View File

@@ -1,67 +0,0 @@
#!/usr/bin/env bash
#
# Runs setup-entry runtime-dependency installation scenarios.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_setup_entry_scenario() {
echo "Running bundled channel setup-entry runtime deps Docker E2E..."
run_bundled_channel_container_with_state \
bundled-channel-setup-entry \
"$DOCKER_RUN_TIMEOUT" \
bundled-channel-setup-entry \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.sh
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_PLUGIN_STAGE_DIR="$HOME/.openclaw/plugin-runtime-deps"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR"
declare -A SETUP_ENTRY_DEP_SENTINELS=(
[feishu]="@larksuiteoapi/node-sdk"
[whatsapp]="@whiskeysockets/baileys"
)
bundled_channel_install_package /tmp/openclaw-setup-entry-install.log
root="$(bundled_channel_package_root)"
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
test -d "$root/dist/extensions/$channel"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
done
echo "Probing real bundled setup entries before channel configuration..."
node scripts/e2e/lib/bundled-channel/loader-probe.mjs setup-entries "$root" feishu whatsapp
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
bundled_channel_assert_no_staged_dep "$channel" "$dep_sentinel" "setup-entry discovery installed $channel external staged deps before channel configuration"
done
echo "Running packaged guided WhatsApp setup; runtime deps should be staged before finalize..."
node scripts/e2e/lib/bundled-channel/guided-whatsapp-setup.mjs "$root"
bundled_channel_assert_no_package_dep_available whatsapp @whiskeysockets/baileys "$root"
bundled_channel_assert_staged_dep whatsapp @whiskeysockets/baileys
echo "Configuring setup-entry channels; doctor should now install bundled runtime deps externally..."
bundled_channel_write_config setup-entry-channels
openclaw doctor --fix --non-interactive >/tmp/openclaw-setup-entry-doctor.log 2>&1
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
bundled_channel_assert_staged_dep "$channel" "$dep_sentinel" /tmp/openclaw-setup-entry-doctor.log
done
echo "bundled channel setup-entry runtime deps Docker E2E passed"
EOF
}

Some files were not shown because too many files have changed in this diff Show More