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

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