From e7d60654b86c3a9a7bfbeed1d40dff2823bd222c Mon Sep 17 00:00:00 2001 From: joshp123 Date: Fri, 8 May 2026 12:00:05 +0800 Subject: [PATCH] fix: package OpenClaw runtime plugin tree Install and validate OpenClaw's dist-runtime tree so bundled runtime plugins such as ACPX are present in the Nix gateway output. Extend the existing plugin flake contract with immutable OpenClaw plugin roots, wire those roots into generated config, and add eval fixtures proving default enablement, user overrides, and disabled entries. Document the boundary: curated plugin artifacts are CI/Garnix-cached by nix-openclaw, while arbitrary npm or ClawHub specs need deterministic lock/hash-backed Nix artifacts cached by the user's store/cache instead of runtime npm installs. Tests: nix build --accept-flake-config .#checks.x86_64-linux.default-instance --no-link --print-out-paths; nix eval --accept-flake-config --raw .#checks.aarch64-darwin.package-contents.drvPath; nix build --accept-flake-config .#checks.aarch64-darwin.package-contents --no-link --dry-run; nix build --impure --accept-flake-config .#darwinConfigurations.mac-mini.system --no-link --override-input nix-openclaw path:/Users/josh/code/nix/nix-openclaw --dry-run Co-authored-by: Codex --- AGENTS.md | 2 + docs/plugins-maintainers.md | 22 ++-- maintainers/packaging.md | 3 + nix/checks/openclaw-default-instance.nix | 63 +++++++++- nix/modules/home-manager/openclaw/config.nix | 23 +++- .../openclaw/options-instance.nix | 2 +- nix/modules/home-manager/openclaw/options.nix | 2 +- nix/modules/home-manager/openclaw/plugins.nix | 110 +++++++++++++----- nix/scripts/check-package-contents.sh | 8 ++ nix/scripts/gateway-build.sh | 3 + nix/scripts/gateway-install.sh | 9 +- .../disabled-plugin/openclaw.plugin.json | 4 + nix/tests/plugins/runtime/flake.nix | 23 ++++ nix/tests/plugins/runtime/plugin/index.js | 3 + .../runtime/plugin/openclaw.plugin.json | 7 ++ 15 files changed, 241 insertions(+), 43 deletions(-) create mode 100644 nix/tests/plugins/runtime/disabled-plugin/openclaw.plugin.json create mode 100644 nix/tests/plugins/runtime/flake.nix create mode 100644 nix/tests/plugins/runtime/plugin/index.js create mode 100644 nix/tests/plugins/runtime/plugin/openclaw.plugin.json diff --git a/AGENTS.md b/AGENTS.md index 0906bba..00c49db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,8 @@ Source: https://github.com/orgs/openclaw/people - Keep maintainer runbooks in `maintainers/`. - Never add internal ExecPlans or agent scratch history to this repo. `.agent/` is ignored for this reason. - If a private deployment exposes a public packaging bug, fix the public package here and keep deployment-specific repair elsewhere. +- OpenClaw plugin loading belongs here: package curated runtime plugin roots as Nix artifacts, expose curated outputs through package/check outputs for Garnix, and let host repos only enable/configure them. +- Do not make host config run npm/ClawHub installs at runtime for the batteries-included path. Arbitrary plugin specs need a lock/hash-backed Nix derivation so Nix caches them locally or in the user's configured cache. ## Packaging Defaults diff --git a/docs/plugins-maintainers.md b/docs/plugins-maintainers.md index 5fda909..13bcfef 100644 --- a/docs/plugins-maintainers.md +++ b/docs/plugins-maintainers.md @@ -7,21 +7,23 @@ Purpose: extend OpenClaw capabilities without bloating core; ship tools + skills - **Not:** new transports/providers; model plumbing; secrets baked in; inline scripts or ad-hoc package-manager installs; a place for random config outside its scope. - Why not skills-only: skills without binaries can hallucinate capability. Plugins ground skills in real tools and deliver versioned, reproducible functionality. -## Native OpenClaw Plugin Gap +## Two Plugin Classes -OpenClaw also has native runtime plugins: a plugin directory with `openclaw.plugin.json` plus a JS/TS entry loaded by the gateway. Channel plugins such as `openclaw-weixin` live in this layer. +Nix capability plugins are the tool/skill/env bundles described below. They do not use OpenClaw's JavaScript plugin loader. They are the right shape for CLIs such as `goplaces`, `gog`, `qmd`, `xuezh`, `camsnap`, and `summarize`. -Current nix-openclaw `customPlugins` only implements the Nix-native capability bundle contract: package binaries on the gateway PATH, materialize skills, create state dirs, validate env files, and render optional tool settings. It does not yet tell OpenClaw to load a native plugin directory or enable the matching `plugins.entries.` entry. +OpenClaw plugins are runtime plugin directories with `openclaw.plugin.json` plus built JavaScript loaded by the gateway. They include bundled upstream plugins, official external plugins from OpenClaw's catalog or ClawHub, and third-party plugins. In Nix-managed deployments, these should be immutable plugin roots, not runtime npm installs hidden in host config. + +Current nix-openclaw `customPlugins` implements both sides of the contract: package binaries on the gateway PATH, materialize skills, create state dirs, validate env files, render optional tool settings, and wire declared OpenClaw plugin roots into `plugins.load.paths` with a default `plugins.entries..enabled = true`. PR #81 (`fix: copy plugin manifests into dist/extensions`) was related but not the missing external-plugin feature. It fixed bundled upstream plugin manifests missing from the packaged gateway `dist/extensions/*/openclaw.plugin.json` tree. Current packaging already copies those manifests and checks them in `openclaw-package-contents`. -The next feature slice should bridge the existing Nix contract to OpenClaw native plugins: +Package authors can bridge the existing Nix contract to OpenClaw plugins: -- Extend `openclawPlugin` with an optional native plugin declaration, for example `nativePlugins = [ { id = "openclaw-weixin"; path = "${pkg}/lib/openclaw/plugins/openclaw-weixin"; enable = true; } ];`. +- Extend `openclawPlugin` with an optional plugin declaration, for example `plugins = [ { id = "openclaw-weixin"; path = "${pkg}/lib/openclaw/plugins/openclaw-weixin"; enable = true; } ];`. - For each enabled instance, append those paths to generated `plugins.load.paths`. - Add default `plugins.entries..enabled = true`, with user config still able to override it. -- Keep native plugin config in `programs.openclaw.config` / `instances..config` so upstream schema validation remains the source of truth. -- Add a fixture shaped like `openclaw-weixin` so `customPlugins = [{ source = ...; }]` proves both package/skill wiring and native plugin load wiring. +- Keep OpenClaw plugin config in `programs.openclaw.config` / `instances..config` so upstream schema validation remains the source of truth. +- Add a fixture shaped like `openclaw-weixin` so `customPlugins = [{ source = ...; }]` proves both package/skill wiring and OpenClaw plugin load wiring. ## Interface Contract (reference implementation: nix-openclaw) Every plugin artifact exposes the same fields (flake output `openclawPlugin` today, but the shape is host-agnostic): @@ -31,6 +33,7 @@ openclawPlugin = { name = "summarize"; # unique; last-wins on collision skills = [ ./skills/summarize ]; # dirs containing SKILL.md packages = [ pkgs.summarize-cli ]; # binaries placed on the OpenClaw runtime PATH + plugins = [ ]; # optional OpenClaw plugin roots needs = { stateDirs = [ ".config/summarize" ]; # created under $HOME requiredEnv = [ "SUMMARIZE_API_KEY" ]; # must point to files @@ -46,6 +49,7 @@ Host responsibilities (what the runtime guarantees): - Copy/symlink each `skills` entry into `workspace/skills//...`. - If host config provides `config.settings`, render it to `config.json` in the first `stateDir`. - Export `config.env` (plus required envs) into the gateway wrapper. +- Add declared OpenClaw plugin roots to `plugins.load.paths`, and set `plugins.entries..enabled = true` as a default. - Reject duplicate skill paths; duplicate plugin names: last entry wins. ### Host-side config shape @@ -67,6 +71,10 @@ programs.openclaw.customPlugins = [ - `config.settings`: JSON-rendered into `config.json` inside the first `stateDir`. - Invariant: providing `settings` requires at least one `stateDir`. +Do not add raw npm package names to host config for the batteries-included path. Curated plugins packaged by this repo or `nix-openclaw-tools` should be exposed through package/check outputs so Garnix caches them. + +Arbitrary user plugins are a separate product surface. A future config like `plugins = [ "scope/plugin@npm:1.2.3" ]` must resolve through a lock/hash-backed npm/ClawHub builder that produces a normal Nix store path. That means our Garnix does not promise to cache every user plugin, but the user's machine also does not rebuild it on every run: Nix reuses the local store or configured binary cache until the spec, lock, or hash changes. OpenClaw must not reinstall it on every gateway start. + ## Dev workflow (fast iteration) - Worktree: build and test plugins outside the core repo; point OpenClaw at a local path source during impure local dev (e.g., `source = "path:/Users/you/code/my-plugin"`). Committed config uses pinned refs. - Rebuild loop: change plugin → `home-manager switch` (or host-equivalent) → gateway restarts with new PATH/skills/config; no manual copying. diff --git a/maintainers/packaging.md b/maintainers/packaging.md index 00ca791..91331a9 100644 --- a/maintainers/packaging.md +++ b/maintainers/packaging.md @@ -21,6 +21,8 @@ This repo ships a working Nix package for OpenClaw users, not just a pin mirror. - Generated config options come from the upstream core schema. - Plugin-owned extension surfaces, such as `channels.`, must remain accepted by the Home Manager module even when core does not type every plugin key. - Runtime tool injection belongs here. If a plugin or battery is enabled, the active OpenClaw harness must see its CLI tools and required environment without asking downstream to expose those tools globally on the user PATH. +- OpenClaw plugin roots belong here too. The Home Manager module consumes `openclawPlugin.plugins` declarations from plugin flakes and writes `plugins.load.paths`/`plugins.entries` into the generated config. +- Raw npm/ClawHub plugin names are not batteries-included deployment config. Curated plugins packaged here must be exposed through packages/checks so CI/Garnix caches them. Arbitrary user specs need a deterministic lock/hash-backed Nix builder so Nix reuses the user's store/cache and only rebuilds when the spec, lock, or hash changes. ## Build Contract @@ -28,6 +30,7 @@ This repo ships a working Nix package for OpenClaw users, not just a pin mirror. - No inline scripts or inline file contents in Nix code. Use repo scripts and explicit file paths. - Keep runtime tools internal to the `openclaw` wrapper unless they are intentionally part of the public package surface. - QMD is the Nix-supported batteries-included local memory backend. Keep `qmd` internal to the `openclaw` wrapper PATH; users opt in with upstream config. +- ACPX is the first bundled OpenClaw plugin proof. It is consumed from OpenClaw's built `dist-runtime/extensions/acpx` tree, not installed or repaired by npm at runtime. - Keep files under 400 lines unless a maintainer explicitly accepts the larger file. ## Investigations diff --git a/nix/checks/openclaw-default-instance.nix b/nix/checks/openclaw-default-instance.nix index b9f4d2d..f63a3be 100644 --- a/nix/checks/openclaw-default-instance.nix +++ b/nix/checks/openclaw-default-instance.nix @@ -33,6 +33,9 @@ let betaPluginSource = lockedPathFlake "openclaw-test-plugin-beta" ../tests/plugins/beta "sha256-lDKtQKHZHqOkOprjLZzBEu8cFJhAdyEzsays9hdVeqE="; + runtimePluginSource = + lockedPathFlake "openclaw-test-plugin-runtime" ../tests/plugins/runtime + "sha256-JquqpIqcXWwwQPXVHsNQEme8xksXBk1A0lXc/pxsnhE="; stubModule = { lib, ... }: @@ -227,14 +230,66 @@ let runtimeProfileCheck = builtins.deepSeq (requireNoAssertionFailures "runtime profile" runtimeProfileEval) ( - if - lib.hasInfix "openclaw-link-codex-runtime-profiles.sh" runtimeProfileActivation - then + if lib.hasInfix "openclaw-link-codex-runtime-profiles.sh" runtimeProfileActivation then "ok" else throw "runtimePackages did not wire the Codex runtime profile activation." ); + openclawPluginEval = moduleEval { + customPlugins = [ + { source = runtimePluginSource; } + ]; + config.plugins.load.paths = [ + "/tmp/user-openclaw-plugin" + ]; + }; + openclawPluginConfig = builtins.fromJSON ( + builtins.unsafeDiscardStringContext + openclawPluginEval.config.home.file.".openclaw/openclaw.json".text + ); + openclawPluginLoadPaths = ((openclawPluginConfig.plugins or { }).load or { }).paths or [ ]; + openclawPluginEntry = ((openclawPluginConfig.plugins or { }).entries or { }).runtime-test or { }; + openclawPluginDisabledEntry = + ((openclawPluginConfig.plugins or { }).entries or { }).runtime-disabled or null; + openclawPluginCheck = + builtins.deepSeq (requireNoAssertionFailures "OpenClaw plugin load" openclawPluginEval) + ( + if !(lib.any (path: lib.hasSuffix "/plugin" path) openclawPluginLoadPaths) then + throw "OpenClaw plugin root was not added to plugins.load.paths." + else if lib.any (path: lib.hasSuffix "/disabled-plugin" path) openclawPluginLoadPaths then + throw "Disabled OpenClaw plugin root was added to plugins.load.paths." + else if !(lib.elem "/tmp/user-openclaw-plugin" openclawPluginLoadPaths) then + throw "User-defined plugins.load.paths entry was not preserved." + else if (openclawPluginEntry.enabled or false) != true then + throw "OpenClaw plugin entry default was not enabled." + else if openclawPluginDisabledEntry != null then + throw "Disabled OpenClaw plugin entry was added to generated config." + else + "ok" + ); + + openclawPluginOverrideEval = moduleEval { + customPlugins = [ + { source = runtimePluginSource; } + ]; + config.plugins.entries.runtime-test.enabled = false; + }; + openclawPluginOverrideConfig = builtins.fromJSON ( + builtins.unsafeDiscardStringContext + openclawPluginOverrideEval.config.home.file.".openclaw/openclaw.json".text + ); + openclawPluginOverrideEntry = + ((openclawPluginOverrideConfig.plugins or { }).entries or { }).runtime-test or { }; + openclawPluginOverrideCheck = + builtins.deepSeq (requireNoAssertionFailures "OpenClaw plugin override" openclawPluginOverrideEval) + ( + if (openclawPluginOverrideEntry.enabled or null) == false then + "ok" + else + throw "User config could not override OpenClaw plugin enabled default." + ); + checkKey = builtins.deepSeq [ defaultCheck customPluginCheck @@ -243,6 +298,8 @@ let secretProviderCheck qmdPrewarmCheck runtimeProfileCheck + openclawPluginCheck + openclawPluginOverrideCheck ] "ok"; in diff --git a/nix/modules/home-manager/openclaw/config.nix b/nix/modules/home-manager/openclaw/config.nix index 1ac0831..9fbadae 100644 --- a/nix/modules/home-manager/openclaw/config.nix +++ b/nix/modules/home-manager/openclaw/config.nix @@ -117,11 +117,26 @@ let inherit key value; plugin = "runtime"; }) (cfg.environment // inst.environment)); - mergedConfig0 = stripNulls ( - lib.recursiveUpdate (lib.recursiveUpdate baseConfig (stripNulls cfg.config)) ( - stripNulls inst.config - ) + userConfig = stripNulls (lib.recursiveUpdate (stripNulls cfg.config) (stripNulls inst.config)); + pluginEntryConfig = plugins.openclawPluginEntriesConfigFor name; + openclawPluginLoadPaths = plugins.openclawPluginLoadPathsFor name; + mergedConfigWithoutLoadPaths = stripNulls ( + lib.recursiveUpdate (lib.recursiveUpdate baseConfig pluginEntryConfig) userConfig ); + existingOpenClawPluginLoadPaths = ( + ((mergedConfigWithoutLoadPaths.plugins or { }).load or { }).paths or [ ] + ); + mergedConfig0 = + if openclawPluginLoadPaths == [ ] then + mergedConfigWithoutLoadPaths + else + lib.recursiveUpdate mergedConfigWithoutLoadPaths { + plugins = { + load = { + paths = lib.unique (openclawPluginLoadPaths ++ existingOpenClawPluginLoadPaths); + }; + }; + }; existingWorkspace = (((mergedConfig0.agents or { }).defaults or { }).workspace or null); mergedConfig = if (cfg.workspace.pinAgentDefaults or true) && existingWorkspace == null then diff --git a/nix/modules/home-manager/openclaw/options-instance.nix b/nix/modules/home-manager/openclaw/options-instance.nix index 563c445..f44adbf 100644 --- a/nix/modules/home-manager/openclaw/options-instance.nix +++ b/nix/modules/home-manager/openclaw/options-instance.nix @@ -83,7 +83,7 @@ options = { source = lib.mkOption { type = lib.types.str; - description = "Plugin source pointer (e.g., github:owner/repo or path:/...)."; + description = "Plugin flake source pointer (e.g., github:owner/repo or path:/...)."; }; config = lib.mkOption { type = lib.types.attrs; diff --git a/nix/modules/home-manager/openclaw/options.nix b/nix/modules/home-manager/openclaw/options.nix index 27ff36a..fdf9701 100644 --- a/nix/modules/home-manager/openclaw/options.nix +++ b/nix/modules/home-manager/openclaw/options.nix @@ -137,7 +137,7 @@ in options = { source = lib.mkOption { type = lib.types.str; - description = "Plugin source pointer (e.g., github:owner/repo or path:/...)."; + description = "Plugin flake source pointer (e.g., github:owner/repo or path:/...)."; }; config = lib.mkOption { type = lib.types.attrs; diff --git a/nix/modules/home-manager/openclaw/plugins.nix b/nix/modules/home-manager/openclaw/plugins.nix index 8c7e085..98b9149 100644 --- a/nix/modules/home-manager/openclaw/plugins.nix +++ b/nix/modules/home-manager/openclaw/plugins.nix @@ -12,8 +12,8 @@ let resolvePlugin = plugin: let - flake = builtins.getFlake plugin.source; system = pkgs.stdenv.hostPlatform.system; + flake = builtins.getFlake plugin.source; openclawPluginRaw = if flake ? openclawPlugin then flake.openclawPlugin @@ -26,13 +26,27 @@ let throw "openclawPlugin is null in ${plugin.source} for ${system}" else openclawPlugin; + name = resolvedPlugin.name or (throw "openclawPlugin.name missing in ${plugin.source}"); needs = resolvedPlugin.needs or { }; + normalizeOpenClawPlugin = + entry: + let + id = entry.id or (throw "openclawPlugin ${name}: plugins entry missing id"); + path = entry.path or (throw "openclawPlugin ${name}: plugins.${id} missing path"); + in + { + inherit id path; + enable = entry.enable or true; + source = plugin.source; + plugin = name; + }; in { source = plugin.source; - name = resolvedPlugin.name or (throw "openclawPlugin.name missing in ${plugin.source}"); + inherit name; skills = resolvedPlugin.skills or [ ]; packages = resolvedPlugin.packages or [ ]; + plugins = map normalizeOpenClawPlugin (resolvedPlugin.plugins or [ ]); needs = { stateDirs = needs.stateDirs or [ ]; requiredEnv = needs.requiredEnv or [ ]; @@ -104,31 +118,72 @@ let in lib.flatten (map toPairs entries); - pluginAssertions = lib.flatten ( - lib.mapAttrsToList ( - instName: inst: - let - plugins = resolvedPluginsByInstance.${instName} or [ ]; - envFor = p: (p.config.env or { }); - missingFor = p: lib.filter (req: !(builtins.hasAttr req (envFor p))) p.needs.requiredEnv; - configMissingStateDir = p: (p.config.settings or { }) != { } && (p.needs.stateDirs or [ ]) == [ ]; - mkAssertion = - p: - let - missing = missingFor p; - in - { - assertion = missing == [ ]; - message = "programs.openclaw.instances.${instName}: plugin ${p.name} missing required env: ${lib.concatStringsSep ", " missing}"; + openclawPluginsFor = + instName: lib.flatten (map (p: p.plugins) (resolvedPluginsByInstance.${instName} or [ ])); + + enabledOpenClawPluginsFor = instName: lib.filter (p: p.enable) (openclawPluginsFor instName); + + openclawPluginLoadPathsFor = + instName: map (p: toString p.path) (enabledOpenClawPluginsFor instName); + + openclawPluginEntriesConfigFor = + instName: + let + entries = enabledOpenClawPluginsFor instName; + in + lib.optionalAttrs (entries != [ ]) { + plugins = { + entries = lib.listToAttrs ( + map (p: { + name = p.id; + value = { + enabled = true; + }; + }) entries + ); + }; + }; + + openclawPluginIdAssertions = lib.mapAttrsToList ( + instName: _inst: + let + ids = map (p: p.id) (enabledOpenClawPluginsFor instName); + counts = lib.foldl' (acc: id: acc // { "${id}" = (acc.${id} or 0) + 1; }) { } ids; + duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts); + in + { + assertion = duplicates == [ ]; + message = "programs.openclaw.instances.${instName}: duplicate OpenClaw plugin ids detected: ${lib.concatStringsSep ", " duplicates}"; + } + ) enabledInstances; + + pluginAssertions = + openclawPluginIdAssertions + ++ lib.flatten ( + lib.mapAttrsToList ( + instName: inst: + let + plugins = resolvedPluginsByInstance.${instName} or [ ]; + envFor = p: (p.config.env or { }); + missingFor = p: lib.filter (req: !(builtins.hasAttr req (envFor p))) p.needs.requiredEnv; + configMissingStateDir = p: (p.config.settings or { }) != { } && (p.needs.stateDirs or [ ]) == [ ]; + mkAssertion = + p: + let + missing = missingFor p; + in + { + assertion = missing == [ ]; + message = "programs.openclaw.instances.${instName}: plugin ${p.name} missing required env: ${lib.concatStringsSep ", " missing}"; + }; + mkConfigAssertion = p: { + assertion = !(configMissingStateDir p); + message = "programs.openclaw.instances.${instName}: plugin ${p.name} provides settings but declares no stateDirs (needed for config.json)."; }; - mkConfigAssertion = p: { - assertion = !(configMissingStateDir p); - message = "programs.openclaw.instances.${instName}: plugin ${p.name} provides settings but declares no stateDirs (needed for config.json)."; - }; - in - (map mkAssertion plugins) ++ (map mkConfigAssertion plugins) - ) enabledInstances - ); + in + (map mkAssertion plugins) ++ (map mkConfigAssertion plugins) + ) enabledInstances + ); pluginConfigFiles = let @@ -192,6 +247,9 @@ in pluginStateDirsAll pluginEnvFor pluginEnvAllFor + openclawPluginsFor + openclawPluginLoadPathsFor + openclawPluginEntriesConfigFor pluginAssertions pluginConfigFiles pluginGuards diff --git a/nix/scripts/check-package-contents.sh b/nix/scripts/check-package-contents.sh index c183679..2fd4615 100755 --- a/nix/scripts/check-package-contents.sh +++ b/nix/scripts/check-package-contents.sh @@ -19,6 +19,14 @@ require_path "${root}/extensions" require_path "${root}/extensions/memory-core" require_path "${root}/extensions/memory-core/openclaw.plugin.json" require_path "${root}/dist/extensions/memory-core/openclaw.plugin.json" +require_path "${root}/dist-runtime/extensions" +require_path "${root}/dist-runtime/extensions/memory-core/openclaw.plugin.json" +require_path "${root}/dist-runtime/extensions/acpx/openclaw.plugin.json" +require_path "${root}/dist-runtime/extensions/acpx/package.json" +require_path "${root}/dist-runtime/extensions/acpx/index.js" +require_path "${root}/dist-runtime/extensions/acpx/error-format.mjs" +require_path "${root}/dist-runtime/extensions/acpx/mcp-command-line.mjs" +require_path "${root}/dist-runtime/extensions/acpx/mcp-proxy.mjs" require_path "${root}/docs/reference/templates" require_path "${root}/docs/reference/templates/AGENTS.md" require_path "${root}/docs/reference/templates/SOUL.md" diff --git a/nix/scripts/gateway-build.sh b/nix/scripts/gateway-build.sh index 0444192..c58f35c 100755 --- a/nix/scripts/gateway-build.sh +++ b/nix/scripts/gateway-build.sh @@ -84,6 +84,9 @@ log_step "build: canvas:a2ui:tsc" pnpm exec tsc -p vendor/a2ui/renderers/lit/tsc log_step "build: canvas:a2ui:rolldown" node node_modules/rolldown/bin/cli.mjs -c apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs log_step "build: tsdown" pnpm exec tsdown log_step "build: runtime-postbuild" node scripts/runtime-postbuild.mjs +if [ -f "scripts/stage-bundled-plugin-runtime.mjs" ]; then + log_step "build: stage bundled plugin runtime" node scripts/stage-bundled-plugin-runtime.mjs +fi log_step "build: plugin-sdk dts" pnpm build:plugin-sdk:dts log_step "build: write-plugin-sdk-entry-dts" node --import tsx scripts/write-plugin-sdk-entry-dts.ts if [ -f "scripts/copy-plugin-sdk-root-alias.mjs" ]; then diff --git a/nix/scripts/gateway-install.sh b/nix/scripts/gateway-install.sh index dd777ce..4bc8f25 100755 --- a/nix/scripts/gateway-install.sh +++ b/nix/scripts/gateway-install.sh @@ -57,7 +57,11 @@ copy_extension_manifests() { mkdir -p "$out/lib/openclaw" "$out/bin" -log_step "copy build outputs" cp -R dist node_modules package.json "$out/lib/openclaw/" +set -- dist node_modules package.json +if [ -d dist-runtime ]; then + set -- "$@" dist-runtime +fi +log_step "copy build outputs" cp -R "$@" "$out/lib/openclaw/" if [ -d extensions ]; then log_step "copy extension manifests" copy_extension_manifests fi @@ -151,5 +155,8 @@ if [ -n "${OPENCLAW_BUILD_ROOT_SH:-}" ]; then fi log_step "validate node_modules symlinks" check_no_broken_symlinks "$out/lib/openclaw/node_modules" +if [ -d "$out/lib/openclaw/dist-runtime" ]; then + log_step "validate dist-runtime symlinks" check_no_broken_symlinks "$out/lib/openclaw/dist-runtime" +fi log_step "wrap openclaw" bash -e -c '. "$STDENV_SETUP"; makeWrapper "$NODE_BIN" "$out/bin/openclaw" --add-flags "$out/lib/openclaw/dist/index.js" --set-default OPENCLAW_NIX_MODE "1"' diff --git a/nix/tests/plugins/runtime/disabled-plugin/openclaw.plugin.json b/nix/tests/plugins/runtime/disabled-plugin/openclaw.plugin.json new file mode 100644 index 0000000..9f771ae --- /dev/null +++ b/nix/tests/plugins/runtime/disabled-plugin/openclaw.plugin.json @@ -0,0 +1,4 @@ +{ + "id": "runtime-disabled", + "name": "Runtime Disabled" +} diff --git a/nix/tests/plugins/runtime/flake.nix b/nix/tests/plugins/runtime/flake.nix new file mode 100644 index 0000000..62df6ae --- /dev/null +++ b/nix/tests/plugins/runtime/flake.nix @@ -0,0 +1,23 @@ +{ + outputs = + { self }: + { + openclawPlugin = { + name = "runtime"; + skills = [ ]; + packages = [ ]; + needs = { }; + plugins = [ + { + id = "runtime-test"; + path = "${self.outPath}/plugin"; + } + { + id = "runtime-disabled"; + path = "${self.outPath}/disabled-plugin"; + enable = false; + } + ]; + }; + }; +} diff --git a/nix/tests/plugins/runtime/plugin/index.js b/nix/tests/plugins/runtime/plugin/index.js new file mode 100644 index 0000000..da90b71 --- /dev/null +++ b/nix/tests/plugins/runtime/plugin/index.js @@ -0,0 +1,3 @@ +export default { + activate() {} +}; diff --git a/nix/tests/plugins/runtime/plugin/openclaw.plugin.json b/nix/tests/plugins/runtime/plugin/openclaw.plugin.json new file mode 100644 index 0000000..d4f2a7d --- /dev/null +++ b/nix/tests/plugins/runtime/plugin/openclaw.plugin.json @@ -0,0 +1,7 @@ +{ + "id": "runtime-test", + "name": "Runtime Test", + "activation": { + "onStartup": true + } +}