From a3e9c6f6c2e1f4381cfb6616ade35eb551d7d7c4 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 07:26:35 -0700 Subject: [PATCH 01/46] fix(onboard): diagnose incomplete custom plugin images Signed-off-by: Aaron Erickson --- .github/workflows/regression-e2e.yaml | 8 +- docs/deployment/install-openclaw-plugins.mdx | 184 +++++++++---- docs/reference/commands-nemohermes.mdx | 1 + docs/reference/commands.mdx | 16 ++ docs/reference/troubleshooting.mdx | 16 ++ src/lib/onboard.ts | 6 +- src/lib/verify-deployment.test.ts | 125 ++++++++- src/lib/verify-deployment.ts | 106 +++++++- test/e2e/fixtures/plugins/weather/.gitignore | 5 + .../plugins/weather/openclaw.plugin.json | 19 ++ .../plugins/weather/package-lock.json | 42 +++ .../e2e/fixtures/plugins/weather/package.json | 37 +++ .../e2e/fixtures/plugins/weather/src/index.ts | 50 ++++ .../fixtures/plugins/weather/tsconfig.json | 14 + .../openclaw-plugin-runtime-exdev.test.ts | 246 ++++++++++++++++-- test/regression-e2e-workflow.test.ts | 8 +- 16 files changed, 796 insertions(+), 87 deletions(-) create mode 100644 test/e2e/fixtures/plugins/weather/.gitignore create mode 100644 test/e2e/fixtures/plugins/weather/openclaw.plugin.json create mode 100644 test/e2e/fixtures/plugins/weather/package-lock.json create mode 100644 test/e2e/fixtures/plugins/weather/package.json create mode 100644 test/e2e/fixtures/plugins/weather/src/index.ts create mode 100644 test/e2e/fixtures/plugins/weather/tsconfig.json diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index 15ee54ccbf..f61ccfb1aa 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -268,8 +268,8 @@ jobs: /tmp/nemoclaw-e2e-model-router-response.log if-no-files-found: ignore - # ── OpenClaw plugin runtime-deps EXDEV E2E ───────────────────── - # Coverage guard for #3513 / #3127. On Ubuntu/OpenShell sandbox layouts + # ── OpenClaw custom-plugin lifecycle and runtime-deps EXDEV E2E ─ + # Coverage guard for #6108 / #3513 / #3127. On Ubuntu/OpenShell sandbox layouts # where /tmp and /sandbox can live on different filesystems, OpenClaw's # first CLI bootstrap must not fail plugin runtime dependency installation # with EXDEV cross-device rename errors. @@ -281,7 +281,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - timeout-minutes: 45 + timeout-minutes: 75 steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -300,7 +300,7 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Run OpenClaw plugin runtime-deps EXDEV Vitest test + - name: Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV Vitest test env: E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openclaw-plugin-runtime-exdev NEMOCLAW_RUN_LIVE_E2E: "1" diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 1f4396b107..4d8009c77e 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -3,8 +3,8 @@ # SPDX-License-Identifier: Apache-2.0 title: "Install OpenClaw Plugins" sidebar-title: "Install OpenClaw Plugins" -description: "How to install OpenClaw plugins in a NemoClaw-managed sandbox today." -description-agent: "Explains the difference between OpenClaw plugins and agent skills, and shows the current Dockerfile-based workflow for baking a plugin into a NemoClaw sandbox, including `.dockerignore` handling for custom build contexts. Use when users ask how to install, build, or configure OpenClaw plugins under NemoClaw." +description: "How to install an OpenClaw plugin from a version-matched full NemoClaw runtime source context." +description-agent: "Explains the difference between OpenClaw plugins and agent skills, the complete-image contract of `nemoclaw onboard --from`, and the source-based workflow for adding a plugin without removing the managed runtime. Use when users ask how to install, build, or configure OpenClaw plugins under NemoClaw." keywords: ["nemoclaw plugins", "openclaw plugins", "install openclaw plugin", "nemoclaw onboard from dockerfile", "nemoclaw dockerignore"] content: type: "how_to" @@ -18,83 +18,160 @@ They are different from NemoClaw-managed agent skills: - **Skills** are `SKILL.md` directories that teach an agent how to perform a task. - **Policy presets** are network-egress rules that control what sandboxed code can reach. -To install supported OpenClaw plugins under NemoClaw, bake the plugin into a custom sandbox image and onboard from that Dockerfile. +Until NemoClaw provides a managed plugin lifecycle, bake the plugin into a version-matched full NemoClaw runtime image. -## Prepare a Build Directory +## Understand the Custom Image Contract -Place the Dockerfile and everything it needs to `COPY` in one directory. -`nemoclaw onboard --from ` uses the Dockerfile's parent directory as the Docker build context. -Add a `.dockerignore` next to the Dockerfile to exclude local caches, generated artifacts, model files, or other paths that are not needed by the image build. -NemoClaw still applies its own secret-safety exclusions for credential-like paths such as `.env*`, `.ssh/`, `.aws/`, `.npmrc`, `secrets/`, `*.pem`, and `*.key`, even if `.dockerignore` negates them. +The `--from` option supplies the complete sandbox image definition. +It does not add your Dockerfile as a layer on top of NemoClaw's normal managed runtime. + + +Do not start a custom OpenClaw image from `ghcr.io/nvidia/nemoclaw/sandbox-base` alone. +That intermediate image contains Node.js, OpenClaw, and other runtime dependencies, but it does not contain `nemoclaw-start`, the generated `openclaw.json`, or the managed gateway health check. +A sandbox created from the base image alone can report a successful create while the gateway and dashboard remain unavailable. + + +`nemoclaw onboard --from ` uses the Dockerfile's parent directory as its Docker build context. +The workaround therefore starts with the complete source context for the installed NemoClaw release. + +For a first-class `add`, `list`, `status`, and `remove` lifecycle that does not require a source checkout, follow [issue #5998](https://github.com/NVIDIA/NemoClaw/issues/5998). + +## Prepare a Version-Matched Build Context + +Use the same NemoClaw release for the installed CLI, source checkout, and `sandbox-base` image. +The following commands reproduce the workflow for NemoClaw `v0.0.71`, which includes OpenClaw `2026.5.27`. + +```bash +nemoclaw --version +git clone --depth 1 --branch v0.0.71 https://github.com/NVIDIA/NemoClaw.git my-plugin-sandbox +cp -R /path/to/my-plugin ./my-plugin-sandbox/my-plugin +cd my-plugin-sandbox +``` + +For a different release, replace `v0.0.71` everywhere in this workflow and start from that release's stock Dockerfile. +Do not reuse this patch across releases because the managed image contract can change. + +The plugin directory must contain the inputs used by its build, including its manifest and dependency lockfile. +This example expects the following files: ```text -my-plugin-sandbox/ -├── Dockerfile -└── my-plugin/ - ├── package-lock.json - ├── package.json - └── src/ +my-plugin/ +├── openclaw.plugin.json +├── package-lock.json +├── package.json +├── src/ +└── tsconfig.json ``` The example Dockerfile uses `npm ci`, so `my-plugin/` must include `package-lock.json`. If your plugin does not have a lockfile yet, create one in the plugin project with `npm install --package-lock-only`. +OpenClaw `2026.5.27` also requires the plugin manifest to declare each registered tool in `contracts.tools`; for example, a weather plugin that registers `get_weather` must declare `"tools": ["get_weather"]`. -## Example Dockerfile +## Extend the Full Managed Dockerfile -Use the custom image to copy the plugin into the OpenClaw extensions directory. -Then let OpenClaw refresh its config before NemoClaw starts the sandbox. +Make two changes to the stock `Dockerfile` from the `v0.0.71` checkout. +First, pin the base image to the same release. -```dockerfile -ARG SANDBOX_BASE=ghcr.io/nvidia/nemoclaw/sandbox-base:latest -FROM ${SANDBOX_BASE} +```diff +-ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/sandbox-base:latest ++ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71 +``` -COPY my-plugin/ /opt/my-plugin/ -WORKDIR /opt/my-plugin -RUN npm ci --no-audit --no-fund && npm run build +Next, name the completed runtime stage so the plugin layer can extend it. + +```diff +-FROM ${BASE_IMAGE} ++FROM ${BASE_IMAGE} AS nemoclaw-runtime +``` -RUN mkdir -p /sandbox/.openclaw/extensions \ - && cp -a /opt/my-plugin /sandbox/.openclaw/extensions/my-plugin \ - && openclaw doctor --fix +Append the following stages to the end of the stock Dockerfile. +Replace `weather` with the plugin ID from `openclaw.plugin.json` in the stage name, destination directory, load path, and OpenClaw plugin commands. -WORKDIR /opt/nemoclaw +```dockerfile +# Build the plugin from its lockfile. +FROM builder AS weather-plugin-builder +WORKDIR /opt/my-plugin +COPY my-plugin/package.json my-plugin/package-lock.json my-plugin/tsconfig.json ./ +RUN npm ci --no-audit --no-fund +COPY my-plugin/openclaw.plugin.json ./ +COPY my-plugin/src/ ./src/ +RUN npm run build && npm prune --omit=dev + +# Extend the completed managed runtime. +FROM nemoclaw-runtime AS weather-runtime +COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ + /opt/my-plugin/package.json \ + /opt/my-plugin/package-lock.json \ + /opt/my-plugin/openclaw.plugin.json \ + /sandbox/.openclaw/extensions/weather/ +COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ + /opt/my-plugin/dist/ /sandbox/.openclaw/extensions/weather/dist/ +COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ + /opt/my-plugin/node_modules/ /sandbox/.openclaw/extensions/weather/node_modules/ + +USER sandbox +RUN HOME=/sandbox openclaw config set plugins.load.paths \ + '["/sandbox/.openclaw/extensions/weather"]' --json \ + && HOME=/sandbox openclaw plugins registry --refresh --json > /dev/null \ + && HOME=/sandbox openclaw plugins enable weather \ + && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null + +# Enabling the plugin changes openclaw.json after the managed runtime hashes it. +# hadolint ignore=DL3002 +USER root +RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ + && chmod 660 /sandbox/.openclaw/openclaw.json \ + && sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash \ + && chown sandbox:sandbox /sandbox/.openclaw/.config-hash \ + && chmod 660 /sandbox/.openclaw/.config-hash ``` -If the plugin needs configuration in `openclaw.json`, apply it after `openclaw doctor --fix` so the base config exists first. +The final stage inherits the stock runtime entrypoint, command, gateway health check, generated configuration, and file permissions. +The explicit `plugins.load.paths` entry gives the copied plugin a tracked load-path provenance before the registry refresh and enable steps. +The last `RUN` refreshes the managed config hash after `openclaw plugins enable` updates `openclaw.json`. -## Create the Sandbox +## Create and Verify the Sandbox -Point `nemoclaw onboard --from` at the Dockerfile in the build directory. +Pin NemoClaw's base-image resolver to the same release when you onboard. ```bash -nemoclaw onboard --from ./my-plugin-sandbox/Dockerfile +NEMOCLAW_SANDBOX_BASE_IMAGE_REF=ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71 \ + nemoclaw onboard \ + --fresh \ + --no-gpu \ + --name weather-agent \ + --from "$PWD/Dockerfile" ``` -To run a second sandbox alongside an existing one, use a dedicated build directory and rerun onboarding with the sandbox name and ports you intend to use. +Verify the managed runtime and plugin after onboarding completes. -## Build Performance +```bash +nemoclaw weather-agent status +nemoclaw weather-agent exec -- test -s /tmp/gateway.log +nemoclaw weather-agent exec -- env HOME=/sandbox openclaw plugins inspect weather --runtime --json +nemoclaw weather-agent exec -- bash -lc \ + ". /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.catalog --params '{\"includePlugins\":true}' --json" +``` -Custom plugin images are normal Docker builds, so build time depends on the build context size and the Docker layer cache rather than on NemoClaw. +The plugin inspection must report the `weather` plugin, and the gateway tool catalog must contain `get_weather`. -Keep the build context small and dedicated. -The Dockerfile's parent directory is staged as the build context before the Docker build starts, so a broad directory can make onboarding look stuck while Docker is only preparing context. -A small build directory stages quickly: +Keep the source checkout and Dockerfile at the recorded path if you plan to run `nemoclaw weather-agent rebuild --yes`. +This source-based workflow can reproduce the plugin during that rebuild, but it is not the durable managed-plugin lifecycle proposed in issue #5998. +Rerun the plugin inspection and tool-catalog checks after a gateway restart or sandbox rebuild to verify that the plugin remains available. -```text -my-plugin-sandbox/ # fast: only what the image needs -├── Dockerfile -├── .dockerignore -└── my-plugin/ -``` +If the plugin imports runtime packages, keep those packages in `dependencies` rather than `devDependencies` so `npm prune --omit=dev` preserves them. +If the plugin needs configuration in `openclaw.json`, apply it before refreshing `.config-hash`. -A Dockerfile placed in a large tree stages slowly: +Never bake plugin credentials into the Dockerfile or `openclaw.json`. +Use OpenShell credential providers for secrets. -```text -~/ # slow: stages the whole home directory -├── Dockerfile -├── Downloads/ -├── datasets/ -└── models/ -``` +## Build Performance + +Custom plugin images are normal Docker builds, so build time depends on the build context size and the Docker layer cache rather than on NemoClaw. + +Keep the Dockerfile at the release checkout root because the full managed image copies repository scripts, blueprint files, and the built-in NemoClaw plugin. +Use the checkout's `.dockerignore`, and do not place unrelated datasets, model files, caches, or credentials under that directory. +NemoClaw also applies secret-safety exclusions for credential-like paths such as `.env*`, `.ssh/`, `.aws/`, `.npmrc`, `secrets/`, `*.pem`, and `*.key`. Distinguish cold builds from warm rebuilds. The first build on a fresh host is a cold build that downloads the base image and package indexes, so it is the slowest run. @@ -126,7 +203,9 @@ For custom preset workflows, refer to [Customize Network Policy](../network-poli The following mistakes commonly mix plugin installation with other NemoClaw extension paths. - Do not use `nemoclaw skill install` for OpenClaw plugins. That command only installs `SKILL.md` agent skills. -- Do not put a Dockerfile in a broad directory such as `/tmp` unless you intend to send that whole directory as the Docker build context. +- Do not use `sandbox-base` as the final custom image. It is an intermediate dependency image. +- Do not combine one NemoClaw release's Dockerfile with another release's base image. +- Do not move or delete the recorded source checkout before rebuilding the sandbox. - Do not rely on `.dockerignore` to include credential-like paths; NemoClaw excludes those from staged custom build contexts for safety. - Keep plugin dependencies in the build stage or plugin directory, and avoid copying unrelated host files into the sandbox image. @@ -135,3 +214,4 @@ The following mistakes commonly mix plugin installation with other NemoClaw exte - Review [Sandbox Hardening](sandbox-hardening) before adding plugin code to a shared or long-lived sandbox. - Review [Network Policies](../reference/network-policies) to plan plugin egress rules. - Follow [Customize Network Policy](../network-policy/customize-network-policy) if the plugin needs a custom preset. +- Follow [issue #5998](https://github.com/NVIDIA/NemoClaw/issues/5998) for the no-source-checkout managed plugin lifecycle. diff --git a/docs/reference/commands-nemohermes.mdx b/docs/reference/commands-nemohermes.mdx index b02cd49379..585de24eea 100644 --- a/docs/reference/commands-nemohermes.mdx +++ b/docs/reference/commands-nemohermes.mdx @@ -279,6 +279,7 @@ The poll count is clamped to a minimum of `1` so the probe always runs at least #### `--from ` Build the sandbox image from a custom Dockerfile instead of the stock NemoClaw image. +The supplied Dockerfile defines the complete sandbox image, and NemoClaw does not layer it on top of the stock managed runtime. The entire parent directory of the specified file is used as the Docker build context, so any files your Dockerfile references (scripts, config, etc.) must live alongside it. If that directory contains a `.dockerignore`, onboarding applies those rules while calculating the context size and staging files for Docker. NemoClaw also applies additional secret-safety exclusions that override `.dockerignore` negation rules: credential-style files and directories such as `.env*`, `.ssh/`, `.aws/`, `.netrc`, `.npmrc`, `secrets/`, `*.pem`, and `*.key` are still skipped even if `.dockerignore` tries to include them. diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 68a2eb2d44..826cc10877 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -373,6 +373,7 @@ The poll count is clamped to a minimum of `1` so the probe always runs at least #### `--from ` Build the sandbox image from a custom Dockerfile instead of the stock NemoClaw image. +The supplied Dockerfile defines the complete sandbox image, and NemoClaw does not layer it on top of the stock managed runtime. The entire parent directory of the specified file is used as the Docker build context, so any files your Dockerfile references (scripts, config, etc.) must live alongside it. If that directory contains a `.dockerignore`, onboarding applies those rules while calculating the context size and staging files for Docker. NemoClaw also applies additional secret-safety exclusions that override `.dockerignore` negation rules: credential-style files and directories such as `.env*`, `.ssh/`, `.aws/`, `.netrc`, `.npmrc`, `secrets/`, `*.pem`, and `*.key` are still skipped even if `.dockerignore` tries to include them. @@ -389,6 +390,14 @@ $$nemoclaw onboard --from path/to/Dockerfile The Dockerfile path must exist. Missing paths fail during command parsing before preflight, gateway setup, inference setup, or sandbox creation starts. + + +If deployment verification cannot reach the gateway for a custom OpenClaw image, NemoClaw checks for `/tmp/gateway.log`, `/usr/local/bin/nemoclaw-start`, and `/sandbox/.openclaw/openclaw.json`. +When all three paths are absent, onboarding reports that the custom image lacks the managed runtime instead of treating repeated port-forward retries as the recovery path. +For the version-matched full-runtime plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). + + + The file can have any name; if it is not already named `Dockerfile`, onboard copies it to `Dockerfile` inside the staged build context automatically. To create an isolated build context, create a dedicated directory that contains only the Dockerfile and the files it needs: @@ -2573,6 +2582,13 @@ Set them before running `$$nemoclaw onboard`. | `NEMOCLAW_AUTO_FIX_FIREWALL` | `1` to enable | Opts in to automatic UFW remediation when Linux Docker-driver sandbox containers cannot reach the host gateway after a proven TCP failure. NemoClaw runs `sudo -n` only, validates the narrow Docker bridge subnet → gateway IP:port rule before invoking UFW, re-probes after applying it, and otherwise falls back to the printed manual command. | | `NEMOCLAW_WECHAT_QUIET` | `1` to enable | Silences the `[wechat]` diagnostic lines printed during the host-side WeChat QR login (poll status, IDC redirects, swallowed gateway errors), which are visible by default while the experimental WeChat path stabilizes; set `1` once the flow is reliable in your environment. | + + +Set `NEMOCLAW_SANDBOX_BASE_IMAGE_REF` to an OpenClaw sandbox-base tag or digest to override base-image resolution during onboarding. +Use a release-matched tag or immutable digest for a source-based custom image; NemoClaw resolves the reference and pins a repository digest into a stock-style `ARG BASE_IMAGE` declaration when possible. + + + ### Onboard Profiling Traces Set `NEMOCLAW_TRACE=1` before `$$nemoclaw onboard` to write an OpenTelemetry-style JSON trace for the run. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 9ff916fb92..a86968ea17 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -428,6 +428,22 @@ sudo ufw allow from "$SUBNET" to any port 8080 proto tcp $$nemoclaw onboard ``` + + +### Custom OpenClaw image creates without a gateway or dashboard + +`$$nemoclaw onboard --from ` treats the supplied Dockerfile as the complete sandbox image rather than adding it on top of the stock managed runtime. +If deployment verification cannot reach the gateway, NemoClaw checks for `/tmp/gateway.log`, `/usr/local/bin/nemoclaw-start`, and `/sandbox/.openclaw/openclaw.json` in the custom sandbox. +When all three paths are absent, the CLI reports that the image lacks the NemoClaw-managed OpenClaw runtime and does not suggest repeated dashboard port-forward retries. +This failure commonly occurs when the custom Dockerfile starts from `ghcr.io/nvidia/nemoclaw/sandbox-base` alone because that image is an intermediate dependency image. + +Rebuild the custom image from the full stock Dockerfile and source context for the same NemoClaw release. +For the version-pinned plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). + +If the sandbox is unreachable or the managed runtime paths are present, NemoClaw retains the existing generic gateway-log and host OpenShell-log guidance because the base-only failure is not proven. + + + ### `connect` exits because the gateway is down `$$nemoclaw connect` checks the OpenShell gateway before it tries dashboard forwarding, SSH, or inference repair. diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 110cc6ab5d..8f20aa6be6 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -5224,6 +5224,7 @@ async function onboard(opts: OnboardOptions = {}): Promise { verifyDeployment: async (name, chain) => { const verifyDeploymentModule: typeof import("./verify-deployment") = require("./verify-deployment"); + // biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail. return verifyDeploymentModule.verifyDeployment(name, chain, { executeSandboxCommand: (sandbox: string, script: string) => executeSandboxCommandForVerification(sandbox, script), @@ -5246,9 +5247,8 @@ async function onboard(opts: OnboardOptions = {}): Promise { captureForwardList: () => runCaptureOpenshell(["forward", "list"], { ignoreError: true }) || null, getMessagingChannels: () => liveFinalFlowContext.selectedMessagingChannels || [], - providerExistsInGateway: (providerName: string) => - providerExistsInGateway(providerName), - }); + providerExistsInGateway: (providerName: string) => providerExistsInGateway(providerName), + }, { diagnoseCustomOpenClawRuntime: verifyDeploymentModule.shouldDiagnoseCustomOpenClawRuntime(liveFinalFlowContext.fromDockerfile, selectedAgentName) }); }, formatVerificationDiagnostics: (result) => { const verifyDeploymentModule: typeof import("./verify-deployment") = diff --git a/src/lib/verify-deployment.test.ts b/src/lib/verify-deployment.test.ts index 32a5894cbd..56c0b2c167 100644 --- a/src/lib/verify-deployment.test.ts +++ b/src/lib/verify-deployment.test.ts @@ -3,7 +3,12 @@ import { describe, expect, it } from "vitest"; import { buildChain } from "./dashboard/contract.js"; -import { formatVerificationDiagnostics, verifyDeployment } from "./verify-deployment.js"; +import { + classifyOpenClawRuntimeFailure, + formatVerificationDiagnostics, + shouldDiagnoseCustomOpenClawRuntime, + verifyDeployment, +} from "./verify-deployment.js"; const chain = buildChain(); @@ -11,6 +16,11 @@ const chain = buildChain(); // Production callers use the default DEFAULT_RETRY_DELAYS_MS. const NO_RETRY = { retryDelaysMs: [], sleep: async (_ms: number) => {} }; +const CUSTOM_OPENCLAW_NO_RETRY = { + ...NO_RETRY, + diagnoseCustomOpenClawRuntime: true, +}; + function makeDeps(overrides: Record = {}) { return { executeSandboxCommand: (_name: string, _script: string) => ({ @@ -26,6 +36,54 @@ function makeDeps(overrides: Record = {}) { }; } +describe("classifyOpenClawRuntimeFailure", () => { + it("recognizes a normal managed runtime when startup and config artifacts exist", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => ({ + status: 0, + stdout: "nemoclaw-runtime-probe-v1 log=0 start=1 config=1", + stderr: "", + })); + expect(result).toEqual({ + kind: "normal_runtime", + gatewayLogPresent: false, + startupScriptPresent: true, + configPresent: true, + }); + }); + + it("recognizes the base-only image when log, startup, and config artifacts are absent (#6108)", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => ({ + status: 0, + stdout: "nemoclaw-runtime-probe-v1 log=0 start=0 config=0", + stderr: "", + })); + expect(result.kind).toBe("base_only_image"); + }); + + it("preserves an unreachable classification when sandbox exec fails", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => null); + expect(result.kind).toBe("sandbox_unreachable"); + }); + + it("recognizes OpenShell-framed probe output", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => ({ + status: 0, + stdout: + "OpenShell sandbox exec output:\nstdout: nemoclaw-runtime-probe-v1 log=0 start=0 config=0", + stderr: "", + })); + expect(result.kind).toBe("base_only_image"); + }); +}); + +describe("shouldDiagnoseCustomOpenClawRuntime", () => { + it("enables the diagnostic only for a custom OpenClaw Dockerfile", () => { + expect(shouldDiagnoseCustomOpenClawRuntime("/tmp/Dockerfile", "openclaw")).toBe(true); + expect(shouldDiagnoseCustomOpenClawRuntime(null, "openclaw")).toBe(false); + expect(shouldDiagnoseCustomOpenClawRuntime("/tmp/Dockerfile", "hermes")).toBe(false); + }); +}); + describe("verifyDeployment", () => { it("reports healthy when gateway and dashboard reachable", async () => { const result = await verifyDeployment("my-sandbox", chain, makeDeps(), NO_RETRY); @@ -57,6 +115,71 @@ describe("verifyDeployment", () => { expect(gwDiag?.hint).toContain("openshell-gateway.log"); }); + it("diagnoses a base-only custom OpenClaw image without suggesting another port-forward retry (#6108)", async () => { + const sleepCalls: number[] = []; + const deps = makeDeps({ + executeSandboxCommand: (_name: string, script: string) => { + if (script.includes("nemoclaw-runtime-probe-v1")) { + return { + status: 0, + stdout: "nemoclaw-runtime-probe-v1 log=0 start=0 config=0", + stderr: "", + }; + } + return { status: 0, stdout: "000", stderr: "" }; + }, + probeHostPort: () => 0, + }); + const result = await verifyDeployment("my-sandbox", chain, deps, { + retryDelaysMs: [10, 20], + sleep: async (delayMs) => { + sleepCalls.push(delayMs); + }, + diagnoseCustomOpenClawRuntime: true, + }); + const gateway = result.diagnostics.find((diagnostic) => diagnostic.link === "gateway"); + const dashboard = result.diagnostics.find((diagnostic) => diagnostic.link === "dashboard"); + expect(gateway?.hint).toContain("does not contain the NemoClaw-managed OpenClaw runtime"); + expect(gateway?.hint).toContain("onboard --from"); + expect(gateway?.hint).toContain("sandbox-base"); + expect(dashboard?.hint).toContain("cannot start until the custom image includes"); + expect(dashboard?.hint).not.toContain("openshell forward start"); + expect(sleepCalls).toEqual([10, 20]); + }); + + it("keeps generic guidance when a custom image has the normal runtime contract", async () => { + const deps = makeDeps({ + executeSandboxCommand: (_name: string, script: string) => { + if (script.includes("nemoclaw-runtime-probe-v1")) { + return { + status: 0, + stdout: "nemoclaw-runtime-probe-v1 log=0 start=1 config=1", + stderr: "", + }; + } + return { status: 0, stdout: "000", stderr: "" }; + }, + probeHostPort: () => 0, + }); + const result = await verifyDeployment("my-sandbox", chain, deps, CUSTOM_OPENCLAW_NO_RETRY); + const gateway = result.diagnostics.find((diagnostic) => diagnostic.link === "gateway"); + const dashboard = result.diagnostics.find((diagnostic) => diagnostic.link === "dashboard"); + expect(gateway?.hint).toContain("nemoclaw my-sandbox logs"); + expect(dashboard?.hint).toContain("openshell forward start"); + }); + + it("keeps generic guidance when the custom sandbox is unreachable", async () => { + const deps = makeDeps({ + executeSandboxCommand: () => null, + probeHostPort: () => 0, + }); + const result = await verifyDeployment("my-sandbox", chain, deps, CUSTOM_OPENCLAW_NO_RETRY); + const gateway = result.diagnostics.find((diagnostic) => diagnostic.link === "gateway"); + const dashboard = result.diagnostics.find((diagnostic) => diagnostic.link === "dashboard"); + expect(gateway?.hint).toContain("openshell-gateway.log"); + expect(dashboard?.hint).toContain("openshell forward start"); + }); + it("hint surfaces both the in-sandbox gateway log (via nemoclaw logs) and the host OpenShell log (#3563)", async () => { const deps = makeDeps({ executeSandboxCommand: () => ({ status: 0, stdout: "000", stderr: "" }), diff --git a/src/lib/verify-deployment.ts b/src/lib/verify-deployment.ts index 8bab1f59ee..0481d401dc 100644 --- a/src/lib/verify-deployment.ts +++ b/src/lib/verify-deployment.ts @@ -17,8 +17,8 @@ * "Health Offline" in the dashboard. */ -import type { DashboardDeliveryChain } from "./dashboard/contract"; import { compareChannelSets, type RuntimeChannelStatus } from "./channel-runtime-status"; +import type { DashboardDeliveryChain } from "./dashboard/contract"; import { listMessagingChannelsWithoutCredentials } from "./messaging/channels"; import { getMessagingProviderNamesForChannel } from "./onboard/messaging-reuse"; @@ -118,6 +118,12 @@ export interface VerifyDeploymentOptions { retryDelaysMs?: number[]; /** Sleep helper, injectable for tests. */ sleep?: (ms: number) => Promise; + /** + * Inspect a failed custom OpenClaw image for the managed runtime contract. + * Keep this disabled for stock images and other agents, whose config and + * startup paths intentionally differ. + */ + diagnoseCustomOpenClawRuntime?: boolean; } const DEFAULT_RETRY_DELAYS_MS: readonly number[] = [1000, 2000, 5000, 7000, 10000]; @@ -137,7 +143,88 @@ const CREDENTIALLESS_MESSAGING_CHANNELS = new Set(listMessagingChannelsWithoutCr // sandbox log is the first thing to check. If the sandbox itself never // came up, the host-side OpenShell gateway log is the right place to // look — see gatewayLogCandidates() in onboard/sandbox-create-failure.ts. -function buildGatewayLogHint(sandboxName: string): string { +export type OpenClawRuntimeFailureKind = + | "normal_runtime" + | "base_only_image" + | "sandbox_unreachable" + | "inconclusive"; + +export interface OpenClawRuntimeFailureDiagnosis { + kind: OpenClawRuntimeFailureKind; + gatewayLogPresent: boolean | null; + startupScriptPresent: boolean | null; + configPresent: boolean | null; +} + +const OPENCLAW_RUNTIME_PROBE = + "printf 'nemoclaw-runtime-probe-v1 '; " + + "if [ -e /tmp/gateway.log ]; then printf 'log=1 '; else printf 'log=0 '; fi; " + + "if [ -x /usr/local/bin/nemoclaw-start ]; then printf 'start=1 '; else printf 'start=0 '; fi; " + + "if [ -e /sandbox/.openclaw/openclaw.json ]; then printf 'config=1\\n'; else printf 'config=0\\n'; fi"; + +export function shouldDiagnoseCustomOpenClawRuntime( + fromDockerfile: string | null | undefined, + selectedAgentName: string | null | undefined, +): boolean { + return Boolean(fromDockerfile && selectedAgentName === "openclaw"); +} + +/** + * Distinguish the documented sandbox-base-only failure from an ordinary + * gateway startup failure without reading config or log contents. + */ +export function classifyOpenClawRuntimeFailure( + sandboxName: string, + executeSandboxCommand: VerifyDeploymentDeps["executeSandboxCommand"], +): OpenClawRuntimeFailureDiagnosis { + const result = executeSandboxCommand(sandboxName, OPENCLAW_RUNTIME_PROBE); + if (!result) { + return { + kind: "sandbox_unreachable", + gatewayLogPresent: null, + startupScriptPresent: null, + configPresent: null, + }; + } + + const match = result.stdout.match( + /(?:^|\n)\s*(?:(?:\[stdout\]|stdout:)\s*)?nemoclaw-runtime-probe-v1 log=([01]) start=([01]) config=([01])(?:\r?\n|$)/i, + ); + if (result.status !== 0 || !match) { + return { + kind: "inconclusive", + gatewayLogPresent: null, + startupScriptPresent: null, + configPresent: null, + }; + } + + const gatewayLogPresent = match[1] === "1"; + const startupScriptPresent = match[2] === "1"; + const configPresent = match[3] === "1"; + let kind: OpenClawRuntimeFailureKind = "inconclusive"; + if (!gatewayLogPresent && !startupScriptPresent && !configPresent) { + kind = "base_only_image"; + } else if (startupScriptPresent && configPresent) { + kind = "normal_runtime"; + } + return { kind, gatewayLogPresent, startupScriptPresent, configPresent }; +} + +function buildGatewayLogHint( + sandboxName: string, + runtimeDiagnosis: OpenClawRuntimeFailureDiagnosis | null, +): string { + if (runtimeDiagnosis?.kind === "base_only_image") { + return ( + "This custom image does not contain the NemoClaw-managed OpenClaw runtime: " + + "`/usr/local/bin/nemoclaw-start` and `/sandbox/.openclaw/openclaw.json` are missing, " + + "and `/tmp/gateway.log` was never created. `nemoclaw onboard --from` uses the supplied " + + "Dockerfile as the complete sandbox image; it does not layer it over the managed runtime. " + + "Build from the full NemoClaw Dockerfile and context for the same release instead of using " + + "`ghcr.io/nvidia/nemoclaw/sandbox-base` directly." + ); + } return ( `The gateway probe failed after retrying. Inspect the in-sandbox gateway log with ` + `\`nemoclaw ${sandboxName} logs\` (the gateway writes to /tmp/gateway.log inside the sandbox when it starts). ` + @@ -466,25 +553,34 @@ export async function verifyDeployment( // 1. Gateway reachable inside sandbox const gateway = await verifyGatewayInSandbox(sandboxName, chain, deps, retryDelaysMs, sleep); + const runtimeDiagnosis = + !gateway.reachable && options.diagnoseCustomOpenClawRuntime + ? classifyOpenClawRuntimeFailure(sandboxName, deps.executeSandboxCommand) + : null; diagnostics.push({ link: "gateway", status: gateway.reachable ? "ok" : "fail", detail: gateway.detail, - hint: gateway.reachable ? "" : buildGatewayLogHint(sandboxName), + hint: gateway.reachable ? "" : buildGatewayLogHint(sandboxName, runtimeDiagnosis), }); // 2. Gateway version (cosmetic — not a health signal) const gatewayVersion = gateway.reachable ? fetchGatewayVersion(sandboxName, deps) : null; // 3. Dashboard reachable from host (port forward) - const dashboard = await verifyDashboardFromHost(chain, deps, retryDelaysMs, sleep); + // A port forward cannot repair an image that has no managed gateway runtime, + // so avoid spending a second retry budget on the dependent dashboard probe. + const dashboardRetryDelays = runtimeDiagnosis?.kind === "base_only_image" ? [] : retryDelaysMs; + const dashboard = await verifyDashboardFromHost(chain, deps, dashboardRetryDelays, sleep); diagnostics.push({ link: "dashboard", status: dashboard.reachable ? "ok" : "fail", detail: dashboard.detail, hint: dashboard.reachable ? "" - : `Port forward on ${chain.port} is not working. Run: openshell forward start ${chain.forwardTarget} ${sandboxName}`, + : runtimeDiagnosis?.kind === "base_only_image" + ? "The dashboard cannot start until the custom image includes the NemoClaw-managed OpenClaw runtime." + : `Port forward on ${chain.port} is not working. Run: openshell forward start ${chain.forwardTarget} ${sandboxName}`, }); // 4. Inference route diff --git a/test/e2e/fixtures/plugins/weather/.gitignore b/test/e2e/fixtures/plugins/weather/.gitignore new file mode 100644 index 0000000000..db079bb87f --- /dev/null +++ b/test/e2e/fixtures/plugins/weather/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +dist/ +node_modules/ diff --git a/test/e2e/fixtures/plugins/weather/openclaw.plugin.json b/test/e2e/fixtures/plugins/weather/openclaw.plugin.json new file mode 100644 index 0000000000..4130bea26e --- /dev/null +++ b/test/e2e/fixtures/plugins/weather/openclaw.plugin.json @@ -0,0 +1,19 @@ +{ + "id": "weather", + "name": "NemoClaw E2E Weather", + "version": "1.0.0", + "description": "Registers a deterministic weather tool for custom-image lifecycle tests", + "activation": { + "onStartup": true + }, + "contracts": { + "tools": [ + "get_weather" + ] + }, + "configSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false + } +} diff --git a/test/e2e/fixtures/plugins/weather/package-lock.json b/test/e2e/fixtures/plugins/weather/package-lock.json new file mode 100644 index 0000000000..15d8f4b957 --- /dev/null +++ b/test/e2e/fixtures/plugins/weather/package-lock.json @@ -0,0 +1,42 @@ +{ + "name": "@nemoclaw/e2e-weather-plugin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nemoclaw/e2e-weather-plugin", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "typebox": "1.1.38" + }, + "devDependencies": { + "typescript": "5.9.3" + }, + "engines": { + "node": ">=22.16.0" + } + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/test/e2e/fixtures/plugins/weather/package.json b/test/e2e/fixtures/plugins/weather/package.json new file mode 100644 index 0000000000..af2eced71a --- /dev/null +++ b/test/e2e/fixtures/plugins/weather/package.json @@ -0,0 +1,37 @@ +{ + "name": "@nemoclaw/e2e-weather-plugin", + "version": "1.0.0", + "description": "Deterministic OpenClaw weather tool fixture for NemoClaw live tests", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/", + "openclaw.plugin.json" + ], + "openclaw": { + "extensions": [ + "./dist/index.js" + ], + "compat": { + "pluginApi": ">=2026.5.22", + "minGatewayVersion": "2026.5.22" + }, + "build": { + "openclawVersion": "2026.5.27" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json" + }, + "dependencies": { + "typebox": "1.1.38" + }, + "devDependencies": { + "typescript": "5.9.3" + }, + "engines": { + "node": ">=22.16.0" + } +} diff --git a/test/e2e/fixtures/plugins/weather/src/index.ts b/test/e2e/fixtures/plugins/weather/src/index.ts new file mode 100644 index 0000000000..e9d3318482 --- /dev/null +++ b/test/e2e/fixtures/plugins/weather/src/index.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Type } from "typebox"; + +type ToolResult = { + content: Array<{ type: "text"; text: string }>; + details: Record; +}; + +type OpenClawPluginApi = { + registerTool(tool: { + name: string; + label: string; + description: string; + parameters: ReturnType; + execute(toolCallId: string, params: Record): Promise; + }): void; +}; + +const WeatherParameters = Type.Object( + { + location: Type.String({ description: "City or place name." }), + }, + { additionalProperties: false }, +); + +const plugin = { + id: "weather", + name: "NemoClaw E2E Weather", + description: "Registers a deterministic weather tool for custom-image lifecycle tests.", + register(api: OpenClawPluginApi): void { + api.registerTool({ + name: "get_weather", + label: "Get Weather", + description: "Return deterministic weather data for a location.", + parameters: WeatherParameters, + async execute(_toolCallId, params) { + const location = typeof params.location === "string" ? params.location : "unknown"; + const details = { location, condition: "clear", temperatureC: 21 }; + return { + content: [{ type: "text", text: JSON.stringify(details) }], + details, + }; + }, + }); + }, +}; + +export default plugin; diff --git a/test/e2e/fixtures/plugins/weather/tsconfig.json b/test/e2e/fixtures/plugins/weather/tsconfig.json new file mode 100644 index 0000000000..f23f9cb5c5 --- /dev/null +++ b/test/e2e/fixtures/plugins/weather/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "target": "ES2022" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index b3dfbd8645..2106f2938c 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -5,19 +5,27 @@ import fs from "node:fs"; import path from "node:path"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; -import { trustedSandboxShellScript, validateSandboxName } from "../fixtures/clients/sandbox.ts"; +import { + type SandboxClient, + trustedSandboxShellScript, + validateSandboxName, +} from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; +import { parseJsonFromText } from "./json-envelope.ts"; -// the contract as a simple live test: onboard a fresh OpenClaw sandbox -// from the repo Dockerfile, capture the sandbox filesystem layout, then run a -// focused in-sandbox Node replacement probe that guards #3513/#3127's EXDEV -// cross-device runtime-deps failure mode. No registry, no ledger, no shared helper. +// Keep this contract as a focused live test: build a deterministic custom plugin +// on top of the complete managed runtime, prove it survives restart/rebuild, then +// run the in-sandbox Node replacement probe that guards #3513/#3127's EXDEV +// cross-device runtime-deps failure mode. No registry or ledger is required. const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js"); +const CUSTOM_DOCKERFILE = path.join(REPO_ROOT, "Dockerfile.e2e-weather-plugin"); +const SANDBOX_BASE_IMAGE_REF = "ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71"; const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-plugin-exdev"; const ONBOARD_TIMEOUT_MS = 25 * 60_000; +const REBUILD_TIMEOUT_MS = 20 * 60_000; const PROBE_TIMEOUT_MS = 60_000; validateSandboxName(SANDBOX_NAME); @@ -31,6 +39,13 @@ function resultText(result: { stdout: string; stderr: string }): string { return [result.stdout, result.stderr].filter(Boolean).join("\n"); } +function normalizeSandboxStdoutFrames(output: string): string { + return output + .split(/\r?\n/) + .map((line) => line.replace(/^\s*(?:\[stdout\]|stdout:)\s*/i, "")) + .join("\n"); +} + function liveEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { return { ...buildAvailabilityProbeEnv(), @@ -81,6 +96,150 @@ function patchPoliciesForDevShm(): () => void { }; } +function createCustomPluginDockerfile(): () => void { + const sourceDockerfile = path.join(REPO_ROOT, "Dockerfile"); + const source = fs.readFileSync(sourceDockerfile, "utf8"); + const baseImageAnchor = "ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/sandbox-base:latest\n"; + const runtimeAnchor = "FROM ${BASE_IMAGE}\n"; + expect( + source.match(/^ARG BASE_IMAGE=ghcr\.io\/nvidia\/nemoclaw\/sandbox-base:latest$/gm)?.length, + ).toBe(1); + expect(source.match(/^FROM \$\{BASE_IMAGE\}$/gm)?.length, "expected one runtime stage").toBe(1); + + const runtime = source + .replace(baseImageAnchor, `ARG BASE_IMAGE=${SANDBOX_BASE_IMAGE_REF}\n`) + .replace(runtimeAnchor, "FROM ${BASE_IMAGE} AS nemoclaw-runtime\n"); + const extension = String.raw` + +# Build the deterministic custom-plugin fixture used by this live contract. +FROM builder AS weather-plugin-builder +WORKDIR /opt/weather +COPY test/e2e/fixtures/plugins/weather/package.json test/e2e/fixtures/plugins/weather/package-lock.json test/e2e/fixtures/plugins/weather/tsconfig.json ./ +RUN npm ci --no-audit --no-fund +COPY test/e2e/fixtures/plugins/weather/openclaw.plugin.json ./ +COPY test/e2e/fixtures/plugins/weather/src/ ./src/ +RUN npm run build \ + && npm prune --omit=dev \ + && sha256sum dist/index.js | cut -d ' ' -f 1 > e2e-weather-plugin.sha256 + +# Extend the completed managed runtime so its entrypoint, health check, config +# generation, and permissions remain the source of truth. +FROM nemoclaw-runtime AS weather-runtime +COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ + /opt/weather/package.json \ + /opt/weather/package-lock.json \ + /opt/weather/openclaw.plugin.json \ + /sandbox/.openclaw/extensions/weather/ +COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ + /opt/weather/dist/ /sandbox/.openclaw/extensions/weather/dist/ +COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ + /opt/weather/node_modules/ /sandbox/.openclaw/extensions/weather/node_modules/ +COPY --from=weather-plugin-builder \ + /opt/weather/e2e-weather-plugin.sha256 \ + /usr/local/share/nemoclaw/e2e-weather-plugin.sha256 + +USER sandbox +RUN HOME=/sandbox openclaw config set plugins.load.paths \ + '["/sandbox/.openclaw/extensions/weather"]' --json \ + && HOME=/sandbox openclaw plugins registry --refresh --json > /dev/null \ + && HOME=/sandbox openclaw plugins enable weather \ + && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null + +# Enabling the plugin changes openclaw.json after the managed runtime hashes it. +# hadolint ignore=DL3002 +USER root +RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ + && chmod 660 /sandbox/.openclaw/openclaw.json \ + && sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash \ + && chown sandbox:sandbox /sandbox/.openclaw/.config-hash \ + && chmod 660 /sandbox/.openclaw/.config-hash +`; + fs.writeFileSync(CUSTOM_DOCKERFILE, runtime.trimEnd() + extension, "utf8"); + return () => fs.rmSync(CUSTOM_DOCKERFILE, { force: true }); +} + +type WeatherPluginInspect = { + plugin?: { id?: unknown; status?: unknown; toolNames?: unknown }; + tools?: Array<{ names?: unknown }>; +}; + +type GatewayToolCatalog = { + groups?: Array<{ tools?: Array<{ id?: unknown }> }>; +}; + +type WeatherRuntimeProof = { + imageMarker: string; + inspectLoaded: boolean; + catalogToolIds: string[]; +}; + +async function assertWeatherPluginRuntime( + sandbox: SandboxClient, + phase: string, +): Promise { + const imageProbe = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript(`set -eu +test -s /tmp/gateway.log +test -s /usr/local/share/nemoclaw/e2e-weather-plugin.sha256 +expected=$(cat /usr/local/share/nemoclaw/e2e-weather-plugin.sha256) +actual=$(sha256sum /sandbox/.openclaw/extensions/weather/dist/index.js | cut -d ' ' -f 1) +[ "$expected" = "$actual" ] +printf '%s\\n' "$actual"`), + { + artifactName: `openclaw-weather-plugin-image-${phase}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, + ); + expect(imageProbe.exitCode, resultText(imageProbe)).toBe(0); + const imageMarker = normalizeSandboxStdoutFrames(imageProbe.stdout).match( + /(?:^|\n)([a-f0-9]{64})(?:\r?\n|$)/, + )?.[1]; + expect(imageMarker).toMatch(/^[a-f0-9]{64}$/); + + const inspectProbe = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript("HOME=/sandbox openclaw plugins inspect weather --runtime --json"), + { + artifactName: `openclaw-weather-plugin-inspect-${phase}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, + ); + expect(inspectProbe.exitCode, resultText(inspectProbe)).toBe(0); + const inspect = parseJsonFromText( + normalizeSandboxStdoutFrames(inspectProbe.stdout), + ) as WeatherPluginInspect; + expect(inspect.plugin?.id).toBe("weather"); + expect(inspect.plugin?.status).toBe("loaded"); + expect(inspect.plugin?.toolNames).toContain("get_weather"); + expect(inspect.tools?.flatMap((tool) => (Array.isArray(tool.names) ? tool.names : []))).toContain( + "get_weather", + ); + + const catalogProbe = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript( + `. /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.catalog --params '{"includePlugins":true}' --json`, + ), + { + artifactName: `openclaw-weather-plugin-catalog-${phase}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, + ); + expect(catalogProbe.exitCode, resultText(catalogProbe)).toBe(0); + const catalog = parseJsonFromText( + normalizeSandboxStdoutFrames(catalogProbe.stdout), + ) as GatewayToolCatalog; + const catalogToolIds = (catalog.groups ?? []).flatMap((group) => + (group.tools ?? []).map((tool) => tool.id).filter((id): id is string => typeof id === "string"), + ); + expect(catalogToolIds).toContain("get_weather"); + return { imageMarker: imageMarker ?? "", inspectLoaded: true, catalogToolIds }; +} + const runtimeDepsReplacementProbeSource = `set -eu rm -rf /sandbox/.openclaw/plugin-runtime-deps/exdev-guard 2>/dev/null || true rm -rf /dev/shm/nemoclaw-exdev-source 2>/dev/null || true @@ -158,20 +317,23 @@ const runtimeDepsReplacementProbe = trustedSandboxShellScript( ); liveTest( - "OpenClaw plugin runtime deps replacement survives cross-filesystem EXDEV layout", - { timeout: ONBOARD_TIMEOUT_MS + PROBE_TIMEOUT_MS + 5 * 60_000 }, + "a custom OpenClaw plugin survives restart and rebuild without EXDEV failures (#6108)", + { timeout: ONBOARD_TIMEOUT_MS + REBUILD_TIMEOUT_MS + 15 * 60_000 }, async ({ artifacts, cleanup, host, sandbox, skip }) => { await artifacts.writeJson("target.json", { id: "openclaw-plugin-runtime-exdev", runner: "vitest", boundary: "fresh-openclaw-sandbox-exec", - regressionTargets: ["#3513", "#3127"], + regressionTargets: ["#6108", "#3513", "#3127"], contract: [ - "fresh OpenClaw sandbox onboards from the checkout Dockerfile", + "fresh OpenClaw sandbox onboards from a full managed custom-plugin Dockerfile", + "gateway log, runtime plugin inspection, and tools.catalog expose weather/get_weather", + "custom-plugin image provenance and the gateway tool survive restart and rebuild", "sandbox proves /dev/shm and plugin-runtime-deps are distinct devices", "legacy source-side staging fails with EXDEV across the same /dev/shm to plugin-runtime-deps boundary", "OpenClaw-style target-side plugin runtime-deps replacement completes without EXDEV", ], + sandboxBaseImageRef: SANDBOX_BASE_IMAGE_REF, }); const docker = await host.command("docker", ["info"], { @@ -228,6 +390,20 @@ liveTest( const restorePolicies = patchPoliciesForDevShm(); cleanup.add("restore EXDEV policy fixture edits", restorePolicies); + const removeCustomDockerfile = createCustomPluginDockerfile(); + cleanup.add("remove custom weather-plugin Dockerfile", removeCustomDockerfile); + + const sandboxEnv = liveEnv({ + COMPATIBLE_API_KEY: "nemoclaw-exdev-dummy-key", + NEMOCLAW_ENDPOINT_URL: "http://host.openshell.internal:65535/v1", + NEMOCLAW_MODEL: "nemoclaw-exdev-probe", + NEMOCLAW_PROVIDER_KEY: "nemoclaw-exdev-dummy-key", + NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, + NEMOCLAW_SANDBOX_BASE_IMAGE_REF: SANDBOX_BASE_IMAGE_REF, + NEMOCLAW_POLICY_MODE: "skip", + NEMOCLAW_PREFERRED_API: "openai-completions", + NEMOCLAW_PROVIDER: "custom", + }); const onboard = await host.command( "node", @@ -240,26 +416,42 @@ liveTest( "--agent", "openclaw", "--from", - path.join(REPO_ROOT, "Dockerfile"), + CUSTOM_DOCKERFILE, ], { artifactName: "openclaw-plugin-exdev-onboard", - env: liveEnv({ - COMPATIBLE_API_KEY: "nemoclaw-exdev-dummy-key", - NEMOCLAW_ENDPOINT_URL: "http://host.openshell.internal:65535/v1", - NEMOCLAW_MODEL: "nemoclaw-exdev-probe", - NEMOCLAW_PROVIDER_KEY: "nemoclaw-exdev-dummy-key", - NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, - NEMOCLAW_POLICY_MODE: "skip", - NEMOCLAW_PREFERRED_API: "openai-completions", - NEMOCLAW_PROVIDER: "custom", - }), + env: sandboxEnv, timeoutMs: ONBOARD_TIMEOUT_MS, }, ); const onboardText = resultText(onboard); expect(onboard.exitCode, onboardText).toBe(0); expect(onboardText).toMatch(/Creating sandbox|Sandbox '.+' created/); + expect(onboardText).toContain("Deployment verified"); + + const weatherAfterOnboard = await assertWeatherPluginRuntime(sandbox, "after-onboard"); + + const restart = await host.command( + "node", + [CLI_ENTRYPOINT, SANDBOX_NAME, "gateway", "restart"], + { + artifactName: "openclaw-weather-plugin-gateway-restart", + env: sandboxEnv, + timeoutMs: 180_000, + }, + ); + expect(restart.exitCode, resultText(restart)).toBe(0); + const weatherAfterRestart = await assertWeatherPluginRuntime(sandbox, "after-restart"); + expect(weatherAfterRestart.imageMarker).toBe(weatherAfterOnboard.imageMarker); + + const rebuild = await host.command("node", [CLI_ENTRYPOINT, SANDBOX_NAME, "rebuild", "--yes"], { + artifactName: "openclaw-weather-plugin-rebuild", + env: sandboxEnv, + timeoutMs: REBUILD_TIMEOUT_MS, + }); + expect(rebuild.exitCode, resultText(rebuild)).toBe(0); + const weatherAfterRebuild = await assertWeatherPluginRuntime(sandbox, "after-rebuild"); + expect(weatherAfterRebuild.imageMarker).toBe(weatherAfterOnboard.imageMarker); const df = await sandbox.execShell( SANDBOX_NAME, @@ -294,9 +486,23 @@ liveTest( await artifacts.writeJson("target-result.json", { id: "openclaw-plugin-runtime-exdev", onboardExitCode: onboard.exitCode, + restartExitCode: restart.exitCode, + rebuildExitCode: rebuild.exitCode, filesystemProbeExitCode: df.exitCode, runtimeDepsProbeExitCode: probe.exitCode, assertions: { + weatherAfterOnboard: + weatherAfterOnboard.inspectLoaded && + weatherAfterOnboard.catalogToolIds.includes("get_weather"), + weatherAfterRestart: + weatherAfterRestart.inspectLoaded && + weatherAfterRestart.catalogToolIds.includes("get_weather"), + weatherAfterRebuild: + weatherAfterRebuild.inspectLoaded && + weatherAfterRebuild.catalogToolIds.includes("get_weather"), + imageMarkerStable: + weatherAfterOnboard.imageMarker === weatherAfterRestart.imageMarker && + weatherAfterRestart.imageMarker === weatherAfterRebuild.imageMarker, distinctDevices: /source_device=\d+ target_device=\d+/.test(probeText), sourceSideExdevSelfCheck: probeText.includes( "source-side staging failure self-check completed", diff --git a/test/regression-e2e-workflow.test.ts b/test/regression-e2e-workflow.test.ts index 8133b31dae..7b93a0d649 100644 --- a/test/regression-e2e-workflow.test.ts +++ b/test/regression-e2e-workflow.test.ts @@ -71,7 +71,7 @@ describe("Regression E2E workflow contract", () => { expect(runStep?.env?.NVIDIA_INFERENCE_API_KEY).toBeUndefined(); }); - it("runs OpenClaw plugin runtime-deps EXDEV through a secret-free Vitest lane", () => { + it("runs the OpenClaw custom-plugin lifecycle and EXDEV guard in a secret-free lane", () => { const job = workflow.jobs?.["openclaw-plugin-runtime-exdev-e2e"]; const steps = job?.steps ?? []; const runText = steps.map((step) => step.run ?? "").join("\n"); @@ -80,14 +80,18 @@ describe("Regression E2E workflow contract", () => { ); const setupNodeStep = steps.find((step) => step.name === "Setup Node"); const runVitestStep = steps.find( - (step) => step.name === "Run OpenClaw plugin runtime-deps EXDEV Vitest test", + (step) => + step.name === "Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV Vitest test", ); + const serializedJob = JSON.stringify(job); expect(job?.permissions).toEqual({ contents: "read" }); expect(checkoutStep?.uses).toMatch(FULL_SHA_ACTION); expect(checkoutStep?.with?.["persist-credentials"]).toBe(false); expect(setupNodeStep?.uses).toMatch(FULL_SHA_ACTION); expect(runVitestStep?.env?.NEMOCLAW_RUN_LIVE_E2E).toBe("1"); + expect(serializedJob).not.toContain("${{ secrets."); + expect(serializedJob).not.toMatch(/"secrets"\s*:\s*"inherit"/); for (const step of steps) { expect( step.env?.NVIDIA_INFERENCE_API_KEY, From 8be68f5c20edd52b2b51976f547d96f19192dd18 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 07:56:33 -0700 Subject: [PATCH 02/46] fix(onboard): harden custom runtime diagnosis Signed-off-by: Aaron Erickson --- docs/deployment/install-openclaw-plugins.mdx | 10 +- .../custom-openclaw-runtime-diagnosis.test.ts | 88 +++++++++++++ .../custom-openclaw-runtime-diagnosis.ts | 108 ++++++++++++++++ src/lib/verify-deployment.test.ts | 118 ++++++------------ src/lib/verify-deployment.ts | 113 ++++------------- .../openclaw-plugin-runtime-exdev.test.ts | 86 ++++++++++--- 6 files changed, 330 insertions(+), 193 deletions(-) create mode 100644 src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts create mode 100644 src/lib/onboard/custom-openclaw-runtime-diagnosis.ts diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 4d8009c77e..0a9dfe0155 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -110,9 +110,7 @@ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ /opt/my-plugin/node_modules/ /sandbox/.openclaw/extensions/weather/node_modules/ USER sandbox -RUN HOME=/sandbox openclaw config set plugins.load.paths \ - '["/sandbox/.openclaw/extensions/weather"]' --json \ - && HOME=/sandbox openclaw plugins registry --refresh --json > /dev/null \ +RUN HOME=/sandbox openclaw plugins install --link /sandbox/.openclaw/extensions/weather \ && HOME=/sandbox openclaw plugins enable weather \ && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null @@ -127,7 +125,7 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ ``` The final stage inherits the stock runtime entrypoint, command, gateway health check, generated configuration, and file permissions. -The explicit `plugins.load.paths` entry gives the copied plugin a tracked load-path provenance before the registry refresh and enable steps. +The linked install preserves existing managed plugin load paths, appends the copied plugin path, refreshes the plugin registry, and records the install before the explicit enable and inspect steps. The last `RUN` refreshes the managed config hash after `openclaw plugins enable` updates `openclaw.json`. ## Create and Verify the Sandbox @@ -151,9 +149,11 @@ nemoclaw weather-agent exec -- test -s /tmp/gateway.log nemoclaw weather-agent exec -- env HOME=/sandbox openclaw plugins inspect weather --runtime --json nemoclaw weather-agent exec -- bash -lc \ ". /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.catalog --params '{\"includePlugins\":true}' --json" +nemoclaw weather-agent exec -- bash -lc \ + ". /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.invoke --params '{\"name\":\"get_weather\",\"args\":{\"location\":\"Santa Clara\"}}' --json" ``` -The plugin inspection must report the `weather` plugin, and the gateway tool catalog must contain `get_weather`. +The plugin inspection must report the `weather` plugin, the gateway tool catalog must contain `get_weather`, and the invocation must return the deterministic `Santa Clara` weather result. Keep the source checkout and Dockerfile at the recorded path if you plan to run `nemoclaw weather-agent rebuild --yes`. This source-based workflow can reproduce the plugin during that rebuild, but it is not the durable managed-plugin lifecycle proposed in issue #5998. diff --git a/src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts b/src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts new file mode 100644 index 0000000000..8ebd1e4e60 --- /dev/null +++ b/src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { + buildCustomOpenClawRuntimeFailureHints, + classifyOpenClawRuntimeFailure, + shouldDiagnoseCustomOpenClawRuntime, +} from "./custom-openclaw-runtime-diagnosis.js"; + +function probeResult(stdout: string) { + return { status: 0, stdout, stderr: "" }; +} + +describe("classifyOpenClawRuntimeFailure", () => { + it("recognizes a normal managed runtime when startup and config artifacts exist", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => + probeResult("nemoclaw-runtime-probe-v1 log=0 start=1 config=1"), + ); + expect(result).toEqual({ + kind: "normal_runtime", + gatewayLogPresent: false, + startupScriptPresent: true, + configPresent: true, + }); + }); + + it("recognizes the base-only image when log, startup, and config artifacts are absent (#6108)", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => + probeResult("nemoclaw-runtime-probe-v1 log=0 start=0 config=0"), + ); + expect(result.kind).toBe("base_only_image"); + expect(buildCustomOpenClawRuntimeFailureHints(result)).toMatchObject({ + gateway: expect.stringContaining("sandbox-base"), + dashboard: expect.stringContaining("cannot start"), + }); + }); + + it.each([ + ["gateway log only", "nemoclaw-runtime-probe-v1 log=1 start=0 config=0"], + ["startup script only", "nemoclaw-runtime-probe-v1 log=0 start=1 config=0"], + ["generated config only", "nemoclaw-runtime-probe-v1 log=0 start=0 config=1"], + ])("keeps a partial managed runtime inconclusive when it has %s", (_name, stdout) => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => probeResult(stdout)); + expect(result.kind).toBe("inconclusive"); + expect(buildCustomOpenClawRuntimeFailureHints(result)).toBeNull(); + }); + + it("preserves an unreachable classification when sandbox exec fails", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => null); + expect(result.kind).toBe("sandbox_unreachable"); + }); + + it("recognizes OpenShell-framed probe output", () => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => + probeResult( + "OpenShell sandbox exec output:\r\nstdout: nemoclaw-runtime-probe-v1 log=0 start=0 config=0\r\n", + ), + ); + expect(result.kind).toBe("base_only_image"); + }); + + it.each([ + [ + "nonzero probe", + { status: 1, stdout: "nemoclaw-runtime-probe-v1 log=0 start=0 config=0", stderr: "" }, + ], + ["malformed output", probeResult("not a runtime probe frame")], + ])("keeps a %s inconclusive", (_name, probe) => { + const result = classifyOpenClawRuntimeFailure("my-sandbox", () => probe); + expect(result).toEqual({ + kind: "inconclusive", + gatewayLogPresent: null, + startupScriptPresent: null, + configPresent: null, + }); + }); +}); + +describe("shouldDiagnoseCustomOpenClawRuntime", () => { + it.each([ + ["custom OpenClaw Dockerfile", "/tmp/Dockerfile", "openclaw", true], + ["stock OpenClaw image", null, "openclaw", false], + ["custom Hermes Dockerfile", "/tmp/Dockerfile", "hermes", false], + ])("returns the expected gate for a %s", (_name, dockerfile, agent, expected) => { + expect(shouldDiagnoseCustomOpenClawRuntime(dockerfile, agent)).toBe(expected); + }); +}); diff --git a/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts b/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts new file mode 100644 index 0000000000..11502e6d1e --- /dev/null +++ b/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export type SandboxCommandExecutor = ( + name: string, + script: string, +) => { status: number; stdout: string; stderr: string } | null; + +export type OpenClawRuntimeFailureKind = + | "normal_runtime" + | "base_only_image" + | "sandbox_unreachable" + | "inconclusive"; + +export interface OpenClawRuntimeFailureDiagnosis { + kind: OpenClawRuntimeFailureKind; + gatewayLogPresent: boolean | null; + startupScriptPresent: boolean | null; + configPresent: boolean | null; +} + +export interface CustomOpenClawRuntimeFailureHints { + gateway: string; + dashboard: string; +} + +const OPENCLAW_RUNTIME_PROBE = + "printf 'nemoclaw-runtime-probe-v1 '; " + + "if [ -e /tmp/gateway.log ]; then printf 'log=1 '; else printf 'log=0 '; fi; " + + "if [ -x /usr/local/bin/nemoclaw-start ]; then printf 'start=1 '; else printf 'start=0 '; fi; " + + "if [ -e /sandbox/.openclaw/openclaw.json ]; then printf 'config=1\\n'; else printf 'config=0\\n'; fi"; + +/** + * This diagnostic handles an invalid state authored at the custom-image source + * boundary: a user Dockerfile passed to `onboard --from` selects the + * intermediate `sandbox-base` image as its final runtime. Preventing that state + * earlier would require reliable static analysis of arbitrary multi-stage + * Dockerfiles or a new build-time image contract. The focused classifier tests + * prevent false positives, and the live custom-plugin E2E proves the supported + * full-runtime path. Remove this runtime fallback when #5998 supplies the + * managed plugin lifecycle or a build-time contract validator replaces it. + */ + +export function shouldDiagnoseCustomOpenClawRuntime( + fromDockerfile: string | null | undefined, + selectedAgentName: string | null | undefined, +): boolean { + return Boolean(fromDockerfile && selectedAgentName === "openclaw"); +} + +/** + * Distinguish the documented sandbox-base-only failure from an ordinary + * gateway startup failure without reading config or log contents. + */ +export function classifyOpenClawRuntimeFailure( + sandboxName: string, + executeSandboxCommand: SandboxCommandExecutor, +): OpenClawRuntimeFailureDiagnosis { + const result = executeSandboxCommand(sandboxName, OPENCLAW_RUNTIME_PROBE); + if (!result) { + return { + kind: "sandbox_unreachable", + gatewayLogPresent: null, + startupScriptPresent: null, + configPresent: null, + }; + } + + const match = result.stdout.match( + /(?:^|\n)\s*(?:(?:\[stdout\]|stdout:)\s*)?nemoclaw-runtime-probe-v1 log=([01]) start=([01]) config=([01])(?:\r?\n|$)/i, + ); + if (result.status !== 0 || !match) { + return { + kind: "inconclusive", + gatewayLogPresent: null, + startupScriptPresent: null, + configPresent: null, + }; + } + + const gatewayLogPresent = match[1] === "1"; + const startupScriptPresent = match[2] === "1"; + const configPresent = match[3] === "1"; + let kind: OpenClawRuntimeFailureKind = "inconclusive"; + if (!gatewayLogPresent && !startupScriptPresent && !configPresent) { + kind = "base_only_image"; + } else if (startupScriptPresent && configPresent) { + kind = "normal_runtime"; + } + return { kind, gatewayLogPresent, startupScriptPresent, configPresent }; +} + +export function buildCustomOpenClawRuntimeFailureHints( + diagnosis: OpenClawRuntimeFailureDiagnosis, +): CustomOpenClawRuntimeFailureHints | null { + if (diagnosis.kind !== "base_only_image") return null; + return { + gateway: + "This custom image does not contain the NemoClaw-managed OpenClaw runtime: " + + "`/usr/local/bin/nemoclaw-start` and `/sandbox/.openclaw/openclaw.json` are missing, " + + "and `/tmp/gateway.log` was never created. `nemoclaw onboard --from` uses the supplied " + + "Dockerfile as the complete sandbox image; it does not layer it over the managed runtime. " + + "Build from the full NemoClaw Dockerfile and context for the same release instead of using " + + "`ghcr.io/nvidia/nemoclaw/sandbox-base` directly.", + dashboard: + "The dashboard cannot start until the custom image includes the NemoClaw-managed OpenClaw runtime.", + }; +} diff --git a/src/lib/verify-deployment.test.ts b/src/lib/verify-deployment.test.ts index 56c0b2c167..77540fb265 100644 --- a/src/lib/verify-deployment.test.ts +++ b/src/lib/verify-deployment.test.ts @@ -3,12 +3,7 @@ import { describe, expect, it } from "vitest"; import { buildChain } from "./dashboard/contract.js"; -import { - classifyOpenClawRuntimeFailure, - formatVerificationDiagnostics, - shouldDiagnoseCustomOpenClawRuntime, - verifyDeployment, -} from "./verify-deployment.js"; +import { formatVerificationDiagnostics, verifyDeployment } from "./verify-deployment.js"; const chain = buildChain(); @@ -36,53 +31,15 @@ function makeDeps(overrides: Record = {}) { }; } -describe("classifyOpenClawRuntimeFailure", () => { - it("recognizes a normal managed runtime when startup and config artifacts exist", () => { - const result = classifyOpenClawRuntimeFailure("my-sandbox", () => ({ - status: 0, - stdout: "nemoclaw-runtime-probe-v1 log=0 start=1 config=1", - stderr: "", - })); - expect(result).toEqual({ - kind: "normal_runtime", - gatewayLogPresent: false, - startupScriptPresent: true, - configPresent: true, - }); - }); - - it("recognizes the base-only image when log, startup, and config artifacts are absent (#6108)", () => { - const result = classifyOpenClawRuntimeFailure("my-sandbox", () => ({ - status: 0, - stdout: "nemoclaw-runtime-probe-v1 log=0 start=0 config=0", - stderr: "", - })); - expect(result.kind).toBe("base_only_image"); - }); - - it("preserves an unreachable classification when sandbox exec fails", () => { - const result = classifyOpenClawRuntimeFailure("my-sandbox", () => null); - expect(result.kind).toBe("sandbox_unreachable"); - }); - - it("recognizes OpenShell-framed probe output", () => { - const result = classifyOpenClawRuntimeFailure("my-sandbox", () => ({ - status: 0, - stdout: - "OpenShell sandbox exec output:\nstdout: nemoclaw-runtime-probe-v1 log=0 start=0 config=0", - stderr: "", - })); - expect(result.kind).toBe("base_only_image"); +function makeFailedCustomOpenClawDeps(runtimeProbeStdout: string) { + return makeDeps({ + executeSandboxCommand: (_name: string, script: string) => + script.includes("nemoclaw-runtime-probe-v1") + ? { status: 0, stdout: runtimeProbeStdout, stderr: "" } + : { status: 0, stdout: "000", stderr: "" }, + probeHostPort: () => 0, }); -}); - -describe("shouldDiagnoseCustomOpenClawRuntime", () => { - it("enables the diagnostic only for a custom OpenClaw Dockerfile", () => { - expect(shouldDiagnoseCustomOpenClawRuntime("/tmp/Dockerfile", "openclaw")).toBe(true); - expect(shouldDiagnoseCustomOpenClawRuntime(null, "openclaw")).toBe(false); - expect(shouldDiagnoseCustomOpenClawRuntime("/tmp/Dockerfile", "hermes")).toBe(false); - }); -}); +} describe("verifyDeployment", () => { it("reports healthy when gateway and dashboard reachable", async () => { @@ -117,19 +74,7 @@ describe("verifyDeployment", () => { it("diagnoses a base-only custom OpenClaw image without suggesting another port-forward retry (#6108)", async () => { const sleepCalls: number[] = []; - const deps = makeDeps({ - executeSandboxCommand: (_name: string, script: string) => { - if (script.includes("nemoclaw-runtime-probe-v1")) { - return { - status: 0, - stdout: "nemoclaw-runtime-probe-v1 log=0 start=0 config=0", - stderr: "", - }; - } - return { status: 0, stdout: "000", stderr: "" }; - }, - probeHostPort: () => 0, - }); + const deps = makeFailedCustomOpenClawDeps("nemoclaw-runtime-probe-v1 log=0 start=0 config=0"); const result = await verifyDeployment("my-sandbox", chain, deps, { retryDelaysMs: [10, 20], sleep: async (delayMs) => { @@ -148,19 +93,7 @@ describe("verifyDeployment", () => { }); it("keeps generic guidance when a custom image has the normal runtime contract", async () => { - const deps = makeDeps({ - executeSandboxCommand: (_name: string, script: string) => { - if (script.includes("nemoclaw-runtime-probe-v1")) { - return { - status: 0, - stdout: "nemoclaw-runtime-probe-v1 log=0 start=1 config=1", - stderr: "", - }; - } - return { status: 0, stdout: "000", stderr: "" }; - }, - probeHostPort: () => 0, - }); + const deps = makeFailedCustomOpenClawDeps("nemoclaw-runtime-probe-v1 log=0 start=1 config=1"); const result = await verifyDeployment("my-sandbox", chain, deps, CUSTOM_OPENCLAW_NO_RETRY); const gateway = result.diagnostics.find((diagnostic) => diagnostic.link === "gateway"); const dashboard = result.diagnostics.find((diagnostic) => diagnostic.link === "dashboard"); @@ -168,6 +101,22 @@ describe("verifyDeployment", () => { expect(dashboard?.hint).toContain("openshell forward start"); }); + it.each([ + ["gateway log only", "nemoclaw-runtime-probe-v1 log=1 start=0 config=0"], + ["startup script only", "nemoclaw-runtime-probe-v1 log=0 start=1 config=0"], + ])("keeps generic guidance for a partial custom runtime with %s", async (_name, stdout) => { + const result = await verifyDeployment( + "my-sandbox", + chain, + makeFailedCustomOpenClawDeps(stdout), + CUSTOM_OPENCLAW_NO_RETRY, + ); + const gateway = result.diagnostics.find((diagnostic) => diagnostic.link === "gateway"); + const dashboard = result.diagnostics.find((diagnostic) => diagnostic.link === "dashboard"); + expect(gateway?.hint).toContain("nemoclaw my-sandbox logs"); + expect(dashboard?.hint).toContain("openshell forward start"); + }); + it("keeps generic guidance when the custom sandbox is unreachable", async () => { const deps = makeDeps({ executeSandboxCommand: () => null, @@ -180,6 +129,19 @@ describe("verifyDeployment", () => { expect(dashboard?.hint).toContain("openshell forward start"); }); + it("does not probe the custom runtime contract when diagnosis is disabled", async () => { + const scripts: string[] = []; + const deps = makeDeps({ + executeSandboxCommand: (_name: string, script: string) => { + scripts.push(script); + return { status: 0, stdout: "000", stderr: "" }; + }, + probeHostPort: () => 0, + }); + await verifyDeployment("my-sandbox", chain, deps, NO_RETRY); + expect(scripts.join("\n")).not.toContain("nemoclaw-runtime-probe-v1"); + }); + it("hint surfaces both the in-sandbox gateway log (via nemoclaw logs) and the host OpenShell log (#3563)", async () => { const deps = makeDeps({ executeSandboxCommand: () => ({ status: 0, stdout: "000", stderr: "" }), diff --git a/src/lib/verify-deployment.ts b/src/lib/verify-deployment.ts index 0481d401dc..c8f359bac6 100644 --- a/src/lib/verify-deployment.ts +++ b/src/lib/verify-deployment.ts @@ -20,8 +20,15 @@ import { compareChannelSets, type RuntimeChannelStatus } from "./channel-runtime-status"; import type { DashboardDeliveryChain } from "./dashboard/contract"; import { listMessagingChannelsWithoutCredentials } from "./messaging/channels"; +import { + buildCustomOpenClawRuntimeFailureHints, + classifyOpenClawRuntimeFailure, + type SandboxCommandExecutor, +} from "./onboard/custom-openclaw-runtime-diagnosis"; import { getMessagingProviderNamesForChannel } from "./onboard/messaging-reuse"; +export { shouldDiagnoseCustomOpenClawRuntime } from "./onboard/custom-openclaw-runtime-diagnosis"; + // ── Types ──────────────────────────────────────────────────────────── export type AccessMethod = "localhost" | "proxy" | "ssh-tunnel"; @@ -72,10 +79,7 @@ export interface VerifyDeploymentResult { export interface VerifyDeploymentDeps { /** Execute a command inside the sandbox via SSH. Returns null if sandbox unreachable. */ - executeSandboxCommand: ( - name: string, - script: string, - ) => { status: number; stdout: string; stderr: string } | null; + executeSandboxCommand: SandboxCommandExecutor; /** Probe an HTTP endpoint on the host. Returns the HTTP status code or 0 on failure. */ probeHostPort: (port: number, path: string) => number; @@ -143,88 +147,8 @@ const CREDENTIALLESS_MESSAGING_CHANNELS = new Set(listMessagingChannelsWithoutCr // sandbox log is the first thing to check. If the sandbox itself never // came up, the host-side OpenShell gateway log is the right place to // look — see gatewayLogCandidates() in onboard/sandbox-create-failure.ts. -export type OpenClawRuntimeFailureKind = - | "normal_runtime" - | "base_only_image" - | "sandbox_unreachable" - | "inconclusive"; - -export interface OpenClawRuntimeFailureDiagnosis { - kind: OpenClawRuntimeFailureKind; - gatewayLogPresent: boolean | null; - startupScriptPresent: boolean | null; - configPresent: boolean | null; -} - -const OPENCLAW_RUNTIME_PROBE = - "printf 'nemoclaw-runtime-probe-v1 '; " + - "if [ -e /tmp/gateway.log ]; then printf 'log=1 '; else printf 'log=0 '; fi; " + - "if [ -x /usr/local/bin/nemoclaw-start ]; then printf 'start=1 '; else printf 'start=0 '; fi; " + - "if [ -e /sandbox/.openclaw/openclaw.json ]; then printf 'config=1\\n'; else printf 'config=0\\n'; fi"; - -export function shouldDiagnoseCustomOpenClawRuntime( - fromDockerfile: string | null | undefined, - selectedAgentName: string | null | undefined, -): boolean { - return Boolean(fromDockerfile && selectedAgentName === "openclaw"); -} - -/** - * Distinguish the documented sandbox-base-only failure from an ordinary - * gateway startup failure without reading config or log contents. - */ -export function classifyOpenClawRuntimeFailure( - sandboxName: string, - executeSandboxCommand: VerifyDeploymentDeps["executeSandboxCommand"], -): OpenClawRuntimeFailureDiagnosis { - const result = executeSandboxCommand(sandboxName, OPENCLAW_RUNTIME_PROBE); - if (!result) { - return { - kind: "sandbox_unreachable", - gatewayLogPresent: null, - startupScriptPresent: null, - configPresent: null, - }; - } - - const match = result.stdout.match( - /(?:^|\n)\s*(?:(?:\[stdout\]|stdout:)\s*)?nemoclaw-runtime-probe-v1 log=([01]) start=([01]) config=([01])(?:\r?\n|$)/i, - ); - if (result.status !== 0 || !match) { - return { - kind: "inconclusive", - gatewayLogPresent: null, - startupScriptPresent: null, - configPresent: null, - }; - } - - const gatewayLogPresent = match[1] === "1"; - const startupScriptPresent = match[2] === "1"; - const configPresent = match[3] === "1"; - let kind: OpenClawRuntimeFailureKind = "inconclusive"; - if (!gatewayLogPresent && !startupScriptPresent && !configPresent) { - kind = "base_only_image"; - } else if (startupScriptPresent && configPresent) { - kind = "normal_runtime"; - } - return { kind, gatewayLogPresent, startupScriptPresent, configPresent }; -} - -function buildGatewayLogHint( - sandboxName: string, - runtimeDiagnosis: OpenClawRuntimeFailureDiagnosis | null, -): string { - if (runtimeDiagnosis?.kind === "base_only_image") { - return ( - "This custom image does not contain the NemoClaw-managed OpenClaw runtime: " + - "`/usr/local/bin/nemoclaw-start` and `/sandbox/.openclaw/openclaw.json` are missing, " + - "and `/tmp/gateway.log` was never created. `nemoclaw onboard --from` uses the supplied " + - "Dockerfile as the complete sandbox image; it does not layer it over the managed runtime. " + - "Build from the full NemoClaw Dockerfile and context for the same release instead of using " + - "`ghcr.io/nvidia/nemoclaw/sandbox-base` directly." - ); - } +function buildGatewayLogHint(sandboxName: string, customRuntimeHint: string | null): string { + if (customRuntimeHint) return customRuntimeHint; return ( `The gateway probe failed after retrying. Inspect the in-sandbox gateway log with ` + `\`nemoclaw ${sandboxName} logs\` (the gateway writes to /tmp/gateway.log inside the sandbox when it starts). ` + @@ -553,15 +477,23 @@ export async function verifyDeployment( // 1. Gateway reachable inside sandbox const gateway = await verifyGatewayInSandbox(sandboxName, chain, deps, retryDelaysMs, sleep); + // Diagnose only after the normal startup budget. A slow custom runtime should + // get the same recovery window as the stock image, and an early unreachable + // exec cannot safely prove that image artifacts are absent. const runtimeDiagnosis = !gateway.reachable && options.diagnoseCustomOpenClawRuntime ? classifyOpenClawRuntimeFailure(sandboxName, deps.executeSandboxCommand) : null; + const customRuntimeHints = runtimeDiagnosis + ? buildCustomOpenClawRuntimeFailureHints(runtimeDiagnosis) + : null; diagnostics.push({ link: "gateway", status: gateway.reachable ? "ok" : "fail", detail: gateway.detail, - hint: gateway.reachable ? "" : buildGatewayLogHint(sandboxName, runtimeDiagnosis), + hint: gateway.reachable + ? "" + : buildGatewayLogHint(sandboxName, customRuntimeHints?.gateway ?? null), }); // 2. Gateway version (cosmetic — not a health signal) @@ -570,7 +502,7 @@ export async function verifyDeployment( // 3. Dashboard reachable from host (port forward) // A port forward cannot repair an image that has no managed gateway runtime, // so avoid spending a second retry budget on the dependent dashboard probe. - const dashboardRetryDelays = runtimeDiagnosis?.kind === "base_only_image" ? [] : retryDelaysMs; + const dashboardRetryDelays = customRuntimeHints ? [] : retryDelaysMs; const dashboard = await verifyDashboardFromHost(chain, deps, dashboardRetryDelays, sleep); diagnostics.push({ link: "dashboard", @@ -578,9 +510,8 @@ export async function verifyDeployment( detail: dashboard.detail, hint: dashboard.reachable ? "" - : runtimeDiagnosis?.kind === "base_only_image" - ? "The dashboard cannot start until the custom image includes the NemoClaw-managed OpenClaw runtime." - : `Port forward on ${chain.port} is not working. Run: openshell forward start ${chain.forwardTarget} ${sandboxName}`, + : (customRuntimeHints?.dashboard ?? + `Port forward on ${chain.port} is not working. Run: openshell forward start ${chain.forwardTarget} ${sandboxName}`), }); // 4. Inference route diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 2106f2938c..3c43c8cf87 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -64,13 +64,18 @@ async function ignoreCleanupError(run: () => Promise): Promise { } } -function patchPoliciesForDevShm(): () => void { +type PolicySourcePatch = { + restore(): void; + assertRestored(): void; +}; + +function patchPoliciesForDevShm(): PolicySourcePatch { // Test-only source-boundary patch: the default OpenClaw policies intentionally // do not grant general /dev access, but this regression needs to create a // source tree on tmpfs (/dev/shm) to reproduce #3127's cross-device rename - // layout. Keep the mutation local, restore it after the test, and remove it - // when OpenShell can mount a dedicated test tmpfs or update live policy before - // first sandbox command without broadening the checked-in production policy. + // layout. Keep the mutation local, restore and verify the source bytes before + // writing final artifacts, and remove this patch when OpenShell can mount a + // dedicated test tmpfs without broadening checked-in production policy. const originals = new Map(); for (const policyPath of [ path.join(REPO_ROOT, "agents", "openclaw", "policy-permissive.yaml"), @@ -78,6 +83,7 @@ function patchPoliciesForDevShm(): () => void { path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"), ]) { const text = fs.readFileSync(policyPath, "utf8"); + originals.set(policyPath, text); const anchor = " read_write:\n - /tmp\n"; expect(text, `could not find read_write /tmp anchor in ${policyPath}`).toContain(anchor); let additions = ""; @@ -85,14 +91,19 @@ function patchPoliciesForDevShm(): () => void { if (!text.includes(` - ${entry}\n`)) additions += ` - ${entry}\n`; } if (additions) { - originals.set(policyPath, text); fs.writeFileSync(policyPath, text.replace(anchor, anchor + additions), "utf8"); } } - return () => { - for (const [policyPath, text] of originals) { - fs.writeFileSync(policyPath, text, "utf8"); - } + const restore = () => { + for (const [policyPath, text] of originals) fs.writeFileSync(policyPath, text, "utf8"); + }; + return { + restore, + assertRestored: () => { + for (const [policyPath, text] of originals) { + expect(fs.readFileSync(policyPath, "utf8"), `${policyPath} was not restored`).toBe(text); + } + }, }; } @@ -139,9 +150,7 @@ COPY --from=weather-plugin-builder \ /usr/local/share/nemoclaw/e2e-weather-plugin.sha256 USER sandbox -RUN HOME=/sandbox openclaw config set plugins.load.paths \ - '["/sandbox/.openclaw/extensions/weather"]' --json \ - && HOME=/sandbox openclaw plugins registry --refresh --json > /dev/null \ +RUN HOME=/sandbox openclaw plugins install --link /sandbox/.openclaw/extensions/weather \ && HOME=/sandbox openclaw plugins enable weather \ && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null @@ -167,10 +176,18 @@ type GatewayToolCatalog = { groups?: Array<{ tools?: Array<{ id?: unknown }> }>; }; +type GatewayToolInvocation = { + ok?: unknown; + toolName?: unknown; + source?: unknown; + output?: { details?: unknown }; +}; + type WeatherRuntimeProof = { imageMarker: string; inspectLoaded: boolean; catalogToolIds: string[]; + toolInvoked: boolean; }; async function assertWeatherPluginRuntime( @@ -237,7 +254,31 @@ printf '%s\\n' "$actual"`), (group.tools ?? []).map((tool) => tool.id).filter((id): id is string => typeof id === "string"), ); expect(catalogToolIds).toContain("get_weather"); - return { imageMarker: imageMarker ?? "", inspectLoaded: true, catalogToolIds }; + + const invokeProbe = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript( + `. /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.invoke --params '{"name":"get_weather","args":{"location":"Santa Clara"}}' --json`, + ), + { + artifactName: `openclaw-weather-plugin-invoke-${phase}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, + ); + expect(invokeProbe.exitCode, resultText(invokeProbe)).toBe(0); + const invocation = parseJsonFromText( + normalizeSandboxStdoutFrames(invokeProbe.stdout), + ) as GatewayToolInvocation; + expect(invocation).toMatchObject({ + ok: true, + toolName: "get_weather", + source: "plugin", + output: { + details: { location: "Santa Clara", condition: "clear", temperatureC: 21 }, + }, + }); + return { imageMarker: imageMarker ?? "", inspectLoaded: true, catalogToolIds, toolInvoked: true }; } const runtimeDepsReplacementProbeSource = `set -eu @@ -327,7 +368,7 @@ liveTest( regressionTargets: ["#6108", "#3513", "#3127"], contract: [ "fresh OpenClaw sandbox onboards from a full managed custom-plugin Dockerfile", - "gateway log, runtime plugin inspection, and tools.catalog expose weather/get_weather", + "gateway log, runtime inspection, tools.catalog, and tools.invoke prove weather/get_weather", "custom-plugin image provenance and the gateway tool survive restart and rebuild", "sandbox proves /dev/shm and plugin-runtime-deps are distinct devices", "legacy source-side staging fails with EXDEV across the same /dev/shm to plugin-runtime-deps boundary", @@ -388,8 +429,8 @@ liveTest( }), ); - const restorePolicies = patchPoliciesForDevShm(); - cleanup.add("restore EXDEV policy fixture edits", restorePolicies); + const policySourcePatch = patchPoliciesForDevShm(); + cleanup.add("restore EXDEV policy fixture edits", policySourcePatch.restore); const removeCustomDockerfile = createCustomPluginDockerfile(); cleanup.add("remove custom weather-plugin Dockerfile", removeCustomDockerfile); @@ -483,6 +524,9 @@ liveTest( expect(probeText).toContain("source-side staging failure self-check completed"); expect(probeText).toContain("runtime deps replacement completed"); + policySourcePatch.restore(); + policySourcePatch.assertRestored(); + await artifacts.writeJson("target-result.json", { id: "openclaw-plugin-runtime-exdev", onboardExitCode: onboard.exitCode, @@ -493,13 +537,16 @@ liveTest( assertions: { weatherAfterOnboard: weatherAfterOnboard.inspectLoaded && - weatherAfterOnboard.catalogToolIds.includes("get_weather"), + weatherAfterOnboard.catalogToolIds.includes("get_weather") && + weatherAfterOnboard.toolInvoked, weatherAfterRestart: weatherAfterRestart.inspectLoaded && - weatherAfterRestart.catalogToolIds.includes("get_weather"), + weatherAfterRestart.catalogToolIds.includes("get_weather") && + weatherAfterRestart.toolInvoked, weatherAfterRebuild: weatherAfterRebuild.inspectLoaded && - weatherAfterRebuild.catalogToolIds.includes("get_weather"), + weatherAfterRebuild.catalogToolIds.includes("get_weather") && + weatherAfterRebuild.toolInvoked, imageMarkerStable: weatherAfterOnboard.imageMarker === weatherAfterRestart.imageMarker && weatherAfterRestart.imageMarker === weatherAfterRebuild.imageMarker, @@ -509,6 +556,7 @@ liveTest( ), noExdevSignature: !EXDEV_PATTERNS.some((pattern) => pattern.test(probeText)), successMarker: probeText.includes("runtime deps replacement completed"), + policySourcesRestored: true, }, }); }, From f46ff19fbd9c36fc6fbca8edcb93943184658bb9 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 08:24:38 -0700 Subject: [PATCH 03/46] fix(e2e): install plugin from staging path Signed-off-by: Aaron Erickson --- docs/deployment/install-openclaw-plugins.mdx | 13 +++++++------ .../live/openclaw-plugin-runtime-exdev.test.ts | 10 +++++----- test/regression-e2e-workflow.test.ts | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 0a9dfe0155..baaa37041b 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -85,7 +85,7 @@ Next, name the completed runtime stage so the plugin layer can extend it. ``` Append the following stages to the end of the stock Dockerfile. -Replace `weather` with the plugin ID from `openclaw.plugin.json` in the stage name, destination directory, load path, and OpenClaw plugin commands. +Replace `weather` with the plugin ID from `openclaw.plugin.json` in the stage name, staging directory, and OpenClaw plugin commands. ```dockerfile # Build the plugin from its lockfile. @@ -103,14 +103,14 @@ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ /opt/my-plugin/package.json \ /opt/my-plugin/package-lock.json \ /opt/my-plugin/openclaw.plugin.json \ - /sandbox/.openclaw/extensions/weather/ + /opt/weather-plugin/ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ - /opt/my-plugin/dist/ /sandbox/.openclaw/extensions/weather/dist/ + /opt/my-plugin/dist/ /opt/weather-plugin/dist/ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ - /opt/my-plugin/node_modules/ /sandbox/.openclaw/extensions/weather/node_modules/ + /opt/my-plugin/node_modules/ /opt/weather-plugin/node_modules/ USER sandbox -RUN HOME=/sandbox openclaw plugins install --link /sandbox/.openclaw/extensions/weather \ +RUN HOME=/sandbox openclaw plugins install /opt/weather-plugin \ && HOME=/sandbox openclaw plugins enable weather \ && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null @@ -125,7 +125,7 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ ``` The final stage inherits the stock runtime entrypoint, command, gateway health check, generated configuration, and file permissions. -The linked install preserves existing managed plugin load paths, appends the copied plugin path, refreshes the plugin registry, and records the install before the explicit enable and inspect steps. +The local install copies the staged plugin into OpenClaw's extensions tree, records the install, and leaves existing managed plugin load paths intact before the explicit enable and inspect steps. The last `RUN` refreshes the managed config hash after `openclaw plugins enable` updates `openclaw.json`. ## Create and Verify the Sandbox @@ -205,6 +205,7 @@ The following mistakes commonly mix plugin installation with other NemoClaw exte - Do not use `nemoclaw skill install` for OpenClaw plugins. That command only installs `SKILL.md` agent skills. - Do not use `sandbox-base` as the final custom image. It is an intermediate dependency image. - Do not combine one NemoClaw release's Dockerfile with another release's base image. +- Do not copy a plugin into `/sandbox/.openclaw/extensions/` and then run `plugins install --link` on that same path. Stage it outside the managed extensions directory and use the local install shown above. - Do not move or delete the recorded source checkout before rebuilding the sandbox. - Do not rely on `.dockerignore` to include credential-like paths; NemoClaw excludes those from staged custom build contexts for safety. - Keep plugin dependencies in the build stage or plugin directory, and avoid copying unrelated host files into the sandbox image. diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 3c43c8cf87..b137b24c02 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -126,7 +126,7 @@ function createCustomPluginDockerfile(): () => void { FROM builder AS weather-plugin-builder WORKDIR /opt/weather COPY test/e2e/fixtures/plugins/weather/package.json test/e2e/fixtures/plugins/weather/package-lock.json test/e2e/fixtures/plugins/weather/tsconfig.json ./ -RUN npm ci --no-audit --no-fund +RUN npm ci --ignore-scripts --no-audit --no-fund COPY test/e2e/fixtures/plugins/weather/openclaw.plugin.json ./ COPY test/e2e/fixtures/plugins/weather/src/ ./src/ RUN npm run build \ @@ -140,17 +140,17 @@ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ /opt/weather/package.json \ /opt/weather/package-lock.json \ /opt/weather/openclaw.plugin.json \ - /sandbox/.openclaw/extensions/weather/ + /opt/weather-plugin/ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ - /opt/weather/dist/ /sandbox/.openclaw/extensions/weather/dist/ + /opt/weather/dist/ /opt/weather-plugin/dist/ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ - /opt/weather/node_modules/ /sandbox/.openclaw/extensions/weather/node_modules/ + /opt/weather/node_modules/ /opt/weather-plugin/node_modules/ COPY --from=weather-plugin-builder \ /opt/weather/e2e-weather-plugin.sha256 \ /usr/local/share/nemoclaw/e2e-weather-plugin.sha256 USER sandbox -RUN HOME=/sandbox openclaw plugins install --link /sandbox/.openclaw/extensions/weather \ +RUN HOME=/sandbox openclaw plugins install /opt/weather-plugin \ && HOME=/sandbox openclaw plugins enable weather \ && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null diff --git a/test/regression-e2e-workflow.test.ts b/test/regression-e2e-workflow.test.ts index 7b93a0d649..b2ae1ede7b 100644 --- a/test/regression-e2e-workflow.test.ts +++ b/test/regression-e2e-workflow.test.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { readYaml, type WorkflowStep } from "./helpers/e2e-workflow-contract"; @@ -104,4 +105,20 @@ describe("Regression E2E workflow contract", () => { expect(runText).toContain("npm ci --ignore-scripts"); expect(runText).toContain("npm run build:cli"); }); + + it("hardens the generated custom-plugin image contract", () => { + const liveTestSource = readFileSync( + new URL("./e2e/live/openclaw-plugin-runtime-exdev.test.ts", import.meta.url), + "utf8", + ); + + expect(liveTestSource).toContain("RUN npm ci --ignore-scripts --no-audit --no-fund"); + expect(liveTestSource).toContain("RUN npm run build"); + expect(liveTestSource).toContain( + "RUN HOME=/sandbox openclaw plugins install /opt/weather-plugin", + ); + expect(liveTestSource).not.toContain( + "openclaw plugins install --link /sandbox/.openclaw/extensions/weather", + ); + }); }); From 5f5f0aa130dbaafc30d7d3b42907fbadec9d1a1e Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 08:30:46 -0700 Subject: [PATCH 04/46] docs: fix plugin guide links Signed-off-by: Aaron Erickson --- docs/reference/commands.mdx | 4 ++-- docs/reference/troubleshooting.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 826cc10877..fdc79cbb4a 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -394,7 +394,7 @@ Missing paths fail during command parsing before preflight, gateway setup, infer If deployment verification cannot reach the gateway for a custom OpenClaw image, NemoClaw checks for `/tmp/gateway.log`, `/usr/local/bin/nemoclaw-start`, and `/sandbox/.openclaw/openclaw.json`. When all three paths are absent, onboarding reports that the custom image lacks the managed runtime instead of treating repeated port-forward retries as the recovery path. -For the version-matched full-runtime plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). +For the version-matched full-runtime plugin workflow, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). @@ -1447,7 +1447,7 @@ Skill names must contain only alphanumeric characters, dots, hyphens, and unders OpenClaw plugins are a different kind of extension. -To install an OpenClaw plugin, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). +To install an OpenClaw plugin, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). For OpenClaw, the command uploads the skill to the OpenClaw state directory and mirrors it into `$HOME/.openclaw/skills/` when the agent home directory differs from the state directory. That mirror makes skills listed by `openclaw skills list` available at session startup. If mirror creation fails, NemoClaw prints a warning so you can reinstall or inspect the home directory permissions. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index a86968ea17..78125a50bd 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -438,7 +438,7 @@ When all three paths are absent, the CLI reports that the image lacks the NemoCl This failure commonly occurs when the custom Dockerfile starts from `ghcr.io/nvidia/nemoclaw/sandbox-base` alone because that image is an intermediate dependency image. Rebuild the custom image from the full stock Dockerfile and source context for the same NemoClaw release. -For the version-pinned plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). +For the version-pinned plugin workflow, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). If the sandbox is unreachable or the managed runtime paths are present, NemoClaw retains the existing generic gateway-log and host OpenShell-log guidance because the base-only failure is not proven. From d41449b890f81b1e025dc84cf630f67a315cb787 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 08:32:48 -0700 Subject: [PATCH 05/46] test: keep plugin contract behavioral Signed-off-by: Aaron Erickson --- test/regression-e2e-workflow.test.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/test/regression-e2e-workflow.test.ts b/test/regression-e2e-workflow.test.ts index b2ae1ede7b..7b93a0d649 100644 --- a/test/regression-e2e-workflow.test.ts +++ b/test/regression-e2e-workflow.test.ts @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { readYaml, type WorkflowStep } from "./helpers/e2e-workflow-contract"; @@ -105,20 +104,4 @@ describe("Regression E2E workflow contract", () => { expect(runText).toContain("npm ci --ignore-scripts"); expect(runText).toContain("npm run build:cli"); }); - - it("hardens the generated custom-plugin image contract", () => { - const liveTestSource = readFileSync( - new URL("./e2e/live/openclaw-plugin-runtime-exdev.test.ts", import.meta.url), - "utf8", - ); - - expect(liveTestSource).toContain("RUN npm ci --ignore-scripts --no-audit --no-fund"); - expect(liveTestSource).toContain("RUN npm run build"); - expect(liveTestSource).toContain( - "RUN HOME=/sandbox openclaw plugins install /opt/weather-plugin", - ); - expect(liveTestSource).not.toContain( - "openclaw plugins install --link /sandbox/.openclaw/extensions/weather", - ); - }); }); From 4414e817e2978ad1c874eb806bdcc13c1dcb84de Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 09:11:52 -0700 Subject: [PATCH 06/46] fix(e2e): handle OpenClaw gateway pairing Signed-off-by: Aaron Erickson --- docs/deployment/install-openclaw-plugins.mdx | 8 +- .../openclaw-plugin-runtime-exdev.test.ts | 112 +++++++++++++----- 2 files changed, 87 insertions(+), 33 deletions(-) diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index baaa37041b..83d94ee005 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -148,12 +148,14 @@ nemoclaw weather-agent status nemoclaw weather-agent exec -- test -s /tmp/gateway.log nemoclaw weather-agent exec -- env HOME=/sandbox openclaw plugins inspect weather --runtime --json nemoclaw weather-agent exec -- bash -lc \ - ". /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.catalog --params '{\"includePlugins\":true}' --json" + ". /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL=\"ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}\" HOME=/sandbox openclaw gateway call tools.invoke --params '{\"agentId\":\"main\",\"name\":\"get_weather\",\"args\":{\"location\":\"Santa Clara\"}}' --json" nemoclaw weather-agent exec -- bash -lc \ - ". /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.invoke --params '{\"name\":\"get_weather\",\"args\":{\"location\":\"Santa Clara\"}}' --json" + ". /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL=\"ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}\" HOME=/sandbox openclaw gateway call tools.catalog --params '{\"agentId\":\"main\",\"includePlugins\":true}' --json" ``` -The plugin inspection must report the `weather` plugin, the gateway tool catalog must contain `get_weather`, and the invocation must return the deterministic `Santa Clara` weather result. +The plugin inspection must report the `weather` plugin, the invocation must return the deterministic `Santa Clara` weather result, and the gateway tool catalog must contain `get_weather`. +Run the invocation before the catalog check as shown: OpenClaw's sandbox-local CLI pairing grants the required write scope on that first call, and the write scope also satisfies the catalog's read requirement. +If the first invocation reports that pairing or a scope upgrade is pending, wait a few seconds for NemoClaw's managed approval watcher and rerun that invocation once before continuing to the catalog check. Keep the source checkout and Dockerfile at the recorded path if you plan to run `nemoclaw weather-agent rebuild --yes`. This source-based workflow can reproduce the plugin during that rebuild, but it is not the durable managed-plugin lifecycle proposed in issue #5998. diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index b137b24c02..284abbbf8a 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -4,6 +4,16 @@ import fs from "node:fs"; import path from "node:path"; +import { + buildAutoPairApprovalScript, + readAutoPairApprovalPolicyModule, +} from "../../../src/lib/actions/sandbox/auto-pair-approval.ts"; +import { + CONNECT_AUTO_PAIR_APPROVE_TIMEOUT_S, + CONNECT_AUTO_PAIR_LIST_TIMEOUT_S, + CONNECT_AUTO_PAIR_MAX_APPROVALS, + CONNECT_AUTO_PAIR_TIMEOUT_MS, +} from "../../../src/lib/actions/sandbox/connect-autopair-budget.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import { type SandboxClient, @@ -12,6 +22,7 @@ import { } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; +import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; import { parseJsonFromText } from "./json-envelope.ts"; // Keep this contract as a focused live test: build a deterministic custom plugin @@ -33,6 +44,8 @@ const EXDEV_PATTERNS = [ /EXDEV: cross-device link not permitted/i, /cross-device link not permitted/i, ]; +const GATEWAY_PAIRING_REQUIRED_PATTERN = + /scope upgrade pending|pairing required|device is not approved/i; const liveTest = shouldRunLiveE2E() ? test : test.skip; function resultText(result: { stdout: string; stderr: string }): string { @@ -190,6 +203,51 @@ type WeatherRuntimeProof = { toolInvoked: boolean; }; +function gatewayPairingApprovalScript() { + const policyModule = readAutoPairApprovalPolicyModule(); + if (!policyModule) { + throw new Error("OpenClaw device approval policy helper is required for the live plugin test"); + } + return trustedSandboxShellScript( + buildAutoPairApprovalScript(Buffer.from(policyModule, "utf8").toString("base64"), { + emitSummary: true, + budget: { + maxApprovals: CONNECT_AUTO_PAIR_MAX_APPROVALS, + listTimeoutS: CONNECT_AUTO_PAIR_LIST_TIMEOUT_S, + approveTimeoutS: CONNECT_AUTO_PAIR_APPROVE_TIMEOUT_S, + }, + }), + ); +} + +async function runGatewayCallWithPairingRetry( + sandbox: SandboxClient, + phase: string, + operation: "catalog" | "invoke", + script: string, +): Promise { + const run = (attempt: number) => + sandbox.execShell(SANDBOX_NAME, trustedSandboxShellScript(script), { + artifactName: `openclaw-weather-plugin-${operation}-${phase}-attempt-${attempt}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }); + + let result = await run(1); + if (result.exitCode === 0 || !GATEWAY_PAIRING_REQUIRED_PATTERN.test(resultText(result))) { + return result; + } + + const approval = await sandbox.execShell(SANDBOX_NAME, gatewayPairingApprovalScript(), { + artifactName: `openclaw-weather-plugin-${operation}-${phase}-pairing-approval`, + env: liveEnv(), + timeoutMs: CONNECT_AUTO_PAIR_TIMEOUT_MS + 5_000, + }); + expect(approval.exitCode, resultText(approval)).toBe(0); + result = await run(2); + return result; +} + async function assertWeatherPluginRuntime( sandbox: SandboxClient, phase: string, @@ -235,36 +293,15 @@ printf '%s\\n' "$actual"`), "get_weather", ); - const catalogProbe = await sandbox.execShell( - SANDBOX_NAME, - trustedSandboxShellScript( - `. /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.catalog --params '{"includePlugins":true}' --json`, - ), - { - artifactName: `openclaw-weather-plugin-catalog-${phase}`, - env: liveEnv(), - timeoutMs: PROBE_TIMEOUT_MS, - }, - ); - expect(catalogProbe.exitCode, resultText(catalogProbe)).toBe(0); - const catalog = parseJsonFromText( - normalizeSandboxStdoutFrames(catalogProbe.stdout), - ) as GatewayToolCatalog; - const catalogToolIds = (catalog.groups ?? []).flatMap((group) => - (group.tools ?? []).map((tool) => tool.id).filter((id): id is string => typeof id === "string"), - ); - expect(catalogToolIds).toContain("get_weather"); - - const invokeProbe = await sandbox.execShell( - SANDBOX_NAME, - trustedSandboxShellScript( - `. /tmp/nemoclaw-proxy-env.sh && HOME=/sandbox openclaw gateway call tools.invoke --params '{"name":"get_weather","args":{"location":"Santa Clara"}}' --json`, - ), - { - artifactName: `openclaw-weather-plugin-invoke-${phase}`, - env: liveEnv(), - timeoutMs: PROBE_TIMEOUT_MS, - }, + // Use the sandbox-local gateway address so OpenClaw can synchronously pair + // this shared-secret CLI client. Invoke first because operator.write also + // satisfies the catalog's operator.read scope; the reverse order would + // create a separate asynchronous scope-upgrade request. + const invokeProbe = await runGatewayCallWithPairingRetry( + sandbox, + phase, + "invoke", + `. /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL="ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}" HOME=/sandbox openclaw gateway call tools.invoke --params '{"agentId":"main","name":"get_weather","args":{"location":"Santa Clara"}}' --json`, ); expect(invokeProbe.exitCode, resultText(invokeProbe)).toBe(0); const invocation = parseJsonFromText( @@ -278,6 +315,21 @@ printf '%s\\n' "$actual"`), details: { location: "Santa Clara", condition: "clear", temperatureC: 21 }, }, }); + + const catalogProbe = await runGatewayCallWithPairingRetry( + sandbox, + phase, + "catalog", + `. /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL="ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}" HOME=/sandbox openclaw gateway call tools.catalog --params '{"agentId":"main","includePlugins":true}' --json`, + ); + expect(catalogProbe.exitCode, resultText(catalogProbe)).toBe(0); + const catalog = parseJsonFromText( + normalizeSandboxStdoutFrames(catalogProbe.stdout), + ) as GatewayToolCatalog; + const catalogToolIds = (catalog.groups ?? []).flatMap((group) => + (group.tools ?? []).map((tool) => tool.id).filter((id): id is string => typeof id === "string"), + ); + expect(catalogToolIds).toContain("get_weather"); return { imageMarker: imageMarker ?? "", inspectLoaded: true, catalogToolIds, toolInvoked: true }; } From 8b21175abaabb885f5d2ea74bf23286a99b71e9c Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 09:19:59 -0700 Subject: [PATCH 07/46] test: keep gateway retry linear Signed-off-by: Aaron Erickson --- .../openclaw-plugin-runtime-exdev.test.ts | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 284abbbf8a..7402134990 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -205,11 +205,12 @@ type WeatherRuntimeProof = { function gatewayPairingApprovalScript() { const policyModule = readAutoPairApprovalPolicyModule(); - if (!policyModule) { - throw new Error("OpenClaw device approval policy helper is required for the live plugin test"); - } + expect( + policyModule, + "OpenClaw device approval policy helper is required for the live plugin test", + ).toBeTruthy(); return trustedSandboxShellScript( - buildAutoPairApprovalScript(Buffer.from(policyModule, "utf8").toString("base64"), { + buildAutoPairApprovalScript(Buffer.from(policyModule ?? "", "utf8").toString("base64"), { emitSummary: true, budget: { maxApprovals: CONNECT_AUTO_PAIR_MAX_APPROVALS, @@ -220,6 +221,21 @@ function gatewayPairingApprovalScript() { ); } +async function approveGatewayPairingAndRetry( + sandbox: SandboxClient, + phase: string, + operation: "catalog" | "invoke", + run: (attempt: number) => Promise, +): Promise { + const approval = await sandbox.execShell(SANDBOX_NAME, gatewayPairingApprovalScript(), { + artifactName: `openclaw-weather-plugin-${operation}-${phase}-pairing-approval`, + env: liveEnv(), + timeoutMs: CONNECT_AUTO_PAIR_TIMEOUT_MS + 5_000, + }); + expect(approval.exitCode, resultText(approval)).toBe(0); + return run(2); +} + async function runGatewayCallWithPairingRetry( sandbox: SandboxClient, phase: string, @@ -233,19 +249,10 @@ async function runGatewayCallWithPairingRetry( timeoutMs: PROBE_TIMEOUT_MS, }); - let result = await run(1); - if (result.exitCode === 0 || !GATEWAY_PAIRING_REQUIRED_PATTERN.test(resultText(result))) { - return result; - } - - const approval = await sandbox.execShell(SANDBOX_NAME, gatewayPairingApprovalScript(), { - artifactName: `openclaw-weather-plugin-${operation}-${phase}-pairing-approval`, - env: liveEnv(), - timeoutMs: CONNECT_AUTO_PAIR_TIMEOUT_MS + 5_000, - }); - expect(approval.exitCode, resultText(approval)).toBe(0); - result = await run(2); - return result; + const result = await run(1); + return result.exitCode !== 0 && GATEWAY_PAIRING_REQUIRED_PATTERN.test(resultText(result)) + ? approveGatewayPairingAndRetry(sandbox, phase, operation, run) + : result; } async function assertWeatherPluginRuntime( From 60c48888eac2598b153bc2f317e3fe0c4f435fd2 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 09:47:20 -0700 Subject: [PATCH 08/46] fix(e2e): use authenticated plugin probes Signed-off-by: Aaron Erickson --- docs/deployment/install-openclaw-plugins.mdx | 13 +- .../openclaw-plugin-runtime-exdev.test.ts | 147 ++++++++---------- 2 files changed, 70 insertions(+), 90 deletions(-) diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 83d94ee005..79ddc0d398 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -148,18 +148,17 @@ nemoclaw weather-agent status nemoclaw weather-agent exec -- test -s /tmp/gateway.log nemoclaw weather-agent exec -- env HOME=/sandbox openclaw plugins inspect weather --runtime --json nemoclaw weather-agent exec -- bash -lc \ - ". /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL=\"ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}\" HOME=/sandbox openclaw gateway call tools.invoke --params '{\"agentId\":\"main\",\"name\":\"get_weather\",\"args\":{\"location\":\"Santa Clara\"}}' --json" -nemoclaw weather-agent exec -- bash -lc \ - ". /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL=\"ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}\" HOME=/sandbox openclaw gateway call tools.catalog --params '{\"agentId\":\"main\",\"includePlugins\":true}' --json" + '. /tmp/nemoclaw-proxy-env.sh && printf "header = \"Authorization: Bearer %s\"\n" "$OPENCLAW_GATEWAY_TOKEN" | curl --noproxy "*" --max-time 30 --silent --show-error --fail-with-body --config - -H "Content-Type: application/json" --data "{\"agentId\":\"main\",\"tool\":\"get_weather\",\"args\":{\"location\":\"Santa Clara\"}}" "http://127.0.0.1:${OPENCLAW_GATEWAY_PORT:-18789}/tools/invoke"' ``` -The plugin inspection must report the `weather` plugin, the invocation must return the deterministic `Santa Clara` weather result, and the gateway tool catalog must contain `get_weather`. -Run the invocation before the catalog check as shown: OpenClaw's sandbox-local CLI pairing grants the required write scope on that first call, and the write scope also satisfies the catalog's read requirement. -If the first invocation reports that pairing or a scope upgrade is pending, wait a few seconds for NemoClaw's managed approval watcher and rerun that invocation once before continuing to the catalog check. +The plugin inspection must report the loaded `weather` plugin and list `get_weather` in `toolNames`. +The authenticated HTTP invocation must return the deterministic `Santa Clara` weather result. +The piped curl config keeps the managed gateway token out of process arguments. +NemoClaw's live regression also verifies that the running gateway's tool catalog contains `get_weather`. Keep the source checkout and Dockerfile at the recorded path if you plan to run `nemoclaw weather-agent rebuild --yes`. This source-based workflow can reproduce the plugin during that rebuild, but it is not the durable managed-plugin lifecycle proposed in issue #5998. -Rerun the plugin inspection and tool-catalog checks after a gateway restart or sandbox rebuild to verify that the plugin remains available. +Rerun the plugin inspection and HTTP invocation after a gateway restart or sandbox rebuild to verify that the plugin remains available. If the plugin imports runtime packages, keep those packages in `dependencies` rather than `devDependencies` so `npm prune --omit=dev` preserves them. If the plugin needs configuration in `openclaw.json`, apply it before refreshing `.config-hash`. diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 7402134990..19188cc0f7 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -4,16 +4,6 @@ import fs from "node:fs"; import path from "node:path"; -import { - buildAutoPairApprovalScript, - readAutoPairApprovalPolicyModule, -} from "../../../src/lib/actions/sandbox/auto-pair-approval.ts"; -import { - CONNECT_AUTO_PAIR_APPROVE_TIMEOUT_S, - CONNECT_AUTO_PAIR_LIST_TIMEOUT_S, - CONNECT_AUTO_PAIR_MAX_APPROVALS, - CONNECT_AUTO_PAIR_TIMEOUT_MS, -} from "../../../src/lib/actions/sandbox/connect-autopair-budget.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import { type SandboxClient, @@ -22,7 +12,6 @@ import { } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; -import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; import { parseJsonFromText } from "./json-envelope.ts"; // Keep this contract as a focused live test: build a deterministic custom plugin @@ -44,10 +33,37 @@ const EXDEV_PATTERNS = [ /EXDEV: cross-device link not permitted/i, /cross-device link not permitted/i, ]; -const GATEWAY_PAIRING_REQUIRED_PATTERN = - /scope upgrade pending|pairing required|device is not approved/i; const liveTest = shouldRunLiveE2E() ? test : test.skip; +const GATEWAY_CATALOG_CALL_SOURCE = String.raw` +import { Buffer } from "node:buffer"; +import { realpathSync } from "node:fs"; +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; + +const port = process.env.OPENCLAW_GATEWAY_PORT || "18789"; +if (!/^[1-9][0-9]{0,4}$/.test(port) || Number(port) > 65535) { + throw new Error("OPENCLAW_GATEWAY_PORT must be a canonical TCP port in 1..65535"); +} +const token = process.env.OPENCLAW_GATEWAY_TOKEN; +if (!token) throw new Error("OPENCLAW_GATEWAY_TOKEN is required"); + +const openclawBin = realpathSync(process.env.OPENCLAW_BIN); +const requireFromOpenclaw = createRequire(openclawBin); +const runtimePath = requireFromOpenclaw.resolve("openclaw/plugin-sdk/gateway-runtime"); +const { callGatewayFromCli } = await import(pathToFileURL(runtimePath).href); +const params = JSON.parse( + Buffer.from(process.env.NEMOCLAW_E2E_GATEWAY_PARAMS_B64 || "e30=", "base64").toString("utf8"), +); +const result = await callGatewayFromCli( + "tools.catalog", + { url: "ws://127.0.0.1:" + port, token, timeout: "30000", json: true }, + params, + { clientName: "gateway-client", mode: "backend", scopes: ["operator.read"], progress: false }, +); +process.stdout.write(JSON.stringify(result) + "\n"); +`.trim(); + function resultText(result: { stdout: string; stderr: string }): string { return [result.stdout, result.stderr].filter(Boolean).join("\n"); } @@ -191,9 +207,7 @@ type GatewayToolCatalog = { type GatewayToolInvocation = { ok?: unknown; - toolName?: unknown; - source?: unknown; - output?: { details?: unknown }; + result?: { details?: unknown }; }; type WeatherRuntimeProof = { @@ -203,56 +217,17 @@ type WeatherRuntimeProof = { toolInvoked: boolean; }; -function gatewayPairingApprovalScript() { - const policyModule = readAutoPairApprovalPolicyModule(); - expect( - policyModule, - "OpenClaw device approval policy helper is required for the live plugin test", - ).toBeTruthy(); - return trustedSandboxShellScript( - buildAutoPairApprovalScript(Buffer.from(policyModule ?? "", "utf8").toString("base64"), { - emitSummary: true, - budget: { - maxApprovals: CONNECT_AUTO_PAIR_MAX_APPROVALS, - listTimeoutS: CONNECT_AUTO_PAIR_LIST_TIMEOUT_S, - approveTimeoutS: CONNECT_AUTO_PAIR_APPROVE_TIMEOUT_S, - }, - }), - ); -} - -async function approveGatewayPairingAndRetry( - sandbox: SandboxClient, - phase: string, - operation: "catalog" | "invoke", - run: (attempt: number) => Promise, -): Promise { - const approval = await sandbox.execShell(SANDBOX_NAME, gatewayPairingApprovalScript(), { - artifactName: `openclaw-weather-plugin-${operation}-${phase}-pairing-approval`, - env: liveEnv(), - timeoutMs: CONNECT_AUTO_PAIR_TIMEOUT_MS + 5_000, - }); - expect(approval.exitCode, resultText(approval)).toBe(0); - return run(2); -} - -async function runGatewayCallWithPairingRetry( - sandbox: SandboxClient, - phase: string, - operation: "catalog" | "invoke", - script: string, -): Promise { - const run = (attempt: number) => - sandbox.execShell(SANDBOX_NAME, trustedSandboxShellScript(script), { - artifactName: `openclaw-weather-plugin-${operation}-${phase}-attempt-${attempt}`, - env: liveEnv(), - timeoutMs: PROBE_TIMEOUT_MS, - }); - - const result = await run(1); - return result.exitCode !== 0 && GATEWAY_PAIRING_REQUIRED_PATTERN.test(resultText(result)) - ? approveGatewayPairingAndRetry(sandbox, phase, operation, run) - : result; +function gatewayCatalogCallScript(params: Record) { + const source = Buffer.from(GATEWAY_CATALOG_CALL_SOURCE, "utf8").toString("base64"); + const encodedParams = Buffer.from(JSON.stringify(params), "utf8").toString("base64"); + return trustedSandboxShellScript(`set -eu +. /tmp/nemoclaw-proxy-env.sh +export HOME=/sandbox +export OPENCLAW_BIN="$(command -v openclaw)" +export NO_PROXY=127.0.0.1,localhost +export no_proxy="$NO_PROXY" +export NEMOCLAW_E2E_GATEWAY_PARAMS_B64='${encodedParams}' +exec node --input-type=module --eval 'await import("data:text/javascript;base64," + process.argv[1])' '${source}'`); } async function assertWeatherPluginRuntime( @@ -300,15 +275,18 @@ printf '%s\\n' "$actual"`), "get_weather", ); - // Use the sandbox-local gateway address so OpenClaw can synchronously pair - // this shared-secret CLI client. Invoke first because operator.write also - // satisfies the catalog's operator.read scope; the reverse order would - // create a separate asynchronous scope-upgrade request. - const invokeProbe = await runGatewayCallWithPairingRetry( - sandbox, - phase, - "invoke", - `. /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL="ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}" HOME=/sandbox openclaw gateway call tools.invoke --params '{"agentId":"main","name":"get_weather","args":{"location":"Santa Clara"}}' --json`, + // Exercise OpenClaw's documented HTTP tool surface with the managed bearer + // token supplied on stdin so the credential never enters process arguments. + const invokeProbe = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript( + `. /tmp/nemoclaw-proxy-env.sh && printf 'header = "Authorization: Bearer %s"\\n' "$OPENCLAW_GATEWAY_TOKEN" | curl --noproxy '*' --max-time 30 --silent --show-error --fail-with-body --config - -H 'Content-Type: application/json' --data '{"agentId":"main","tool":"get_weather","args":{"location":"Santa Clara"}}' "http://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}/tools/invoke"`, + ), + { + artifactName: `openclaw-weather-plugin-invoke-${phase}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, ); expect(invokeProbe.exitCode, resultText(invokeProbe)).toBe(0); const invocation = parseJsonFromText( @@ -316,18 +294,21 @@ printf '%s\\n' "$actual"`), ) as GatewayToolInvocation; expect(invocation).toMatchObject({ ok: true, - toolName: "get_weather", - source: "plugin", - output: { + result: { details: { location: "Santa Clara", condition: "clear", temperatureC: 21 }, }, }); - const catalogProbe = await runGatewayCallWithPairingRetry( - sandbox, - phase, - "catalog", - `. /tmp/nemoclaw-proxy-env.sh && OPENCLAW_GATEWAY_URL="ws://127.0.0.1:\${OPENCLAW_GATEWAY_PORT:-18789}" HOME=/sandbox openclaw gateway call tools.catalog --params '{"agentId":"main","includePlugins":true}' --json`, + // Mirror NemoClaw's trusted internal read-only gateway client for the RPC + // catalog proof without creating a user-facing CLI device or weakening auth. + const catalogProbe = await sandbox.execShell( + SANDBOX_NAME, + gatewayCatalogCallScript({ agentId: "main", includePlugins: true }), + { + artifactName: `openclaw-weather-plugin-catalog-${phase}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, ); expect(catalogProbe.exitCode, resultText(catalogProbe)).toBe(0); const catalog = parseJsonFromText( From d43397751a081f9711b63148cc44fd7cf028ed2e Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 10:08:31 -0700 Subject: [PATCH 09/46] fix(e2e): resolve OpenClaw catalog binary Signed-off-by: Aaron Erickson --- .../live/openclaw-plugin-runtime-exdev.test.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 19188cc0f7..5386183c6e 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -37,10 +37,23 @@ const liveTest = shouldRunLiveE2E() ? test : test.skip; const GATEWAY_CATALOG_CALL_SOURCE = String.raw` import { Buffer } from "node:buffer"; -import { realpathSync } from "node:fs"; +import { accessSync, constants, realpathSync } from "node:fs"; import { createRequire } from "node:module"; +import { join } from "node:path"; import { pathToFileURL } from "node:url"; +function findOnPath(command) { + for (const dir of (process.env.PATH || "").split(":")) { + if (!dir) continue; + const candidate = join(dir, command); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch {} + } + throw new Error("Could not find " + command + " on PATH"); +} + const port = process.env.OPENCLAW_GATEWAY_PORT || "18789"; if (!/^[1-9][0-9]{0,4}$/.test(port) || Number(port) > 65535) { throw new Error("OPENCLAW_GATEWAY_PORT must be a canonical TCP port in 1..65535"); @@ -48,7 +61,7 @@ if (!/^[1-9][0-9]{0,4}$/.test(port) || Number(port) > 65535) { const token = process.env.OPENCLAW_GATEWAY_TOKEN; if (!token) throw new Error("OPENCLAW_GATEWAY_TOKEN is required"); -const openclawBin = realpathSync(process.env.OPENCLAW_BIN); +const openclawBin = realpathSync(findOnPath("openclaw")); const requireFromOpenclaw = createRequire(openclawBin); const runtimePath = requireFromOpenclaw.resolve("openclaw/plugin-sdk/gateway-runtime"); const { callGatewayFromCli } = await import(pathToFileURL(runtimePath).href); @@ -223,7 +236,6 @@ function gatewayCatalogCallScript(params: Record) { return trustedSandboxShellScript(`set -eu . /tmp/nemoclaw-proxy-env.sh export HOME=/sandbox -export OPENCLAW_BIN="$(command -v openclaw)" export NO_PROXY=127.0.0.1,localhost export no_proxy="$NO_PROXY" export NEMOCLAW_E2E_GATEWAY_PARAMS_B64='${encodedParams}' From 63e979a8d5cfae4d95144b104ff4e093f78b8510 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 10:48:58 -0700 Subject: [PATCH 10/46] test: create lazy plugin cache before probe Signed-off-by: Aaron Erickson --- test/e2e/live/openclaw-plugin-runtime-exdev.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 5386183c6e..d41004e299 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -549,7 +549,7 @@ liveTest( const df = await sandbox.execShell( SANDBOX_NAME, trustedSandboxShellScript( - "df -PT / /tmp /dev/shm /sandbox /sandbox/.openclaw/plugin-runtime-deps", + "mkdir -p /sandbox/.openclaw/plugin-runtime-deps && df -PT / /tmp /dev/shm /sandbox /sandbox/.openclaw/plugin-runtime-deps", ), { artifactName: "openclaw-plugin-exdev-filesystem-layout", From e6eaa0b43a830ba1e2bb6c1b4f70e62a66a30c4b Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 13:36:06 -0700 Subject: [PATCH 11/46] fix: address custom plugin review findings Signed-off-by: Aaron Erickson --- docs/deployment/install-openclaw-plugins.mdx | 37 +- docs/reference/commands.mdx | 4 +- docs/reference/troubleshooting.mdx | 2 +- src/lib/actions/sandbox/rebuild-flow.test.ts | 2 + src/lib/actions/sandbox/rebuild.ts | 4 +- src/lib/state/openclaw-plugin-restore.test.ts | 76 + src/lib/state/openclaw-plugin-restore.ts | 92 + src/lib/state/sandbox.ts | 152 +- .../plugins/weather/package-lock.json | 5111 +++++++++++++++++ .../e2e/fixtures/plugins/weather/package.json | 4 + .../e2e/fixtures/plugins/weather/src/index.ts | 40 +- .../fixtures/plugins/weather/src/version.ts | 4 + .../fixtures/plugins/weather/tsconfig.json | 1 + .../openclaw-plugin-runtime-exdev.test.ts | 74 +- test/snapshot.test.ts | 63 +- tsconfig.cli.json | 2 +- 16 files changed, 5557 insertions(+), 111 deletions(-) create mode 100644 src/lib/state/openclaw-plugin-restore.test.ts create mode 100644 src/lib/state/openclaw-plugin-restore.ts create mode 100644 test/e2e/fixtures/plugins/weather/src/version.ts diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 79ddc0d398..7b0bfa31e1 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -67,6 +67,23 @@ The example Dockerfile uses `npm ci`, so `my-plugin/` must include `package-lock If your plugin does not have a lockfile yet, create one in the plugin project with `npm install --package-lock-only`. OpenClaw `2026.5.27` also requires the plugin manifest to declare each registered tool in `contracts.tools`; for example, a weather plugin that registers `get_weather` must declare `"tools": ["get_weather"]`. +Declare OpenClaw as both a runtime peer and an exact, release-matched development dependency. +The peer range expresses runtime compatibility, while the development dependency provides the plugin SDK during the build: + +```json +{ + "devDependencies": { + "openclaw": "2026.5.27" + }, + "peerDependencies": { + "openclaw": ">=2026.5.17" + } +} +``` + +With current npm versions, `npm ci` installs peer dependencies automatically. +The build stage below therefore omits both development and peer dependencies after compilation so the staged plugin does not carry a private OpenClaw runtime. + ## Extend the Full Managed Dockerfile Make two changes to the stock `Dockerfile` from the `v0.0.71` checkout. @@ -92,10 +109,12 @@ Replace `weather` with the plugin ID from `openclaw.plugin.json` in the stage na FROM builder AS weather-plugin-builder WORKDIR /opt/my-plugin COPY my-plugin/package.json my-plugin/package-lock.json my-plugin/tsconfig.json ./ -RUN npm ci --no-audit --no-fund +RUN npm ci --ignore-scripts --no-audit --no-fund COPY my-plugin/openclaw.plugin.json ./ COPY my-plugin/src/ ./src/ -RUN npm run build && npm prune --omit=dev +RUN npm run build \ + && npm prune --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund \ + && test ! -e node_modules/openclaw # Extend the completed managed runtime. FROM nemoclaw-runtime AS weather-runtime @@ -110,7 +129,10 @@ COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ /opt/my-plugin/node_modules/ /opt/weather-plugin/node_modules/ USER sandbox -RUN HOME=/sandbox openclaw plugins install /opt/weather-plugin \ +RUN test ! -e /opt/weather-plugin/node_modules/openclaw \ + && HOME=/sandbox openclaw plugins install /opt/weather-plugin \ + && test -L /sandbox/.openclaw/extensions/weather/node_modules/openclaw \ + && test "$(realpath /sandbox/.openclaw/extensions/weather/node_modules/openclaw)" = /usr/local/lib/node_modules/openclaw \ && HOME=/sandbox openclaw plugins enable weather \ && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null @@ -125,7 +147,9 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ ``` The final stage inherits the stock runtime entrypoint, command, gateway health check, generated configuration, and file permissions. -The local install copies the staged plugin into OpenClaw's extensions tree, records the install, and leaves existing managed plugin load paths intact before the explicit enable and inspect steps. +The local install copies the staged plugin into OpenClaw's extensions tree, records the install, links it to the image's OpenClaw runtime, and leaves existing managed plugin load paths intact before the explicit enable and inspect steps. +The pre-install `test` commands fail the image build if npm retained a private `node_modules/openclaw` directory that would prevent OpenClaw from creating its runtime link. +The post-install checks then prove that OpenClaw created the link and that it resolves to the image's global runtime. The last `RUN` refreshes the managed config hash after `openclaw plugins enable` updates `openclaw.json`. ## Create and Verify the Sandbox @@ -158,9 +182,12 @@ NemoClaw's live regression also verifies that the running gateway's tool catalog Keep the source checkout and Dockerfile at the recorded path if you plan to run `nemoclaw weather-agent rebuild --yes`. This source-based workflow can reproduce the plugin during that rebuild, but it is not the durable managed-plugin lifecycle proposed in issue #5998. +During rebuild, NemoClaw validates plugin install IDs and paths recorded by the freshly built image, preserves those fresh extension directories, and restores backup-only extension state around them. +That reconciliation keeps a rebuilt plugin update instead of replacing it with the pre-rebuild copy from the state backup. Rerun the plugin inspection and HTTP invocation after a gateway restart or sandbox rebuild to verify that the plugin remains available. -If the plugin imports runtime packages, keep those packages in `dependencies` rather than `devDependencies` so `npm prune --omit=dev` preserves them. +If the plugin imports runtime packages, keep those packages in `dependencies` rather than `devDependencies` so the prune step preserves them. +Keep `openclaw` in the release-matched peer and development dependency fields shown above; do not move it to `dependencies`. If the plugin needs configuration in `openclaw.json`, apply it before refreshing `.config-hash`. Never bake plugin credentials into the Dockerfile or `openclaw.json`. diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index fdc79cbb4a..826cc10877 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -394,7 +394,7 @@ Missing paths fail during command parsing before preflight, gateway setup, infer If deployment verification cannot reach the gateway for a custom OpenClaw image, NemoClaw checks for `/tmp/gateway.log`, `/usr/local/bin/nemoclaw-start`, and `/sandbox/.openclaw/openclaw.json`. When all three paths are absent, onboarding reports that the custom image lacks the managed runtime instead of treating repeated port-forward retries as the recovery path. -For the version-matched full-runtime plugin workflow, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). +For the version-matched full-runtime plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). @@ -1447,7 +1447,7 @@ Skill names must contain only alphanumeric characters, dots, hyphens, and unders OpenClaw plugins are a different kind of extension. -To install an OpenClaw plugin, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). +To install an OpenClaw plugin, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). For OpenClaw, the command uploads the skill to the OpenClaw state directory and mirrors it into `$HOME/.openclaw/skills/` when the agent home directory differs from the state directory. That mirror makes skills listed by `openclaw skills list` available at session startup. If mirror creation fails, NemoClaw prints a warning so you can reinstall or inspect the home directory permissions. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 78125a50bd..a86968ea17 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -438,7 +438,7 @@ When all three paths are absent, the CLI reports that the image lacks the NemoCl This failure commonly occurs when the custom Dockerfile starts from `ghcr.io/nvidia/nemoclaw/sandbox-base` alone because that image is an intermediate dependency image. Rebuild the custom image from the full stock Dockerfile and source context for the same NemoClaw release. -For the version-pinned plugin workflow, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). +For the version-pinned plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). If the sandbox is unreachable or the managed runtime paths are present, NemoClaw retains the existing generic gateway-log and host OpenShell-log guidance because the base-only failure is not proven. diff --git a/src/lib/actions/sandbox/rebuild-flow.test.ts b/src/lib/actions/sandbox/rebuild-flow.test.ts index e8b0aaedc8..bc12858793 100644 --- a/src/lib/actions/sandbox/rebuild-flow.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow.test.ts @@ -489,6 +489,7 @@ describe("rebuildSandbox flow", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", "/tmp/nemoclaw-rebuild-backup", + { preserveFreshOpenClawPluginInstalls: true }, ); expect(harness.applyPresetSpy).toHaveBeenCalledWith("alpha", "npm"); expect(harness.applyPresetSpy).toHaveBeenCalledWith("alpha", "bad"); @@ -527,6 +528,7 @@ describe("rebuildSandbox flow", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", recoveryManifest.backupPath, + { preserveFreshOpenClawPluginInstalls: true }, ); }); diff --git a/src/lib/actions/sandbox/rebuild.ts b/src/lib/actions/sandbox/rebuild.ts index ec39843a6d..b0dda8703e 100644 --- a/src/lib/actions/sandbox/rebuild.ts +++ b/src/lib/actions/sandbox/rebuild.ts @@ -1139,7 +1139,9 @@ export async function rebuildSandbox( console.log(""); console.log(" Restoring workspace state..."); log(`Restoring from: ${backupManifest.backupPath} into sandbox: ${sandboxName}`); - const restore = sandboxState.restoreSandboxState(sandboxName, backupManifest.backupPath); + const restore = sandboxState.restoreSandboxState(sandboxName, backupManifest.backupPath, { + preserveFreshOpenClawPluginInstalls: true, + }); log( `Restore result: success=${restore.success}, restored=${restore.restoredDirs.join(",")}; files=${restore.restoredFiles.join(",")}, failed=${restore.failedDirs.join(",")}; failedFiles=${restore.failedFiles.join(",")}`, ); diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts new file mode 100644 index 0000000000..56ae0be6b7 --- /dev/null +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { parseFreshOpenClawPluginExtensionDirs } from "./openclaw-plugin-restore"; + +const OPENCLAW_DIR = "/sandbox/.openclaw"; + +function install(installPath: string): Record { + return { source: "path", installPath }; +} + +describe("parseFreshOpenClawPluginExtensionDirs", () => { + it("returns sorted validated directories for direct extension installs", () => { + expect( + parseFreshOpenClawPluginExtensionDirs( + { + version: 1, + installRecords: { + weather: install(`${OPENCLAW_DIR}/extensions/weather`), + nemoclaw: install(`${OPENCLAW_DIR}/extensions/nemoclaw`), + }, + }, + OPENCLAW_DIR, + ), + ).toEqual({ ok: true, extensionDirs: ["nemoclaw", "weather"] }); + }); + + it("ignores npm installs outside extensions and accepts scoped IDs with encoded directories", () => { + expect( + parseFreshOpenClawPluginExtensionDirs( + { + version: 1, + installRecords: { + "@scope/weather": install(`${OPENCLAW_DIR}/extensions/scope__weather`), + diagnostics: install(`${OPENCLAW_DIR}/npm/node_modules/diagnostics`), + "foo+bar": install(`${OPENCLAW_DIR}/extensions/foo+bar`), + "my plugin": install(`${OPENCLAW_DIR}/extensions/my plugin`), + }, + }, + OPENCLAW_DIR, + ), + ).toEqual({ ok: true, extensionDirs: ["foo+bar", "my plugin", "scope__weather"] }); + }); + + it.each([ + ["a traversal ID", { "../weather": install(`${OPENCLAW_DIR}/extensions/../weather`) }], + ["a nested install path", { weather: install(`${OPENCLAW_DIR}/extensions/nested/weather`) }], + ["a noncanonical install path", { weather: install(`${OPENCLAW_DIR}/extensions/../weather`) }], + ["a glob extension directory", { weather: install(`${OPENCLAW_DIR}/extensions/*`) }], + ["non-object metadata", { weather: "invalid" }], + ])("rejects %s", (_label, installs) => { + const result = parseFreshOpenClawPluginExtensionDirs( + { version: 1, installRecords: installs }, + OPENCLAW_DIR, + ); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatch(/unsafe|invalid/); + }); + + it("rejects an unbounded install set before constructing restore commands", () => { + const installs = Object.fromEntries( + Array.from({ length: 129 }, (_, index) => { + const id = `plugin-${index}`; + return [id, install(`${OPENCLAW_DIR}/extensions/${id}`)]; + }), + ); + expect( + parseFreshOpenClawPluginExtensionDirs({ version: 1, installRecords: installs }, OPENCLAW_DIR), + ).toEqual({ + ok: false, + error: "fresh OpenClaw registry has too many plugin installs (129)", + }); + }); +}); diff --git a/src/lib/state/openclaw-plugin-restore.ts b/src/lib/state/openclaw-plugin-restore.ts new file mode 100644 index 0000000000..fb3b9cabb5 --- /dev/null +++ b/src/lib/state/openclaw-plugin-restore.ts @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; + +import { isRecord } from "../core/json-types.js"; + +const MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS = 128; +const OPENCLAW_EXTENSION_GLOB_CHARS = ["/", "\\", "*", "?", "[", "]"] as const; + +export type OpenClawManagedExtensionDiscoveryResult = + | { ok: true; extensionDirs: string[] } + | { ok: false; error: string }; + +function isSafeOpenClawPluginInstallId(id: string): boolean { + if (id.length === 0 || id.length > 256 || /[\u0000-\u001f\u007f]/.test(id)) return false; + const slash = id.indexOf("/"); + if (slash === -1) return true; + return id.startsWith("@") && slash > 1 && slash === id.lastIndexOf("/") && slash < id.length - 1; +} + +function isSafeOpenClawExtensionDirName(name: string): boolean { + return ( + name.length > 0 && + name.length <= 128 && + name !== "." && + name !== ".." && + !/[\u0000-\u001f\u007f]/.test(name) && + !OPENCLAW_EXTENSION_GLOB_CHARS.some((char) => name.includes(char)) + ); +} + +export function parseFreshOpenClawPluginExtensionDirs( + registryIndex: unknown, + dir: string, +): OpenClawManagedExtensionDiscoveryResult { + if (!isRecord(registryIndex) || registryIndex.version !== 1) { + return { ok: false, error: "fresh OpenClaw plugin install registry is invalid" }; + } + const installs = registryIndex.installRecords; + if (!isRecord(installs)) { + return { ok: false, error: "fresh OpenClaw plugin install records are invalid" }; + } + + const ids = Object.keys(installs).sort(); + if (ids.length > MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS) { + return { + ok: false, + error: `fresh OpenClaw registry has too many plugin installs (${ids.length})`, + }; + } + const extensionsDir = `${dir.replace(/\/+$/, "")}/extensions`; + const extensionDirs = new Set(); + for (const id of ids) { + if (!isSafeOpenClawPluginInstallId(id)) { + return { ok: false, error: `fresh OpenClaw registry has unsafe plugin install id: ${id}` }; + } + const install = installs[id]; + if (!isRecord(install)) { + return { ok: false, error: `fresh OpenClaw plugin install metadata is invalid: ${id}` }; + } + if (typeof install.installPath !== "string" || !path.posix.isAbsolute(install.installPath)) { + return { ok: false, error: `fresh OpenClaw plugin install path is invalid for ${id}` }; + } + const normalizedInstallPath = path.posix.normalize(install.installPath); + if (normalizedInstallPath !== install.installPath) { + return { ok: false, error: `fresh OpenClaw plugin install path is invalid for ${id}` }; + } + + // npm-origin installs legitimately live under .openclaw/npm/node_modules + // and are not part of the backed-up extensions directory. Only direct + // children of extensions can be overwritten by extension restore. + const relativeInstallPath = path.posix.relative(extensionsDir, normalizedInstallPath); + if ( + relativeInstallPath.startsWith("../") || + relativeInstallPath === ".." || + path.posix.isAbsolute(relativeInstallPath) + ) { + continue; + } + if (relativeInstallPath.length === 0 || relativeInstallPath.includes("/")) { + return { ok: false, error: `fresh OpenClaw plugin install path is invalid for ${id}` }; + } + + const extensionDir = relativeInstallPath; + if (!isSafeOpenClawExtensionDirName(extensionDir)) { + return { ok: false, error: `fresh OpenClaw extension directory is invalid for ${id}` }; + } + extensionDirs.add(extensionDir); + } + return { ok: true, extensionDirs: [...extensionDirs].sort() }; +} diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 38b10f91fc..6e4811c83b 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -38,9 +38,13 @@ import { buildOpenClawConfigRestoreInputFromSandbox, shouldMergeOpenClawConfigStateFile, } from "./openclaw-config-restore-input.js"; +import { + type OpenClawManagedExtensionDiscoveryResult, + parseFreshOpenClawPluginExtensionDirs, +} from "./openclaw-plugin-restore.js"; import type { CustomPolicyEntry } from "./registry.js"; -import { isSshTransportFailure } from "./ssh-transport.js"; import * as registry from "./registry.js"; +import { isSshTransportFailure } from "./ssh-transport.js"; import { runTarListing } from "./tar-listing.js"; const HOME_DIR = path.resolve(process.env.HOME || os.homedir()); @@ -136,6 +140,15 @@ export interface RestoreResult { failedFiles: string[]; } +export interface RestoreOptions { + /** + * The target is a just-recreated OpenClaw sandbox, so its plugin install + * registry came from the fresh image. Preserve those validated extension + * directories over the backup. + */ + preserveFreshOpenClawPluginInstalls?: boolean; +} + export interface TarValidationResult { safe: boolean; entries: string[]; @@ -580,7 +593,7 @@ const AUDIT_SYMLINK_WHITELIST: ReadonlyMap = new Map([ ]); const EXTENSION_NPM_BIN_RE = /^extensions\/[^/]+\/node_modules\/\.bin\/[^/]+$/; -const OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS = ["nemoclaw", "openclaw-weixin"] as const; +const OPENCLAW_BUILTIN_IMAGE_MANAGED_EXTENSION_DIRS = ["nemoclaw", "openclaw-weixin"] as const; function isAllowedExtensionNpmBinSymlink(relPath: string, linkTarget: string): boolean { const normalizedRelPath = relPath.split(path.sep).join("/"); @@ -686,36 +699,79 @@ function shouldPreserveOpenClawManagedExtensions( ); } +function discoverFreshOpenClawPluginExtensionDirs( + configFile: string, + sandboxName: string, + dir: string, +): OpenClawManagedExtensionDiscoveryResult { + const spec: StateFileSpec = { path: "plugins/installs.json", strategy: "copy" }; + const command = buildStateFileBackupCommand(dir, spec); + const result = spawnSync("ssh", [...sshArgs(configFile, sandboxName), command], { + stdio: ["ignore", "pipe", "pipe"], + timeout: 30000, + maxBuffer: 4 * 1024 * 1024, + }); + if (result.status !== 0 || result.error || result.signal || !result.stdout) { + const detail = + (result.stderr?.toString() || "").trim() || + result.error?.message || + (result.signal ? `signal ${result.signal}` : `exit ${String(result.status)}`); + return { ok: false, error: `could not read fresh OpenClaw plugin install registry: ${detail}` }; + } + + let config: unknown; + try { + config = JSON.parse(result.stdout.toString("utf-8")) as unknown; + } catch (error) { + return { + ok: false, + error: `fresh OpenClaw plugin install registry is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, + }; + } + return parseFreshOpenClawPluginExtensionDirs(config, dir); +} + function buildRestoreTarArgs( backupPath: string, localDirs: readonly string[], - preserveManagedExtensions: boolean, + managedExtensionDirs: readonly string[], ): string[] { const args = ["-cf", "-", "-C", backupPath]; - if (preserveManagedExtensions) { - for (const extensionName of OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS) { - args.push("--exclude", `extensions/${extensionName}`); - } + for (const extensionDir of managedExtensionDirs) { + args.push("--exclude", `extensions/${extensionDir}`); } args.push("--", ...localDirs); return args; } -function buildOpenClawExtensionsCleanupCommand(dir: string): string { +function buildOpenClawExtensionsCleanupCommand( + dir: string, + managedExtensionDirs: readonly string[], + requiredExtensionDirs: ReadonlySet, +): string { const extensionsDir = `${dir}/extensions`; const quotedExtensionsDir = shellQuote(extensionsDir); - const validationCommands = OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS.map((extensionName) => { - const managedPath = `${extensionsDir}/${extensionName}`; - return ( - `p=${shellQuote(managedPath)}; ` + - 'if [ -e "$p" ] && { [ ! -d "$p" ] || [ -L "$p" ]; }; then ' + - 'echo "refusing to preserve unsafe managed extension: $p" >&2; exit 20; fi' - ); - }).join("; "); + const validationCommands = managedExtensionDirs + .map((extensionDir) => { + const managedPath = `${extensionsDir}/${extensionDir}`; + if (requiredExtensionDirs.has(extensionDir)) { + return ( + `p=${shellQuote(managedPath)}; ` + + 'if [ ! -d "$p" ] || [ -L "$p" ]; then ' + + 'echo "refusing missing or unsafe image-managed extension: $p" >&2; exit 20; fi' + ); + } + return ( + `p=${shellQuote(managedPath)}; ` + + 'if [ -e "$p" ] && { [ ! -d "$p" ] || [ -L "$p" ]; }; then ' + + 'echo "refusing to preserve unsafe managed extension: $p" >&2; exit 20; fi' + ); + }) + .join("; "); const validateManagedPaths = `{ ${validationCommands}; }`; - const preservedNames = OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS.map( - (extensionName) => `! -name ${shellQuote(extensionName)}`, - ).join(" "); + const preservedNames = managedExtensionDirs + .map((extensionDir) => `! -name ${shellQuote(extensionDir)}`) + .join(" "); return [ `mkdir -p -- ${quotedExtensionsDir}`, @@ -727,15 +783,19 @@ function buildOpenClawExtensionsCleanupCommand(dir: string): string { function buildRestoreCleanupCommand( dir: string, localDirs: readonly string[], - preserveManagedExtensions: boolean, + managedExtensionDirs: readonly string[], + requiredExtensionDirs: ReadonlySet, ): string { + const preserveManagedExtensions = managedExtensionDirs.length > 0; const commands: string[] = []; for (const dirName of localDirs) { if (preserveManagedExtensions && dirName === "extensions") continue; commands.push(`rm -rf -- ${shellQuote(`${dir}/${dirName}`)}`); } if (preserveManagedExtensions) { - commands.push(buildOpenClawExtensionsCleanupCommand(dir)); + commands.push( + buildOpenClawExtensionsCleanupCommand(dir, managedExtensionDirs, requiredExtensionDirs), + ); } return commands.length > 0 ? commands.join(" && ") : ":"; } @@ -1435,7 +1495,11 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions = /** * Restore state directories into a sandbox from a prior backup. */ -export function restoreSandboxState(sandboxName: string, backupPath: string): RestoreResult { +export function restoreSandboxState( + sandboxName: string, + backupPath: string, + options: RestoreOptions = {}, +): RestoreResult { _log(`restoreSandboxState: sandbox=${sandboxName}, backupPath=${backupPath}`); const manifest = readManifest(backupPath); if (!manifest) { @@ -1499,18 +1563,43 @@ export function restoreSandboxState(sandboxName: string, backupPath: string): Re const tempSshConfig = createTempSshConfig(sshConfig, "nemoclaw-state-"); const configFile = tempSshConfig.file; + let freshImageManagedExtensionDirs: string[] = []; try { + const preserveManagedExtensions = shouldPreserveOpenClawManagedExtensions( + manifest, + dir, + localDirs, + ); + if (options.preserveFreshOpenClawPluginInstalls && preserveManagedExtensions) { + const discovery = discoverFreshOpenClawPluginExtensionDirs(configFile, sandboxName, dir); + if (!discovery.ok) { + _log(`FAILED: ${discovery.error}`); + return { + success: false, + restoredDirs, + failedDirs: [...localDirs], + restoredFiles, + failedFiles: localFiles.map((f) => f.path), + }; + } + freshImageManagedExtensionDirs = discovery.extensionDirs; + _log(`Fresh image-managed OpenClaw extensions: [${discovery.extensionDirs.join(",")}]`); + } + const managedExtensionDirs = preserveManagedExtensions + ? [ + ...new Set([ + ...OPENCLAW_BUILTIN_IMAGE_MANAGED_EXTENSION_DIRS, + ...freshImageManagedExtensionDirs, + ]), + ].sort() + : []; + if (localDirs.length > 0) { // Upload via tar pipe // NC-2227-04: Removed -h flag from restore as well — no symlink following. - const preserveManagedExtensions = shouldPreserveOpenClawManagedExtensions( - manifest, - dir, - localDirs, - ); const tarResult = spawnSync( "tar", - buildRestoreTarArgs(backupPath, localDirs, preserveManagedExtensions), + buildRestoreTarArgs(backupPath, localDirs, managedExtensionDirs), { stdio: ["ignore", "pipe", "pipe"], timeout: 60000, @@ -1533,7 +1622,12 @@ export function restoreSandboxState(sandboxName: string, backupPath: string): Re // image-managed extensions are preserved from the freshly built image and // excluded from the restore tar; only user/non-managed extension entries // are cleared and restored from the backup. - const rmCmd = buildRestoreCleanupCommand(dir, localDirs, preserveManagedExtensions); + const rmCmd = buildRestoreCleanupCommand( + dir, + localDirs, + managedExtensionDirs, + new Set(freshImageManagedExtensionDirs), + ); _log(`Cleaning target dirs before restore: ${rmCmd}`); const rmResult = spawnSync("ssh", [...sshArgs(configFile, sandboxName), rmCmd], { stdio: ["ignore", "pipe", "pipe"], diff --git a/test/e2e/fixtures/plugins/weather/package-lock.json b/test/e2e/fixtures/plugins/weather/package-lock.json index 15d8f4b957..e58ef537b8 100644 --- a/test/e2e/fixtures/plugins/weather/package-lock.json +++ b/test/e2e/fixtures/plugins/weather/package-lock.json @@ -12,10 +12,5121 @@ "typebox": "1.1.38" }, "devDependencies": { + "openclaw": "2026.5.27", "typescript": "5.9.3" }, "engines": { "node": ">=22.16.0" + }, + "peerDependencies": { + "openclaw": ">=2026.5.17" + } + }, + "node_modules/openclaw": { + "version": "2026.5.27", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.27.tgz", + "integrity": "sha512-2N93zhdAo88KAbHt6T7KvYXf4s7XIkYXBgv1npYpn7e1Y9FvrtgtpsA38my9rtFW+70uXEojRPX5/OqnuDqJPw==", + "dev": true, + "hasInstallScript": true, + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@agentclientprotocol/sdk": "0.22.1", + "@clack/core": "1.3.1", + "@clack/prompts": "1.4.0", + "@earendil-works/pi-agent-core": "0.75.5", + "@earendil-works/pi-ai": "0.75.5", + "@earendil-works/pi-coding-agent": "0.75.5", + "@earendil-works/pi-tui": "0.75.5", + "@google/genai": "2.6.0", + "@grammyjs/runner": "2.0.3", + "@grammyjs/transformer-throttler": "1.2.1", + "@homebridge/ciao": "1.3.8", + "@lydell/node-pty": "1.2.0-beta.12", + "@modelcontextprotocol/sdk": "1.29.0", + "@mozilla/readability": "0.6.0", + "@openclaw/fs-safe": "0.3.0", + "@openclaw/proxyline": "0.3.3", + "chalk": "5.6.2", + "chokidar": "5.0.0", + "commander": "14.0.3", + "croner": "10.0.1", + "dotenv": "17.4.2", + "express": "5.2.1", + "file-type": "22.0.1", + "grammy": "1.43.0", + "ipaddr.js": "2.4.0", + "jiti": "2.7.0", + "json5": "2.2.3", + "jszip": "3.10.1", + "kysely": "0.29.2", + "linkedom": "0.18.12", + "markdown-it": "14.1.1", + "node-edge-tts": "1.2.10", + "openai": "6.39.0", + "pdfjs-dist": "5.7.284", + "playwright-core": "1.60.0", + "qrcode": "1.5.4", + "quickjs-wasi": "2.2.0", + "rastermill": "0.3.0", + "tar": "7.5.15", + "tokenjuice": "0.7.1", + "tree-sitter-bash": "0.25.1", + "tslog": "4.10.2", + "typebox": "1.1.38", + "typescript": "6.0.3", + "undici": "8.3.0", + "web-push": "3.6.7", + "web-tree-sitter": "0.26.9", + "ws": "8.21.0", + "yaml": "2.9.0", + "zod": "4.4.3" + }, + "bin": { + "openclaw": "openclaw.mjs" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "sqlite-vec": "0.1.9" + } + }, + "node_modules/openclaw/node_modules/@agentclientprotocol/sdk": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.22.1.tgz", + "integrity": "sha512-DfqXtl/8gO9NImq094MTaCXEU2vkhh6v7q/kT+9UjZxUqj8hYaya2OjLVIqn16MzNHcXEpShTR2RIauLSYeDQQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/openclaw/node_modules/@anthropic-ai/sdk": { + "version": "0.98.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.98.0.tgz", + "integrity": "sha512-N7aXtCvC5g6T1Y4V29lJjceu/zTkVkIZF0jdBvagr0TRFHuKeImffalGWEfqZKrvjH+IQbzJWw6TmSmUzrlMgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1053.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1053.0.tgz", + "integrity": "sha512-I5dua8y1logE+Mx6r5kvI1tjM+XyC3H42KDCpEqmhrJfanor/x/AdOavyv3HnS4sBqUxx2IrjLP3ouEumjeTzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/credential-provider-node": "^3.972.44", + "@aws-sdk/eventstream-handler-node": "^3.972.17", + "@aws-sdk/middleware-eventstream": "^3.972.13", + "@aws-sdk/middleware-websocket": "^3.972.21", + "@aws-sdk/token-providers": "3.1053.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/node-http-handler": "^4.7.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/core": { + "version": "3.974.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.13.tgz", + "integrity": "sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@aws-sdk/xml-builder": "^3.972.25", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.3", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.39.tgz", + "integrity": "sha512-29wX9zpAvEt1vcj0psha+y6ygBHy2V/S72mp6e7q0KARLWXq+pwE/lR6qGkwknQvruh52lXvlqZIga8Hdxkucw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.41.tgz", + "integrity": "sha512-IA3CQTjtJkb6u1H4mE4936c8OPBMa9Jggtwe8U2Mqw/vvb/tZ5Ebd0mcZcX0uKWQhOyYo/+qNIwkV5Xh+FeJJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/node-http-handler": "^4.7.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.43.tgz", + "integrity": "sha512-4mzII+3mZEVXXE1xzrLQrCJL7/r62A63bA6SVzZoNL5rqCJghpf+xgGltVrIBBs0n+mOZBKrQl2tRREtvZ5l6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/credential-provider-env": "^3.972.39", + "@aws-sdk/credential-provider-http": "^3.972.41", + "@aws-sdk/credential-provider-login": "^3.972.43", + "@aws-sdk/credential-provider-process": "^3.972.39", + "@aws-sdk/credential-provider-sso": "^3.972.43", + "@aws-sdk/credential-provider-web-identity": "^3.972.43", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.43.tgz", + "integrity": "sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.44.tgz", + "integrity": "sha512-sDaBIT0yrNNIPfvlsiTCmANm07zKju+ipWODjEXgZlsjMeIJR3LVp7RDyAOzUoAsTbDfYKDWp+i5WrFiQP6rmQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.39", + "@aws-sdk/credential-provider-http": "^3.972.41", + "@aws-sdk/credential-provider-ini": "^3.972.43", + "@aws-sdk/credential-provider-process": "^3.972.39", + "@aws-sdk/credential-provider-sso": "^3.972.43", + "@aws-sdk/credential-provider-web-identity": "^3.972.43", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.39.tgz", + "integrity": "sha512-2k/amBifLd75eXNwgvPw/2lKYSQ3NhvHQgkVKVjfUq13/eJ3JRtHmznuFenn74OK3sSfp4SMy1YB2w+UVXoKqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.43.tgz", + "integrity": "sha512-LPc3+Y4vhH1T4x6CMqwCM6hk5+SRf/Lwmgm8INm95wxTtIRHcMwQUVkDzWu4Iw/RSncxYM2BC01OrYbxOPZvyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/token-providers": "3.1052.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.43.tgz", + "integrity": "sha512-wQtL34lUD/09VXjwAUo2T+I3aEXRDxMB3DKmTJL/Zj0Gi6sLDTrVhae1XVt01yzkquOWajI/sZW72JGDZ1ciTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.17.tgz", + "integrity": "sha512-WFwdNcjchKZr7jKYgGimUZO8sSKQF/le7GGqgeCzz/lHozInE6b0gFJ1YMr8NaIeAoWJwgtrF7RE4/qMgosAdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.13.tgz", + "integrity": "sha512-ECfsw7mf6G/sxNbKbGE3/h1xeIArY/yRI1IjDGYkLgDIankh+aDOtDRSr40LVlIHGL9+jEH1cVuxmbJ8NLL/1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.21.tgz", + "integrity": "sha512-yr+5+C7v9R55sAJ89A55Wrm7wIKPVn5cm6J3Hztnd5s/iwEUKxyJqCnIxJu4fVXgG9XBQD1Jc4rsWC1ozahJjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.11.tgz", + "integrity": "sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.28", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/node-http-handler": "^4.7.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.28.tgz", + "integrity": "sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/token-providers": { + "version": "3.1053.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1053.0.tgz", + "integrity": "sha512-laSwHLYMMrXQRl2mFDXszF43m/F4pKWyGr7hCLfJmV8rn8c6CnI/hp/bf/Gn7gLcjz0SY4evd7SBpqtnIhzA/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/types": { + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", + "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz", + "integrity": "sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.2", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/openclaw/node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/@clack/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.1.tgz", + "integrity": "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/openclaw/node_modules/@clack/prompts": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.4.0.tgz", + "integrity": "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.3.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-agent-core": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.5.tgz", + "integrity": "sha512-LHygOgsW2pgXKb3IkXkOAeZPovHr9VF+EixgXVsDNuB4jmhEOXgshy/zksZ7slkUAx10OQ9W1Ed/2jsnhd1NqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.75.5", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-ai": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.5.tgz", + "integrity": "sha512-zf1F5kXk1pqZeFShXOqq9ibUk8QdtRoLCDPAjO+hj44e3EUs9/GFO2qnhTC5+JA2uwVCx+WCNe1PiCjlBYWm5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.1", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-ai/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-ai/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-coding-agent": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.75.5.tgz", + "integrity": "sha512-O3CCQDYy28D4uwtP6zZkdEwzHN6X22v49Sb0+SZTC7x37V/YfmogrWPiaFoWeoc2hmdKhSATI7ZAK5bQbJG5NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.75.5", + "@earendil-works/pi-ai": "^0.75.5", + "@earendil-works/pi-tui": "^0.75.5", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "typebox": "1.1.38", + "undici": "8.3.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.6" + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.5.tgz", + "integrity": "sha512-LkXUM1/49pvzzeI39Y5wjBMlgafcCf67HCLhB9Z7yuXHy4XgT+VqxWcZVW5hBdhQsHZd0znjJotfGH1BzxMfiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/@google/genai": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz", + "integrity": "sha512-HjoW3mPuEn7pnuKABJl9VbDoWDSF4nbwYKYvYYor7YjPeDxrrBxHzu2d1Prcd+BAuC4w+85UP6y7ZdcrQAoO7g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@grammyjs/runner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz", + "integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0" + }, + "engines": { + "node": ">=12.20.0 || >=14.13.1" + }, + "peerDependencies": { + "grammy": "^1.13.1" + } + }, + "node_modules/openclaw/node_modules/@grammyjs/transformer-throttler": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz", + "integrity": "sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bottleneck": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + }, + "peerDependencies": { + "grammy": "^1.0.0" + } + }, + "node_modules/openclaw/node_modules/@grammyjs/types": { + "version": "3.27.3", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.27.3.tgz", + "integrity": "sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/@homebridge/ciao": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.8.tgz", + "integrity": "sha512-lNhpCsZVbdbjz2trFjQdzQ3cUIMZQMIMksi7wd3ntTIYgdaGLqT1Ms97DfVIJYHzRuduf56ISvgU8RRLTpK/ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "fast-deep-equal": "^3.1.3", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1" + }, + "bin": { + "ciao-bcs": "lib/bonjour-conformance-testing.js" + } + }, + "node_modules/openclaw/node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/openclaw/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@lydell/node-pty": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.2.0-beta.12.tgz", + "integrity": "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", + "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", + "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", + "@lydell/node-pty-linux-x64": "1.2.0-beta.12", + "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", + "@lydell/node-pty-win32-x64": "1.2.0-beta.12" + } + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-darwin-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-darwin-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-linux-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-linux-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-win32-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-win32-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.6", + "@mariozechner/clipboard-darwin-universal": "0.3.6", + "@mariozechner/clipboard-darwin-x64": "0.3.6", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-musl": "0.3.6", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/openclaw/node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/openclaw/node_modules/@mozilla/readability": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", + "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "dev": true, + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/openclaw/node_modules/@openclaw/fs-safe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.3.0.tgz", + "integrity": "sha512-uIBE441CIt1kIURoP9qRGKZ8LkGyfD9ZzeESjwAd29ZPWtghws/5GR3Pjb67jKdcJHP1I6roNXcvnhzAU7lHlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.11" + }, + "optionalDependencies": { + "jszip": "^3.10.1", + "tar": "7.5.13" + } + }, + "node_modules/openclaw/node_modules/@openclaw/proxyline": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@openclaw/proxyline/-/proxyline-0.3.3.tgz", + "integrity": "sha512-sftHnW69NHQqLjCxBTvQ8f/eQl+peZ5pHCBQtuTWBbeuYRHZ0/GXVTmw/O/YKsShMbqPWhJB0UYtPPdvCUSS8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + }, + "peerDependencies": { + "undici": ">=8.3.0 <9" + } + }, + "node_modules/openclaw/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/openclaw/node_modules/@smithy/core": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.4.tgz", + "integrity": "sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.4.tgz", + "integrity": "sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.4.tgz", + "integrity": "sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/node-http-handler": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.4.tgz", + "integrity": "sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/signature-v4": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.4.tgz", + "integrity": "sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openclaw/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openclaw/node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/openclaw/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/openclaw/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/openclaw/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/openclaw/node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/openclaw/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/openclaw/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/openclaw/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/openclaw/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/openclaw/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/openclaw/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/openclaw/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/openclaw/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/openclaw/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/openclaw/node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/croner": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/croner/-/croner-10.0.1.tgz", + "integrity": "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==", + "dev": true, + "funding": [ + { + "type": "other", + "url": "https://paypal.me/hexagonpp" + }, + { + "type": "github", + "url": "https://github.com/sponsors/hexagon" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/openclaw/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/openclaw/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/openclaw/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/openclaw/node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/openclaw/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/openclaw/node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/openclaw/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/openclaw/node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/openclaw/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/openclaw/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/openclaw/node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/openclaw/node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/openclaw/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/openclaw/node_modules/fast-xml-parser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.0.tgz", + "integrity": "sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/openclaw/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/openclaw/node_modules/file-type": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", + "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/openclaw/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/openclaw/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/openclaw/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/openclaw/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/grammy": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.43.0.tgz", + "integrity": "sha512-7dYm06A945mXuIk/5HUlSjeyIYChW8vCEiU2dkOKKqJJzwAWxTkCc91Eqbz7TgODh2rtFFKWI/fekowWHOkmjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@grammyjs/types": "3.27.3", + "abort-controller": "^3.0.0", + "debug": "^4.4.3", + "node-fetch": "^2.7.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "node_modules/openclaw/node_modules/grammy/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/openclaw/node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/openclaw/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/openclaw/node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/openclaw/node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/openclaw/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/openclaw/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/openclaw/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/openclaw/node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/openclaw/node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/openclaw/node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openclaw/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/openclaw/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/openclaw/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/openclaw/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/openclaw/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/openclaw/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/openclaw/node_modules/kysely": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.2.tgz", + "integrity": "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/openclaw/node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/openclaw/node_modules/linkedom": { + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz", + "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.0.0", + "uhyphen": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": ">= 2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/openclaw/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/openclaw/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/openclaw/node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/openclaw/node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/openclaw/node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/openclaw/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/openclaw/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/openclaw/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/openclaw/node_modules/node-domexception": { + "name": "@nolyfill/domexception", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@nolyfill/domexception/-/domexception-1.0.28.tgz", + "integrity": "sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/openclaw/node_modules/node-edge-tts": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/node-edge-tts/-/node-edge-tts-1.2.10.tgz", + "integrity": "sha512-bV2i4XU54D45+US0Zm1HcJRkifuB3W438dWyuJEHLQdKxnuqlI1kim2MOvR6Q3XUQZvfF9PoDyR1Rt7aeXhPdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^7.0.1", + "ws": "^8.13.0", + "yargs": "^17.7.2" + }, + "bin": { + "node-edge-tts": "bin.js" + } + }, + "node_modules/openclaw/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/openclaw/node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/openclaw/node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openclaw/node_modules/openai": { + "version": "6.39.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.0.tgz", + "integrity": "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/openclaw/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openclaw/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/openclaw/node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/pdfjs-dist": { + "version": "5.7.284", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", + "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.100" + } + }, + "node_modules/openclaw/node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/openclaw/node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/openclaw/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/openclaw/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/openclaw/node_modules/protobufjs": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", + "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/openclaw/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/openclaw/node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/openclaw/node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/quickjs-wasi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", + "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/rastermill": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.0.tgz", + "integrity": "sha512-4g2i0I7M5sba//lFBh19Wi0hDGw8o+isnt/BtEyqQXIZaYclhcNBwL/Fw/6gDCp7aaLwQHADuUvyHCB0Oat5Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@silvia-odwyer/photon-node": "0.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/openclaw/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/openclaw/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/openclaw/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/openclaw/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/openclaw/node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/openclaw/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/openclaw/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/openclaw/node_modules/sqlite-vec": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", + "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9" + } + }, + "node_modules/openclaw/node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/openclaw/node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", + "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/openclaw/node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", + "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/openclaw/node_modules/sqlite-vec-linux-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", + "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/openclaw/node_modules/sqlite-vec-windows-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", + "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/openclaw/node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/openclaw/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/openclaw/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/openclaw/node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/openclaw/node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/tokenjuice": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/tokenjuice/-/tokenjuice-0.7.1.tgz", + "integrity": "sha512-eO048hm9UcGHASjYkIWEij8QN68amGp+S1nJyo685qB1/ol+VGEYjPglcVPvCbJbZyFHvI+BBAMvOfnqYCtpsQ==", + "dev": true, + "license": "MIT", + "bin": { + "tokenjuice": "dist/cli/main.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/vincentkoc" + } + }, + "node_modules/openclaw/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/tree-sitter-bash": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", + "integrity": "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/openclaw/node_modules/tslog": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.10.2.tgz", + "integrity": "sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/fullstack-build/tslog?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/openclaw/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/undici": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "bin": { + "web-push": "src/cli.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/openclaw/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/openclaw/node_modules/web-tree-sitter": { + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.9.tgz", + "integrity": "sha512-YJwSHANl6XFgeEjB8nitgj0qZYt5gkIesJ4w2srS2wcLB4GUa4xcOkM0YaMsU6WNR53YVIkDSY7Ej4pf3IXtCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openclaw/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/openclaw/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/openclaw/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/openclaw/node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/openclaw/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/openclaw/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/openclaw/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/openclaw/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/openclaw/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/openclaw/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/openclaw/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" } }, "node_modules/typebox": { diff --git a/test/e2e/fixtures/plugins/weather/package.json b/test/e2e/fixtures/plugins/weather/package.json index af2eced71a..9f0399b2ae 100644 --- a/test/e2e/fixtures/plugins/weather/package.json +++ b/test/e2e/fixtures/plugins/weather/package.json @@ -29,8 +29,12 @@ "typebox": "1.1.38" }, "devDependencies": { + "openclaw": "2026.5.27", "typescript": "5.9.3" }, + "peerDependencies": { + "openclaw": ">=2026.5.17" + }, "engines": { "node": ">=22.16.0" } diff --git a/test/e2e/fixtures/plugins/weather/src/index.ts b/test/e2e/fixtures/plugins/weather/src/index.ts index e9d3318482..0a98845062 100644 --- a/test/e2e/fixtures/plugins/weather/src/index.ts +++ b/test/e2e/fixtures/plugins/weather/src/index.ts @@ -1,22 +1,10 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin"; import { Type } from "typebox"; -type ToolResult = { - content: Array<{ type: "text"; text: string }>; - details: Record; -}; - -type OpenClawPluginApi = { - registerTool(tool: { - name: string; - label: string; - description: string; - parameters: ReturnType; - execute(toolCallId: string, params: Record): Promise; - }): void; -}; +import { WEATHER_FIXTURE_VERSION } from "./version.js"; const WeatherParameters = Type.Object( { @@ -25,26 +13,24 @@ const WeatherParameters = Type.Object( { additionalProperties: false }, ); -const plugin = { +export default defineToolPlugin({ id: "weather", name: "NemoClaw E2E Weather", description: "Registers a deterministic weather tool for custom-image lifecycle tests.", - register(api: OpenClawPluginApi): void { - api.registerTool({ + tools: (tool) => [ + tool({ name: "get_weather", label: "Get Weather", description: "Return deterministic weather data for a location.", parameters: WeatherParameters, - async execute(_toolCallId, params) { - const location = typeof params.location === "string" ? params.location : "unknown"; - const details = { location, condition: "clear", temperatureC: 21 }; + async execute({ location }) { return { - content: [{ type: "text", text: JSON.stringify(details) }], - details, + location, + condition: "clear", + temperatureC: 21, + fixtureVersion: WEATHER_FIXTURE_VERSION, }; }, - }); - }, -}; - -export default plugin; + }), + ], +}); diff --git a/test/e2e/fixtures/plugins/weather/src/version.ts b/test/e2e/fixtures/plugins/weather/src/version.ts new file mode 100644 index 0000000000..a6ae4e134c --- /dev/null +++ b/test/e2e/fixtures/plugins/weather/src/version.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const WEATHER_FIXTURE_VERSION = "v1"; diff --git a/test/e2e/fixtures/plugins/weather/tsconfig.json b/test/e2e/fixtures/plugins/weather/tsconfig.json index f23f9cb5c5..ec4502799d 100644 --- a/test/e2e/fixtures/plugins/weather/tsconfig.json +++ b/test/e2e/fixtures/plugins/weather/tsconfig.json @@ -5,6 +5,7 @@ "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src", + "skipLibCheck": true, "strict": true, "target": "ES2022" }, diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index d41004e299..7e3399f20d 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -22,6 +22,10 @@ import { parseJsonFromText } from "./json-envelope.ts"; const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js"); const CUSTOM_DOCKERFILE = path.join(REPO_ROOT, "Dockerfile.e2e-weather-plugin"); +const CUSTOM_PLUGIN_VERSION_SOURCE = path.join( + REPO_ROOT, + "Dockerfile.e2e-weather-plugin.version.ts", +); const SANDBOX_BASE_IMAGE_REF = "ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71"; const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-plugin-exdev"; const ONBOARD_TIMEOUT_MS = 25 * 60_000; @@ -34,6 +38,7 @@ const EXDEV_PATTERNS = [ /cross-device link not permitted/i, ]; const liveTest = shouldRunLiveE2E() ? test : test.skip; +type WeatherFixtureVersion = "v1" | "v2"; const GATEWAY_CATALOG_CALL_SOURCE = String.raw` import { Buffer } from "node:buffer"; @@ -149,6 +154,14 @@ function patchPoliciesForDevShm(): PolicySourcePatch { }; } +function writeCustomPluginVersion(version: WeatherFixtureVersion): void { + fs.writeFileSync( + CUSTOM_PLUGIN_VERSION_SOURCE, + `// Generated by the OpenClaw plugin lifecycle E2E.\nexport const WEATHER_FIXTURE_VERSION = ${JSON.stringify(version)};\n`, + "utf8", + ); +} + function createCustomPluginDockerfile(): () => void { const sourceDockerfile = path.join(REPO_ROOT, "Dockerfile"); const source = fs.readFileSync(sourceDockerfile, "utf8"); @@ -171,9 +184,11 @@ COPY test/e2e/fixtures/plugins/weather/package.json test/e2e/fixtures/plugins/we RUN npm ci --ignore-scripts --no-audit --no-fund COPY test/e2e/fixtures/plugins/weather/openclaw.plugin.json ./ COPY test/e2e/fixtures/plugins/weather/src/ ./src/ +COPY Dockerfile.e2e-weather-plugin.version.ts ./src/version.ts RUN npm run build \ - && npm prune --omit=dev \ - && sha256sum dist/index.js | cut -d ' ' -f 1 > e2e-weather-plugin.sha256 + && npm prune --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund \ + && test ! -e node_modules/openclaw \ + && sha256sum dist/index.js dist/version.js | sha256sum | cut -d ' ' -f 1 > e2e-weather-plugin.sha256 # Extend the completed managed runtime so its entrypoint, health check, config # generation, and permissions remain the source of truth. @@ -192,7 +207,10 @@ COPY --from=weather-plugin-builder \ /usr/local/share/nemoclaw/e2e-weather-plugin.sha256 USER sandbox -RUN HOME=/sandbox openclaw plugins install /opt/weather-plugin \ +RUN test ! -e /opt/weather-plugin/node_modules/openclaw \ + && HOME=/sandbox openclaw plugins install /opt/weather-plugin \ + && test -L /sandbox/.openclaw/extensions/weather/node_modules/openclaw \ + && test "$(realpath /sandbox/.openclaw/extensions/weather/node_modules/openclaw)" = /usr/local/lib/node_modules/openclaw \ && HOME=/sandbox openclaw plugins enable weather \ && HOME=/sandbox openclaw plugins inspect weather --json > /dev/null @@ -205,8 +223,12 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ && chown sandbox:sandbox /sandbox/.openclaw/.config-hash \ && chmod 660 /sandbox/.openclaw/.config-hash `; + writeCustomPluginVersion("v1"); fs.writeFileSync(CUSTOM_DOCKERFILE, runtime.trimEnd() + extension, "utf8"); - return () => fs.rmSync(CUSTOM_DOCKERFILE, { force: true }); + return () => { + fs.rmSync(CUSTOM_DOCKERFILE, { force: true }); + fs.rmSync(CUSTOM_PLUGIN_VERSION_SOURCE, { force: true }); + }; } type WeatherPluginInspect = { @@ -225,6 +247,7 @@ type GatewayToolInvocation = { type WeatherRuntimeProof = { imageMarker: string; + fixtureVersion: WeatherFixtureVersion; inspectLoaded: boolean; catalogToolIds: string[]; toolInvoked: boolean; @@ -245,14 +268,17 @@ exec node --input-type=module --eval 'await import("data:text/javascript;base64, async function assertWeatherPluginRuntime( sandbox: SandboxClient, phase: string, + expectedFixtureVersion: WeatherFixtureVersion, ): Promise { const imageProbe = await sandbox.execShell( SANDBOX_NAME, trustedSandboxShellScript(`set -eu test -s /tmp/gateway.log test -s /usr/local/share/nemoclaw/e2e-weather-plugin.sha256 +test -L /sandbox/.openclaw/extensions/weather/node_modules/openclaw +test "$(realpath /sandbox/.openclaw/extensions/weather/node_modules/openclaw)" = /usr/local/lib/node_modules/openclaw expected=$(cat /usr/local/share/nemoclaw/e2e-weather-plugin.sha256) -actual=$(sha256sum /sandbox/.openclaw/extensions/weather/dist/index.js | cut -d ' ' -f 1) +actual=$(cd /sandbox/.openclaw/extensions/weather && sha256sum dist/index.js dist/version.js | sha256sum | cut -d ' ' -f 1) [ "$expected" = "$actual" ] printf '%s\\n' "$actual"`), { @@ -307,7 +333,12 @@ printf '%s\\n' "$actual"`), expect(invocation).toMatchObject({ ok: true, result: { - details: { location: "Santa Clara", condition: "clear", temperatureC: 21 }, + details: { + location: "Santa Clara", + condition: "clear", + temperatureC: 21, + fixtureVersion: expectedFixtureVersion, + }, }, }); @@ -330,7 +361,13 @@ printf '%s\\n' "$actual"`), (group.tools ?? []).map((tool) => tool.id).filter((id): id is string => typeof id === "string"), ); expect(catalogToolIds).toContain("get_weather"); - return { imageMarker: imageMarker ?? "", inspectLoaded: true, catalogToolIds, toolInvoked: true }; + return { + imageMarker: imageMarker ?? "", + fixtureVersion: expectedFixtureVersion, + inspectLoaded: true, + catalogToolIds, + toolInvoked: true, + }; } const runtimeDepsReplacementProbeSource = `set -eu @@ -420,8 +457,9 @@ liveTest( regressionTargets: ["#6108", "#3513", "#3127"], contract: [ "fresh OpenClaw sandbox onboards from a full managed custom-plugin Dockerfile", + "release-matched peer/dev dependencies prune private OpenClaw and link the host runtime", "gateway log, runtime inspection, tools.catalog, and tools.invoke prove weather/get_weather", - "custom-plugin image provenance and the gateway tool survive restart and rebuild", + "custom-plugin v1 survives restart and a rebuilt v2 replaces it without backup rollback", "sandbox proves /dev/shm and plugin-runtime-deps are distinct devices", "legacy source-side staging fails with EXDEV across the same /dev/shm to plugin-runtime-deps boundary", "OpenClaw-style target-side plugin runtime-deps replacement completes without EXDEV", @@ -522,7 +560,7 @@ liveTest( expect(onboardText).toMatch(/Creating sandbox|Sandbox '.+' created/); expect(onboardText).toContain("Deployment verified"); - const weatherAfterOnboard = await assertWeatherPluginRuntime(sandbox, "after-onboard"); + const weatherAfterOnboard = await assertWeatherPluginRuntime(sandbox, "after-onboard", "v1"); const restart = await host.command( "node", @@ -534,17 +572,21 @@ liveTest( }, ); expect(restart.exitCode, resultText(restart)).toBe(0); - const weatherAfterRestart = await assertWeatherPluginRuntime(sandbox, "after-restart"); + const weatherAfterRestart = await assertWeatherPluginRuntime(sandbox, "after-restart", "v1"); expect(weatherAfterRestart.imageMarker).toBe(weatherAfterOnboard.imageMarker); + // Change an actual build-context input so rebuild must produce a distinct + // plugin artifact. Restore must preserve that fresh image-managed v2 + // extension instead of replacing it with the backed-up v1 directory. + writeCustomPluginVersion("v2"); const rebuild = await host.command("node", [CLI_ENTRYPOINT, SANDBOX_NAME, "rebuild", "--yes"], { artifactName: "openclaw-weather-plugin-rebuild", env: sandboxEnv, timeoutMs: REBUILD_TIMEOUT_MS, }); expect(rebuild.exitCode, resultText(rebuild)).toBe(0); - const weatherAfterRebuild = await assertWeatherPluginRuntime(sandbox, "after-rebuild"); - expect(weatherAfterRebuild.imageMarker).toBe(weatherAfterOnboard.imageMarker); + const weatherAfterRebuild = await assertWeatherPluginRuntime(sandbox, "after-rebuild", "v2"); + expect(weatherAfterRebuild.imageMarker).not.toBe(weatherAfterOnboard.imageMarker); const df = await sandbox.execShell( SANDBOX_NAME, @@ -599,9 +641,13 @@ liveTest( weatherAfterRebuild.inspectLoaded && weatherAfterRebuild.catalogToolIds.includes("get_weather") && weatherAfterRebuild.toolInvoked, - imageMarkerStable: + v1MarkerStableThroughRestart: weatherAfterOnboard.imageMarker === weatherAfterRestart.imageMarker && - weatherAfterRestart.imageMarker === weatherAfterRebuild.imageMarker, + weatherAfterOnboard.fixtureVersion === "v1" && + weatherAfterRestart.fixtureVersion === "v1", + rebuiltV2ReplacedV1: + weatherAfterRebuild.imageMarker !== weatherAfterOnboard.imageMarker && + weatherAfterRebuild.fixtureVersion === "v2", distinctDevices: /source_device=\d+ target_device=\d+/.test(probeText), sourceSideExdevSelfCheck: probeText.includes( "source-side staging failure self-check completed", diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts index f23985b5c6..00add11c38 100644 --- a/test/snapshot.test.ts +++ b/test/snapshot.test.ts @@ -599,36 +599,38 @@ process.exit(0); } }); - it("preserves fresh image-managed OpenClaw extensions while restoring user extensions", () => { + it("preserves fresh custom image-managed OpenClaw extensions while restoring user extensions", () => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-extension-restore-")); const oldPath = process.env.PATH; const oldOpenshell = process.env.NEMOCLAW_OPENSHELL_BIN; try { const binDir = path.join(fixture, "bin"); const openclawDir = path.join(fixture, "sandbox-root", ".openclaw"); - const sshLog = path.join(fixture, "ssh-log.jsonl"); + const freshRegistryPath = path.join(fixture, "fresh-installs.json"); const extensionsDir = path.join(openclawDir, "extensions"); fs.mkdirSync(binDir, { recursive: true }); - fs.mkdirSync(path.join(extensionsDir, "nemoclaw"), { recursive: true }); - fs.mkdirSync(path.join(extensionsDir, "openclaw-weixin"), { recursive: true }); + fs.mkdirSync(path.join(extensionsDir, "weather"), { recursive: true }); fs.mkdirSync(path.join(extensionsDir, "stale-user-extension"), { recursive: true }); - fs.writeFileSync(path.join(extensionsDir, "nemoclaw", "marker.txt"), "fresh-nemoclaw\n"); - fs.writeFileSync(path.join(extensionsDir, "openclaw-weixin", "marker.txt"), "fresh-weixin\n"); + fs.writeFileSync(path.join(extensionsDir, "weather", "marker.txt"), "fresh-weather-v2\n"); fs.writeFileSync(path.join(extensionsDir, "stale-user-extension", "marker.txt"), "stale\n"); + fs.writeFileSync( + freshRegistryPath, + JSON.stringify({ + version: 1, + installRecords: { + weather: { installPath: "/sandbox/.openclaw/extensions/weather" }, + }, + }), + ); const manifest = writeBackup("alpha", "2026-05-19T12-00-00-000Z", { stateDirs: ["extensions"], backedUpDirs: ["extensions"], }); const backupExtensionsDir = path.join(String(manifest.backupPath), "extensions"); - fs.mkdirSync(path.join(backupExtensionsDir, "nemoclaw"), { recursive: true }); - fs.mkdirSync(path.join(backupExtensionsDir, "openclaw-weixin"), { recursive: true }); + fs.mkdirSync(path.join(backupExtensionsDir, "weather"), { recursive: true }); fs.mkdirSync(path.join(backupExtensionsDir, "user-extension"), { recursive: true }); - fs.writeFileSync(path.join(backupExtensionsDir, "nemoclaw", "marker.txt"), "old-nemoclaw\n"); - fs.writeFileSync( - path.join(backupExtensionsDir, "openclaw-weixin", "marker.txt"), - "old-weixin\n", - ); + fs.writeFileSync(path.join(backupExtensionsDir, "weather", "marker.txt"), "old-weather-v1\n"); fs.writeFileSync( path.join(backupExtensionsDir, "user-extension", "marker.txt"), "restored\n", @@ -642,7 +644,6 @@ const fs = require("node:fs"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); const cmd = process.argv[process.argv.length - 1] || ""; -fs.appendFileSync(${JSON.stringify(sshLog)}, JSON.stringify({ cmd }) + "\\n"); function readStdin() { const chunks = []; for (;;) { @@ -653,11 +654,12 @@ function readStdin() { } return Buffer.concat(chunks); } +if (cmd.includes("plugins/installs.json") && cmd.includes("cat --")) { process.stdout.write(fs.readFileSync(${JSON.stringify(freshRegistryPath)})); process.exit(0); } if (cmd.includes("/sandbox/.openclaw/extensions") && cmd.includes("-exec rm -rf")) { const extensionsDir = ${JSON.stringify(extensionsDir)}; fs.mkdirSync(extensionsDir, { recursive: true }); for (const entry of fs.readdirSync(extensionsDir)) { - if (entry === "nemoclaw" || entry === "openclaw-weixin") continue; + if (entry === "weather") continue; fs.rmSync(path.join(extensionsDir, entry), { recursive: true, force: true }); } process.exit(0); @@ -682,31 +684,30 @@ process.exit(0); process.env.NEMOCLAW_OPENSHELL_BIN = openshell; process.env.PATH = `${binDir}:${oldPath || ""}`; - const restore = sandboxState.restoreSandboxState("alpha", String(manifest.backupPath)); + const restore = sandboxState.restoreSandboxState("alpha", String(manifest.backupPath), { + preserveFreshOpenClawPluginInstalls: true, + }); expect(restore.success).toBe(true); expect(restore.restoredDirs).toEqual(["extensions"]); - expect(fs.readFileSync(path.join(extensionsDir, "nemoclaw", "marker.txt"), "utf-8")).toBe( - "fresh-nemoclaw\n", + expect(fs.readFileSync(path.join(extensionsDir, "weather", "marker.txt"), "utf-8")).toBe( + "fresh-weather-v2\n", ); - expect( - fs.readFileSync(path.join(extensionsDir, "openclaw-weixin", "marker.txt"), "utf-8"), - ).toBe("fresh-weixin\n"); expect(fs.existsSync(path.join(extensionsDir, "stale-user-extension"))).toBe(false); expect( fs.readFileSync(path.join(extensionsDir, "user-extension", "marker.txt"), "utf-8"), ).toBe("restored\n"); - const loggedCommands = fs - .readFileSync(sshLog, "utf-8") - .trim() - .split("\n") - .map((line) => JSON.parse(line).cmd as string); - const cleanupCommand = loggedCommands.find((cmd) => - cmd.includes("/sandbox/.openclaw/extensions"), + fs.writeFileSync( + freshRegistryPath, + '{"version":1,"installRecords":{"../weather":{"installPath":"/sandbox/.openclaw/extensions/../weather"}}}', + ); + const rejected = sandboxState.restoreSandboxState("alpha", String(manifest.backupPath), { + preserveFreshOpenClawPluginInstalls: true, + }); + expect(rejected.success).toBe(false); + expect(fs.readFileSync(path.join(extensionsDir, "weather", "marker.txt"), "utf-8")).toBe( + "fresh-weather-v2\n", ); - expect(cleanupCommand).not.toContain("rm -rf -- /sandbox/.openclaw/extensions"); - expect(cleanupCommand).toContain("! -name 'nemoclaw'"); - expect(cleanupCommand).toContain("! -name 'openclaw-weixin'"); } finally { if (oldOpenshell === undefined) { delete process.env.NEMOCLAW_OPENSHELL_BIN; diff --git a/tsconfig.cli.json b/tsconfig.cli.json index 94c4ddfc81..957a1bee49 100644 --- a/tsconfig.cli.json +++ b/tsconfig.cli.json @@ -17,5 +17,5 @@ "types": ["node"] }, "include": ["agents/hermes/**/*.ts", "bin/**/*.ts", "scripts/**/*.ts", "scripts/**/*.mts", "src/**/*.ts", "test/**/*.ts", "tools/**/*.ts", "tools/**/*.mts", "nemoclaw-blueprint/scripts/**/*.ts"], - "exclude": ["node_modules", "nemoclaw"] + "exclude": ["node_modules", "nemoclaw", "test/e2e/fixtures/plugins/weather"] } From 4e9471b1c9cc9f7f77ce0cf153ef5e6d0a8bfa97 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 13:38:56 -0700 Subject: [PATCH 12/46] test: keep plugin restore cases linear Signed-off-by: Aaron Erickson --- src/lib/state/openclaw-plugin-restore.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts index 56ae0be6b7..a8a4100c45 100644 --- a/src/lib/state/openclaw-plugin-restore.test.ts +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -55,8 +55,12 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { { version: 1, installRecords: installs }, OPENCLAW_DIR, ); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toMatch(/unsafe|invalid/); + expect(result).toEqual( + expect.objectContaining({ + ok: false, + error: expect.stringMatching(/unsafe|invalid/), + }), + ); }); it("rejects an unbounded install set before constructing restore commands", () => { From 4d140d9408cc490e234ff447d7af94f90acddaa6 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 13:49:44 -0700 Subject: [PATCH 13/46] test: update DCode rebuild restore contract Signed-off-by: Aaron Erickson --- src/lib/actions/sandbox/rebuild-dcode-flow.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts b/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts index 84ecbc9492..0171758807 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts @@ -467,6 +467,7 @@ describe("rebuildSandbox DCode flow", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", recoveryManifest.backupPath, + { preserveFreshOpenClawPluginInstalls: true }, ); }); }); From ff2f9ef28385da60b579e9990323627e00c152c5 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 14:22:10 -0700 Subject: [PATCH 14/46] fix: allow validated OpenClaw peer links Signed-off-by: Aaron Erickson --- src/lib/state/openclaw-plugin-restore.ts | 2 +- src/lib/state/sandbox.ts | 31 +++++++++++------ test/security-sandbox-tar-traversal.test.ts | 38 ++++++++++++++------- test/snapshot.test.ts | 16 ++++----- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/lib/state/openclaw-plugin-restore.ts b/src/lib/state/openclaw-plugin-restore.ts index fb3b9cabb5..675da48ddf 100644 --- a/src/lib/state/openclaw-plugin-restore.ts +++ b/src/lib/state/openclaw-plugin-restore.ts @@ -19,7 +19,7 @@ function isSafeOpenClawPluginInstallId(id: string): boolean { return id.startsWith("@") && slash > 1 && slash === id.lastIndexOf("/") && slash < id.length - 1; } -function isSafeOpenClawExtensionDirName(name: string): boolean { +export function isSafeOpenClawExtensionDirName(name: string): boolean { return ( name.length > 0 && name.length <= 128 && diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 6e4811c83b..5d224f2ac4 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -39,6 +39,7 @@ import { shouldMergeOpenClawConfigStateFile, } from "./openclaw-config-restore-input.js"; import { + isSafeOpenClawExtensionDirName, type OpenClawManagedExtensionDiscoveryResult, parseFreshOpenClawPluginExtensionDirs, } from "./openclaw-plugin-restore.js"; @@ -344,13 +345,11 @@ function auditExtractedSymlinks(dirPath: string, allowedRoots: string[]): string if (stat.isSymbolicLink()) { const linkTarget = readlinkSync(fullPath); - // Whitelisted npm symlinks baked into the base image at build time - // (see AUDIT_SYMLINK_WHITELIST). Accepting them here matches the - // pre-backup audit so legitimate plugin installs in extensions/ - // can survive a rebuild without tripping the post-extraction check. - // Match both the source path AND the link target — a whitelisted - // path with a tampered target falls through to the normal - // containment check. + // Allowed npm symlinks baked into managed or custom images. The + // shared matcher checks both source shape and exact target so the + // pre-backup and post-extraction audits enforce the same contract. + // A recognized path with a tampered target falls through to the + // normal containment check. const relFromDir = path.relative(dirPath, fullPath).split(path.sep).join("/"); if (isAllowedStateSymlink(relFromDir, linkTarget)) { continue; @@ -593,6 +592,11 @@ const AUDIT_SYMLINK_WHITELIST: ReadonlyMap = new Map([ ]); const EXTENSION_NPM_BIN_RE = /^extensions\/[^/]+\/node_modules\/\.bin\/[^/]+$/; +// `openclaw plugins install` links a declared OpenClaw peer dependency to the +// image's global runtime. Accept that link only for a direct, validated +// extension directory and the exact runtime path used by the stock image. +const EXTENSION_OPENCLAW_PEER_RE = /^extensions\/([^/]+)\/node_modules\/openclaw$/; +const OPENCLAW_GLOBAL_INSTALL = "/usr/local/lib/node_modules/openclaw"; const OPENCLAW_BUILTIN_IMAGE_MANAGED_EXTENSION_DIRS = ["nemoclaw", "openclaw-weixin"] as const; function isAllowedExtensionNpmBinSymlink(relPath: string, linkTarget: string): boolean { @@ -614,9 +618,16 @@ function isAllowedExtensionNpmBinSymlink(relPath: string, linkTarget: string): b } function isAllowedStateSymlink(relPath: string, linkTarget: string): boolean { - const exactTarget = AUDIT_SYMLINK_WHITELIST.get(relPath.split(path.sep).join("/")); + const normalizedRelPath = relPath.split(path.sep).join("/"); + const exactTarget = AUDIT_SYMLINK_WHITELIST.get(normalizedRelPath); if (exactTarget !== undefined) return exactTarget === linkTarget; - return isAllowedExtensionNpmBinSymlink(relPath, linkTarget); + const peerMatch = EXTENSION_OPENCLAW_PEER_RE.exec(normalizedRelPath); + return ( + (peerMatch !== null && + isSafeOpenClawExtensionDirName(peerMatch[1]) && + linkTarget === OPENCLAW_GLOBAL_INSTALL) || + isAllowedExtensionNpmBinSymlink(normalizedRelPath, linkTarget) + ); } function _log(msg: string): void { @@ -1337,7 +1348,7 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions = } if (whitelisted.length > 0) { _log( - `Pre-backup audit whitelisted ${whitelisted.length} entries (base-image npm symlinks): ${whitelisted.slice(0, 5).join("; ")}`, + `Pre-backup audit whitelisted ${whitelisted.length} entries (image npm symlinks): ${whitelisted.slice(0, 5).join("; ")}`, ); } if (violations.length > 0) { diff --git a/test/security-sandbox-tar-traversal.test.ts b/test/security-sandbox-tar-traversal.test.ts index 12c8308ae5..f43ed5e19f 100644 --- a/test/security-sandbox-tar-traversal.test.ts +++ b/test/security-sandbox-tar-traversal.test.ts @@ -438,19 +438,19 @@ describe("Fix: safeTarExtract blocks malicious archives and extracts safe ones", } }); - it("allows whitelisted npm symlinks baked into base image (extensions/openclaw-weixin/node_modules/openclaw)", async () => { + it("allows a generic extension peer link to the exact global OpenClaw install", async () => { const { safeTarExtract } = await loadSandboxState(); const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-audit-whitelist-extract-")); try { const targetDir = path.join(workDir, "backup"); fs.mkdirSync(targetDir, { recursive: true }); - // The WeChat plugin install symlinks `node_modules/openclaw` to the - // global npm install. Target escapes both the archive and /sandbox/, - // so it would be rejected without the whitelist. + // OpenClaw installs plugin peers as a link to the global npm install. + // The target escapes both the archive and /sandbox/, so only this exact + // direct extension peer shape and target are allowed. const tar = buildTar([ { - path: "extensions/openclaw-weixin/node_modules/openclaw", + path: "extensions/weather/node_modules/openclaw", type: "2", linkTarget: "/usr/local/lib/node_modules/openclaw", }, @@ -463,12 +463,24 @@ describe("Fix: safeTarExtract blocks malicious archives and extracts safe ones", } }); - it("rejects whitelisted source path when the symlink target is tampered", async () => { - // The path matches AUDIT_SYMLINK_WHITELIST, but the linkTarget points to - // /etc/passwd instead of the expected /usr/local/lib/node_modules/openclaw. - // Source-only matching would let a compromised sandbox repoint a known npm - // symlink at arbitrary host paths; the post-extraction audit must compare - // both fields. + it.each([ + ["a tampered target", "extensions/weather/node_modules/openclaw", "/etc/passwd"], + [ + "a glob basename", + "extensions/*/node_modules/openclaw", + "/usr/local/lib/node_modules/openclaw", + ], + [ + "a nested extension path", + "extensions/nested/weather/node_modules/openclaw", + "/usr/local/lib/node_modules/openclaw", + ], + [ + "a noncanonical target", + "extensions/weather/node_modules/openclaw", + "/usr/local/lib/node_modules/openclaw/", + ], + ])("rejects a generic extension peer link with %s", async (_case, source, target) => { const { safeTarExtract } = await loadSandboxState(); const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-audit-target-tampered-")); try { @@ -477,9 +489,9 @@ describe("Fix: safeTarExtract blocks malicious archives and extracts safe ones", const tar = buildTar([ { - path: "extensions/openclaw-weixin/node_modules/openclaw", + path: source, type: "2", - linkTarget: "/etc/passwd", + linkTarget: target, }, ]); diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts index 1e17064fae..c1cd893349 100644 --- a/test/snapshot.test.ts +++ b/test/snapshot.test.ts @@ -719,7 +719,7 @@ process.exit(0); } }); - it("accepts whitelisted npm symlinks under extensions/ during pre-backup audit", () => { + it("accepts generic OpenClaw peer links during the pre-backup audit", () => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-audit-whitelist-")); const oldPath = process.env.PATH; const oldOpenshell = process.env.NEMOCLAW_OPENSHELL_BIN; @@ -732,7 +732,7 @@ process.exit(0); const auditLines = [ "l\t/sandbox/.openclaw/extensions/openclaw-weixin/node_modules/.bin/qrcode-terminal\t../qrcode-terminal/bin/qrcode-terminal.js", - "l\t/sandbox/.openclaw/extensions/openclaw-weixin/node_modules/openclaw\t/usr/local/lib/node_modules/openclaw", + "l\t/sandbox/.openclaw/extensions/weather/node_modules/openclaw\t/usr/local/lib/node_modules/openclaw", ].join("\n"); const openshell = writeFakeOpenshell(binDir); @@ -949,11 +949,9 @@ process.exit(0); } }); - it("rejects whitelisted-path symlinks with a tampered target", () => { - // Source path matches the whitelist, but linkTarget points to /etc/passwd - // instead of the expected /usr/local/lib/node_modules/openclaw. The audit - // must compare both fields and reject — source-only matching would let a - // compromised agent repoint these symlinks at arbitrary host paths. + it("rejects a generic OpenClaw peer link with a tampered target", () => { + // The generic peer path is valid, but its target must remain the exact + // global OpenClaw install rather than an arbitrary absolute path. const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-audit-target-tampered-")); const oldPath = process.env.PATH; const oldOpenshell = process.env.NEMOCLAW_OPENSHELL_BIN; @@ -965,7 +963,7 @@ process.exit(0); for (const d of existingDirs) fs.mkdirSync(path.join(openclawDir, d), { recursive: true }); const auditLines = [ - "l\t/sandbox/.openclaw/extensions/openclaw-weixin/node_modules/openclaw\t/etc/passwd", + "l\t/sandbox/.openclaw/extensions/weather/node_modules/openclaw\t/etc/passwd", ].join("\n"); const openshell = writeFakeOpenshell(binDir); @@ -992,7 +990,7 @@ process.exit(0); const backup = sandboxState.backupSandboxState("alpha"); expect(backup.success).toBe(false); - expect(backup.error).toMatch(/openclaw-weixin/); + expect(backup.error).toMatch(/weather/); expect(backup.error).toMatch(/\/etc\/passwd/); } finally { if (oldOpenshell === undefined) { From 29e2c1315fc0e882c45e72a6dc0511cb32f99821 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 3 Jul 2026 15:36:30 -0700 Subject: [PATCH 15/46] fix: tolerate rebuild version cache updates Signed-off-by: Aaron Erickson --- .../sandbox/rebuild-dcode-flow.test.ts | 11 +-- .../sandbox/rebuild-preflight-guards.ts | 20 ++++++ .../sandbox/rebuild-preflight-phase.ts | 47 +++++++------ test/helpers/rebuild-flow-lifecycle-cases.ts | 70 +++++++++++++++++++ test/helpers/rebuild-flow-test-harness.ts | 30 +++++--- test/helpers/rebuild-flow-test-support.ts | 4 ++ 6 files changed, 148 insertions(+), 34 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts b/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts index 0171758807..ee6ad36d91 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-flow.test.ts @@ -184,8 +184,9 @@ describe("rebuildSandbox DCode flow", () => { sandboxEntry: originalEntry, sandboxEntryReads: [ originalEntry, // Initial rebuild target. - originalEntry, // Messaging-conflict gateway selection (#5954). - originalEntry, // Prepared DCode target capture. + originalEntry, // Exact post-confirmation lock guard. + originalEntry, // Messaging config hydration. + originalEntry, // Messaging-conflict gateway lookup (#5954). driftedEntry, // Final pre-backup target verification. ], dcodeRouteResults: [{ ok: true }, { ok: true }], @@ -235,10 +236,10 @@ describe("rebuildSandbox DCode flow", () => { sandboxEntry: originalEntry, sandboxEntryReads: [ originalEntry, // Initial rebuild target. - originalEntry, // Messaging-conflict gateway selection (#5954). - originalEntry, // Prepared DCode target capture. + originalEntry, // Exact post-confirmation lock guard. + originalEntry, // Messaging config hydration. + originalEntry, // Messaging-conflict gateway lookup (#5954). originalEntry, // Final pre-backup target verification. - originalEntry, // Delete-edge target verification input. driftedEntry, // Registry reread at the destructive boundary. ], dcodeRouteResults: [{ ok: true }, { ok: true }, { ok: true }], diff --git a/src/lib/actions/sandbox/rebuild-preflight-guards.ts b/src/lib/actions/sandbox/rebuild-preflight-guards.ts index 11037a9a77..a7fac73bbb 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-guards.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-guards.ts @@ -11,6 +11,7 @@ import * as onboardSession from "../../state/onboard-session"; import * as registry from "../../state/registry"; import type { RebuildBail } from "./rebuild-credential-preflight"; import type { RebuildSandboxEntry } from "./rebuild-flow-helpers"; +import type { RebuildVersionCheck } from "./rebuild-preflight-confirmation"; import { printRebuildPreflightFailure } from "./rebuild-preflight-error"; export function checkRebuildGatewaySchemaPreflight( @@ -78,6 +79,25 @@ export function acquireRebuildOnboardLock(sandboxName: string, bail: RebuildBail return release; } +/** + * Derive the only registry transition rebuild itself can cause before locking. + * Keep the pre-confirmation entry intact except for a successful live probe's + * agent-version cache write, so every concurrent recreate/config change still + * fails the exact locked-entry comparison below. + */ +export function expectedRebuildEntryAfterVersionCheck( + confirmedEntry: RebuildSandboxEntry, + confirmedEntrySnapshot: string, + versionCheck: RebuildVersionCheck, +): RebuildSandboxEntry { + if (versionCheck.detectionMethod !== "ssh-exec" || versionCheck.sandboxVersion === null) { + return confirmedEntry; + } + const expectedEntry = JSON.parse(confirmedEntrySnapshot) as RebuildSandboxEntry; + expectedEntry.agentVersion = versionCheck.sandboxVersion; + return expectedEntry; +} + export function assertRebuildEntryUnchanged( sandboxName: string, confirmedEntrySnapshot: string, diff --git a/src/lib/actions/sandbox/rebuild-preflight-phase.ts b/src/lib/actions/sandbox/rebuild-preflight-phase.ts index 3335fbcdc9..b41717e626 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-phase.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-phase.ts @@ -32,6 +32,7 @@ import { acquireRebuildOnboardLock, assertRebuildEntryUnchanged, checkRebuildGatewaySchemaPreflight, + expectedRebuildEntryAfterVersionCheck, getRebuildSandboxEntryOrBail, isSingleAgentRebuildSupported, } from "./rebuild-preflight-guards"; @@ -85,15 +86,34 @@ export async function runRebuildPreflightPhase( const rebuildAgent = sandboxEntry.agent || null; const agentName = getRebuildAgentDisplayName(sandboxName); + if ( + !isDcodeRebuildAgent(rebuildAgent) && + !checkRebuildGatewaySchemaPreflight(sandboxName, sandboxEntry, bail) + ) { + return null; + } + const versionCheck = await confirmRebuildIntent( + sandboxName, + agentName, + skipConfirm, + activeSessionCount, + bail, + ); + if (!versionCheck) return null; + const expectedSandboxEntry = expectedRebuildEntryAfterVersionCheck( + sandboxEntry, + confirmedEntrySnapshot, + versionCheck, + ); const dcodePreflight = createDcodeRebuildOrchestrator({ sandboxName, - entry: sandboxEntry, + entry: expectedSandboxEntry, rebuildAgent, log, bail, deps: { checkGatewaySchema: (name, scopedBail) => - checkRebuildGatewaySchemaPreflight(name, sandboxEntry, scopedBail), + checkRebuildGatewaySchemaPreflight(name, expectedSandboxEntry, scopedBail), preflightCredentials: (_name, entry, scopedLog, scopedBail) => preflightRebuildCredentials(entry, scopedLog, scopedBail), // Non-DCode rebuilds stay on the existing typed base-image preflight. @@ -103,28 +123,13 @@ export async function runRebuildPreflightPhase( }); let retainDcodePreflight = false; try { - if ( - !isDcodeRebuildAgent(rebuildAgent) && - !checkRebuildGatewaySchemaPreflight(sandboxName, sandboxEntry, bail) - ) { - return null; - } - const versionCheck = await confirmRebuildIntent( - sandboxName, - agentName, - skipConfirm, - activeSessionCount, - bail, - ); - if (!versionCheck) return null; - const releaseOnboardLock = acquireRebuildOnboardLock(sandboxName, bail); let retainOnboardLock = false; try { - assertRebuildEntryUnchanged(sandboxName, confirmedEntrySnapshot, bail); + assertRebuildEntryUnchanged(sandboxName, JSON.stringify(expectedSandboxEntry), bail); const preparedTarget = await prepareRebuildTargetPreflights({ sandboxName, - sandboxEntry, + sandboxEntry: expectedSandboxEntry, rebuildAgent, // Reaching this point means either --yes was supplied or confirmation // succeeded, matching the previous `skipConfirm || confirmed` contract. @@ -134,7 +139,7 @@ export async function runRebuildPreflightPhase( }); if (!preparedTarget) return null; - const liveState = await resolveRebuildLiveState(sandboxName, sandboxEntry, log, bail); + const liveState = await resolveRebuildLiveState(sandboxName, expectedSandboxEntry, log, bail); if (!liveState) return null; if (isDcodeRebuildAgent(rebuildAgent)) { const recoveryRecreate = liveState.staleRecovery || recoveryManifest !== null; @@ -149,7 +154,7 @@ export async function runRebuildPreflightPhase( retainOnboardLock = true; retainDcodePreflight = true; return { - sandboxEntry, + sandboxEntry: expectedSandboxEntry, rebuildAgent, versionCheck, ...preparedTarget, diff --git a/test/helpers/rebuild-flow-lifecycle-cases.ts b/test/helpers/rebuild-flow-lifecycle-cases.ts index 91b6bb5d39..01db479445 100644 --- a/test/helpers/rebuild-flow-lifecycle-cases.ts +++ b/test/helpers/rebuild-flow-lifecycle-cases.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from "vitest"; +import { makePreparedRecoveryManifest } from "../../src/lib/actions/sandbox/rebuild-flow-test-fixtures"; import { createRebuildFlowHarness, installRebuildFlowTestHooks, @@ -104,6 +105,75 @@ export function registerRebuildFlowLifecycleTests(): void { ); }); + it("accepts the agent version cached by the confirmation probe before lock acquisition", async () => { + const harness = createRebuildFlowHarness({ + sandboxEntry: { agentVersion: null }, + entryUpdatesAfterVersionCheck: { agentVersion: "0.2.0" }, + versionCheck: { + sandboxVersion: "0.2.0", + expectedVersion: "0.2.0", + isStale: false, + verificationFailed: false, + detectionMethod: "ssh-exec", + }, + }); + + await expect( + harness.rebuildSandbox("alpha", ["--yes"], { + throwOnError: true, + recoveryManifest: makePreparedRecoveryManifest(), + }), + ).resolves.toBeUndefined(); + + expect(harness.backupSandboxStateSpy).not.toHaveBeenCalled(); + expect(harness.onboardSpy).toHaveBeenCalledOnce(); + }); + + it("rejects an agent version cache value that differs from the probe result", async () => { + const harness = createRebuildFlowHarness({ + sandboxEntry: { agentVersion: null }, + entryUpdatesAfterVersionCheck: { agentVersion: "unexpected-version" }, + versionCheck: { + sandboxVersion: "0.2.0", + expectedVersion: "0.2.0", + isStale: false, + verificationFailed: false, + detectionMethod: "ssh-exec", + }, + }); + + await expect( + harness.rebuildSandbox("alpha", ["--yes"], { throwOnError: true }), + ).rejects.toThrow("Sandbox configuration changed before rebuild lock acquisition"); + + expect(harness.backupSandboxStateSpy).not.toHaveBeenCalled(); + expect(harness.onboardSpy).not.toHaveBeenCalled(); + }); + + it("rejects real registry drift alongside the confirmation probe cache write", async () => { + const harness = createRebuildFlowHarness({ + sandboxEntry: { agentVersion: null }, + entryUpdatesAfterVersionCheck: { + agentVersion: "0.2.0", + model: "changed-during-confirmation", + }, + versionCheck: { + sandboxVersion: "0.2.0", + expectedVersion: "0.2.0", + isStale: false, + verificationFailed: false, + detectionMethod: "ssh-exec", + }, + }); + + await expect( + harness.rebuildSandbox("alpha", ["--yes"], { throwOnError: true }), + ).rejects.toThrow("Sandbox configuration changed before rebuild lock acquisition"); + + expect(harness.backupSandboxStateSpy).not.toHaveBeenCalled(); + expect(harness.onboardSpy).not.toHaveBeenCalled(); + }); + it("relocks as absent when registry cleanup throws after confirmed delete", async () => { const harness = createRebuildFlowHarness({ removeSandboxRegistryEntry: () => { diff --git a/test/helpers/rebuild-flow-test-harness.ts b/test/helpers/rebuild-flow-test-harness.ts index 002b09aac8..24ea00f69c 100644 --- a/test/helpers/rebuild-flow-test-harness.ts +++ b/test/helpers/rebuild-flow-test-harness.ts @@ -113,7 +113,7 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): vi.spyOn(onboardSession, "acquireOnboardLock").mockReturnValue({ acquired: true }); const markStepFailedSpy = installTerminalStepFailureMock(onboardSession, session); session.sandboxName = overrides.sessionSandboxName ?? session.sandboxName; - const sandboxEntry = { + const currentSandboxEntry = { name: "alpha", provider: "ollama-local", model: "nvidia/nemotron", @@ -127,7 +127,8 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): gatewayPort: 8080, ...(overrides.sandboxEntry ?? {}), }; - vi.spyOn(registry, "getSandbox").mockReturnValue(sandboxEntry); + const readCurrentSandboxEntry = () => structuredClone(currentSandboxEntry); + vi.spyOn(registry, "getSandbox").mockImplementation(readCurrentSandboxEntry); vi.spyOn(registry, "getDefault").mockReturnValue(overrides.defaultSandbox ?? null); let registryLoadCount = 0; vi.spyOn(registry, "load").mockImplementation(() => { @@ -142,14 +143,19 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): sandboxes: { alpha: isPreDeleteRead && overrides.preDeleteSandboxEntry - ? overrides.preDeleteSandboxEntry - : sandboxEntry, + ? structuredClone(overrides.preDeleteSandboxEntry) + : readCurrentSandboxEntry(), }, defaultSandbox, }; }); vi.spyOn(registry, "listSandboxes").mockReturnValue({ sandboxes: [] }); - const registryUpdateSpy = vi.spyOn(registry, "updateSandbox").mockReturnValue(true); + const registryUpdateSpy = vi + .spyOn(registry, "updateSandbox") + .mockImplementation((_name, updates) => { + Object.assign(currentSandboxEntry, updates); + return true; + }); const restoreSandboxEntrySpy = vi .spyOn(registry, "restoreSandboxEntry") .mockImplementation(() => undefined); @@ -157,9 +163,17 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): detected: false, sessions: [], }); - vi.spyOn(sandboxVersion, "checkAgentVersion").mockReturnValue({ - expectedVersion: "0.2.0", - sandboxVersion: "0.1.0", + vi.spyOn(sandboxVersion, "checkAgentVersion").mockImplementation(() => { + Object.assign(currentSandboxEntry, overrides.entryUpdatesAfterVersionCheck ?? {}); + return ( + overrides.versionCheck ?? { + expectedVersion: "0.2.0", + sandboxVersion: "0.1.0", + isStale: true, + verificationFailed: false, + detectionMethod: "registry", + } + ); }); vi.spyOn(rebuildShields, "openRebuildShieldsWindow").mockReturnValue(rebuildShieldsWindow); const relockSpy = vi diff --git a/test/helpers/rebuild-flow-test-support.ts b/test/helpers/rebuild-flow-test-support.ts index 638820299a..5309f0a125 100644 --- a/test/helpers/rebuild-flow-test-support.ts +++ b/test/helpers/rebuild-flow-test-support.ts @@ -3,6 +3,8 @@ import { type MockInstance, vi } from "vitest"; +import type { VersionCheckResult } from "../../src/lib/sandbox/version"; + export type RebuildSandbox = typeof import("../../src/lib/actions/sandbox/rebuild")["rebuildSandbox"]; export type RebuildFlowStep = { @@ -24,6 +26,7 @@ export type RebuildFlowSession = Record & { steps: Record; }; export type RebuildFlowOverrides = { + entryUpdatesAfterVersionCheck?: Record; applyPreset?: (presetName: string) => boolean; baseImagePreflight?: { ok: boolean; @@ -72,6 +75,7 @@ export type RebuildFlowOverrides = { ensureValidatedWebSearchCredential?: () => Promise; hermesCredentialKeys?: string[] | null; hermesProviderExists?: boolean; + versionCheck?: VersionCheckResult; customImagePreflight?: { ok: true; imageTag: string | null } | { ok: false; detail: string }; removeSandboxRegistryEntry?: () => void; clearShieldsState?: () => void; From 347585a35117c46a6d9f697e106315aacbe52a98 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 07:16:35 -0700 Subject: [PATCH 16/46] fix: preserve tool disclosure in plugin image Signed-off-by: Aaron Erickson --- docs/deployment/install-openclaw-plugins.mdx | 3 +++ test/e2e/live/openclaw-plugin-runtime-exdev.test.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 9b9615e992..5b813aaf84 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -118,6 +118,8 @@ RUN npm run build \ # Extend the completed managed runtime. FROM nemoclaw-runtime AS weather-runtime +ARG NEMOCLAW_TOOL_DISCLOSURE=progressive +ENV NEMOCLAW_TOOL_DISCLOSURE=${NEMOCLAW_TOOL_DISCLOSURE} COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ /opt/my-plugin/package.json \ /opt/my-plugin/package-lock.json \ @@ -147,6 +149,7 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ ``` The final stage inherits the stock runtime entrypoint, command, gateway health check, generated configuration, and file permissions. +It redeclares and promotes the tool-disclosure build argument because Docker build arguments are scoped to a stage and the appended plugin stage becomes the final image stage. The local install copies the staged plugin into OpenClaw's extensions tree, records the install, links it to the image's OpenClaw runtime, and leaves existing managed plugin load paths intact before the explicit enable and inspect steps. The pre-install `test` commands fail the image build if npm retained a private `node_modules/openclaw` directory that would prevent OpenClaw from creating its runtime link. The post-install checks then prove that OpenClaw created the link and that it resolves to the image's global runtime. diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 7e3399f20d..1dcddda90a 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -27,6 +27,7 @@ const CUSTOM_PLUGIN_VERSION_SOURCE = path.join( "Dockerfile.e2e-weather-plugin.version.ts", ); const SANDBOX_BASE_IMAGE_REF = "ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71"; +const TOOL_DISCLOSURE_ENV_REFERENCE = "${NEMOCLAW_TOOL_DISCLOSURE}"; const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-plugin-exdev"; const ONBOARD_TIMEOUT_MS = 25 * 60_000; const REBUILD_TIMEOUT_MS = 20 * 60_000; @@ -193,6 +194,8 @@ RUN npm run build \ # Extend the completed managed runtime so its entrypoint, health check, config # generation, and permissions remain the source of truth. FROM nemoclaw-runtime AS weather-runtime +ARG NEMOCLAW_TOOL_DISCLOSURE=progressive +ENV NEMOCLAW_TOOL_DISCLOSURE=${TOOL_DISCLOSURE_ENV_REFERENCE} COPY --from=weather-plugin-builder --chown=sandbox:sandbox \ /opt/weather/package.json \ /opt/weather/package-lock.json \ From 6f58a298f73c572ec8325e0c445ff4b0acd386c5 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 09:05:02 -0700 Subject: [PATCH 17/46] fix: harden plugin rebuild diagnostics Signed-off-by: Aaron Erickson --- .../sandbox/rebuild-restore-phase.test.ts | 30 + .../actions/sandbox/rebuild-restore-phase.ts | 5 +- src/lib/state/sandbox.ts | 15 +- .../plugins/weather/package-lock.json | 1714 +++-------------- .../e2e/fixtures/plugins/weather/package.json | 4 +- .../openclaw-plugin-runtime-exdev.test.ts | 30 + ...apshot-openclaw-managed-extensions.test.ts | 11 +- 7 files changed, 322 insertions(+), 1487 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-restore-phase.test.ts b/src/lib/actions/sandbox/rebuild-restore-phase.test.ts index 1b3a785233..439ab91531 100644 --- a/src/lib/actions/sandbox/rebuild-restore-phase.test.ts +++ b/src/lib/actions/sandbox/rebuild-restore-phase.test.ts @@ -14,6 +14,36 @@ describe("rebuild policy restore fidelity", () => { vi.restoreAllMocks(); }); + it("surfaces a fresh OpenClaw plugin registry precondition failure", () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); + const log = vi.fn(); + vi.spyOn(sandboxState, "restoreSandboxState").mockReturnValue({ + success: false, + restoredDirs: [], + restoredFiles: [], + failedDirs: ["extensions"], + failedFiles: [], + error: "could not read fresh OpenClaw plugin install registry", + }); + + const result = runRebuildRestorePhase({ + sandboxName: "alpha", + backupManifest: { backupPath: "/tmp/rebuild-backup" } as never, + policyPresets: [], + customPolicies: [], + log, + }); + + expect(result.restoreSucceeded).toBe(false); + expect(consoleError).toHaveBeenCalledWith( + " Restore blocked: could not read fresh OpenClaw plugin install registry", + ); + expect(log).toHaveBeenCalledWith( + expect.stringContaining("error=could not read fresh OpenClaw plugin install registry"), + ); + }); + it("replays custom web-policy names from exact content instead of same-name built-ins", () => { vi.spyOn(console, "log").mockImplementation(() => undefined); vi.spyOn(console, "error").mockImplementation(() => undefined); diff --git a/src/lib/actions/sandbox/rebuild-restore-phase.ts b/src/lib/actions/sandbox/rebuild-restore-phase.ts index fca2a992ad..9bd4062b3c 100644 --- a/src/lib/actions/sandbox/rebuild-restore-phase.ts +++ b/src/lib/actions/sandbox/rebuild-restore-phase.ts @@ -40,10 +40,13 @@ export function runRebuildRestorePhase(input: RebuildRestorePhaseInput): Rebuild preserveFreshOpenClawPluginInstalls: true, }); log( - `Restore result: success=${restore.success}, restored=${restore.restoredDirs.join(",")}; files=${restore.restoredFiles.join(",")}, failed=${restore.failedDirs.join(",")}; failedFiles=${restore.failedFiles.join(",")}`, + `Restore result: success=${restore.success}, restored=${restore.restoredDirs.join(",")}; files=${restore.restoredFiles.join(",")}, failed=${restore.failedDirs.join(",")}; failedFiles=${restore.failedFiles.join(",")}${restore.error ? `; error=${restore.error}` : ""}`, ); restoreSucceeded = restore.success; if (!restore.success) { + if (restore.error) { + console.error(` Restore blocked: ${restore.error}`); + } console.error(` Partial restore: ${restore.restoredDirs.join(", ") || "none"}`); console.error(` Failed: ${restore.failedDirs.join(", ")}`); if (restore.failedFiles.length > 0) { diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 90be9e1f18..f84480c2a2 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -669,23 +669,22 @@ function discoverFreshOpenClawPluginExtensionDirs( maxBuffer: 4 * 1024 * 1024, }); if (result.status !== 0 || result.error || result.signal || !result.stdout) { - const detail = - (result.stderr?.toString() || "").trim() || - result.error?.message || - (result.signal ? `signal ${result.signal}` : `exit ${String(result.status)}`); - return { ok: false, error: `could not read fresh OpenClaw plugin install registry: ${detail}` }; + return { ok: false, error: "could not read fresh OpenClaw plugin install registry" }; } let config: unknown; try { config = JSON.parse(result.stdout.toString("utf-8")) as unknown; - } catch (error) { + } catch { return { ok: false, - error: `fresh OpenClaw plugin install registry is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, + error: "fresh OpenClaw plugin install registry is not valid JSON", }; } - return parseFreshOpenClawPluginExtensionDirs(config, dir); + const parsed = parseFreshOpenClawPluginExtensionDirs(config, dir); + return parsed.ok + ? parsed + : { ok: false, error: "fresh OpenClaw plugin install registry failed validation" }; } function normalizeStateFileSpec(spec: AgentStateFile | StateFileSpec): StateFileSpec | null { diff --git a/test/e2e/fixtures/plugins/weather/package-lock.json b/test/e2e/fixtures/plugins/weather/package-lock.json index e58ef537b8..a3e1e00343 100644 --- a/test/e2e/fixtures/plugins/weather/package-lock.json +++ b/test/e2e/fixtures/plugins/weather/package-lock.json @@ -12,7 +12,7 @@ "typebox": "1.1.38" }, "devDependencies": { - "openclaw": "2026.5.27", + "openclaw": "2026.6.10", "typescript": "5.9.3" }, "engines": { @@ -23,59 +23,63 @@ } }, "node_modules/openclaw": { - "version": "2026.5.27", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.27.tgz", - "integrity": "sha512-2N93zhdAo88KAbHt6T7KvYXf4s7XIkYXBgv1npYpn7e1Y9FvrtgtpsA38my9rtFW+70uXEojRPX5/OqnuDqJPw==", + "version": "2026.6.10", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.10.tgz", + "integrity": "sha512-LcooND2tBQw8A+kc1Ujltu3lg30bJ0w7XaeRy7eYzobb8BBdcW6DOGbwJL4vpj1vl9+gjRceOtlh5nh9OARcug==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, "license": "MIT", "dependencies": { "@agentclientprotocol/sdk": "0.22.1", + "@anthropic-ai/sdk": "0.100.1", "@clack/core": "1.3.1", "@clack/prompts": "1.4.0", - "@earendil-works/pi-agent-core": "0.75.5", - "@earendil-works/pi-ai": "0.75.5", - "@earendil-works/pi-coding-agent": "0.75.5", - "@earendil-works/pi-tui": "0.75.5", - "@google/genai": "2.6.0", + "@earendil-works/pi-tui": "0.78.0", + "@google/genai": "2.7.0", "@grammyjs/runner": "2.0.3", "@grammyjs/transformer-throttler": "1.2.1", - "@homebridge/ciao": "1.3.8", + "@homebridge/ciao": "1.3.9", "@lydell/node-pty": "1.2.0-beta.12", + "@mistralai/mistralai": "2.2.5", "@modelcontextprotocol/sdk": "1.29.0", "@mozilla/readability": "0.6.0", "@openclaw/fs-safe": "0.3.0", "@openclaw/proxyline": "0.3.3", "chalk": "5.6.2", "chokidar": "5.0.0", + "clawpdf": "0.3.0", "commander": "14.0.3", "croner": "10.0.1", + "diff": "9.0.0", "dotenv": "17.4.2", "express": "5.2.1", "file-type": "22.0.1", + "glob": "13.0.6", "grammy": "1.43.0", - "ipaddr.js": "2.4.0", + "highlight.js": "11.11.1", + "hosted-git-info": "10.1.1", + "ignore": "7.0.5", "jiti": "2.7.0", "json5": "2.2.3", "jszip": "3.10.1", "kysely": "0.29.2", "linkedom": "0.18.12", - "markdown-it": "14.1.1", + "minimatch": "10.2.5", "node-edge-tts": "1.2.10", - "openai": "6.39.0", - "pdfjs-dist": "5.7.284", + "openai": "6.39.1", + "partial-json": "0.1.7", "playwright-core": "1.60.0", + "proper-lockfile": "4.1.2", "qrcode": "1.5.4", - "quickjs-wasi": "2.2.0", - "rastermill": "0.3.0", - "tar": "7.5.15", - "tokenjuice": "0.7.1", + "quickjs-wasi": "3.0.0", + "rastermill": "0.3.1", + "tar": "7.5.16", "tree-sitter-bash": "0.25.1", "tslog": "4.10.2", - "typebox": "1.1.38", + "typebox": "1.1.39", "typescript": "6.0.3", - "undici": "8.3.0", + "undici": "8.5.0", "web-push": "3.6.7", "web-tree-sitter": "0.26.9", "ws": "8.21.0", @@ -103,9 +107,9 @@ } }, "node_modules/openclaw/node_modules/@anthropic-ai/sdk": { - "version": "0.98.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.98.0.tgz", - "integrity": "sha512-N7aXtCvC5g6T1Y4V29lJjceu/zTkVkIZF0jdBvagr0TRFHuKeImffalGWEfqZKrvjH+IQbzJWw6TmSmUzrlMgg==", + "version": "0.100.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.100.1.tgz", + "integrity": "sha512-RANcEe7LpiLczkKGOwoXOTuFdPhuubS0i4xaAKOMpcqc55YO0mukgxppV7eygx3DXNjxWT6RYOLPyOy0aIAmwg==", "dev": true, "license": "MIT", "dependencies": { @@ -124,441 +128,10 @@ } } }, - "node_modules/openclaw/node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/openclaw/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/openclaw/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1053.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1053.0.tgz", - "integrity": "sha512-I5dua8y1logE+Mx6r5kvI1tjM+XyC3H42KDCpEqmhrJfanor/x/AdOavyv3HnS4sBqUxx2IrjLP3ouEumjeTzA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/credential-provider-node": "^3.972.44", - "@aws-sdk/eventstream-handler-node": "^3.972.17", - "@aws-sdk/middleware-eventstream": "^3.972.13", - "@aws-sdk/middleware-websocket": "^3.972.21", - "@aws-sdk/token-providers": "3.1053.0", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/node-http-handler": "^4.7.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/core": { - "version": "3.974.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.13.tgz", - "integrity": "sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@aws-sdk/xml-builder": "^3.972.25", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.3", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.2", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.39.tgz", - "integrity": "sha512-29wX9zpAvEt1vcj0psha+y6ygBHy2V/S72mp6e7q0KARLWXq+pwE/lR6qGkwknQvruh52lXvlqZIga8Hdxkucw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.41.tgz", - "integrity": "sha512-IA3CQTjtJkb6u1H4mE4936c8OPBMa9Jggtwe8U2Mqw/vvb/tZ5Ebd0mcZcX0uKWQhOyYo/+qNIwkV5Xh+FeJJA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/node-http-handler": "^4.7.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.43.tgz", - "integrity": "sha512-4mzII+3mZEVXXE1xzrLQrCJL7/r62A63bA6SVzZoNL5rqCJghpf+xgGltVrIBBs0n+mOZBKrQl2tRREtvZ5l6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/credential-provider-env": "^3.972.39", - "@aws-sdk/credential-provider-http": "^3.972.41", - "@aws-sdk/credential-provider-login": "^3.972.43", - "@aws-sdk/credential-provider-process": "^3.972.39", - "@aws-sdk/credential-provider-sso": "^3.972.43", - "@aws-sdk/credential-provider-web-identity": "^3.972.43", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.43.tgz", - "integrity": "sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.44", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.44.tgz", - "integrity": "sha512-sDaBIT0yrNNIPfvlsiTCmANm07zKju+ipWODjEXgZlsjMeIJR3LVp7RDyAOzUoAsTbDfYKDWp+i5WrFiQP6rmQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.39", - "@aws-sdk/credential-provider-http": "^3.972.41", - "@aws-sdk/credential-provider-ini": "^3.972.43", - "@aws-sdk/credential-provider-process": "^3.972.39", - "@aws-sdk/credential-provider-sso": "^3.972.43", - "@aws-sdk/credential-provider-web-identity": "^3.972.43", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.39.tgz", - "integrity": "sha512-2k/amBifLd75eXNwgvPw/2lKYSQ3NhvHQgkVKVjfUq13/eJ3JRtHmznuFenn74OK3sSfp4SMy1YB2w+UVXoKqA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.43.tgz", - "integrity": "sha512-LPc3+Y4vhH1T4x6CMqwCM6hk5+SRf/Lwmgm8INm95wxTtIRHcMwQUVkDzWu4Iw/RSncxYM2BC01OrYbxOPZvyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/token-providers": "3.1052.0", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.43.tgz", - "integrity": "sha512-wQtL34lUD/09VXjwAUo2T+I3aEXRDxMB3DKmTJL/Zj0Gi6sLDTrVhae1XVt01yzkquOWajI/sZW72JGDZ1ciTw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.17.tgz", - "integrity": "sha512-WFwdNcjchKZr7jKYgGimUZO8sSKQF/le7GGqgeCzz/lHozInE6b0gFJ1YMr8NaIeAoWJwgtrF7RE4/qMgosAdQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.13.tgz", - "integrity": "sha512-ECfsw7mf6G/sxNbKbGE3/h1xeIArY/yRI1IjDGYkLgDIankh+aDOtDRSr40LVlIHGL9+jEH1cVuxmbJ8NLL/1A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.21.tgz", - "integrity": "sha512-yr+5+C7v9R55sAJ89A55Wrm7wIKPVn5cm6J3Hztnd5s/iwEUKxyJqCnIxJu4fVXgG9XBQD1Jc4rsWC1ozahJjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/nested-clients": { - "version": "3.997.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.11.tgz", - "integrity": "sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.28", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/node-http-handler": "^4.7.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.28.tgz", - "integrity": "sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/token-providers": { - "version": "3.1053.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1053.0.tgz", - "integrity": "sha512-laSwHLYMMrXQRl2mFDXszF43m/F4pKWyGr7hCLfJmV8rn8c6CnI/hp/bf/Gn7gLcjz0SY4evd7SBpqtnIhzA/A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/types": { - "version": "3.973.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", - "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws-sdk/xml-builder": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz", - "integrity": "sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.2", - "fast-xml-parser": "5.7.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/openclaw/node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/openclaw/node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "dev": true, "license": "MIT", "engines": { @@ -606,51 +179,24 @@ "node": ">= 20.12.0" } }, - "node_modules/openclaw/node_modules/@earendil-works/pi-agent-core": { - "version": "0.75.5", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.5.tgz", - "integrity": "sha512-LHygOgsW2pgXKb3IkXkOAeZPovHr9VF+EixgXVsDNuB4jmhEOXgshy/zksZ7slkUAx10OQ9W1Ed/2jsnhd1NqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "^0.75.5", - "ignore": "7.0.5", - "typebox": "1.1.38", - "yaml": "2.9.0" - }, - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/openclaw/node_modules/@earendil-works/pi-ai": { - "version": "0.75.5", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.5.tgz", - "integrity": "sha512-zf1F5kXk1pqZeFShXOqq9ibUk8QdtRoLCDPAjO+hj44e3EUs9/GFO2qnhTC5+JA2uwVCx+WCNe1PiCjlBYWm5w==", + "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.78.0.tgz", + "integrity": "sha512-3a705FnsVVUhAyceShNB3kS2rpxcxLcx+hqB0u6MMMpHwQGbW+m++MqA6r7eOzq/8FLx5e3vDh38h/SVTk2qzw==", "dev": true, "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "0.91.1", - "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", - "@smithy/node-http-handler": "4.7.3", - "http-proxy-agent": "7.0.2", - "https-proxy-agent": "7.0.6", - "openai": "6.26.0", - "partial-json": "0.1.7", - "typebox": "1.1.38" - }, - "bin": { - "pi-ai": "dist/cli.js" + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" }, "engines": { "node": ">=22.19.0" } }, - "node_modules/openclaw/node_modules/@earendil-works/pi-ai/node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "node_modules/openclaw/node_modules/@google/genai": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.7.0.tgz", + "integrity": "sha512-tv0DRtcndt2oEhBYy+5mA0TaXH98+L1Gt0AP9unBfH7DP20KhB7+O3QqAN1Lz+laMARGTHS7BFQSNpLbl4gm1g==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -672,145 +218,49 @@ } } }, - "node_modules/openclaw/node_modules/@earendil-works/pi-ai/node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "node_modules/openclaw/node_modules/@grammyjs/runner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz", + "integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==", "dev": true, - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0" }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" + "engines": { + "node": ">=12.20.0 || >=14.13.1" }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } + "peerDependencies": { + "grammy": "^1.13.1" } }, - "node_modules/openclaw/node_modules/@earendil-works/pi-coding-agent": { - "version": "0.75.5", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.75.5.tgz", - "integrity": "sha512-O3CCQDYy28D4uwtP6zZkdEwzHN6X22v49Sb0+SZTC7x37V/YfmogrWPiaFoWeoc2hmdKhSATI7ZAK5bQbJG5NA==", + "node_modules/openclaw/node_modules/@grammyjs/transformer-throttler": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz", + "integrity": "sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==", "dev": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.5", - "@earendil-works/pi-ai": "^0.75.5", - "@earendil-works/pi-tui": "^0.75.5", - "@silvia-odwyer/photon-node": "0.3.4", - "chalk": "5.6.2", - "cross-spawn": "7.0.6", - "diff": "8.0.4", - "glob": "13.0.6", - "highlight.js": "10.7.3", - "hosted-git-info": "9.0.3", - "ignore": "7.0.5", - "jiti": "2.7.0", - "minimatch": "10.2.5", - "proper-lockfile": "4.1.2", - "typebox": "1.1.38", - "undici": "8.3.0", - "yaml": "2.9.0" - }, - "bin": { - "pi": "dist/cli.js" + "bottleneck": "^2.0.0" }, "engines": { - "node": ">=22.19.0" + "node": "^12.20.0 || >=14.13.1" }, - "optionalDependencies": { - "@mariozechner/clipboard": "0.3.6" + "peerDependencies": { + "grammy": "^1.0.0" } }, - "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { - "version": "0.75.5", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.5.tgz", - "integrity": "sha512-LkXUM1/49pvzzeI39Y5wjBMlgafcCf67HCLhB9Z7yuXHy4XgT+VqxWcZVW5hBdhQsHZd0znjJotfGH1BzxMfiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "1.6.0", - "marked": "15.0.12" - }, - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/openclaw/node_modules/@google/genai": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz", - "integrity": "sha512-HjoW3mPuEn7pnuKABJl9VbDoWDSF4nbwYKYvYYor7YjPeDxrrBxHzu2d1Prcd+BAuC4w+85UP6y7ZdcrQAoO7g==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/openclaw/node_modules/@grammyjs/runner": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz", - "integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==", - "dev": true, - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0" - }, - "engines": { - "node": ">=12.20.0 || >=14.13.1" - }, - "peerDependencies": { - "grammy": "^1.13.1" - } - }, - "node_modules/openclaw/node_modules/@grammyjs/transformer-throttler": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz", - "integrity": "sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "bottleneck": "^2.0.0" - }, - "engines": { - "node": "^12.20.0 || >=14.13.1" - }, - "peerDependencies": { - "grammy": "^1.0.0" - } - }, - "node_modules/openclaw/node_modules/@grammyjs/types": { - "version": "3.27.3", - "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.27.3.tgz", - "integrity": "sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==", + "node_modules/openclaw/node_modules/@grammyjs/types": { + "version": "3.27.3", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.27.3.tgz", + "integrity": "sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==", "dev": true, "license": "MIT" }, "node_modules/openclaw/node_modules/@homebridge/ciao": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.8.tgz", - "integrity": "sha512-lNhpCsZVbdbjz2trFjQdzQ3cUIMZQMIMksi7wd3ntTIYgdaGLqT1Ms97DfVIJYHzRuduf56ISvgU8RRLTpK/ng==", + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.9.tgz", + "integrity": "sha512-TMy9zy173jDOpnFXDqL3BPIQn5lfcAkSsivYQatCCakoHk4fLGd7QjfAaNGYE3Ox+/ZI6Lq0e1gGcz1qdw/IbA==", "dev": true, "license": "MIT", "dependencies": { @@ -948,200 +398,10 @@ "win32" ] }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", - "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.6", - "@mariozechner/clipboard-darwin-universal": "0.3.6", - "@mariozechner/clipboard-darwin-x64": "0.3.6", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-musl": "0.3.6", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", - "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", - "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", - "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", - "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", - "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", - "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", - "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", - "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", - "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/openclaw/node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", - "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/openclaw/node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.5.tgz", + "integrity": "sha512-ATbWzKkNzNAZ+gtw9MI/c/ULTMG80tKUiRNIbQFfg4OP0uEZZpTfXZeBCNfs5Dq0uqMQ/tQWc4o6RRJQtMrpDA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1192,289 +452,14 @@ } }, "node_modules/openclaw/node_modules/@mozilla/readability": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", - "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", - "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", - "dev": true, - "license": "MIT", - "optional": true, - "workspaces": [ - "e2e/*" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.100", - "@napi-rs/canvas-darwin-arm64": "0.1.100", - "@napi-rs/canvas-darwin-x64": "0.1.100", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", - "@napi-rs/canvas-linux-arm64-musl": "0.1.100", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", - "@napi-rs/canvas-linux-x64-gnu": "0.1.100", - "@napi-rs/canvas-linux-x64-musl": "0.1.100", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", - "@napi-rs/canvas-win32-x64-msvc": "0.1.100" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", - "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", - "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", - "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", - "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", - "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", - "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", - "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", - "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", - "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", - "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", - "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/openclaw/node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", + "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/openclaw/node_modules/@openclaw/fs-safe": { "version": "0.3.0", @@ -1503,141 +488,85 @@ "undici": ">=8.3.0 <9" } }, - "node_modules/openclaw/node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "node_modules/openclaw/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "dev": true, - "license": "Apache-2.0" + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/core": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.4.tgz", - "integrity": "sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==", + "node_modules/openclaw/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/credential-provider-imds": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.4.tgz", - "integrity": "sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==", + "node_modules/openclaw/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/fetch-http-handler": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.4.tgz", - "integrity": "sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==", + "node_modules/openclaw/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "node_modules/openclaw/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "dev": true, - "license": "Apache-2.0", + "license": "BSD-3-Clause", "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" + "@protobufjs/aspromise": "^1.1.1" } }, - "node_modules/openclaw/node_modules/@smithy/node-http-handler": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.4.tgz", - "integrity": "sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==", + "node_modules/openclaw/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/signature-v4": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.4.tgz", - "integrity": "sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==", + "node_modules/openclaw/node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/types": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "node_modules/openclaw/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "node_modules/openclaw/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/openclaw/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "node_modules/openclaw/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/openclaw/node_modules/@stablelib/base64": { "version": "1.0.1", @@ -1671,10 +600,20 @@ "dev": true, "license": "MIT" }, + "node_modules/openclaw/node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, "node_modules/openclaw/node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", "dev": true, "license": "MIT" }, @@ -1776,13 +715,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/openclaw/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/openclaw/node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -1883,13 +815,6 @@ "dev": true, "license": "MIT" }, - "node_modules/openclaw/node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "dev": true, - "license": "MIT" - }, "node_modules/openclaw/node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -2007,6 +932,19 @@ "node": ">=18" } }, + "node_modules/openclaw/node_modules/clawpdf": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/clawpdf/-/clawpdf-0.3.0.tgz", + "integrity": "sha512-41+3AnKk9yek2sm+/9XvUlDTN8Wi+ag7fmxZuqw+ySn4lqaf/fCgLeamqPLiXY4gVbizKEHGoTG/JrIIFNE2rw==", + "dev": true, + "license": "MIT", + "bin": { + "clawpdf": "dist/cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/openclaw/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2242,9 +1180,9 @@ } }, "node_modules/openclaw/node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2413,9 +1351,9 @@ } }, "node_modules/openclaw/node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -2476,9 +1414,9 @@ } }, "node_modules/openclaw/node_modules/eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "dev": true, "license": "MIT", "engines": { @@ -2613,45 +1551,6 @@ "fast-string-width": "^3.0.2" } }, - "node_modules/openclaw/node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" - } - }, - "node_modules/openclaw/node_modules/fast-xml-parser": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.0.tgz", - "integrity": "sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/openclaw/node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -2775,9 +1674,9 @@ } }, "node_modules/openclaw/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2885,9 +1784,9 @@ } }, "node_modules/openclaw/node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2983,9 +1882,9 @@ } }, "node_modules/openclaw/node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -2996,19 +1895,19 @@ } }, "node_modules/openclaw/node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": "*" + "node": ">=12.0.0" } }, "node_modules/openclaw/node_modules/hono": { - "version": "4.12.18", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", - "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "dev": true, "license": "MIT", "engines": { @@ -3016,16 +1915,16 @@ } }, "node_modules/openclaw/node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", + "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^11.1.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/openclaw/node_modules/html-escaper": { @@ -3099,20 +1998,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/openclaw/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/openclaw/node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -3200,13 +2085,13 @@ } }, "node_modules/openclaw/node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 0.10" } }, "node_modules/openclaw/node_modules/is-fullwidth-code-point": { @@ -3392,16 +2277,6 @@ } } }, - "node_modules/openclaw/node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, "node_modules/openclaw/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3423,33 +2298,15 @@ "license": "Apache-2.0" }, "node_modules/openclaw/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/openclaw/node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, "node_modules/openclaw/node_modules/marked": { "version": "15.0.12", "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", @@ -3473,13 +2330,6 @@ "node": ">= 0.4" } }, - "node_modules/openclaw/node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true, - "license": "MIT" - }, "node_modules/openclaw/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -3604,9 +2454,9 @@ } }, "node_modules/openclaw/node_modules/node-addon-api": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", - "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", + "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", "dev": true, "license": "MIT", "engines": { @@ -3730,9 +2580,9 @@ } }, "node_modules/openclaw/node_modules/openai": { - "version": "6.39.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.0.tgz", - "integrity": "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==", + "version": "6.39.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.1.tgz", + "integrity": "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3838,22 +2688,6 @@ "node": ">=8" } }, - "node_modules/openclaw/node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/openclaw/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3892,19 +2726,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/openclaw/node_modules/pdfjs-dist": { - "version": "5.7.284", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", - "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=22.13.0 || >=24" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.100" - } - }, "node_modules/openclaw/node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -3968,13 +2789,24 @@ } }, "node_modules/openclaw/node_modules/protobufjs": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", - "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz", + "integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", "long": "^5.3.2" }, "engines": { @@ -3995,26 +2827,6 @@ "node": ">= 0.10" } }, - "node_modules/openclaw/node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/openclaw/node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/openclaw/node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -4121,9 +2933,9 @@ } }, "node_modules/openclaw/node_modules/quickjs-wasi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", - "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-3.0.0.tgz", + "integrity": "sha512-X7ouKC4ZVf9bXQ8rsE7+L6TeBbesejAJH61x16xRaGAQGfBHHRcniWgzJZZVtHc8rS9yVsY+Tvk8/usAosg4bg==", "dev": true, "license": "MIT" }, @@ -4138,16 +2950,16 @@ } }, "node_modules/openclaw/node_modules/rastermill": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.0.tgz", - "integrity": "sha512-4g2i0I7M5sba//lFBh19Wi0hDGw8o+isnt/BtEyqQXIZaYclhcNBwL/Fw/6gDCp7aaLwQHADuUvyHCB0Oat5Vw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.1.tgz", + "integrity": "sha512-CX4nij6+ZLHYIaojJNfLTr7W+AiH/IPJi6E9Aw1br2///1KZL2KBOHd68rkcLedc47MPvb4hhH+fzYeGFa4A/Q==", "dev": true, "license": "MIT", "dependencies": { "@silvia-odwyer/photon-node": "0.3.4" }, "engines": { - "node": ">=20" + "node": ">=22" } }, "node_modules/openclaw/node_modules/raw-body": { @@ -4638,19 +3450,6 @@ "node": ">=8" } }, - "node_modules/openclaw/node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/openclaw/node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", @@ -4669,9 +3468,9 @@ } }, "node_modules/openclaw/node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -4714,23 +3513,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/openclaw/node_modules/tokenjuice": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/tokenjuice/-/tokenjuice-0.7.1.tgz", - "integrity": "sha512-eO048hm9UcGHASjYkIWEij8QN68amGp+S1nJyo685qB1/ol+VGEYjPglcVPvCbJbZyFHvI+BBAMvOfnqYCtpsQ==", - "dev": true, - "license": "MIT", - "bin": { - "tokenjuice": "dist/cli/main.js" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/vincentkoc" - } - }, "node_modules/openclaw/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -4819,9 +3601,9 @@ } }, "node_modules/openclaw/node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "version": "1.1.39", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.39.tgz", + "integrity": "sha512-vj0afVtOfLQvv0GR0VxVagYxsXN64btL7Z9XoaG0ZggH3mruMMkOO6hXdgMsjCY3shZgEvooAWVeznQVs5c43w==", "dev": true, "license": "MIT" }, @@ -4839,13 +3621,6 @@ "node": ">=14.17" } }, - "node_modules/openclaw/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, "node_modules/openclaw/node_modules/uhyphen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", @@ -4867,15 +3642,22 @@ } }, "node_modules/openclaw/node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "dev": true, "license": "MIT", "engines": { "node": ">=22.19.0" } }, + "node_modules/openclaw/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, "node_modules/openclaw/node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5028,22 +3810,6 @@ } } }, - "node_modules/openclaw/node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/openclaw/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/test/e2e/fixtures/plugins/weather/package.json b/test/e2e/fixtures/plugins/weather/package.json index 9f0399b2ae..b6bc8612cd 100644 --- a/test/e2e/fixtures/plugins/weather/package.json +++ b/test/e2e/fixtures/plugins/weather/package.json @@ -19,7 +19,7 @@ "minGatewayVersion": "2026.5.22" }, "build": { - "openclawVersion": "2026.5.27" + "openclawVersion": "2026.6.10" } }, "scripts": { @@ -29,7 +29,7 @@ "typebox": "1.1.38" }, "devDependencies": { - "openclaw": "2026.5.27", + "openclaw": "2026.6.10", "typescript": "5.9.3" }, "peerDependencies": { diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 1dcddda90a..cb057640fb 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -26,6 +26,26 @@ const CUSTOM_PLUGIN_VERSION_SOURCE = path.join( REPO_ROOT, "Dockerfile.e2e-weather-plugin.version.ts", ); +const WEATHER_FIXTURE_PACKAGE_PATH = path.join( + REPO_ROOT, + "test/e2e/fixtures/plugins/weather/package.json", +); +const WEATHER_FIXTURE_PACKAGE = JSON.parse( + fs.readFileSync(WEATHER_FIXTURE_PACKAGE_PATH, "utf8"), +) as { + openclaw?: { build?: { openclawVersion?: unknown } }; + devDependencies?: { openclaw?: unknown }; +}; +const WEATHER_OPENCLAW_VERSION = WEATHER_FIXTURE_PACKAGE.openclaw?.build?.openclawVersion; +if ( + typeof WEATHER_OPENCLAW_VERSION !== "string" || + !/^\d+(?:\.\d+)+$/.test(WEATHER_OPENCLAW_VERSION) +) { + throw new Error("weather fixture must declare a canonical OpenClaw build version"); +} +// Keep the dependency layer reproducible while the current managed Dockerfile +// upgrades its OpenClaw runtime to WEATHER_OPENCLAW_VERSION. The assertions in +// createCustomPluginDockerfile and the in-sandbox probe make that boundary explicit. const SANDBOX_BASE_IMAGE_REF = "ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71"; const TOOL_DISCLOSURE_ENV_REFERENCE = "${NEMOCLAW_TOOL_DISCLOSURE}"; const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-plugin-exdev"; @@ -172,6 +192,14 @@ function createCustomPluginDockerfile(): () => void { source.match(/^ARG BASE_IMAGE=ghcr\.io\/nvidia\/nemoclaw\/sandbox-base:latest$/gm)?.length, ).toBe(1); expect(source.match(/^FROM \$\{BASE_IMAGE\}$/gm)?.length, "expected one runtime stage").toBe(1); + expect( + source.match(/^ARG OPENCLAW_VERSION=([0-9.]+)$/m)?.[1], + "weather fixture SDK must match the current managed runtime target", + ).toBe(WEATHER_OPENCLAW_VERSION); + expect( + WEATHER_FIXTURE_PACKAGE.devDependencies?.openclaw, + "weather fixture devDependency must match its declared OpenClaw build target", + ).toBe(WEATHER_OPENCLAW_VERSION); const runtime = source .replace(baseImageAnchor, `ARG BASE_IMAGE=${SANDBOX_BASE_IMAGE_REF}\n`) @@ -278,6 +306,7 @@ async function assertWeatherPluginRuntime( trustedSandboxShellScript(`set -eu test -s /tmp/gateway.log test -s /usr/local/share/nemoclaw/e2e-weather-plugin.sha256 +test "$(openclaw --version 2>/dev/null | awk '{print $2}')" = "${WEATHER_OPENCLAW_VERSION}" test -L /sandbox/.openclaw/extensions/weather/node_modules/openclaw test "$(realpath /sandbox/.openclaw/extensions/weather/node_modules/openclaw)" = /usr/local/lib/node_modules/openclaw expected=$(cat /usr/local/share/nemoclaw/e2e-weather-plugin.sha256) @@ -468,6 +497,7 @@ liveTest( "OpenClaw-style target-side plugin runtime-deps replacement completes without EXDEV", ], sandboxBaseImageRef: SANDBOX_BASE_IMAGE_REF, + openclawVersion: WEATHER_OPENCLAW_VERSION, }); const docker = await host.command("docker", ["info"], { diff --git a/test/snapshot-openclaw-managed-extensions.test.ts b/test/snapshot-openclaw-managed-extensions.test.ts index f1e5e0e4ac..61ec34622e 100644 --- a/test/snapshot-openclaw-managed-extensions.test.ts +++ b/test/snapshot-openclaw-managed-extensions.test.ts @@ -228,13 +228,20 @@ process.exit(0); fs.writeFileSync( freshRegistryPath, - '{"version":1,"installRecords":{"../weather":{"installPath":"/sandbox/.openclaw/extensions/../weather"}}}', + JSON.stringify({ + version: 1, + installRecords: { + "\u001b[31m../weather": { + installPath: "/sandbox/.openclaw/extensions/../weather", + }, + }, + }), ); const rejected = sandboxState.restoreSandboxState("alpha", manifest.backupPath, { preserveFreshOpenClawPluginInstalls: true, }); expect(rejected.success).toBe(false); - expect(rejected.error).toMatch(/unsafe plugin install id/); + expect(rejected.error).toBe("fresh OpenClaw plugin install registry failed validation"); expect(fs.readFileSync(path.join(extensionsDir, "weather", "marker.txt"), "utf-8")).toBe( "fresh-weather-v2\n", ); From 8fcd82ce2f796c3219ae6b8953452e0a4e1cc9a3 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 09:10:14 -0700 Subject: [PATCH 18/46] test: keep plugin regressions branch-free Signed-off-by: Aaron Erickson --- .../openclaw-plugin-runtime-exdev.test.ts | 20 ++++++++++------ ...apshot-openclaw-managed-extensions.test.ts | 24 ++++++++----------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index cb057640fb..01704371d8 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; @@ -36,13 +37,18 @@ const WEATHER_FIXTURE_PACKAGE = JSON.parse( openclaw?: { build?: { openclawVersion?: unknown } }; devDependencies?: { openclaw?: unknown }; }; -const WEATHER_OPENCLAW_VERSION = WEATHER_FIXTURE_PACKAGE.openclaw?.build?.openclawVersion; -if ( - typeof WEATHER_OPENCLAW_VERSION !== "string" || - !/^\d+(?:\.\d+)+$/.test(WEATHER_OPENCLAW_VERSION) -) { - throw new Error("weather fixture must declare a canonical OpenClaw build version"); -} +const weatherOpenClawVersion = WEATHER_FIXTURE_PACKAGE.openclaw?.build?.openclawVersion; +assert.equal( + typeof weatherOpenClawVersion, + "string", + "weather fixture must declare an OpenClaw build version", +); +const WEATHER_OPENCLAW_VERSION = String(weatherOpenClawVersion); +assert.match( + WEATHER_OPENCLAW_VERSION, + /^\d+(?:\.\d+)+$/, + "weather fixture must declare a canonical OpenClaw build version", +); // Keep the dependency layer reproducible while the current managed Dockerfile // upgrades its OpenClaw runtime to WEATHER_OPENCLAW_VERSION. The assertions in // createCustomPluginDockerfile and the in-sandbox probe make that boundary explicit. diff --git a/test/snapshot-openclaw-managed-extensions.test.ts b/test/snapshot-openclaw-managed-extensions.test.ts index 61ec34622e..d507678334 100644 --- a/test/snapshot-openclaw-managed-extensions.test.ts +++ b/test/snapshot-openclaw-managed-extensions.test.ts @@ -1,12 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterAll, beforeEach, describe, expect, it } from "vitest"; +import { restoreEnv, restoreEnvBulk } from "./helpers/env-test-helpers"; + const ORIGINAL_HOME = process.env.HOME; const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-managed-extensions-")); process.env.HOME = TMP_HOME; @@ -16,9 +19,11 @@ type SandboxStateModule = typeof import("../src/lib/state/sandbox.js"); const loadedSandboxState = await import( pathToFileURL(path.join(REPO_ROOT, "src", "lib", "state", "sandbox.ts")).href ); -if (typeof loadedSandboxState.restoreSandboxState !== "function") { - throw new Error("Expected sandbox-state restore export to be available"); -} +assert.equal( + typeof loadedSandboxState.restoreSandboxState, + "function", + "Expected sandbox-state restore export to be available", +); const sandboxState = loadedSandboxState as SandboxStateModule; const BACKUPS_ROOT = path.join(TMP_HOME, ".nemoclaw", "rebuild-backups"); @@ -86,11 +91,7 @@ process.exit(0); } afterAll(() => { - if (ORIGINAL_HOME === undefined) { - delete process.env.HOME; - } else { - process.env.HOME = ORIGINAL_HOME; - } + restoreEnv("HOME", ORIGINAL_HOME); fs.rmSync(TMP_HOME, { recursive: true, force: true }); }); @@ -256,12 +257,7 @@ process.exit(0); ), ).toHaveLength(1); } finally { - if (oldOpenshell === undefined) { - delete process.env.NEMOCLAW_OPENSHELL_BIN; - } else { - process.env.NEMOCLAW_OPENSHELL_BIN = oldOpenshell; - } - process.env.PATH = oldPath; + restoreEnvBulk({ NEMOCLAW_OPENSHELL_BIN: oldOpenshell, PATH: oldPath }); fs.rmSync(fixture, { recursive: true, force: true }); } }); From c662222e79836193f023c0c1f3ed82cd589098c3 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 09:31:49 -0700 Subject: [PATCH 19/46] fix: read OpenClaw plugin index from SQLite Signed-off-by: Aaron Erickson --- .../rebuild-local-provider-recreate.test.ts | 1 + .../sandbox/rebuild-prepared-recovery.test.ts | 1 + src/lib/state/openclaw-plugin-restore.test.ts | 86 ++++++++++++++++++- src/lib/state/openclaw-plugin-restore.ts | 29 +++++++ src/lib/state/sandbox.ts | 40 +++++++-- ...apshot-openclaw-managed-extensions.test.ts | 19 +++- 6 files changed, 166 insertions(+), 10 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts b/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts index 5b39b40f22..d822f48dea 100644 --- a/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts +++ b/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts @@ -207,6 +207,7 @@ describe("rebuild local-provider recreation", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", "/tmp/nemoclaw-rebuild-backup", + { preserveFreshOpenClawPluginInstalls: true }, ); }); }); diff --git a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts index 3f0935d67d..462089c13f 100644 --- a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts @@ -47,6 +47,7 @@ describe("prepared rebuild recovery", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", recoveryManifest.backupPath, + { preserveFreshOpenClawPluginInstalls: true }, ); }); diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts index a8a4100c45..dcddf5ff69 100644 --- a/src/lib/state/openclaw-plugin-restore.test.ts +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -1,9 +1,17 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { execFileSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + import { describe, expect, it } from "vitest"; -import { parseFreshOpenClawPluginExtensionDirs } from "./openclaw-plugin-restore"; +import { + buildFreshOpenClawPluginIndexSqliteReadCommand, + parseFreshOpenClawPluginExtensionDirs, +} from "./openclaw-plugin-restore"; const OPENCLAW_DIR = "/sandbox/.openclaw"; @@ -11,6 +19,82 @@ function install(installPath: string): Record { return { source: "path", installPath }; } +const CREATE_PLUGIN_INDEX_SQLITE_PY = [ + "import json, sqlite3, sys", + "conn = sqlite3.connect(sys.argv[1])", + "conn.execute('CREATE TABLE installed_plugin_index (index_key TEXT PRIMARY KEY, install_records_json TEXT)')", + "records = json.loads(sys.argv[2])", + "if records is not None: conn.execute('INSERT INTO installed_plugin_index VALUES (?, ?)', ('installed-plugin-index', json.dumps(records)))", + "conn.commit()", + "conn.close()", +].join("\n"); + +function createPluginIndexDatabase(dbPath: string, records: unknown): void { + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + execFileSync("python3", ["-c", CREATE_PLUGIN_INDEX_SQLITE_PY, dbPath, JSON.stringify(records)]); +} + +describe("buildFreshOpenClawPluginIndexSqliteReadCommand", () => { + it("reads canonical install records from the OpenClaw SQLite index", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-plugin-index-")); + try { + const dbPath = path.join(root, "state", "openclaw.sqlite"); + const records = { weather: install(`${OPENCLAW_DIR}/extensions/weather`) }; + createPluginIndexDatabase(dbPath, records); + + const stdout = execFileSync( + "bash", + ["-c", buildFreshOpenClawPluginIndexSqliteReadCommand(root)], + { encoding: "utf8" }, + ); + expect(JSON.parse(stdout)).toEqual({ version: 1, installRecords: records }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it("fails closed when the SQLite database has no installed-plugin-index row", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-plugin-index-")); + try { + createPluginIndexDatabase(path.join(root, "state", "openclaw.sqlite"), null); + expect(() => + execFileSync("bash", ["-c", buildFreshOpenClawPluginIndexSqliteReadCommand(root)]), + ).toThrow(); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it("uses exit status 2 only when the canonical SQLite database is absent", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-plugin-index-")); + try { + const result = spawnSync("bash", [ + "-c", + buildFreshOpenClawPluginIndexSqliteReadCommand(root), + ]); + expect(result.status).toBe(2); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it("rejects a broken SQLite database symlink instead of using legacy fallback", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-plugin-index-")); + try { + const dbPath = path.join(root, "state", "openclaw.sqlite"); + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + fs.symlinkSync(path.join(root, "missing.sqlite"), dbPath); + const result = spawnSync("bash", [ + "-c", + buildFreshOpenClawPluginIndexSqliteReadCommand(root), + ]); + expect(result.status).toBe(10); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); +}); + describe("parseFreshOpenClawPluginExtensionDirs", () => { it("returns sorted validated directories for direct extension installs", () => { expect( diff --git a/src/lib/state/openclaw-plugin-restore.ts b/src/lib/state/openclaw-plugin-restore.ts index 675da48ddf..5671091b31 100644 --- a/src/lib/state/openclaw-plugin-restore.ts +++ b/src/lib/state/openclaw-plugin-restore.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { isRecord } from "../core/json-types.js"; +import { shellQuote } from "../core/shell-quote.js"; const MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS = 128; const OPENCLAW_EXTENSION_GLOB_CHARS = ["/", "\\", "*", "?", "[", "]"] as const; @@ -12,6 +13,34 @@ export type OpenClawManagedExtensionDiscoveryResult = | { ok: true; extensionDirs: string[] } | { ok: false; error: string }; +const OPENCLAW_PLUGIN_INDEX_SQLITE_PY = [ + "import json, sqlite3, sys, urllib.parse", + 'uri = "file:" + urllib.parse.quote(sys.argv[1], safe="/") + "?mode=ro"', + "conn = sqlite3.connect(uri, uri=True, timeout=30)", + "try:", + ' conn.execute("PRAGMA query_only=ON")', + ' conn.execute("PRAGMA busy_timeout=30000")', + " row = conn.execute(\"SELECT install_records_json FROM installed_plugin_index WHERE index_key = 'installed-plugin-index'\").fetchone()", + " if not row or not row[0]: raise SystemExit(12)", + " records = json.loads(row[0])", + " print(json.dumps({'version': 1, 'installRecords': records}, separators=(',', ':')))", + "finally:", + " conn.close()", +].join("\n"); + +export function buildFreshOpenClawPluginIndexSqliteReadCommand(dir: string): string { + const sqlitePath = `${dir.replace(/\/+$/, "")}/state/openclaw.sqlite`; + const quotedSqlitePath = shellQuote(sqlitePath); + return [ + `db=${quotedSqlitePath}`, + '[ -e "$db" ] || [ -L "$db" ] || exit 2', + '[ -f "$db" ] && [ ! -L "$db" ] || { echo "unsafe OpenClaw state database: $db" >&2; exit 10; }', + 'hardlink_count="$(find "$db" -maxdepth 0 -type f -links +1 -print 2>/dev/null | wc -l | tr -d " ")"', + '[ "${hardlink_count:-0}" = "0" ] || { echo "hard-linked OpenClaw state database rejected: $db" >&2; exit 11; }', + `python3 -c ${shellQuote(OPENCLAW_PLUGIN_INDEX_SQLITE_PY)} "$db"`, + ].join("; "); +} + function isSafeOpenClawPluginInstallId(id: string): boolean { if (id.length === 0 || id.length > 256 || /[\u0000-\u001f\u007f]/.test(id)) return false; const slash = id.indexOf("/"); diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index f84480c2a2..c5096a504b 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -46,6 +46,7 @@ import { shouldPreserveOpenClawManagedExtensions, } from "./openclaw-managed-extensions.js"; import { + buildFreshOpenClawPluginIndexSqliteReadCommand, type OpenClawManagedExtensionDiscoveryResult, parseFreshOpenClawPluginExtensionDirs, } from "./openclaw-plugin-restore.js"; @@ -656,18 +657,43 @@ function existingBackupDirs(backupPath: string, dirNames: string[]): string[] { return existing; } +function readFreshOpenClawPluginInstallIndex( + configFile: string, + sandboxName: string, + dir: string, +): ReturnType { + // OpenClaw 2026.6.10 moved install records into its shared SQLite state. + // Fall back only when that database is absent so a corrupt/incomplete + // canonical index cannot be masked by stale legacy JSON. + const sqliteResult = spawnSync( + "ssh", + [...sshArgs(configFile, sandboxName), buildFreshOpenClawPluginIndexSqliteReadCommand(dir)], + { + stdio: ["ignore", "pipe", "pipe"], + timeout: 30000, + maxBuffer: 4 * 1024 * 1024, + }, + ); + if (sqliteResult.status !== 2 || sqliteResult.error || sqliteResult.signal) return sqliteResult; + + const legacySpec: StateFileSpec = { path: "plugins/installs.json", strategy: "copy" }; + return spawnSync( + "ssh", + [...sshArgs(configFile, sandboxName), buildStateFileBackupCommand(dir, legacySpec)], + { + stdio: ["ignore", "pipe", "pipe"], + timeout: 30000, + maxBuffer: 4 * 1024 * 1024, + }, + ); +} + function discoverFreshOpenClawPluginExtensionDirs( configFile: string, sandboxName: string, dir: string, ): OpenClawManagedExtensionDiscoveryResult { - const spec: StateFileSpec = { path: "plugins/installs.json", strategy: "copy" }; - const command = buildStateFileBackupCommand(dir, spec); - const result = spawnSync("ssh", [...sshArgs(configFile, sandboxName), command], { - stdio: ["ignore", "pipe", "pipe"], - timeout: 30000, - maxBuffer: 4 * 1024 * 1024, - }); + const result = readFreshOpenClawPluginInstallIndex(configFile, sandboxName, dir); if (result.status !== 0 || result.error || result.signal || !result.stdout) { return { ok: false, error: "could not read fresh OpenClaw plugin install registry" }; } diff --git a/test/snapshot-openclaw-managed-extensions.test.ts b/test/snapshot-openclaw-managed-extensions.test.ts index d507678334..e26199750c 100644 --- a/test/snapshot-openclaw-managed-extensions.test.ts +++ b/test/snapshot-openclaw-managed-extensions.test.ts @@ -100,7 +100,10 @@ beforeEach(() => { }); describe("OpenClaw managed extension snapshot restore", () => { - it("preserves fresh built-in and custom image-managed OpenClaw extensions while restoring user extensions", () => { + it.each([ + "sqlite", + "legacy", + ] as const)("preserves fresh built-in and custom image-managed OpenClaw extensions from the %s install index", (installIndexSource) => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-extension-restore-")); const oldPath = process.env.PATH; const oldOpenshell = process.env.NEMOCLAW_OPENSHELL_BIN; @@ -155,6 +158,7 @@ const fs = require("node:fs"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); const cmd = process.argv[process.argv.length - 1] || ""; +const installIndexSource = ${JSON.stringify(installIndexSource)}; fs.appendFileSync(${JSON.stringify(sshLog)}, JSON.stringify({ cmd }) + "\\n"); function readStdin() { const chunks = []; @@ -166,7 +170,14 @@ function readStdin() { } return Buffer.concat(chunks); } -if (cmd.includes("plugins/installs.json") && cmd.includes("cat --")) { process.stdout.write(fs.readFileSync(${JSON.stringify(freshRegistryPath)})); process.exit(0); } +if (cmd.includes("installed_plugin_index") && cmd.includes("state/openclaw.sqlite")) { + if (installIndexSource === "sqlite") process.stdout.write(fs.readFileSync(${JSON.stringify(freshRegistryPath)})); + process.exit(installIndexSource === "sqlite" ? 0 : 2); +} +if (cmd.includes("plugins/installs.json") && cmd.includes("cat --")) { + if (installIndexSource === "legacy") process.stdout.write(fs.readFileSync(${JSON.stringify(freshRegistryPath)})); + process.exit(installIndexSource === "legacy" ? 0 : 2); +} if (cmd.includes("/sandbox/.openclaw/extensions") && cmd.includes("-exec rm -rf")) { const extensionsDir = ${JSON.stringify(extensionsDir)}; const managedExtensions = new Set(${JSON.stringify(managedExtensions)}); @@ -221,6 +232,10 @@ process.exit(0); (cmd) => cmd.includes("/sandbox/.openclaw/extensions") && cmd.includes("-exec rm -rf"), ); expect(cleanupCommands).toHaveLength(1); + expect(loggedCommands.some((cmd) => cmd.includes("installed_plugin_index"))).toBe(true); + expect(loggedCommands.some((cmd) => cmd.includes("plugins/installs.json"))).toBe( + installIndexSource === "legacy", + ); const cleanupCommand = cleanupCommands[0]; expect(cleanupCommand).not.toContain("rm -rf -- /sandbox/.openclaw/extensions"); for (const extensionName of managedExtensions) { From 05afc91f44e0a4ab6e9e47de63903946d09e7439 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 09:39:06 -0700 Subject: [PATCH 20/46] fix: address plugin restore review findings Signed-off-by: Aaron Erickson --- .../state/openclaw-managed-extensions.test.ts | 3 + src/lib/state/openclaw-managed-extensions.ts | 1 + .../openclaw-plugin-runtime-exdev.test.ts | 64 +++++++++++++------ 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/lib/state/openclaw-managed-extensions.test.ts b/src/lib/state/openclaw-managed-extensions.test.ts index d0be3dbe28..c6ce0a7a5f 100644 --- a/src/lib/state/openclaw-managed-extensions.test.ts +++ b/src/lib/state/openclaw-managed-extensions.test.ts @@ -139,6 +139,9 @@ describe("OpenClaw managed extension symlink policy", () => { "../../../../openclaw.json", ), ).toBe(false); + expect(isAllowedStateSymlink("extensions/nemoclaw/node_modules/.bin/leak", "../..")).toBe( + false, + ); expect( isAllowedStateSymlink("extensions/nemoclaw/node_modules/.bin/loop", "../.bin/other"), ).toBe(false); diff --git a/src/lib/state/openclaw-managed-extensions.ts b/src/lib/state/openclaw-managed-extensions.ts index 94eae1a2b2..2a07e2c75d 100644 --- a/src/lib/state/openclaw-managed-extensions.ts +++ b/src/lib/state/openclaw-managed-extensions.ts @@ -52,6 +52,7 @@ function isAllowedExtensionNpmBinSymlink(relPath: string, linkTarget: string): b return ( targetWithinNodeModules.length > 0 && + targetWithinNodeModules !== ".." && !targetWithinNodeModules.startsWith("../") && !path.posix.isAbsolute(targetWithinNodeModules) && !targetWithinNodeModules.startsWith(".bin/") diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 01704371d8..f21408c703 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; @@ -143,7 +144,13 @@ type PolicySourcePatch = { assertRestored(): void; }; -function patchPoliciesForDevShm(): PolicySourcePatch { +function patchPoliciesForDevShm( + policyPaths: readonly string[] = [ + path.join(REPO_ROOT, "agents", "openclaw", "policy-permissive.yaml"), + path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"), + path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"), + ], +): PolicySourcePatch { // Test-only source-boundary patch: the default OpenClaw policies intentionally // do not grant general /dev access, but this regression needs to create a // source tree on tmpfs (/dev/shm) to reproduce #3127's cross-device rename @@ -151,26 +158,27 @@ function patchPoliciesForDevShm(): PolicySourcePatch { // writing final artifacts, and remove this patch when OpenShell can mount a // dedicated test tmpfs without broadening checked-in production policy. const originals = new Map(); - for (const policyPath of [ - path.join(REPO_ROOT, "agents", "openclaw", "policy-permissive.yaml"), - path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"), - path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"), - ]) { - const text = fs.readFileSync(policyPath, "utf8"); - originals.set(policyPath, text); - const anchor = " read_write:\n - /tmp\n"; - expect(text, `could not find read_write /tmp anchor in ${policyPath}`).toContain(anchor); - let additions = ""; - for (const entry of ["/dev", "/dev/shm"]) { - if (!text.includes(` - ${entry}\n`)) additions += ` - ${entry}\n`; - } - if (additions) { - fs.writeFileSync(policyPath, text.replace(anchor, anchor + additions), "utf8"); - } - } const restore = () => { for (const [policyPath, text] of originals) fs.writeFileSync(policyPath, text, "utf8"); }; + try { + for (const policyPath of policyPaths) { + const text = fs.readFileSync(policyPath, "utf8"); + originals.set(policyPath, text); + const anchor = " read_write:\n - /tmp\n"; + expect(text, `could not find read_write /tmp anchor in ${policyPath}`).toContain(anchor); + let additions = ""; + for (const entry of ["/dev", "/dev/shm"]) { + if (!text.includes(` - ${entry}\n`)) additions += ` - ${entry}\n`; + } + if (additions) { + fs.writeFileSync(policyPath, text.replace(anchor, anchor + additions), "utf8"); + } + } + } catch (error) { + restore(); + throw error; + } return { restore, assertRestored: () => { @@ -181,6 +189,26 @@ function patchPoliciesForDevShm(): PolicySourcePatch { }; } +test("policy fixture setup restores earlier mutations when a later policy fails validation", () => { + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-exdev-policy-patch-")); + const firstPolicy = path.join(fixture, "first.yaml"); + const invalidPolicy = path.join(fixture, "invalid.yaml"); + const firstOriginal = "filesystem:\n read_write:\n - /tmp\n"; + const invalidOriginal = "filesystem:\n read_write:\n - /var/tmp\n"; + try { + fs.writeFileSync(firstPolicy, firstOriginal, "utf8"); + fs.writeFileSync(invalidPolicy, invalidOriginal, "utf8"); + + expect(() => patchPoliciesForDevShm([firstPolicy, invalidPolicy])).toThrow( + `could not find read_write /tmp anchor in ${invalidPolicy}`, + ); + expect(fs.readFileSync(firstPolicy, "utf8")).toBe(firstOriginal); + expect(fs.readFileSync(invalidPolicy, "utf8")).toBe(invalidOriginal); + } finally { + fs.rmSync(fixture, { recursive: true, force: true }); + } +}); + function writeCustomPluginVersion(version: WeatherFixtureVersion): void { fs.writeFileSync( CUSTOM_PLUGIN_VERSION_SOURCE, From 50a22ffd733dc05b7823c11efb19ebdf31ccc154 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 10:09:43 -0700 Subject: [PATCH 21/46] test: isolate EXDEV probe from production policy Signed-off-by: Aaron Erickson --- .../openclaw-plugin-runtime-exdev.test.ts | 360 ++++++++++++++---- 1 file changed, 276 insertions(+), 84 deletions(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index f21408c703..c8e2e089e6 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -2,11 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { resolveOpenshell } from "../../../src/lib/adapters/openshell/resolve.ts"; +import { hasRequiredOpenshellMessagingFeatures } from "../../../src/lib/onboard/openshell-feature-gate.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; +import { shellQuote } from "../fixtures/clients/command.ts"; +import type { HostCliClient } from "../fixtures/clients/host.ts"; import { type SandboxClient, trustedSandboxShellScript, @@ -59,6 +64,28 @@ const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-plugin-e const ONBOARD_TIMEOUT_MS = 25 * 60_000; const REBUILD_TIMEOUT_MS = 20 * 60_000; const PROBE_TIMEOUT_MS = 60_000; +const EXDEV_TMPFS_MOUNT = "/tmp/nemoclaw-exdev-tmpfs"; +const EXDEV_TMPFS_SOURCE = `${EXDEV_TMPFS_MOUNT}/source`; +const EXDEV_TMPFS_MOUNT_CONFIG = { + type: "tmpfs", + target: EXDEV_TMPFS_MOUNT, + options: ["rw", "nosuid", "nodev", "noexec"], + size_bytes: 16_777_216, + mode: 0o1777, +} as const; +const EXDEV_TMPFS_DRIVER_CONFIG = JSON.stringify({ + docker: { + mounts: [EXDEV_TMPFS_MOUNT_CONFIG], + }, + podman: { + mounts: [EXDEV_TMPFS_MOUNT_CONFIG], + }, +}); +const STOCK_OPENCLAW_POLICY_PATHS = [ + path.join(REPO_ROOT, "agents", "openclaw", "policy-permissive.yaml"), + path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"), + path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"), +] as const; validateSandboxName(SANDBOX_NAME); const EXDEV_PATTERNS = [ @@ -139,74 +166,195 @@ async function ignoreCleanupError(run: () => Promise): Promise { } } -type PolicySourcePatch = { - restore(): void; - assertRestored(): void; +type OpenShellTmpfsWrapper = { + directory: string; + executable: string; + remove(): void; }; -function patchPoliciesForDevShm( - policyPaths: readonly string[] = [ - path.join(REPO_ROOT, "agents", "openclaw", "policy-permissive.yaml"), - path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"), - path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"), - ], -): PolicySourcePatch { - // Test-only source-boundary patch: the default OpenClaw policies intentionally - // do not grant general /dev access, but this regression needs to create a - // source tree on tmpfs (/dev/shm) to reproduce #3127's cross-device rename - // layout. Keep the mutation local, restore and verify the source bytes before - // writing final artifacts, and remove this patch when OpenShell can mount a - // dedicated test tmpfs without broadening checked-in production policy. - const originals = new Map(); - const restore = () => { - for (const [policyPath, text] of originals) fs.writeFileSync(policyPath, text, "utf8"); - }; - try { - for (const policyPath of policyPaths) { - const text = fs.readFileSync(policyPath, "utf8"); - originals.set(policyPath, text); - const anchor = " read_write:\n - /tmp\n"; - expect(text, `could not find read_write /tmp anchor in ${policyPath}`).toContain(anchor); - let additions = ""; - for (const entry of ["/dev", "/dev/shm"]) { - if (!text.includes(` - ${entry}\n`)) additions += ` - ${entry}\n`; - } - if (additions) { - fs.writeFileSync(policyPath, text.replace(anchor, anchor + additions), "utf8"); - } - } - } catch (error) { - restore(); - throw error; +type PinnedOpenShellComponents = { + cli: string; + gateway: string; + sandbox: string; +}; + +function createOpenShellTmpfsWrapper(realOpenshellPath: string): OpenShellTmpfsWrapper { + if (!path.isAbsolute(realOpenshellPath)) { + throw new Error("real OpenShell path must be absolute"); } + fs.accessSync(realOpenshellPath, fs.constants.X_OK); + + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-exdev-openshell-wrapper-")); + const executable = path.join(directory, "openshell"); + const script = `#!/bin/sh +set -eu +if [ "$#" -ge 2 ] && [ "$1" = sandbox ] && [ "$2" = create ]; then + shift 2 + for argument in "$@"; do + case "$argument" in + --driver-config-json|--driver-config-json=*) + printf '%s\n' 'refusing duplicate --driver-config-json in EXDEV test wrapper' >&2 + exit 64 + ;; + esac + done + exec ${shellQuote(realOpenshellPath)} sandbox create --driver-config-json ${shellQuote(EXDEV_TMPFS_DRIVER_CONFIG)} "$@" +fi +exec ${shellQuote(realOpenshellPath)} "$@" +`; + fs.writeFileSync(executable, script, { encoding: "utf8", mode: 0o700 }); + return { - restore, - assertRestored: () => { - for (const [policyPath, text] of originals) { - expect(fs.readFileSync(policyPath, "utf8"), `${policyPath} was not restored`).toBe(text); - } - }, + directory, + executable, + remove: () => fs.rmSync(directory, { recursive: true, force: true }), }; } -test("policy fixture setup restores earlier mutations when a later policy fails validation", () => { - const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-exdev-policy-patch-")); - const firstPolicy = path.join(fixture, "first.yaml"); - const invalidPolicy = path.join(fixture, "invalid.yaml"); - const firstOriginal = "filesystem:\n read_write:\n - /tmp\n"; - const invalidOriginal = "filesystem:\n read_write:\n - /var/tmp\n"; - try { - fs.writeFileSync(firstPolicy, firstOriginal, "utf8"); - fs.writeFileSync(invalidPolicy, invalidOriginal, "utf8"); +function withOpenShellWrapperEnv( + env: NodeJS.ProcessEnv, + wrapper: OpenShellTmpfsWrapper, + components: PinnedOpenShellComponents, +): NodeJS.ProcessEnv { + return { + ...env, + PATH: `${wrapper.directory}${path.delimiter}${env.PATH ?? ""}`, + NEMOCLAW_OPENSHELL_BIN: wrapper.executable, + NEMOCLAW_OPENSHELL_GATEWAY_BIN: components.gateway, + NEMOCLAW_OPENSHELL_SANDBOX_BIN: components.sandbox, + }; +} + +function resolvePinnedOpenShellComponents(openshellPath: string): PinnedOpenShellComponents { + const cli = fs.realpathSync(openshellPath); + fs.accessSync(cli, fs.constants.X_OK); + const installDirectory = path.dirname(cli); + const canonicalSibling = (name: string): string => { + const sibling = fs.realpathSync(path.join(installDirectory, name)); + fs.accessSync(sibling, fs.constants.X_OK); + return sibling; + }; + return { + cli, + gateway: canonicalSibling("openshell-gateway"), + sandbox: canonicalSibling("openshell-sandbox"), + }; +} - expect(() => patchPoliciesForDevShm([firstPolicy, invalidPolicy])).toThrow( - `could not find read_write /tmp anchor in ${invalidPolicy}`, +async function installAndResolvePinnedOpenShell( + host: HostCliClient, +): Promise { + const install = await host.command( + "bash", + [path.join(REPO_ROOT, "scripts", "install-openshell.sh")], + { + artifactName: "install-pinned-openshell-for-exdev-wrapper", + env: liveEnv(), + timeoutMs: 5 * 60_000, + }, + ); + expect(install.exitCode, resultText(install)).toBe(0); + const resolved = resolveOpenshell(); + expect(resolved, "pinned OpenShell installer did not leave an executable CLI").not.toBeNull(); + return resolvePinnedOpenShellComponents(resolved as string); +} + +type PolicySourceSnapshot = ReadonlyArray<{ policyPath: string; bytes: Buffer }>; + +function snapshotPolicySources(): PolicySourceSnapshot { + return STOCK_OPENCLAW_POLICY_PATHS.map((policyPath) => ({ + policyPath, + bytes: fs.readFileSync(policyPath), + })); +} + +function assertPolicySourcesUnchanged(snapshot: PolicySourceSnapshot, phase: string): void { + for (const { policyPath, bytes } of snapshot) { + expect(fs.readFileSync(policyPath), `${policyPath} changed during ${phase}`).toEqual(bytes); + } +} + +function runWrapper(wrapper: string, args: readonly string[]): string[] { + const result = spawnSync(wrapper, args, { encoding: "utf8" }); + expect(result.status, result.stderr).toBe(0); + return result.stdout.trimEnd().split("\n"); +} + +test("OpenShell wrapper injects only the reviewed tmpfs config into sandbox create", () => { + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-exdev-wrapper-contract-")); + const delegate = path.join(fixture, "real-openshell"); + const gateway = path.join(fixture, "openshell-gateway"); + const sandbox = path.join(fixture, "openshell-sandbox"); + const executableSource = "#!/bin/sh\nprintf '%s\\n' \"$@\"\n"; + for (const executable of [delegate, gateway, sandbox]) { + fs.writeFileSync(executable, executableSource, { + encoding: "utf8", + mode: 0o700, + }); + } + const components = resolvePinnedOpenShellComponents(delegate); + const wrapper = createOpenShellTmpfsWrapper(components.cli); + try { + expect(components).toEqual({ + cli: fs.realpathSync(delegate), + gateway: fs.realpathSync(gateway), + sandbox: fs.realpathSync(sandbox), + }); + expect(withOpenShellWrapperEnv({ PATH: "/usr/bin" }, wrapper, components)).toMatchObject({ + PATH: `${wrapper.directory}${path.delimiter}/usr/bin`, + NEMOCLAW_OPENSHELL_BIN: wrapper.executable, + NEMOCLAW_OPENSHELL_GATEWAY_BIN: components.gateway, + NEMOCLAW_OPENSHELL_SANDBOX_BIN: components.sandbox, + }); + expect(JSON.parse(EXDEV_TMPFS_DRIVER_CONFIG)).toEqual({ + docker: { + mounts: [EXDEV_TMPFS_MOUNT_CONFIG], + }, + podman: { + mounts: [EXDEV_TMPFS_MOUNT_CONFIG], + }, + }); + expect( + runWrapper(wrapper.executable, [ + "sandbox", + "create", + "--name", + "demo", + "--", + "sh", + "-lc", + "printf value", + ]), + ).toEqual([ + "sandbox", + "create", + "--driver-config-json", + EXDEV_TMPFS_DRIVER_CONFIG, + "--name", + "demo", + "--", + "sh", + "-lc", + "printf value", + ]); + expect(runWrapper(wrapper.executable, ["sandbox", "delete", "demo"])).toEqual([ + "sandbox", + "delete", + "demo", + ]); + expect(runWrapper(wrapper.executable, ["--version"])).toEqual(["--version"]); + const duplicateConfig = spawnSync( + wrapper.executable, + ["sandbox", "create", "--driver-config-json", "{}"], + { encoding: "utf8" }, ); - expect(fs.readFileSync(firstPolicy, "utf8")).toBe(firstOriginal); - expect(fs.readFileSync(invalidPolicy, "utf8")).toBe(invalidOriginal); + expect(duplicateConfig.status).toBe(64); + expect(duplicateConfig.stderr).toContain("refusing duplicate --driver-config-json"); } finally { + wrapper.remove(); fs.rmSync(fixture, { recursive: true, force: true }); } + expect(fs.existsSync(wrapper.directory)).toBe(false); }); function writeCustomPluginVersion(version: WeatherFixtureVersion): void { @@ -436,16 +584,39 @@ printf '%s\\n' "$actual"`), }; } +async function assertExdevTmpfsMounted(sandbox: SandboxClient, phase: string): Promise { + const result = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript(`set -eu +awk -v target='${EXDEV_TMPFS_MOUNT}' '$5 == target { found = 1 } END { exit found ? 0 : 1 }' /proc/self/mountinfo +mkdir -p ${EXDEV_TMPFS_SOURCE} +test -d ${EXDEV_TMPFS_SOURCE} +mount_device=$(stat -c '%d' ${EXDEV_TMPFS_MOUNT}) +tmp_device=$(stat -c '%d' /tmp) +test "$mount_device" != "$tmp_device" +printf 'tmpfs_mount=%s source=%s mount_device=%s tmp_device=%s\n' '${EXDEV_TMPFS_MOUNT}' '${EXDEV_TMPFS_SOURCE}' "$mount_device" "$tmp_device"`), + { + artifactName: `openclaw-plugin-exdev-tmpfs-${phase}`, + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, + ); + expect(result.exitCode, resultText(result)).toBe(0); + expect(resultText(result)).toContain(`tmpfs_mount=${EXDEV_TMPFS_MOUNT}`); + expect(resultText(result)).toContain(`source=${EXDEV_TMPFS_SOURCE}`); + return true; +} + const runtimeDepsReplacementProbeSource = `set -eu rm -rf /sandbox/.openclaw/plugin-runtime-deps/exdev-guard 2>/dev/null || true -rm -rf /dev/shm/nemoclaw-exdev-source 2>/dev/null || true -mkdir -p /dev/shm/nemoclaw-exdev-source /sandbox/.openclaw/plugin-runtime-deps/exdev-guard -printf 'ok\n' >/dev/shm/nemoclaw-exdev-source/package.txt -source_device=$(stat -c '%d' /dev/shm/nemoclaw-exdev-source) +rm -rf ${EXDEV_TMPFS_SOURCE} +mkdir -p ${EXDEV_TMPFS_SOURCE} /sandbox/.openclaw/plugin-runtime-deps/exdev-guard +printf 'ok\n' >${EXDEV_TMPFS_SOURCE}/package.txt +source_device=$(stat -c '%d' ${EXDEV_TMPFS_SOURCE}) target_device=$(stat -c '%d' /sandbox/.openclaw/plugin-runtime-deps/exdev-guard) printf 'source_device=%s target_device=%s\n' "$source_device" "$target_device" if [ "$source_device" = "$target_device" ]; then - printf 'EXDEV guard did not get distinct filesystems for /dev/shm and /sandbox plugin-runtime-deps\n' >&2 + printf 'EXDEV guard did not get distinct filesystems for ${EXDEV_TMPFS_SOURCE} and /sandbox plugin-runtime-deps\n' >&2 exit 2 fi node --input-type=module - <<'NODE' @@ -502,9 +673,9 @@ function replaceNodeModulesDir(targetDir, sourceDir) { } assertLegacySourceSideStagingFailsWithExdev( '/sandbox/.openclaw/plugin-runtime-deps/exdev-guard/source-side-regression/node_modules', - '/dev/shm/nemoclaw-exdev-source', + '${EXDEV_TMPFS_SOURCE}', ); -replaceNodeModulesDir('/sandbox/.openclaw/plugin-runtime-deps/exdev-guard/node_modules', '/dev/shm/nemoclaw-exdev-source'); +replaceNodeModulesDir('/sandbox/.openclaw/plugin-runtime-deps/exdev-guard/node_modules', '${EXDEV_TMPFS_SOURCE}'); console.log('runtime deps replacement completed'); NODE`; @@ -526,8 +697,10 @@ liveTest( "release-matched peer/dev dependencies prune private OpenClaw and link the host runtime", "gateway log, runtime inspection, tools.catalog, and tools.invoke prove weather/get_weather", "custom-plugin v1 survives restart and a rebuilt v2 replaces it without backup rollback", - "sandbox proves /dev/shm and plugin-runtime-deps are distinct devices", - "legacy source-side staging fails with EXDEV across the same /dev/shm to plugin-runtime-deps boundary", + `test-only driver config mounts tmpfs at ${EXDEV_TMPFS_MOUNT} without changing production policies`, + "stock OpenClaw policy source bytes remain unchanged through onboard and rebuild", + `sandbox proves ${EXDEV_TMPFS_SOURCE} and plugin-runtime-deps are distinct devices`, + `legacy source-side staging fails with EXDEV across the same ${EXDEV_TMPFS_SOURCE} to plugin-runtime-deps boundary`, "OpenClaw-style target-side plugin runtime-deps replacement completes without EXDEV", ], sandboxBaseImageRef: SANDBOX_BASE_IMAGE_REF, @@ -586,22 +759,38 @@ liveTest( }), ); - const policySourcePatch = patchPoliciesForDevShm(); - cleanup.add("restore EXDEV policy fixture edits", policySourcePatch.restore); + const policySourceSnapshot = snapshotPolicySources(); + const pinnedOpenshell = await installAndResolvePinnedOpenShell(host); + const openshellWrapper = createOpenShellTmpfsWrapper(pinnedOpenshell.cli); + cleanup.add("remove EXDEV OpenShell PATH wrapper", openshellWrapper.remove); + expect( + hasRequiredOpenshellMessagingFeatures({ + openshellBin: openshellWrapper.executable, + gatewayBin: pinnedOpenshell.gateway, + sandboxBin: pinnedOpenshell.sandbox, + allowExternalGatewayBin: true, + allowExternalSandboxBin: true, + }), + "OpenShell wrapper and explicit pinned components must pass onboard coherence preflight", + ).toBe(true); const removeCustomDockerfile = createCustomPluginDockerfile(); cleanup.add("remove custom weather-plugin Dockerfile", removeCustomDockerfile); - const sandboxEnv = liveEnv({ - COMPATIBLE_API_KEY: "nemoclaw-exdev-dummy-key", - NEMOCLAW_ENDPOINT_URL: "http://host.openshell.internal:65535/v1", - NEMOCLAW_MODEL: "nemoclaw-exdev-probe", - NEMOCLAW_PROVIDER_KEY: "nemoclaw-exdev-dummy-key", - NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, - NEMOCLAW_SANDBOX_BASE_IMAGE_REF: SANDBOX_BASE_IMAGE_REF, - NEMOCLAW_POLICY_MODE: "skip", - NEMOCLAW_PREFERRED_API: "openai-completions", - NEMOCLAW_PROVIDER: "custom", - }); + const sandboxEnv = withOpenShellWrapperEnv( + liveEnv({ + COMPATIBLE_API_KEY: "nemoclaw-exdev-dummy-key", + NEMOCLAW_ENDPOINT_URL: "http://host.openshell.internal:65535/v1", + NEMOCLAW_MODEL: "nemoclaw-exdev-probe", + NEMOCLAW_PROVIDER_KEY: "nemoclaw-exdev-dummy-key", + NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, + NEMOCLAW_SANDBOX_BASE_IMAGE_REF: SANDBOX_BASE_IMAGE_REF, + NEMOCLAW_POLICY_MODE: "skip", + NEMOCLAW_PREFERRED_API: "openai-completions", + NEMOCLAW_PROVIDER: "custom", + }), + openshellWrapper, + pinnedOpenshell, + ); const onboard = await host.command( "node", @@ -626,6 +815,8 @@ liveTest( expect(onboard.exitCode, onboardText).toBe(0); expect(onboardText).toMatch(/Creating sandbox|Sandbox '.+' created/); expect(onboardText).toContain("Deployment verified"); + const tmpfsMountedAfterOnboard = await assertExdevTmpfsMounted(sandbox, "after-onboard"); + assertPolicySourcesUnchanged(policySourceSnapshot, "onboard"); const weatherAfterOnboard = await assertWeatherPluginRuntime(sandbox, "after-onboard", "v1"); @@ -652,13 +843,15 @@ liveTest( timeoutMs: REBUILD_TIMEOUT_MS, }); expect(rebuild.exitCode, resultText(rebuild)).toBe(0); + const tmpfsMountedAfterRebuild = await assertExdevTmpfsMounted(sandbox, "after-rebuild"); + assertPolicySourcesUnchanged(policySourceSnapshot, "rebuild"); const weatherAfterRebuild = await assertWeatherPluginRuntime(sandbox, "after-rebuild", "v2"); expect(weatherAfterRebuild.imageMarker).not.toBe(weatherAfterOnboard.imageMarker); const df = await sandbox.execShell( SANDBOX_NAME, trustedSandboxShellScript( - "mkdir -p /sandbox/.openclaw/plugin-runtime-deps && df -PT / /tmp /dev/shm /sandbox /sandbox/.openclaw/plugin-runtime-deps", + `mkdir -p ${EXDEV_TMPFS_SOURCE} /sandbox/.openclaw/plugin-runtime-deps && df -PT / /tmp ${EXDEV_TMPFS_MOUNT} ${EXDEV_TMPFS_SOURCE} /sandbox /sandbox/.openclaw/plugin-runtime-deps`, ), { artifactName: "openclaw-plugin-exdev-filesystem-layout", @@ -668,7 +861,7 @@ liveTest( ); await artifacts.writeText("filesystem-layout.txt", resultText(df)); expect(df.exitCode, resultText(df)).toBe(0); - expect(resultText(df)).toContain("/dev/shm"); + expect(resultText(df)).toContain(EXDEV_TMPFS_MOUNT); const probe = await sandbox.execShell(SANDBOX_NAME, runtimeDepsReplacementProbe, { artifactName: "openclaw-plugin-exdev-runtime-deps-replacement", @@ -685,9 +878,6 @@ liveTest( expect(probeText).toContain("source-side staging failure self-check completed"); expect(probeText).toContain("runtime deps replacement completed"); - policySourcePatch.restore(); - policySourcePatch.assertRestored(); - await artifacts.writeJson("target-result.json", { id: "openclaw-plugin-runtime-exdev", onboardExitCode: onboard.exitCode, @@ -695,6 +885,7 @@ liveTest( rebuildExitCode: rebuild.exitCode, filesystemProbeExitCode: df.exitCode, runtimeDepsProbeExitCode: probe.exitCode, + testOnlyTmpfsSource: EXDEV_TMPFS_SOURCE, assertions: { weatherAfterOnboard: weatherAfterOnboard.inspectLoaded && @@ -721,7 +912,8 @@ liveTest( ), noExdevSignature: !EXDEV_PATTERNS.some((pattern) => pattern.test(probeText)), successMarker: probeText.includes("runtime deps replacement completed"), - policySourcesRestored: true, + testOnlyTmpfsMounted: tmpfsMountedAfterOnboard && tmpfsMountedAfterRebuild, + stockPolicySourcesUnchanged: true, }, }); }, From 29ce5b3d8e4be321f680c601cc38fc32f63c2b99 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 10:19:06 -0700 Subject: [PATCH 22/46] test: attest delegated OpenShell capabilities Signed-off-by: Aaron Erickson --- .../openclaw-plugin-runtime-exdev.test.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index c8e2e089e6..fdbc864306 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -8,7 +8,10 @@ import os from "node:os"; import path from "node:path"; import { resolveOpenshell } from "../../../src/lib/adapters/openshell/resolve.ts"; -import { hasRequiredOpenshellMessagingFeatures } from "../../../src/lib/onboard/openshell-feature-gate.ts"; +import { + hasRequiredOpenshellMessagingFeatures, + REQUIRED_OPENSHELL_MCP_FEATURES, +} from "../../../src/lib/onboard/openshell-feature-gate.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import { shellQuote } from "../fixtures/clients/command.ts"; import type { HostCliClient } from "../fixtures/clients/host.ts"; @@ -81,6 +84,8 @@ const EXDEV_TMPFS_DRIVER_CONFIG = JSON.stringify({ mounts: [EXDEV_TMPFS_MOUNT_CONFIG], }, }); +const DELEGATED_CAPABILITY_COMMENT_PREFIX = + "# TEST-ONLY delegated-capability marker from validated canonical OpenShell: "; const STOCK_OPENCLAW_POLICY_PATHS = [ path.join(REPO_ROOT, "agents", "openclaw", "policy-permissive.yaml"), path.join(REPO_ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"), @@ -186,7 +191,12 @@ function createOpenShellTmpfsWrapper(realOpenshellPath: string): OpenShellTmpfsW const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-exdev-openshell-wrapper-")); const executable = path.join(directory, "openshell"); + const delegatedCapabilityComments = REQUIRED_OPENSHELL_MCP_FEATURES.map((marker) => { + assert.match(marker, /^[A-Za-z0-9_-]+$/, "delegated OpenShell capability marker must be safe"); + return `${DELEGATED_CAPABILITY_COMMENT_PREFIX}${marker}`; + }).join("\n"); const script = `#!/bin/sh +${delegatedCapabilityComments} set -eu if [ "$#" -ge 2 ] && [ "$1" = sandbox ] && [ "$2" = create ]; then shift 2 @@ -295,6 +305,19 @@ test("OpenShell wrapper injects only the reviewed tmpfs config into sandbox crea const components = resolvePinnedOpenShellComponents(delegate); const wrapper = createOpenShellTmpfsWrapper(components.cli); try { + const wrapperSource = fs.readFileSync(wrapper.executable, "utf8"); + expect( + wrapperSource + .split("\n") + .filter((line) => line.startsWith(DELEGATED_CAPABILITY_COMMENT_PREFIX)), + ).toEqual( + REQUIRED_OPENSHELL_MCP_FEATURES.map( + (marker) => `${DELEGATED_CAPABILITY_COMMENT_PREFIX}${marker}`, + ), + ); + for (const marker of REQUIRED_OPENSHELL_MCP_FEATURES) { + expect(wrapperSource.split(marker)).toHaveLength(2); + } expect(components).toEqual({ cli: fs.realpathSync(delegate), gateway: fs.realpathSync(gateway), @@ -761,6 +784,14 @@ liveTest( const policySourceSnapshot = snapshotPolicySources(); const pinnedOpenshell = await installAndResolvePinnedOpenShell(host); + expect( + hasRequiredOpenshellMessagingFeatures({ + openshellBin: pinnedOpenshell.cli, + gatewayBin: pinnedOpenshell.gateway, + sandboxBin: pinnedOpenshell.sandbox, + }), + "canonical pinned OpenShell components must pass coherence preflight before delegation", + ).toBe(true); const openshellWrapper = createOpenShellTmpfsWrapper(pinnedOpenshell.cli); cleanup.add("remove EXDEV OpenShell PATH wrapper", openshellWrapper.remove); expect( From 60c031a4b51f97599d6f74d4d6c13bbeed8bf35a Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 4 Jul 2026 10:35:40 -0700 Subject: [PATCH 23/46] test(e2e): use portable tmpfs options Signed-off-by: Aaron Erickson --- test/e2e/live/openclaw-plugin-runtime-exdev.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index fdbc864306..b4a68a54b5 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -72,7 +72,9 @@ const EXDEV_TMPFS_SOURCE = `${EXDEV_TMPFS_MOUNT}/source`; const EXDEV_TMPFS_MOUNT_CONFIG = { type: "tmpfs", target: EXDEV_TMPFS_MOUNT, - options: ["rw", "nosuid", "nodev", "noexec"], + // tmpfs is read-write by default. Docker's MountTmpfsOptions rejects `rw`, + // `nosuid`, and `nodev`; `noexec` is supported by both pinned drivers. + options: ["noexec"], size_bytes: 16_777_216, mode: 0o1777, } as const; @@ -329,6 +331,7 @@ test("OpenShell wrapper injects only the reviewed tmpfs config into sandbox crea NEMOCLAW_OPENSHELL_GATEWAY_BIN: components.gateway, NEMOCLAW_OPENSHELL_SANDBOX_BIN: components.sandbox, }); + expect(EXDEV_TMPFS_MOUNT_CONFIG.options).toEqual(["noexec"]); expect(JSON.parse(EXDEV_TMPFS_DRIVER_CONFIG)).toEqual({ docker: { mounts: [EXDEV_TMPFS_MOUNT_CONFIG], From d36bec35b3b2a59f72d01786daf5f60d232294ab Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sun, 5 Jul 2026 20:56:26 -0700 Subject: [PATCH 24/46] test(package): allow cold oclif discovery Signed-off-by: Aaron Erickson --- test/package-contract/cli/config-set-cli-dispatch.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index 5ba2a454e1..b052a532a3 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -93,7 +93,8 @@ describe("config set CLI dispatch", () => { settled = true; }); - await vi.waitFor(() => expect(configSet).toHaveBeenCalledTimes(1)); + // Cold oclif command discovery can exceed waitFor's 1s default on shared CI runners. + await vi.waitFor(() => expect(configSet).toHaveBeenCalledTimes(1), { timeout: 5_000 }); expect(configSet).toHaveBeenCalledTimes(1); expect(configSet).toHaveBeenCalledWith("test-sandbox", { key: "inference.endpoints", @@ -125,5 +126,5 @@ describe("config set CLI dispatch", () => { if (priorRunner) requireCache[runnerPath] = priorRunner; else delete requireCache[runnerPath]; } - }); + }, 10_000); }); From cad32ae00abee40737bd30baeee87ecbf7bd6bb1 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 10:54:57 -0700 Subject: [PATCH 25/46] test(rebuild): isolate prepared recovery notice state Signed-off-by: Aaron Erickson --- .../sandbox/rebuild-prepared-recovery.test.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts index ede97c4bbd..6fe1dbedbe 100644 --- a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts @@ -1,24 +1,17 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createRebuildFlowHarness, makePreparedRecoveryManifest, - snapshotEnv, + resetRebuildFlowTestEnvironment, + restoreRebuildFlowTestEnvironment, } from "../../../../test/helpers/rebuild-flow-harness"; -const restoreSandboxEnv = snapshotEnv(["NEMOCLAW_SANDBOX_NAME"]); - describe("prepared rebuild recovery", () => { - beforeEach(() => { - delete process.env.NEMOCLAW_SANDBOX_NAME; - }); - - afterEach(() => { - vi.restoreAllMocks(); - restoreSandboxEnv(); - }); + beforeEach(resetRebuildFlowTestEnvironment); + afterEach(restoreRebuildFlowTestEnvironment); it("restores the validated pre-upgrade manifest without taking a second backup (#6114)", async () => { const harness = createRebuildFlowHarness({ From 57f32d8a33b4d9e20bac22b55d28418f5ee7a479 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 12:16:45 -0700 Subject: [PATCH 26/46] docs: align plugin links with current route Signed-off-by: Aaron Erickson --- docs/reference/commands.mdx | 2 +- docs/reference/troubleshooting.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 0b6fb17621..66b821a09f 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -484,7 +484,7 @@ Missing paths fail during command parsing before preflight, gateway setup, infer If deployment verification cannot reach the gateway for a custom OpenClaw image, NemoClaw checks for `/tmp/gateway.log`, `/usr/local/bin/nemoclaw-start`, and `/sandbox/.openclaw/openclaw.json`. When all three paths are absent, onboarding reports that the custom image lacks the managed runtime instead of treating repeated port-forward retries as the recovery path. -For the version-matched full-runtime plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). +For the version-matched full-runtime plugin workflow, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 321c62017b..21fa7266a1 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -492,7 +492,7 @@ When all three paths are absent, the CLI reports that the image lacks the NemoCl This failure commonly occurs when the custom Dockerfile starts from `ghcr.io/nvidia/nemoclaw/sandbox-base` alone because that image is an intermediate dependency image. Rebuild the custom image from the full stock Dockerfile and source context for the same NemoClaw release. -For the version-pinned plugin workflow, refer to [Install OpenClaw Plugins](../manage-sandboxes/install-openclaw-plugins). +For the version-pinned plugin workflow, refer to [Install OpenClaw Plugins](../deployment/install-openclaw-plugins). If the sandbox is unreachable or the managed runtime paths are present, NemoClaw retains the existing generic gateway-log and host OpenShell-log guidance because the base-only failure is not proven. From ec51a235c113af6a9ed14e35a8d09e4cb6e08024 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 12:54:42 -0700 Subject: [PATCH 27/46] refactor(state): consolidate restore options Signed-off-by: Aaron Erickson --- src/lib/state/sandbox.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 2a3fead4a2..50714c70cc 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -158,9 +158,6 @@ export interface RestoreOptions { * directories over the backup. */ preserveFreshOpenClawPluginInstalls?: boolean; -} - -export interface RestoreOptions { /** Optional file-specific restore capability authorized by the caller. */ stateFileRestorePolicy?: StateFileRestorePolicy; } From ff99de2456c2036c8677ff1ca5e2cc8a17e4c718 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 13:05:52 -0700 Subject: [PATCH 28/46] test(e2e): skip dcode re-onboard for other agents Signed-off-by: Aaron Erickson --- .../04-deepagents-code-fresh-reonboard.sh | 8 +++++ ...platform-parity-cloud-experimental.test.ts | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh index 4e5de37a30..acf81b5265 100755 --- a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh +++ b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh @@ -24,6 +24,11 @@ fail() { exit 1 } +skip() { + printf '%s: SKIP: %s\n' "$PREFIX" "$1" + exit 0 +} + pass() { printf '%s: OK (%s)\n' "$PREFIX" "$1" } @@ -136,6 +141,9 @@ PY } [ -n "$SANDBOX_NAME" ] || fail "sandbox name is required" +if ! sandbox_exec "test -d /sandbox/.deepagents && command -v dcode >/dev/null 2>&1" >/dev/null; then + skip "sandbox '${SANDBOX_NAME}' is not a Deep Agents Code sandbox" +fi [ -n "${COMPATIBLE_API_KEY:-}" ] || fail "COMPATIBLE_API_KEY is required" [ -x "$CLI" ] || fail "NemoClaw CLI is not executable at $CLI" diff --git a/test/e2e/support/platform-parity-cloud-experimental.test.ts b/test/e2e/support/platform-parity-cloud-experimental.test.ts index 0fa4e277a5..5303ba9d6b 100644 --- a/test/e2e/support/platform-parity-cloud-experimental.test.ts +++ b/test/e2e/support/platform-parity-cloud-experimental.test.ts @@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; @@ -32,6 +33,36 @@ function shellResult(exitCode: number, stdout: string, stderr = ""): ShellProbeR } describe("P0-E cloud-experimental parity guardrails", () => { + it("skips the destructive fresh re-onboard check outside a Deep Agents sandbox", () => { + const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-fake-openshell-")); + try { + fs.writeFileSync(path.join(binDir, "openshell"), "#!/bin/sh\nexit 1\n", { mode: 0o755 }); + const result = spawnSync( + "bash", + [ + path.join( + process.cwd(), + "test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh", + ), + ], + { + encoding: "utf8", + env: { + PATH: `${binDir}:${process.env.PATH ?? "/usr/bin:/bin"}`, + SANDBOX_NAME: "openclaw-sandbox", + }, + }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain( + "SKIP: sandbox 'openclaw-sandbox' is not a Deep Agents Code sandbox", + ); + } finally { + fs.rmSync(binDir, { force: true, recursive: true }); + } + }); + it("fails required Deep Agents cloud-experimental checks when scripts print SKIP", () => { expect(() => assertRequiredCloudExperimentalResult( From 052bf441ef100462da2ca5ba474c7fbda4702423 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 13:47:58 -0700 Subject: [PATCH 29/46] fix(state): bound plugin restore inputs Signed-off-by: Aaron Erickson --- .github/workflows/regression-e2e.yaml | 3 +- docs/deployment/install-openclaw-plugins.mdx | 10 +- ...eather-plugin-fixture-dependency-review.md | 51 ++++++++ .../custom-openclaw-runtime-diagnosis.test.ts | 10 ++ .../custom-openclaw-runtime-diagnosis.ts | 2 +- src/lib/state/openclaw-plugin-restore.test.ts | 15 +++ src/lib/state/openclaw-plugin-restore.ts | 11 +- .../sandbox-openclaw-plugin-restore.test.ts | 110 ++++++++++++++++++ src/lib/state/sandbox.ts | 17 ++- test/e2e-fixture-dependency-review.test.ts | 87 ++++++++++++++ 10 files changed, 310 insertions(+), 6 deletions(-) create mode 100644 docs/security/e2e-weather-plugin-fixture-dependency-review.md create mode 100644 src/lib/state/sandbox-openclaw-plugin-restore.test.ts create mode 100644 test/e2e-fixture-dependency-review.test.ts diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index 6209002866..df173657b2 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -207,7 +207,6 @@ jobs: /tmp/nemoclaw-e2e-openshell-version-pin-downloads.log if-no-files-found: ignore - # ── Gateway drift preflight E2E ───────────────────────────── # Coverage guard for #3399 / #3423. A stale OpenShell gateway image can # make sandbox-state RPCs fail with protobuf invalid-wire decode errors. @@ -281,6 +280,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + # Cold-cache custom-image onboarding, gateway restart, rebuild, and EXDEV + # proof can exceed 45 minutes, so this lane keeps a bounded 75-minute budget. timeout-minutes: 75 steps: - name: Checkout diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 5b813aaf84..39394b15fc 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -11,6 +11,14 @@ content: skill: priority: 20 --- + + +This is a source-based workaround until the managed plugin lifecycle in [issue #5998](https://github.com/NVIDIA/NemoClaw/issues/5998) is available. +It requires a source checkout and base image that exactly match your installed NemoClaw CLI. +The example below targets NemoClaw `v0.0.71` and its pinned OpenClaw `2026.5.27` runtime. +For another release, replace every `v0.0.71` occurrence and use the OpenClaw version pinned by that release wherever this example uses `2026.5.27`. + + OpenClaw plugins extend the OpenClaw runtime with hooks, services, tools, or provider integrations. They are different from NemoClaw-managed agent skills: @@ -18,7 +26,7 @@ They are different from NemoClaw-managed agent skills: - **Skills** are `SKILL.md` directories that teach an agent how to perform a task. - **Policy presets** are network-egress rules that control what sandboxed code can reach. -Until NemoClaw provides a managed plugin lifecycle, bake the plugin into a version-matched full NemoClaw runtime image. +Until NemoClaw provides that managed lifecycle, bake the plugin into a version-matched full NemoClaw runtime image. ## Understand the Custom Image Contract diff --git a/docs/security/e2e-weather-plugin-fixture-dependency-review.md b/docs/security/e2e-weather-plugin-fixture-dependency-review.md new file mode 100644 index 0000000000..d8dd1d5a2e --- /dev/null +++ b/docs/security/e2e-weather-plugin-fixture-dependency-review.md @@ -0,0 +1,51 @@ +# E2E Weather Plugin Fixture Dependency Review + +Review date: 2026-07-06 + +Scope: `test/e2e/fixtures/plugins/weather/package-lock.json` and the secret-free OpenClaw custom-plugin lifecycle regression lane. + +## Checked-in Fixture Waiver + +The following fixture lockfile is intentionally committed and covered by this review: + +- `test/e2e/fixtures/plugins/weather/package-lock.json` + +The fixture reproduces a real, version-matched OpenClaw plugin build. +Its lockfile must remain committed so the release-matched peer and development dependency graph is deterministic; generating the lockfile during E2E would allow registry state to change the build without a repository diff. +Automated dependency updates are not enabled for this fixture because its OpenClaw version must move with NemoClaw's reviewed runtime pin rather than independently. + +This waiver is limited to test fixture code. +It does not waive review for production dependencies, and it must be revalidated whenever the fixture manifest or lockfile changes. + +## Accepted Residual Risk + +Registry packages can later be found vulnerable or compromised, and downloaded package code still participates in fixture compilation and the test plugin runtime despite integrity verification and lifecycle-script suppression. +The accepted residual risk is limited to this secret-free E2E lane with read-only contents permission and must be reconsidered on every fixture manifest or lockfile change. + +## Compensating Controls + +- `typebox`, `openclaw`, and `typescript` use exact installed versions in `package.json`; the OpenClaw peer range expresses runtime compatibility but is not the lockfile's install selector. +- The committed npm lockfile records registry integrity for the resolved dependency graph. +- Every fixture install uses `npm ci --ignore-scripts`; the Docker build also uses `--no-audit --no-fund` and prunes development and peer dependencies before staging the plugin. +- The image build fails if a private `node_modules/openclaw` remains, then verifies that OpenClaw creates the expected link to the stock global runtime. +- The GitHub Actions job has read-only `contents` permission, uses full-SHA-pinned actions, disables checkout credential persistence, and receives no repository secrets. +- The lane is isolated to deterministic test data and uploads only its path-scoped E2E artifact directory. + +## Advisory Audit + +Run from `test/e2e/fixtures/plugins/weather`: + +```bash +npm audit --package-lock-only --ignore-scripts --json +``` + +Revalidated on 2026-07-06: npm audit exited `0` and reported 0 info, low, moderate, high, or critical vulnerabilities across 308 total dependencies. +The reviewed lockfile has SHA-256 `f32b55ad39698fee28a863f88739c99cebd5c7ab3970af4dd44019510a6e6572`, and every non-root package entry records both its resolved registry URL and integrity value. + +The audit is a point-in-time advisory check, not a substitute for the exact lockfile, lifecycle-script suppression, or secret-free workflow boundary. +Rerun it whenever `package.json` or `package-lock.json` changes and again before merge if npm advisory state changes. + +## Enforcement and Removal + +`test/e2e-fixture-dependency-review.test.ts` fails if any committed `test/e2e/fixtures/**/package-lock.json` is absent from this review and binds the weather fixture to the controls above. +Remove this waiver when the fixture is deleted or when repository-wide automated dependency review explicitly covers E2E fixture lockfiles while preserving the release-matched OpenClaw pin. diff --git a/src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts b/src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts index 8ebd1e4e60..6a56d3467a 100644 --- a/src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts +++ b/src/lib/onboard/custom-openclaw-runtime-diagnosis.test.ts @@ -66,6 +66,16 @@ describe("classifyOpenClawRuntimeFailure", () => { { status: 1, stdout: "nemoclaw-runtime-probe-v1 log=0 start=0 config=0", stderr: "" }, ], ["malformed output", probeResult("not a runtime probe frame")], + [ + "ANSI-prefixed output", + probeResult("\u001b[31mnemoclaw-runtime-probe-v1 log=0 start=0 config=0"), + ], + ["tab-prefixed output", probeResult("\tnemoclaw-runtime-probe-v1 log=0 start=0 config=0")], + [ + "form-feed-prefixed output", + probeResult("\fnemoclaw-runtime-probe-v1 log=0 start=0 config=0"), + ], + ["case-altered marker", probeResult("NEMOCLAW-runtime-probe-v1 log=0 start=0 config=0")], ])("keeps a %s inconclusive", (_name, probe) => { const result = classifyOpenClawRuntimeFailure("my-sandbox", () => probe); expect(result).toEqual({ diff --git a/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts b/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts index 11502e6d1e..cafa54a08e 100644 --- a/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts +++ b/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts @@ -67,7 +67,7 @@ export function classifyOpenClawRuntimeFailure( } const match = result.stdout.match( - /(?:^|\n)\s*(?:(?:\[stdout\]|stdout:)\s*)?nemoclaw-runtime-probe-v1 log=([01]) start=([01]) config=([01])(?:\r?\n|$)/i, + /(?:^|\n)[ ]*(?:(?:\[stdout\]|stdout:)[ ]*)?nemoclaw-runtime-probe-v1 log=([01]) start=([01]) config=([01])(?:\r?\n|$)/, ); if (result.status !== 0 || !match) { return { diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts index dcddf5ff69..7b0c57105b 100644 --- a/src/lib/state/openclaw-plugin-restore.test.ts +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -132,6 +132,11 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { ["a traversal ID", { "../weather": install(`${OPENCLAW_DIR}/extensions/../weather`) }], ["a nested install path", { weather: install(`${OPENCLAW_DIR}/extensions/nested/weather`) }], ["a noncanonical install path", { weather: install(`${OPENCLAW_DIR}/extensions/../weather`) }], + ["an oversized install path", { weather: install(`/${"a".repeat(4096)}`) }], + [ + "an excessively deep install path", + { weather: install(`/${Array.from({ length: 65 }, () => "a").join("/")}`) }, + ], ["a glob extension directory", { weather: install(`${OPENCLAW_DIR}/extensions/*`) }], ["non-object metadata", { weather: "invalid" }], ])("rejects %s", (_label, installs) => { @@ -161,4 +166,14 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { error: "fresh OpenClaw registry has too many plugin installs (129)", }); }); + + it("accepts the 64-component install-path boundary", () => { + const installPath = `/${Array.from({ length: 64 }, () => "a").join("/")}`; + expect( + parseFreshOpenClawPluginExtensionDirs( + { version: 1, installRecords: { weather: install(installPath) } }, + OPENCLAW_DIR, + ), + ).toEqual({ ok: true, extensionDirs: [] }); + }); }); diff --git a/src/lib/state/openclaw-plugin-restore.ts b/src/lib/state/openclaw-plugin-restore.ts index 5671091b31..ee9d56484b 100644 --- a/src/lib/state/openclaw-plugin-restore.ts +++ b/src/lib/state/openclaw-plugin-restore.ts @@ -7,6 +7,9 @@ import { isRecord } from "../core/json-types.js"; import { shellQuote } from "../core/shell-quote.js"; const MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS = 128; +// Bound sandbox-controlled registry strings before path normalization. +const MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH = 4096; +const MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS = 64; const OPENCLAW_EXTENSION_GLOB_CHARS = ["/", "\\", "*", "?", "[", "]"] as const; export type OpenClawManagedExtensionDiscoveryResult = @@ -88,7 +91,13 @@ export function parseFreshOpenClawPluginExtensionDirs( if (!isRecord(install)) { return { ok: false, error: `fresh OpenClaw plugin install metadata is invalid: ${id}` }; } - if (typeof install.installPath !== "string" || !path.posix.isAbsolute(install.installPath)) { + if ( + typeof install.installPath !== "string" || + install.installPath.length > MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH || + install.installPath.split("/").filter(Boolean).length > + MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS || + !path.posix.isAbsolute(install.installPath) + ) { return { ok: false, error: `fresh OpenClaw plugin install path is invalid for ${id}` }; } const normalizedInstallPath = path.posix.normalize(install.installPath); diff --git a/src/lib/state/sandbox-openclaw-plugin-restore.test.ts b/src/lib/state/sandbox-openclaw-plugin-restore.test.ts new file mode 100644 index 0000000000..979835544b --- /dev/null +++ b/src/lib/state/sandbox-openclaw-plugin-restore.test.ts @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "child_process"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, spawnSync: vi.fn() }; +}); + +import { __test } from "./sandbox"; + +const OPENCLAW_DIR = "/sandbox/.openclaw"; +const MAX_PLUGIN_REGISTRY_BYTES = 1024 * 1024; + +function spawnResult( + status: number | null, + stdout: Buffer, + options: { error?: Error; signal?: NodeJS.Signals | null } = {}, +): ReturnType { + return { + error: options.error, + status, + signal: options.signal ?? null, + output: [null, stdout, Buffer.alloc(0)], + pid: 1234, + stdout, + stderr: Buffer.alloc(0), + } as ReturnType; +} + +function discover() { + return __test.discoverFreshOpenClawPluginExtensionDirs( + "/tmp/ssh-config", + "sandbox-one", + OPENCLAW_DIR, + ); +} + +describe("fresh OpenClaw plugin registry reads", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("rejects an oversized SQLite registry response before JSON.parse", () => { + const parseSpy = vi.spyOn(JSON, "parse"); + vi.mocked(spawnSync).mockReturnValue(spawnResult(0, Buffer.alloc(5 * 1024 * 1024, " "))); + + expect(discover()).toEqual({ + ok: false, + error: "fresh OpenClaw plugin install registry response too large", + }); + expect(parseSpy).not.toHaveBeenCalled(); + expect(spawnSync).toHaveBeenCalledOnce(); + expect(vi.mocked(spawnSync).mock.calls[0]?.[2]).toEqual( + expect.objectContaining({ maxBuffer: MAX_PLUGIN_REGISTRY_BYTES }), + ); + }); + + it("classifies a real maxBuffer ENOBUFS result before parsing truncated output", () => { + const parseSpy = vi.spyOn(JSON, "parse"); + const error = Object.assign(new Error("spawnSync ssh ENOBUFS"), { code: "ENOBUFS" }); + vi.mocked(spawnSync).mockReturnValue( + spawnResult(null, Buffer.alloc(MAX_PLUGIN_REGISTRY_BYTES + 8192, " "), { + error, + signal: "SIGTERM", + }), + ); + + expect(discover()).toEqual({ + ok: false, + error: "fresh OpenClaw plugin install registry response too large", + }); + expect(parseSpy).not.toHaveBeenCalled(); + expect(spawnSync).toHaveBeenCalledOnce(); + }); + + it("rejects an oversized legacy registry response after the status-2 fallback", () => { + const parseSpy = vi.spyOn(JSON, "parse"); + vi.mocked(spawnSync) + .mockReturnValueOnce(spawnResult(2, Buffer.alloc(0))) + .mockReturnValueOnce(spawnResult(0, Buffer.alloc(5 * 1024 * 1024, " "))); + + expect(discover()).toEqual({ + ok: false, + error: "fresh OpenClaw plugin install registry response too large", + }); + expect(parseSpy).not.toHaveBeenCalled(); + expect(spawnSync).toHaveBeenCalledTimes(2); + for (const call of vi.mocked(spawnSync).mock.calls) { + expect(call[2]).toEqual(expect.objectContaining({ maxBuffer: MAX_PLUGIN_REGISTRY_BYTES })); + } + }); + + it.each([10, 11])("does not use the legacy fallback for SQLite status %i", (status) => { + vi.mocked(spawnSync).mockReturnValue(spawnResult(status, Buffer.alloc(0))); + + expect(discover()).toEqual({ + ok: false, + error: "could not read fresh OpenClaw plugin install registry", + }); + expect(spawnSync).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 50714c70cc..6a6d4a26b0 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -60,6 +60,9 @@ const HOME_DIR = path.resolve(process.env.HOME || os.homedir()); const REBUILD_BACKUPS_DIR = path.join(HOME_DIR, ".nemoclaw", "rebuild-backups"); const MANIFEST_VERSION = 1; +// The parser accepts at most 128 records with 4 KiB install paths, leaving +// ample room for IDs and metadata while bounding sandbox-controlled output. +const OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES = 1024 * 1024; function parseJson(text: string): T { return JSON.parse(text); @@ -674,7 +677,7 @@ function readFreshOpenClawPluginInstallIndex( { stdio: ["ignore", "pipe", "pipe"], timeout: 30000, - maxBuffer: 4 * 1024 * 1024, + maxBuffer: OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES, }, ); if (sqliteResult.status !== 2 || sqliteResult.error || sqliteResult.signal) return sqliteResult; @@ -686,7 +689,7 @@ function readFreshOpenClawPluginInstallIndex( { stdio: ["ignore", "pipe", "pipe"], timeout: 30000, - maxBuffer: 4 * 1024 * 1024, + maxBuffer: OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES, }, ); } @@ -697,6 +700,12 @@ function discoverFreshOpenClawPluginExtensionDirs( dir: string, ): OpenClawManagedExtensionDiscoveryResult { const result = readFreshOpenClawPluginInstallIndex(configFile, sandboxName, dir); + if ( + result.stdout && + Buffer.byteLength(result.stdout) > OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES + ) { + return { ok: false, error: "fresh OpenClaw plugin install registry response too large" }; + } if (result.status !== 0 || result.error || result.signal || !result.stdout) { return { ok: false, error: "could not read fresh OpenClaw plugin install registry" }; } @@ -716,6 +725,10 @@ function discoverFreshOpenClawPluginExtensionDirs( : { ok: false, error: "fresh OpenClaw plugin install registry failed validation" }; } +export const __test = { + discoverFreshOpenClawPluginExtensionDirs, +}; + function normalizeStateFileSpec(spec: AgentStateFile | StateFileSpec): StateFileSpec | null { const normalized = normalizeStateFilePath(spec.path); if (!normalized) return null; diff --git a/test/e2e-fixture-dependency-review.test.ts b/test/e2e-fixture-dependency-review.test.ts new file mode 100644 index 0000000000..0c3a85115b --- /dev/null +++ b/test/e2e-fixture-dependency-review.test.ts @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +const REPO_ROOT = path.resolve(__dirname, ".."); +const FIXTURES_ROOT = path.join(REPO_ROOT, "test", "e2e", "fixtures"); +const REVIEW_PATH = path.join( + REPO_ROOT, + "docs", + "security", + "e2e-weather-plugin-fixture-dependency-review.md", +); + +function collectFixtureLockfiles(dir: string): string[] { + const lockfiles: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === "node_modules") continue; + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + lockfiles.push(...collectFixtureLockfiles(entryPath)); + } else if (entry.name === "package-lock.json") { + lockfiles.push(path.relative(REPO_ROOT, entryPath).split(path.sep).join("/")); + } + } + return lockfiles.sort(); +} + +describe("E2E fixture dependency review", () => { + const review = fs.readFileSync(REVIEW_PATH, "utf8"); + + it("records every committed fixture lockfile in the checked-in review", () => { + const lockfiles = collectFixtureLockfiles(FIXTURES_ROOT); + expect(lockfiles.length).toBeGreaterThan(0); + for (const lockfile of lockfiles) { + expect(review, lockfile).toContain(`- \`${lockfile}\``); + } + }); + + it("records the fixture threat controls and revalidation contract", () => { + for (const marker of [ + "npm ci --ignore-scripts", + "read-only `contents` permission", + "full-SHA-pinned actions", + "disables checkout credential persistence", + "receives no repository secrets", + "npm audit --package-lock-only --ignore-scripts --json", + "accepted residual risk is limited to this secret-free E2E lane with read-only contents permission", + "Rerun it whenever `package.json` or `package-lock.json` changes", + ]) { + expect(review).toContain(marker); + } + }); + + it("keeps installed fixture dependencies on exact versions", () => { + const weatherFixture = path.join(FIXTURES_ROOT, "plugins", "weather"); + const manifest = JSON.parse( + fs.readFileSync(path.join(weatherFixture, "package.json"), "utf8"), + ) as { + dependencies?: Record; + devDependencies?: Record; + }; + for (const [name, version] of Object.entries({ + ...manifest.dependencies, + ...manifest.devDependencies, + })) { + expect(version, name).toMatch(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/); + } + + const lockfileText = fs.readFileSync(path.join(weatherFixture, "package-lock.json"), "utf8"); + const lockfileDigest = createHash("sha256").update(lockfileText).digest("hex"); + expect(review).toContain(`SHA-256 \`${lockfileDigest}\``); + + const lockfile = JSON.parse(lockfileText) as { + packages?: Record; + }; + for (const [packagePath, entry] of Object.entries(lockfile.packages ?? {})) { + if (packagePath.length === 0) continue; + expect(entry.resolved, packagePath).toEqual(expect.any(String)); + expect(entry.integrity, packagePath).toEqual(expect.any(String)); + } + }); +}); From 04902e146c6e1300fe02fa24a6d849dfc766b176 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 13:53:50 -0700 Subject: [PATCH 30/46] test: keep fixture review guard linear Signed-off-by: Aaron Erickson --- test/e2e-fixture-dependency-review.test.ts | 30 ++++++++++------------ 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/test/e2e-fixture-dependency-review.test.ts b/test/e2e-fixture-dependency-review.test.ts index 0c3a85115b..4a203520b9 100644 --- a/test/e2e-fixture-dependency-review.test.ts +++ b/test/e2e-fixture-dependency-review.test.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; @@ -16,25 +17,19 @@ const REVIEW_PATH = path.join( "e2e-weather-plugin-fixture-dependency-review.md", ); -function collectFixtureLockfiles(dir: string): string[] { - const lockfiles: string[] = []; - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - if (entry.name === "node_modules") continue; - const entryPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - lockfiles.push(...collectFixtureLockfiles(entryPath)); - } else if (entry.name === "package-lock.json") { - lockfiles.push(path.relative(REPO_ROOT, entryPath).split(path.sep).join("/")); - } - } - return lockfiles.sort(); -} - describe("E2E fixture dependency review", () => { const review = fs.readFileSync(REVIEW_PATH, "utf8"); it("records every committed fixture lockfile in the checked-in review", () => { - const lockfiles = collectFixtureLockfiles(FIXTURES_ROOT); + const lockfiles = execFileSync( + "git", + ["ls-files", "--", "test/e2e/fixtures/**/package-lock.json"], + { cwd: REPO_ROOT, encoding: "utf8" }, + ) + .trim() + .split("\n") + .filter(Boolean) + .sort(); expect(lockfiles.length).toBeGreaterThan(0); for (const lockfile of lockfiles) { expect(review, lockfile).toContain(`- \`${lockfile}\``); @@ -78,8 +73,9 @@ describe("E2E fixture dependency review", () => { const lockfile = JSON.parse(lockfileText) as { packages?: Record; }; - for (const [packagePath, entry] of Object.entries(lockfile.packages ?? {})) { - if (packagePath.length === 0) continue; + for (const [packagePath, entry] of Object.entries(lockfile.packages ?? {}).filter( + ([packagePath]) => packagePath.length > 0, + )) { expect(entry.resolved, packagePath).toEqual(expect.any(String)); expect(entry.integrity, packagePath).toEqual(expect.any(String)); } From 00d2883f48032c6358b4fac2f48ad85be67e83d6 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 15:00:49 -0700 Subject: [PATCH 31/46] fix(state): preserve image plugins across recreation Signed-off-by: Aaron Erickson --- .github/workflows/e2e.yaml | 49 + ci/test-file-size-budget.json | 2 +- ...eather-plugin-fixture-dependency-review.md | 6 +- .../sandbox/rebuild-dcode-recovery.test.ts | 2 +- .../rebuild-local-provider-recreate.test.ts | 2 +- .../sandbox/rebuild-prepared-recovery.test.ts | 2 +- .../sandbox/rebuild-restore-phase.test.ts | 25 +- .../actions/sandbox/rebuild-restore-phase.ts | 8 +- src/lib/onboard.ts | 14 +- .../created-sandbox-finalization.test.ts | 169 +- .../onboard/created-sandbox-finalization.ts | 45 +- src/lib/onboard/sandbox-registration.test.ts | 9 + src/lib/onboard/sandbox-registration.ts | 9 + src/lib/state/openclaw-config-merge.test.ts | 90 + src/lib/state/openclaw-config-merge.ts | 75 +- .../state/openclaw-config-restore-input.ts | 12 +- src/lib/state/openclaw-plugin-restore.test.ts | 76 +- src/lib/state/openclaw-plugin-restore.ts | 153 +- src/lib/state/registry.ts | 6 + src/lib/state/sandbox.ts | 170 +- .../plugins/weather/package-lock.json | 1698 ++++++++++++++--- .../e2e/fixtures/plugins/weather/package.json | 4 +- .../openclaw-plugin-runtime-exdev.test.ts | 268 ++- test/e2e/support/e2e-workflow.test.ts | 4 + ...in-runtime-exdev-workflow-boundary.test.ts | 123 ++ ...ad-e2e-artifacts-workflow-boundary.test.ts | 6 +- test/helpers/onboard-openshell-fixture.ts | 20 + test/helpers/rebuild-flow-harness.ts | 23 +- test/helpers/rebuild-flow-lifecycle-cases.ts | 2 +- test/helpers/rebuild-flow-recovery-cases.ts | 2 +- test/helpers/rebuild-flow-test-harness.ts | 26 +- test/onboard-installer-restore-intent.test.ts | 12 +- test/onboard.test.ts | 22 +- test/registry.test.ts | 40 + ...apshot-openclaw-managed-extensions.test.ts | 81 +- test/snapshot-recovery-validation.test.ts | 35 + ...upload-e2e-artifacts-workflow-boundary.mts | 4 +- 37 files changed, 2812 insertions(+), 482 deletions(-) create mode 100644 test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts create mode 100644 test/helpers/onboard-openshell-fixture.ts diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 8d65390489..73603aae1f 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -3662,6 +3662,54 @@ jobs: if: always() uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 + # Scheduled coverage for #6108 / #3513 / #3127. This exercises the complete + # managed v0.0.71 runtime with a custom plugin, then proves restart/rebuild + # persistence and target-side runtime-dependency replacement across devices. + openclaw-plugin-runtime-exdev: + needs: generate-matrix + if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',openclaw-plugin-runtime-exdev,') || contains(format(',{0},', inputs.targets), ',openclaw-plugin-runtime-exdev,') }} + runs-on: ubuntu-latest + permissions: + contents: read + # Cold-cache custom-image onboarding, recreation, rebuild, and EXDEV proof + # can exceed an hour, so this lane keeps a bounded 90-minute budget. + timeout-minutes: 90 + env: + E2E_JOB: "1" + E2E_TARGET_ID: "openclaw-plugin-runtime-exdev" + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openclaw-plugin-runtime-exdev + NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js + NEMOCLAW_RUN_LIVE_E2E: "1" + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + NEMOCLAW_SANDBOX_NAME: "e2e-openclaw-plugin-exdev" + OPENSHELL_GATEWAY: "nemoclaw" + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - *dockerhub-auth + + - name: Prepare E2E workspace + uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@50281ee84c4a6fc759da95ea28fc0b7d9c378a28 + + - name: Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV live test + run: | + set -euo pipefail + npx vitest run --project e2e-live \ + test/e2e/live/openclaw-plugin-runtime-exdev.test.ts \ + --silent=false --reporter=default + + - name: Upload OpenClaw plugin runtime-deps EXDEV artifacts + if: always() + uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 + + - name: Clean up Docker auth + if: always() + shell: bash + run: bash .github/scripts/docker-auth-cleanup.sh + # The #2603/#3145 OpenClaw websocket protocol/history contract. openclaw-tui-chat-correlation: needs: generate-matrix @@ -4623,6 +4671,7 @@ jobs: diagnostics, snapshot-commands, gateway-drift-preflight, + openclaw-plugin-runtime-exdev, openclaw-tui-chat-correlation, gateway-guard-recovery, issue-4434-tui-unreachable-inference, diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index d062973372..2b4cc768c4 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -11,7 +11,7 @@ "test/nemoclaw-start.test.ts": 4827, "test/onboard-messaging.test.ts": 2062, "test/onboard-selection.test.ts": 5835, - "test/onboard.test.ts": 4057, + "test/onboard.test.ts": 4053, "test/policies.test.ts": 2332 } } diff --git a/docs/security/e2e-weather-plugin-fixture-dependency-review.md b/docs/security/e2e-weather-plugin-fixture-dependency-review.md index d8dd1d5a2e..2cf6d98a5c 100644 --- a/docs/security/e2e-weather-plugin-fixture-dependency-review.md +++ b/docs/security/e2e-weather-plugin-fixture-dependency-review.md @@ -21,6 +21,7 @@ It does not waive review for production dependencies, and it must be revalidated Registry packages can later be found vulnerable or compromised, and downloaded package code still participates in fixture compilation and the test plugin runtime despite integrity verification and lifecycle-script suppression. The accepted residual risk is limited to this secret-free E2E lane with read-only contents permission and must be reconsidered on every fixture manifest or lockfile change. +The release-matched `openclaw@2026.5.27` development graph currently has known advisories, but upgrading it independently would stop this fixture from testing the documented NemoClaw `v0.0.71` runtime contract. ## Compensating Controls @@ -39,8 +40,9 @@ Run from `test/e2e/fixtures/plugins/weather`: npm audit --package-lock-only --ignore-scripts --json ``` -Revalidated on 2026-07-06: npm audit exited `0` and reported 0 info, low, moderate, high, or critical vulnerabilities across 308 total dependencies. -The reviewed lockfile has SHA-256 `f32b55ad39698fee28a863f88739c99cebd5c7ab3970af4dd44019510a6e6572`, and every non-root package entry records both its resolved registry URL and integrity value. +Revalidated on 2026-07-06: npm audit exited `1` and reported 9 vulnerable packages (3 moderate and 6 high; 0 info, low, or critical) across 374 total dependencies. +The advisories are in the release-pinned OpenClaw development graph; the Docker build suppresses lifecycle scripts and prunes development and peer dependencies before copying the plugin into the runtime image. +The reviewed lockfile has SHA-256 `1fa44d136d4bf5396f592dbf901da2c43740b38f1ebe52d23efc01ca0ba6f3da`, and every non-root package entry records both its resolved registry URL and integrity value. The audit is a point-in-time advisory check, not a substitute for the exact lockfile, lifecycle-script suppression, or secret-free workflow boundary. Rerun it whenever `package.json` or `package-lock.json` changes and again before merge if npm advisory state changes. diff --git a/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts b/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts index 0e685f3438..fa64b59bb0 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts @@ -50,7 +50,7 @@ describe("rebuildSandbox DCode flow: recovery", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", recoveryManifest.backupPath, - { preserveFreshOpenClawPluginInstalls: true }, + { targetAgentType: "langchain-deepagents-code" }, ); }); it("replays captured custom policies during stale DCode recovery without a backup (#6195)", async () => { diff --git a/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts b/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts index 63a7b227e3..e7439fe3e3 100644 --- a/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts +++ b/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts @@ -214,7 +214,7 @@ describe("rebuild local-provider recreation", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", "/tmp/nemoclaw-rebuild-backup", - { preserveFreshOpenClawPluginInstalls: true }, + { targetAgentType: "openclaw" }, ); }); }); diff --git a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts index d62691a52b..da69c45648 100644 --- a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts @@ -44,7 +44,7 @@ describe("prepared rebuild recovery", () => { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", recoveryManifest.backupPath, - { preserveFreshOpenClawPluginInstalls: true }, + { targetAgentType: "openclaw" }, ); }); diff --git a/src/lib/actions/sandbox/rebuild-restore-phase.test.ts b/src/lib/actions/sandbox/rebuild-restore-phase.test.ts index 439ab91531..ce5c308062 100644 --- a/src/lib/actions/sandbox/rebuild-restore-phase.test.ts +++ b/src/lib/actions/sandbox/rebuild-restore-phase.test.ts @@ -18,7 +18,7 @@ describe("rebuild policy restore fidelity", () => { vi.spyOn(console, "log").mockImplementation(() => undefined); const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); const log = vi.fn(); - vi.spyOn(sandboxState, "restoreSandboxState").mockReturnValue({ + vi.spyOn(sandboxState, "restoreRecreatedSandboxState").mockReturnValue({ success: false, restoredDirs: [], restoredFiles: [], @@ -29,7 +29,7 @@ describe("rebuild policy restore fidelity", () => { const result = runRebuildRestorePhase({ sandboxName: "alpha", - backupManifest: { backupPath: "/tmp/rebuild-backup" } as never, + backupManifest: { agentType: "openclaw", backupPath: "/tmp/rebuild-backup" } as never, policyPresets: [], customPolicies: [], log, @@ -47,13 +47,15 @@ describe("rebuild policy restore fidelity", () => { it("replays custom web-policy names from exact content instead of same-name built-ins", () => { vi.spyOn(console, "log").mockImplementation(() => undefined); vi.spyOn(console, "error").mockImplementation(() => undefined); - const restoreSandboxState = vi.spyOn(sandboxState, "restoreSandboxState").mockReturnValue({ - success: true, - restoredDirs: [], - restoredFiles: [], - failedDirs: [], - failedFiles: [], - }); + const restoreRecreatedSandboxState = vi + .spyOn(sandboxState, "restoreRecreatedSandboxState") + .mockReturnValue({ + success: true, + restoredDirs: [], + restoredFiles: [], + failedDirs: [], + failedFiles: [], + }); const applyPreset = vi.spyOn(policies, "applyPreset").mockReturnValue(true); const applyPresetContent = vi.spyOn(policies, "applyPresetContent").mockReturnValue(true); const customPolicies = ["brave", "tavily", "nous-web"].map((name) => ({ @@ -65,6 +67,7 @@ describe("rebuild policy restore fidelity", () => { const result = runRebuildRestorePhase({ sandboxName: "alpha", backupManifest: { + agentType: "openclaw", backupPath: "/tmp/rebuild-backup", customPolicies, } as never, @@ -73,8 +76,8 @@ describe("rebuild policy restore fidelity", () => { log: vi.fn(), }); - expect(restoreSandboxState).toHaveBeenCalledWith("alpha", "/tmp/rebuild-backup", { - preserveFreshOpenClawPluginInstalls: true, + expect(restoreRecreatedSandboxState).toHaveBeenCalledWith("alpha", "/tmp/rebuild-backup", { + targetAgentType: "openclaw", }); expect(applyPreset).toHaveBeenCalledOnce(); expect(applyPreset).toHaveBeenCalledWith("alpha", "npm"); diff --git a/src/lib/actions/sandbox/rebuild-restore-phase.ts b/src/lib/actions/sandbox/rebuild-restore-phase.ts index 9bd4062b3c..b5f6bd9bd7 100644 --- a/src/lib/actions/sandbox/rebuild-restore-phase.ts +++ b/src/lib/actions/sandbox/rebuild-restore-phase.ts @@ -36,9 +36,11 @@ export function runRebuildRestorePhase(input: RebuildRestorePhaseInput): Rebuild console.log(""); console.log(" Restoring workspace state..."); log(`Restoring from: ${backupManifest.backupPath} into sandbox: ${sandboxName}`); - const restore = sandboxState.restoreSandboxState(sandboxName, backupManifest.backupPath, { - preserveFreshOpenClawPluginInstalls: true, - }); + const restore = sandboxState.restoreRecreatedSandboxState( + sandboxName, + backupManifest.backupPath, + { targetAgentType: backupManifest.agentType }, + ); log( `Restore result: success=${restore.success}, restored=${restore.restoredDirs.join(",")}; files=${restore.restoredFiles.join(",")}, failed=${restore.failedDirs.join(",")}; failedFiles=${restore.failedFiles.join(",")}${restore.error ? `; error=${restore.error}` : ""}`, ); diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index adad6e928b..4e372366ea 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2961,27 +2961,26 @@ async function createSandboxWithBaseImageResolution( hermesDashboardForwarding.ensureForState(finalHermesDashboardState, sandboxName, true); } - // Resolve registry metadata now, but publish it only after restored state is - // reconciled and the live agent selection is verified. // openshell tags images with seconds; buildId is ms. Parse actual tag from output. Fixes #2672. const resolvedImageTag = prebuild.imageRef ?? resolveSandboxImageTagFromCreateOutput(createResult.output, buildId); const sandboxRuntimeFields = getSandboxRuntimeRegistryFields(effectiveSandboxGpuConfig); - const inferenceSelection = sandboxRegistration.selection; - finalizeCreatedSandbox( { sandboxName, restoreBackupPath, preUpgradeBackup: pendingStateRestoreBackupPath !== null, + targetAgentType: agent?.name ?? "openclaw", validateManagedDcode: isManagedDcodeAgent, provider, model, preferredInferenceApi, }, { - restoreSandboxState: sandboxState.restoreSandboxState, + discoverFreshOpenClawImagePluginInstalls: + sandboxState.discoverFreshOpenClawImagePluginInstalls, + restoreRecreatedSandboxState: sandboxState.restoreRecreatedSandboxState, getDcodeSelectionDrift: (name, selectedProvider, selectedModel, selectedApi) => getDcodeSelectionDrift(name, selectedProvider, selectedModel, selectedApi, { runCaptureOpenshell, @@ -2989,10 +2988,10 @@ async function createSandboxWithBaseImageResolution( note, error: console.error, exitProcess: (code) => process.exit(code), - register: () => + register: (openclawImagePluginInstalls) => sandboxRegistration.registerCreatedSandbox({ sandboxName, - inferenceSelection: inferenceSelection( + inferenceSelection: sandboxRegistration.selection( sandboxName, provider, model, @@ -3002,6 +3001,7 @@ async function createSandboxWithBaseImageResolution( agent, agentVersionKnown: !fromDockerfile, imageTag: resolvedImageTag, + openclawImagePluginInstalls, appliedPolicies: initialSandboxPolicy.appliedPresets, toolDisclosure: effectiveToolDisclosure, // biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail. diff --git a/src/lib/onboard/created-sandbox-finalization.test.ts b/src/lib/onboard/created-sandbox-finalization.test.ts index 938bd00359..903e85db13 100644 --- a/src/lib/onboard/created-sandbox-finalization.test.ts +++ b/src/lib/onboard/created-sandbox-finalization.test.ts @@ -216,16 +216,22 @@ describe("created DCode sandbox finalization", () => { sandboxName: "dcode", restoreBackupPath: fixture.backupPath, preUpgradeBackup: false, + targetAgentType: "langchain-deepagents-code", validateManagedDcode: true, provider: "nvidia-prod", model: "new-model", preferredInferenceApi: null, }, { - restoreSandboxState: (name, backup, options) => { + discoverFreshOpenClawImagePluginInstalls: () => ({ + ok: true, + extensionDirs: [], + pluginInstalls: [], + }), + restoreRecreatedSandboxState: (name, backup, options) => { order.push("restore"); - expect(options?.stateFileRestorePolicy).toBe(managedDcodeConfigRestorePolicy); - return sandboxState.restoreSandboxState(name, backup, options); + expect(options.stateFileRestorePolicy).toBe(managedDcodeConfigRestorePolicy); + return sandboxState.restoreRecreatedSandboxState(name, backup, options); }, getDcodeSelectionDrift: (name, provider, model, api) => { order.push("validate"); @@ -266,13 +272,15 @@ describe("created DCode sandbox finalization", () => { sandboxName: "dcode", restoreBackupPath: null, preUpgradeBackup: false, + targetAgentType: "langchain-deepagents-code", validateManagedDcode: true, provider: "nvidia-prod", model: "new-model", preferredInferenceApi: null, }, { - restoreSandboxState: vi.fn(), + discoverFreshOpenClawImagePluginInstalls: vi.fn(), + restoreRecreatedSandboxState: vi.fn(), getDcodeSelectionDrift: () => ({ changed: true, providerChanged: false, @@ -307,14 +315,16 @@ describe("created DCode sandbox finalization", () => { sandboxName: "dcode", restoreBackupPath: fixture.backupPath, preUpgradeBackup: false, + targetAgentType: "langchain-deepagents-code", validateManagedDcode: true, provider: "nvidia-prod", model: "new-model", preferredInferenceApi: null, }, { - restoreSandboxState: (name, backup, options) => { - const restored = sandboxState.restoreSandboxState(name, backup, options); + discoverFreshOpenClawImagePluginInstalls: vi.fn(), + restoreRecreatedSandboxState: (name, backup, options) => { + const restored = sandboxState.restoreRecreatedSandboxState(name, backup, options); return { ...restored, success: false, failedDirs: ["skills"] }; }, getDcodeSelectionDrift: (name, provider, model, api) => @@ -347,7 +357,7 @@ describe("created DCode sandbox finalization", () => { }); it("keeps custom-image restores outside the managed config merge (#6311)", () => { - const restoreSandboxState = vi.fn(() => ({ + const restoreRecreatedSandboxState = vi.fn(() => ({ success: true, restoredDirs: [], failedDirs: [], @@ -360,13 +370,15 @@ describe("created DCode sandbox finalization", () => { sandboxName: "custom-dcode", restoreBackupPath: "/tmp/custom-backup", preUpgradeBackup: false, + targetAgentType: "langchain-deepagents-code", validateManagedDcode: false, provider: "custom-provider", model: "custom-model", preferredInferenceApi: null, }, { - restoreSandboxState, + discoverFreshOpenClawImagePluginInstalls: vi.fn(), + restoreRecreatedSandboxState, getDcodeSelectionDrift: vi.fn(), register: vi.fn(), note: vi.fn(), @@ -377,10 +389,147 @@ describe("created DCode sandbox finalization", () => { }, ); - expect(restoreSandboxState).toHaveBeenCalledWith( + expect(restoreRecreatedSandboxState).toHaveBeenCalledWith( "custom-dcode", "/tmp/custom-backup", - undefined, + { targetAgentType: "langchain-deepagents-code" }, + ); + }); +}); + +describe("created OpenClaw sandbox finalization", () => { + const pluginInstalls = [ + { + id: "weather", + installPath: "/sandbox/.openclaw/extensions/weather", + }, + ]; + + it("captures and registers a fresh image plugin baseline without a restore", () => { + const order: string[] = []; + const restoreRecreatedSandboxState = vi.fn(); + const register = vi.fn(() => order.push("register")); + + finalizeCreatedSandbox( + { + sandboxName: "openclaw", + restoreBackupPath: null, + preUpgradeBackup: false, + targetAgentType: "openclaw", + validateManagedDcode: false, + provider: "compatible-endpoint", + model: "demo", + preferredInferenceApi: "openai-completions", + }, + { + discoverFreshOpenClawImagePluginInstalls: () => { + order.push("discover"); + return { ok: true, extensionDirs: ["weather"], pluginInstalls }; + }, + restoreRecreatedSandboxState, + getDcodeSelectionDrift: vi.fn(), + register, + note: vi.fn(), + error: vi.fn(), + exitProcess: (code): never => { + throw new Error(`exit ${code}`); + }, + }, + ); + + expect(order).toEqual(["discover", "register"]); + expect(restoreRecreatedSandboxState).not.toHaveBeenCalled(); + expect(register).toHaveBeenCalledWith(pluginInstalls); + }); + + it("preserves the fresh image plugin baseline across recreation before registration", () => { + const order: string[] = []; + const register = vi.fn(() => order.push("register")); + const restoreRecreatedSandboxState = vi.fn(() => { + order.push("restore"); + return { + success: true, + restoredDirs: ["extensions"], + failedDirs: [], + restoredFiles: ["openclaw.json"], + failedFiles: [], + }; + }); + + finalizeCreatedSandbox( + { + sandboxName: "openclaw", + restoreBackupPath: "/tmp/openclaw-backup", + preUpgradeBackup: false, + targetAgentType: "openclaw", + validateManagedDcode: false, + provider: "compatible-endpoint", + model: "demo", + preferredInferenceApi: "openai-completions", + }, + { + discoverFreshOpenClawImagePluginInstalls: () => { + order.push("discover"); + return { ok: true, extensionDirs: ["weather"], pluginInstalls }; + }, + restoreRecreatedSandboxState, + getDcodeSelectionDrift: vi.fn(), + register, + note: vi.fn(), + error: vi.fn(), + exitProcess: (code): never => { + throw new Error(`exit ${code}`); + }, + }, + ); + + expect(order).toEqual(["discover", "restore", "register"]); + expect(restoreRecreatedSandboxState).toHaveBeenCalledWith("openclaw", "/tmp/openclaw-backup", { + targetAgentType: "openclaw", + freshOpenClawImagePluginInstalls: pluginInstalls, + }); + expect(register).toHaveBeenCalledWith(pluginInstalls); + }); + + it("fails closed before restore and registration when provenance discovery fails", () => { + const restoreRecreatedSandboxState = vi.fn(); + const register = vi.fn(); + const error = vi.fn(); + + expect(() => + finalizeCreatedSandbox( + { + sandboxName: "openclaw", + restoreBackupPath: "/tmp/openclaw-backup", + preUpgradeBackup: false, + targetAgentType: "openclaw", + validateManagedDcode: false, + provider: "compatible-endpoint", + model: "demo", + preferredInferenceApi: "openai-completions", + }, + { + discoverFreshOpenClawImagePluginInstalls: () => ({ + ok: false, + error: "registry unreadable", + }), + restoreRecreatedSandboxState, + getDcodeSelectionDrift: vi.fn(), + register, + note: vi.fn(), + error, + exitProcess: (code): never => { + throw new Error(`exit ${code}`); + }, + }, + ), + ).toThrow("exit 1"); + + expect(restoreRecreatedSandboxState).not.toHaveBeenCalled(); + expect(register).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith(expect.stringContaining("registry unreadable")); + expect(error).toHaveBeenCalledWith( + " State was not restored and registry metadata was not updated.", ); }); }); diff --git a/src/lib/onboard/created-sandbox-finalization.ts b/src/lib/onboard/created-sandbox-finalization.ts index fa38a311ba..ec61b2ac50 100644 --- a/src/lib/onboard/created-sandbox-finalization.ts +++ b/src/lib/onboard/created-sandbox-finalization.ts @@ -2,13 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import { managedDcodeConfigRestorePolicy } from "../state/dcode-config-restore-input"; -import type { RestoreOptions, RestoreResult } from "../state/sandbox"; +import type { + OpenClawImagePluginInstall, + OpenClawManagedExtensionDiscoveryResult, +} from "../state/openclaw-plugin-restore"; +import type { RecreatedSandboxRestoreOptions, RestoreResult } from "../state/sandbox"; import type { SelectionDrift } from "./selection-drift"; export type CreatedSandboxFinalizationOptions = { sandboxName: string; restoreBackupPath: string | null; preUpgradeBackup: boolean; + targetAgentType: string; validateManagedDcode: boolean; provider: string; model: string; @@ -16,10 +21,13 @@ export type CreatedSandboxFinalizationOptions = { }; export type CreatedSandboxFinalizationDeps = { - restoreSandboxState( + discoverFreshOpenClawImagePluginInstalls( + sandboxName: string, + ): OpenClawManagedExtensionDiscoveryResult; + restoreRecreatedSandboxState( sandboxName: string, backupPath: string, - options?: RestoreOptions, + options: RecreatedSandboxRestoreOptions, ): RestoreResult; getDcodeSelectionDrift( sandboxName: string, @@ -27,7 +35,7 @@ export type CreatedSandboxFinalizationDeps = { model: string, preferredInferenceApi: string | null, ): SelectionDrift; - register(): void; + register(openclawImagePluginInstalls?: readonly OpenClawImagePluginInstall[]): void; note(message: string): void; error(message: string): void; exitProcess(code: number): never; @@ -38,18 +46,37 @@ export function finalizeCreatedSandbox( options: CreatedSandboxFinalizationOptions, deps: CreatedSandboxFinalizationDeps, ): void { + let freshOpenClawImagePluginInstalls: readonly OpenClawImagePluginInstall[] | undefined; + if (options.targetAgentType === "openclaw") { + const discovery = deps.discoverFreshOpenClawImagePluginInstalls(options.sandboxName); + if (!discovery.ok) { + deps.error( + ` OpenClaw image plugin discovery failed for sandbox '${options.sandboxName}': ${discovery.error}`, + ); + deps.error(" State was not restored and registry metadata was not updated."); + return deps.exitProcess(1); + } + freshOpenClawImagePluginInstalls = discovery.pluginInstalls; + } + if (options.restoreBackupPath) { deps.note( options.preUpgradeBackup ? " Restoring workspace state from pre-upgrade backup..." : " Restoring workspace state from pre-recreate backup...", ); - const restore = deps.restoreSandboxState( + const restore = deps.restoreRecreatedSandboxState( options.sandboxName, options.restoreBackupPath, - options.validateManagedDcode - ? { stateFileRestorePolicy: managedDcodeConfigRestorePolicy } - : undefined, + { + targetAgentType: options.targetAgentType, + ...(freshOpenClawImagePluginInstalls !== undefined + ? { freshOpenClawImagePluginInstalls } + : {}), + ...(options.validateManagedDcode + ? { stateFileRestorePolicy: managedDcodeConfigRestorePolicy } + : {}), + }, ); if (restore.success) { deps.note( @@ -90,5 +117,5 @@ export function finalizeCreatedSandbox( } } - deps.register(); + deps.register(freshOpenClawImagePluginInstalls); } diff --git a/src/lib/onboard/sandbox-registration.test.ts b/src/lib/onboard/sandbox-registration.test.ts index f9c381260c..4d56e25afd 100644 --- a/src/lib/onboard/sandbox-registration.test.ts +++ b/src/lib/onboard/sandbox-registration.test.ts @@ -26,6 +26,9 @@ describe("buildCreatedSandboxRegistryEntry", () => { schemaVersion: 1 as const, plan: { sandboxName: "demo" }, }; + const openclawImagePluginInstalls = [ + { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather" }, + ]; const entry = buildCreatedSandboxRegistryEntry({ sandboxName: "demo", @@ -42,6 +45,7 @@ describe("buildCreatedSandboxRegistryEntry", () => { agent: null, agentVersionKnown: true, imageTag: "nemoclaw-demo:123", + openclawImagePluginInstalls, appliedPolicies: ["discord", "slack"], webSearchEnabled: true, fromDockerfile: "/tmp/Dockerfile.custom", @@ -65,6 +69,7 @@ describe("buildCreatedSandboxRegistryEntry", () => { credentialEnv: "COMPATIBLE_API_KEY", preferredInferenceApi: "openai-completions", imageTag: "nemoclaw-demo:123", + openclawImagePluginInstalls, policies: ["discord", "slack"], toolDisclosure: "progressive", webSearchEnabled: true, @@ -85,6 +90,8 @@ describe("buildCreatedSandboxRegistryEntry", () => { expect(entry.agent).toBeNull(); expect(entry.agentVersion).toBeTruthy(); expect(entry.nemoclawVersion).toBeTruthy(); + expect(entry.openclawImagePluginInstalls).not.toBe(openclawImagePluginInstalls); + expect(entry.openclawImagePluginInstalls?.[0]).not.toBe(openclawImagePluginInstalls[0]); expect(entry.messaging).toBe(plannedMessagingState); const rawEntry = entry as unknown as Record; expect(rawEntry.messagingChannels).toBeUndefined(); @@ -318,6 +325,7 @@ describe("registerCreatedSandbox", () => { agent: null, agentVersionKnown: true, imageTag: null, + openclawImagePluginInstalls: [], appliedPolicies: [], plannedMessagingState: undefined, hermesToolGateways: [], @@ -330,5 +338,6 @@ describe("registerCreatedSandbox", () => { expect(registerSandbox).toHaveBeenCalledWith(entry); expect(entry.name).toBe("demo"); + expect(entry.openclawImagePluginInstalls).toEqual([]); }); }); diff --git a/src/lib/onboard/sandbox-registration.ts b/src/lib/onboard/sandbox-registration.ts index b807f08976..cdd4b33759 100644 --- a/src/lib/onboard/sandbox-registration.ts +++ b/src/lib/onboard/sandbox-registration.ts @@ -6,6 +6,7 @@ import type { InferenceSelection } from "../inference/selection"; import { inferenceSelectionRegistryFields } from "../inference/selection"; import { type WebSearchConfig, webSearchProviderForConfig } from "../inference/web-search"; import * as onboardSession from "../state/onboard-session"; +import type { OpenClawImagePluginInstall } from "../state/openclaw-plugin-restore"; import type { SandboxEntry, SandboxMcpState, SandboxMessagingState } from "../state/registry"; import * as registry from "../state/registry"; import { DEFAULT_TOOL_DISCLOSURE, type ToolDisclosure } from "../tool-disclosure"; @@ -34,6 +35,7 @@ export interface CreatedSandboxRegistryEntryInput { agent: AgentDefinition | null | undefined; agentVersionKnown: boolean; imageTag: string | null; + openclawImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; appliedPolicies: string[]; toolDisclosure?: ToolDisclosure; webSearchEnabled?: boolean; @@ -111,6 +113,13 @@ export function buildCreatedSandboxRegistryEntry( ...input.runtimeFields, ...getSandboxAgentRegistryFields(input.agent, input.agentVersionKnown), imageTag: input.imageTag, + ...(input.openclawImagePluginInstalls !== undefined + ? { + openclawImagePluginInstalls: input.openclawImagePluginInstalls.map((install) => ({ + ...install, + })), + } + : {}), policies: input.appliedPolicies, toolDisclosure: input.toolDisclosure ?? DEFAULT_TOOL_DISCLOSURE, webSearchEnabled: input.webSearchEnabled === true, diff --git a/src/lib/state/openclaw-config-merge.test.ts b/src/lib/state/openclaw-config-merge.test.ts index 2586d172a3..4aee0c686c 100644 --- a/src/lib/state/openclaw-config-merge.test.ts +++ b/src/lib/state/openclaw-config-merge.test.ts @@ -5,6 +5,18 @@ import { describe, expect, it } from "vitest"; import { mergeOpenClawRestoredConfig } from "./openclaw-config-merge"; +const WEATHER_V1_PATH = "/sandbox/.openclaw/extensions/weather"; +const WEATHER_V2_PATH = "/sandbox/.openclaw/extensions/weather-v2"; +const USER_PLUGIN_PATH = "/sandbox/.openclaw/extensions/user-plugin"; + +function pluginConfig( + entries: Record, + installs: Record, + paths: string[], +) { + return { plugins: { entries, installs, load: { paths } } }; +} + describe("mergeOpenClawRestoredConfig", () => { it("keeps rebuilt runtime-owned config while restoring durable backup-only settings", () => { const merged = mergeOpenClawRestoredConfig( @@ -385,4 +397,82 @@ describe("mergeOpenClawRestoredConfig", () => { expect(merged.plugins.entries.tavily).toBeUndefined(); expect(merged.plugins.entries.customPlugin).toEqual({ enabled: true }); }); + + it("does not resurrect an image plugin removed by the fresh image", () => { + const merged = mergeOpenClawRestoredConfig( + pluginConfig( + { weather: { enabled: true, config: { revision: "v1" } } }, + { weather: { installPath: WEATHER_V1_PATH } }, + [WEATHER_V1_PATH], + ), + pluginConfig({}, {}, []), + { previousImagePluginInstalls: [{ id: "weather", installPath: WEATHER_V1_PATH }] }, + ) as { + plugins: { + entries: Record; + installs: Record; + load?: { paths?: string[] }; + }; + }; + + expect(merged.plugins.entries.weather).toBeUndefined(); + expect(merged.plugins.installs.weather).toBeUndefined(); + expect(merged.plugins.load).toBeUndefined(); + }); + + it("keeps only the fresh plugin when an image plugin is renamed", () => { + const merged = mergeOpenClawRestoredConfig( + pluginConfig( + { weather: { enabled: true, config: { revision: "v1" } } }, + { weather: { installPath: WEATHER_V1_PATH } }, + [WEATHER_V1_PATH], + ), + pluginConfig( + { "weather-v2": { enabled: true, config: { revision: "v2" } } }, + { "weather-v2": { installPath: WEATHER_V2_PATH } }, + [WEATHER_V2_PATH], + ), + { previousImagePluginInstalls: [{ id: "weather", installPath: WEATHER_V1_PATH }] }, + ) as { plugins: Record }; + + expect(merged.plugins).toEqual({ + entries: { "weather-v2": { enabled: true, config: { revision: "v2" } } }, + installs: { "weather-v2": { installPath: WEATHER_V2_PATH } }, + load: { paths: [WEATHER_V2_PATH] }, + }); + }); + + it("preserves backup-only user plugins while removing a retired image plugin", () => { + const merged = mergeOpenClawRestoredConfig( + pluginConfig( + { + weather: { enabled: true }, + "user-plugin": { enabled: true, config: { owner: "user" } }, + }, + { + weather: { installPath: WEATHER_V1_PATH }, + "user-plugin": { installPath: USER_PLUGIN_PATH }, + }, + [WEATHER_V1_PATH, USER_PLUGIN_PATH], + ), + pluginConfig({}, {}, []), + { previousImagePluginInstalls: [{ id: "weather", installPath: WEATHER_V1_PATH }] }, + ) as { plugins: Record }; + + expect(merged.plugins).toEqual({ + entries: { "user-plugin": { enabled: true, config: { owner: "user" } } }, + installs: { "user-plugin": { installPath: USER_PLUGIN_PATH } }, + load: { paths: [USER_PLUGIN_PATH] }, + }); + }); + + it("keeps legacy backup-only plugins when no image provenance was recorded", () => { + const backup = pluginConfig( + { weather: { enabled: true, config: { revision: "v1" } } }, + { weather: { installPath: WEATHER_V1_PATH } }, + [WEATHER_V1_PATH], + ); + + expect(mergeOpenClawRestoredConfig(backup, pluginConfig({}, {}, []))).toMatchObject(backup); + }); }); diff --git a/src/lib/state/openclaw-config-merge.ts b/src/lib/state/openclaw-config-merge.ts index b3bd0cad36..d3357416f3 100644 --- a/src/lib/state/openclaw-config-merge.ts +++ b/src/lib/state/openclaw-config-merge.ts @@ -3,6 +3,7 @@ import { isRecord } from "../core/json-types.js"; import { listOpenClawManagedChannelNames } from "../messaging/channels/index.js"; +import type { OpenClawImagePluginInstall } from "./openclaw-plugin-restore.js"; const MANAGED_OPENCLAW_CHANNEL_NAMES = listOpenClawManagedChannelNames(); @@ -109,11 +110,13 @@ function mergeOpenClawChannels(backupChannels: unknown, currentChannels: unknown function mergeOpenClawEntryMap( backupEntries: unknown, currentEntries: unknown, + previousImagePluginIds?: ReadonlySet, ): Record | undefined { if (!isPlainJsonObject(backupEntries) && !isPlainJsonObject(currentEntries)) return undefined; const merged: Record = {}; if (isPlainJsonObject(backupEntries)) { for (const [key, value] of Object.entries(backupEntries)) { + if (previousImagePluginIds?.has(key)) continue; // Search-provider plugins are selected by the fresh rebuild. Omitting // one is meaningful: provider switches and disablement must not restore // a stale Brave/Tavily entry from the durable snapshot. @@ -129,6 +132,34 @@ function mergeOpenClawEntryMap( return merged; } +function mergeOpenClawPluginLoad( + backupLoad: unknown, + currentLoad: unknown, + previousImagePluginInstallPaths?: ReadonlySet, +): unknown { + if (!previousImagePluginInstallPaths) { + if (!isPlainJsonObject(backupLoad)) return cloneJson(currentLoad); + if (!isPlainJsonObject(currentLoad)) return cloneJson(backupLoad); + return mergeJsonObjects(currentLoad, backupLoad); + } + const backup = isPlainJsonObject(backupLoad) ? backupLoad : {}; + const current = isPlainJsonObject(currentLoad) ? currentLoad : {}; + const merged = mergeJsonObjects(current, backup); + const currentPaths = Array.isArray(current.paths) + ? current.paths.filter((entry): entry is string => typeof entry === "string") + : []; + const backupPaths = Array.isArray(backup.paths) + ? backup.paths.filter( + (entry): entry is string => + typeof entry === "string" && !previousImagePluginInstallPaths.has(entry), + ) + : []; + const paths = [...new Set([...currentPaths, ...backupPaths])]; + if (paths.length > 0) merged.paths = paths; + else delete merged.paths; + return merged; +} + function mergeOpenClawTools(backupTools: unknown, currentTools: unknown): unknown { if (!isPlainJsonObject(backupTools)) return cloneJson(currentTools); if (!isPlainJsonObject(currentTools) && currentTools !== undefined && currentTools !== null) { @@ -271,19 +302,53 @@ function mergeOpenClawModels(backupModels: unknown, currentModels: unknown): unk return merged; } -function mergeOpenClawPlugins(backupPlugins: unknown, currentPlugins: unknown): unknown { +function mergeOpenClawPlugins( + backupPlugins: unknown, + currentPlugins: unknown, + previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[], +): unknown { if (!isPlainJsonObject(backupPlugins)) return cloneJson(currentPlugins); const current = isPlainJsonObject(currentPlugins) ? currentPlugins : {}; + const previousImagePluginIds = previousImagePluginInstalls + ? new Set(previousImagePluginInstalls.map((install) => install.id)) + : undefined; + const previousImagePluginInstallPaths = previousImagePluginInstalls + ? new Set(previousImagePluginInstalls.map((install) => install.installPath)) + : undefined; const merged = mergeJsonObjects(current, backupPlugins); - const entries = mergeOpenClawEntryMap(backupPlugins.entries, current.entries); + const entries = mergeOpenClawEntryMap( + backupPlugins.entries, + current.entries, + previousImagePluginIds, + ); if (entries) merged.entries = entries; + const installs = mergeOpenClawEntryMap( + backupPlugins.installs, + current.installs, + previousImagePluginIds, + ); + if (installs) merged.installs = installs; + if (previousImagePluginInstalls !== undefined) { + const load = mergeOpenClawPluginLoad( + backupPlugins.load, + current.load, + previousImagePluginInstallPaths, + ); + if (isPlainJsonObject(load) && Object.keys(load).length === 0) delete merged.load; + else merged.load = load; + } return merged; } +export interface OpenClawConfigMergeOptions { + previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; +} + export function mergeOpenClawRestoredConfig( backedUpConfig: unknown, currentConfig: unknown, + options: OpenClawConfigMergeOptions = {}, ): unknown { if (!isPlainJsonObject(backedUpConfig)) return cloneJson(currentConfig ?? backedUpConfig); if (!isPlainJsonObject(currentConfig)) return cloneJson(backedUpConfig); @@ -297,7 +362,11 @@ export function mergeOpenClawRestoredConfig( merged.channels = mergeOpenClawChannels(backedUpConfig.channels, currentConfig.channels); merged.models = mergeOpenClawModels(backedUpConfig.models, currentConfig.models); - merged.plugins = mergeOpenClawPlugins(backedUpConfig.plugins, currentConfig.plugins); + merged.plugins = mergeOpenClawPlugins( + backedUpConfig.plugins, + currentConfig.plugins, + options.previousImagePluginInstalls, + ); merged.tools = mergeOpenClawTools(backedUpConfig.tools, currentConfig.tools); return merged; diff --git a/src/lib/state/openclaw-config-restore-input.ts b/src/lib/state/openclaw-config-restore-input.ts index e270af10fd..4dd3b278d5 100644 --- a/src/lib/state/openclaw-config-restore-input.ts +++ b/src/lib/state/openclaw-config-restore-input.ts @@ -4,7 +4,11 @@ import { spawnSync } from "child_process"; import { shellQuote } from "../runner.js"; -import { mergeOpenClawRestoredConfig } from "./openclaw-config-merge.js"; +import { + mergeOpenClawRestoredConfig, + type OpenClawConfigMergeOptions, +} from "./openclaw-config-merge.js"; +import type { OpenClawImagePluginInstall } from "./openclaw-plugin-restore.js"; export interface OpenClawConfigStateFileSpec { path: string; @@ -47,6 +51,7 @@ export interface OpenClawConfigRestoreFromSandboxOptions { backupContents: Buffer; dir: string; log?: (message: string) => void; + previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; specPath: string; sshArgs: readonly string[]; } @@ -92,6 +97,7 @@ function readCurrentOpenClawConfig( export function buildOpenClawConfigRestoreInput( backupContents: Buffer, currentContents: Buffer | null, + options: OpenClawConfigMergeOptions = {}, ): OpenClawConfigRestoreInputResult { if (!currentContents) { return { ok: false, error: "openclaw.json selective merge requires current rebuilt config" }; @@ -100,7 +106,7 @@ export function buildOpenClawConfigRestoreInput( try { const backedUpConfig = JSON.parse(backupContents.toString("utf-8")) as unknown; const currentConfig = JSON.parse(currentContents.toString("utf-8")) as unknown; - const merged = mergeOpenClawRestoredConfig(backedUpConfig, currentConfig); + const merged = mergeOpenClawRestoredConfig(backedUpConfig, currentConfig, options); return { ok: true, input: Buffer.from(`${JSON.stringify(merged, null, 2)}\n`) }; } catch (err) { const detail = err instanceof Error ? err.message : String(err); @@ -115,11 +121,13 @@ export function buildOpenClawConfigRestoreInputFromSandbox({ backupContents, dir, log = () => {}, + previousImagePluginInstalls, specPath, sshArgs, }: OpenClawConfigRestoreFromSandboxOptions): OpenClawConfigRestoreInputResult { return buildOpenClawConfigRestoreInput( backupContents, readCurrentOpenClawConfig(sshArgs, dir, specPath, log), + { previousImagePluginInstalls }, ); } diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts index 7b0c57105b..25d0036d02 100644 --- a/src/lib/state/openclaw-plugin-restore.test.ts +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -11,6 +11,7 @@ import { describe, expect, it } from "vitest"; import { buildFreshOpenClawPluginIndexSqliteReadCommand, parseFreshOpenClawPluginExtensionDirs, + parseOpenClawImagePluginInstalls, } from "./openclaw-plugin-restore"; const OPENCLAW_DIR = "/sandbox/.openclaw"; @@ -108,7 +109,14 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { }, OPENCLAW_DIR, ), - ).toEqual({ ok: true, extensionDirs: ["nemoclaw", "weather"] }); + ).toEqual({ + ok: true, + extensionDirs: ["nemoclaw", "weather"], + pluginInstalls: [ + { id: "nemoclaw", installPath: `${OPENCLAW_DIR}/extensions/nemoclaw` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + ], + }); }); it("ignores npm installs outside extensions and accepts scoped IDs with encoded directories", () => { @@ -125,7 +133,16 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { }, OPENCLAW_DIR, ), - ).toEqual({ ok: true, extensionDirs: ["foo+bar", "my plugin", "scope__weather"] }); + ).toEqual({ + ok: true, + extensionDirs: ["foo+bar", "my plugin", "scope__weather"], + pluginInstalls: expect.arrayContaining([ + { id: "@scope/weather", installPath: `${OPENCLAW_DIR}/extensions/scope__weather` }, + { id: "diagnostics", installPath: `${OPENCLAW_DIR}/npm/node_modules/diagnostics` }, + { id: "foo+bar", installPath: `${OPENCLAW_DIR}/extensions/foo+bar` }, + { id: "my plugin", installPath: `${OPENCLAW_DIR}/extensions/my plugin` }, + ]), + }); }); it.each([ @@ -174,6 +191,59 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { { version: 1, installRecords: { weather: install(installPath) } }, OPENCLAW_DIR, ), - ).toEqual({ ok: true, extensionDirs: [] }); + ).toEqual({ + ok: true, + extensionDirs: [], + pluginInstalls: [{ id: "weather", installPath }], + }); + }); +}); + +describe("parseOpenClawImagePluginInstalls", () => { + it("preserves known-empty provenance and validates populated records", () => { + expect(parseOpenClawImagePluginInstalls([], OPENCLAW_DIR)).toEqual({ + ok: true, + extensionDirs: [], + pluginInstalls: [], + }); + expect( + parseOpenClawImagePluginInstalls( + [ + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { id: "npm-tool", installPath: `${OPENCLAW_DIR}/npm/node_modules/npm-tool` }, + ], + OPENCLAW_DIR, + ), + ).toEqual({ + ok: true, + extensionDirs: ["weather"], + pluginInstalls: [ + { id: "npm-tool", installPath: `${OPENCLAW_DIR}/npm/node_modules/npm-tool` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + ], + }); + }); + + it.each([ + ["unsafe ID", [{ id: "../weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }]], + [ + "duplicate ID", + [ + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/forecast` }, + ], + ], + [ + "duplicate install path", + [ + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { id: "forecast", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + ], + ], + ["relative path", [{ id: "weather", installPath: "extensions/weather" }]], + ])("rejects %s", (_label, provenance) => { + expect(parseOpenClawImagePluginInstalls(provenance, OPENCLAW_DIR)).toEqual( + expect.objectContaining({ ok: false }), + ); }); }); diff --git a/src/lib/state/openclaw-plugin-restore.ts b/src/lib/state/openclaw-plugin-restore.ts index ee9d56484b..3d5a444898 100644 --- a/src/lib/state/openclaw-plugin-restore.ts +++ b/src/lib/state/openclaw-plugin-restore.ts @@ -13,9 +13,14 @@ const MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS = 64; const OPENCLAW_EXTENSION_GLOB_CHARS = ["/", "\\", "*", "?", "[", "]"] as const; export type OpenClawManagedExtensionDiscoveryResult = - | { ok: true; extensionDirs: string[] } + | { ok: true; extensionDirs: string[]; pluginInstalls: OpenClawImagePluginInstall[] } | { ok: false; error: string }; +export interface OpenClawImagePluginInstall { + readonly id: string; + readonly installPath: string; +} + const OPENCLAW_PLUGIN_INDEX_SQLITE_PY = [ "import json, sqlite3, sys, urllib.parse", 'uri = "file:" + urllib.parse.quote(sys.argv[1], safe="/") + "?mode=ro"', @@ -62,69 +67,117 @@ export function isSafeOpenClawExtensionDirName(name: string): boolean { ); } -export function parseFreshOpenClawPluginExtensionDirs( - registryIndex: unknown, +function validateOpenClawImagePluginInstall( + id: unknown, + installPath: unknown, +): OpenClawImagePluginInstall | null { + if (typeof id !== "string" || !isSafeOpenClawPluginInstallId(id)) return null; + if ( + typeof installPath !== "string" || + installPath.length > MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH || + installPath.split("/").filter(Boolean).length > MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS || + !path.posix.isAbsolute(installPath) || + path.posix.normalize(installPath) !== installPath + ) { + return null; + } + return { id, installPath }; +} + +function extensionDirForInstall( + install: OpenClawImagePluginInstall, dir: string, -): OpenClawManagedExtensionDiscoveryResult { - if (!isRecord(registryIndex) || registryIndex.version !== 1) { - return { ok: false, error: "fresh OpenClaw plugin install registry is invalid" }; +): { ok: true; extensionDir: string | null } | { ok: false; error: string } { + const extensionsDir = `${dir.replace(/\/+$/, "")}/extensions`; + const relativeInstallPath = path.posix.relative(extensionsDir, install.installPath); + if ( + relativeInstallPath.startsWith("../") || + relativeInstallPath === ".." || + path.posix.isAbsolute(relativeInstallPath) + ) { + return { ok: true, extensionDir: null }; } - const installs = registryIndex.installRecords; - if (!isRecord(installs)) { - return { ok: false, error: "fresh OpenClaw plugin install records are invalid" }; + if ( + relativeInstallPath.length === 0 || + relativeInstallPath.includes("/") || + !isSafeOpenClawExtensionDirName(relativeInstallPath) + ) { + return { + ok: false, + error: `fresh OpenClaw plugin install path is invalid for ${install.id}`, + }; } + return { ok: true, extensionDir: relativeInstallPath }; +} - const ids = Object.keys(installs).sort(); - if (ids.length > MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS) { +function validateOpenClawImagePluginInstalls( + entries: readonly (readonly [string, unknown])[], + dir: string, +): OpenClawManagedExtensionDiscoveryResult { + if (entries.length > MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS) { return { ok: false, - error: `fresh OpenClaw registry has too many plugin installs (${ids.length})`, + error: `fresh OpenClaw registry has too many plugin installs (${entries.length})`, }; } - const extensionsDir = `${dir.replace(/\/+$/, "")}/extensions`; + + const ids = new Set(); + const installPaths = new Set(); const extensionDirs = new Set(); - for (const id of ids) { - if (!isSafeOpenClawPluginInstallId(id)) { - return { ok: false, error: `fresh OpenClaw registry has unsafe plugin install id: ${id}` }; - } - const install = installs[id]; - if (!isRecord(install)) { + const pluginInstalls: OpenClawImagePluginInstall[] = []; + for (const [id, metadata] of entries) { + const installPath = isRecord(metadata) ? metadata.installPath : undefined; + const install = validateOpenClawImagePluginInstall(id, installPath); + if (!install) { return { ok: false, error: `fresh OpenClaw plugin install metadata is invalid: ${id}` }; } - if ( - typeof install.installPath !== "string" || - install.installPath.length > MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH || - install.installPath.split("/").filter(Boolean).length > - MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS || - !path.posix.isAbsolute(install.installPath) - ) { - return { ok: false, error: `fresh OpenClaw plugin install path is invalid for ${id}` }; + if (ids.has(install.id) || installPaths.has(install.installPath)) { + return { ok: false, error: `fresh OpenClaw plugin install provenance is duplicated: ${id}` }; } - const normalizedInstallPath = path.posix.normalize(install.installPath); - if (normalizedInstallPath !== install.installPath) { - return { ok: false, error: `fresh OpenClaw plugin install path is invalid for ${id}` }; + const projected = extensionDirForInstall(install, dir); + if (!projected.ok) return projected; + if (projected.extensionDir && extensionDirs.has(projected.extensionDir)) { + return { ok: false, error: `fresh OpenClaw extension directory is duplicated: ${id}` }; } + ids.add(install.id); + installPaths.add(install.installPath); + pluginInstalls.push(install); + if (projected.extensionDir) extensionDirs.add(projected.extensionDir); + } + return { + ok: true, + extensionDirs: [...extensionDirs].sort(), + pluginInstalls: pluginInstalls.sort((a, b) => a.id.localeCompare(b.id)), + }; +} - // npm-origin installs legitimately live under .openclaw/npm/node_modules - // and are not part of the backed-up extensions directory. Only direct - // children of extensions can be overwritten by extension restore. - const relativeInstallPath = path.posix.relative(extensionsDir, normalizedInstallPath); - if ( - relativeInstallPath.startsWith("../") || - relativeInstallPath === ".." || - path.posix.isAbsolute(relativeInstallPath) - ) { - continue; - } - if (relativeInstallPath.length === 0 || relativeInstallPath.includes("/")) { - return { ok: false, error: `fresh OpenClaw plugin install path is invalid for ${id}` }; - } +export function parseOpenClawImagePluginInstalls( + value: unknown, + dir: string, +): OpenClawManagedExtensionDiscoveryResult { + if (!Array.isArray(value)) { + return { ok: false, error: "OpenClaw image plugin provenance is invalid" }; + } + return validateOpenClawImagePluginInstalls( + value.map((entry) => [isRecord(entry) && typeof entry.id === "string" ? entry.id : "", entry]), + dir, + ); +} - const extensionDir = relativeInstallPath; - if (!isSafeOpenClawExtensionDirName(extensionDir)) { - return { ok: false, error: `fresh OpenClaw extension directory is invalid for ${id}` }; - } - extensionDirs.add(extensionDir); +export function parseFreshOpenClawPluginExtensionDirs( + registryIndex: unknown, + dir: string, +): OpenClawManagedExtensionDiscoveryResult { + if (!isRecord(registryIndex) || registryIndex.version !== 1) { + return { ok: false, error: "fresh OpenClaw plugin install registry is invalid" }; } - return { ok: true, extensionDirs: [...extensionDirs].sort() }; + const installs = registryIndex.installRecords; + if (!isRecord(installs)) { + return { ok: false, error: "fresh OpenClaw plugin install records are invalid" }; + } + + const entries = Object.keys(installs) + .sort() + .map((id) => [id, installs[id]] as const); + return validateOpenClawImagePluginInstalls(entries, dir); } diff --git a/src/lib/state/registry.ts b/src/lib/state/registry.ts index 2b72ae66a6..9ad26ae540 100644 --- a/src/lib/state/registry.ts +++ b/src/lib/state/registry.ts @@ -15,6 +15,7 @@ import { normalizeExtraProviders, readExtraProviders, } from "./extra-providers"; +import type { OpenClawImagePluginInstall } from "./openclaw-plugin-restore"; import { normalizeSandboxMcpState, type SandboxMcpState, @@ -104,6 +105,8 @@ export interface SandboxEntry extends Partial { webSearchProvider?: WebSearchProvider | null; agent?: string | null; agentVersion?: string | null; + /** Plugin install baseline captured before state is restored into a fresh OpenClaw image. */ + openclawImagePluginInstalls?: OpenClawImagePluginInstall[]; // NemoClaw build fingerprint (the NemoClaw CLI/build version) stamped only on // NemoClaw-managed images at create/rebuild time. `upgrade-sandboxes` compares // it against the running NemoClaw build so an image/build change with an @@ -490,6 +493,9 @@ export function registerSandbox(entry: SandboxEntry): void { // cannot inherit a stale finalized marker. See #4621. agent: entry.agent || null, agentVersion: entry.agentVersion || null, + openclawImagePluginInstalls: Array.isArray(entry.openclawImagePluginInstalls) + ? entry.openclawImagePluginInstalls.map((install) => ({ ...install })) + : undefined, nemoclawVersion: entry.nemoclawVersion || null, fromDockerfile: entry.fromDockerfile || null, hermesAuthMethod: diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 6a6d4a26b0..65f218bcd6 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -47,8 +47,10 @@ import { } from "./openclaw-managed-extensions.js"; import { buildFreshOpenClawPluginIndexSqliteReadCommand, + type OpenClawImagePluginInstall, type OpenClawManagedExtensionDiscoveryResult, parseFreshOpenClawPluginExtensionDirs, + parseOpenClawImagePluginInstalls, } from "./openclaw-plugin-restore.js"; import type { CustomPolicyEntry } from "./registry.js"; import * as registry from "./registry.js"; @@ -77,6 +79,8 @@ export interface RebuildManifest { agentType: string; agentVersion: string | null; expectedVersion: string | null; + /** Fresh-image plugin baseline captured before user state was restored. */ + openclawImagePluginInstalls?: OpenClawImagePluginInstall[]; stateDirs: string[]; /** Directories verified as safe to restore. Absent on older manifests. */ backedUpDirs?: string[]; @@ -155,16 +159,21 @@ export interface RestoreResult { } export interface RestoreOptions { - /** - * The target is a just-recreated OpenClaw sandbox, so its plugin install - * registry came from the fresh image. Preserve those validated extension - * directories over the backup. - */ - preserveFreshOpenClawPluginInstalls?: boolean; /** Optional file-specific restore capability authorized by the caller. */ stateFileRestorePolicy?: StateFileRestorePolicy; } +export interface RecreatedSandboxRestoreOptions extends RestoreOptions { + /** Agent in the newly created target image, not the backup manifest agent. */ + targetAgentType: string; + /** Pre-captured baseline avoids a second remote read during onboarding finalization. */ + freshOpenClawImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; +} + +interface InternalRestoreOptions extends RestoreOptions { + freshOpenClawImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; +} + export interface TarValidationResult { safe: boolean; entries: string[]; @@ -216,6 +225,7 @@ function isCustomPolicyEntryArray(value: unknown): value is CustomPolicyEntry[] function isRebuildManifest(value: unknown): value is RebuildManifest { if (!isRecord(value) || !isStateDirArray(value.stateDirs)) return false; + const dir = typeof value.dir === "string" ? value.dir : value.writableDir; return ( typeof value.version === "number" && typeof value.sandboxName === "string" && @@ -224,7 +234,9 @@ function isRebuildManifest(value: unknown): value is RebuildManifest { (value.agentVersion === null || typeof value.agentVersion === "string") && (value.expectedVersion === null || typeof value.expectedVersion === "string") && (value.backedUpDirs === undefined || isBackedUpDirArray(value.backedUpDirs, value.stateDirs)) && - (typeof value.dir === "string" || typeof value.writableDir === "string") && + typeof dir === "string" && + (value.openclawImagePluginInstalls === undefined || + parseOpenClawImagePluginInstalls(value.openclawImagePluginInstalls, dir).ok) && typeof value.backupPath === "string" && (value.stateFiles === undefined || (Array.isArray(value.stateFiles) && value.stateFiles.every(isStateFileSpec))) && @@ -725,6 +737,22 @@ function discoverFreshOpenClawPluginExtensionDirs( : { ok: false, error: "fresh OpenClaw plugin install registry failed validation" }; } +export function discoverFreshOpenClawImagePluginInstalls( + sandboxName: string, + dir = "/sandbox/.openclaw", +): OpenClawManagedExtensionDiscoveryResult { + const sshConfig = getSshConfig(sandboxName); + if (!sshConfig) { + return { ok: false, error: "could not get SSH config for OpenClaw plugin discovery" }; + } + const tempSshConfig = createTempSshConfig(sshConfig, "nemoclaw-plugin-discovery-"); + try { + return discoverFreshOpenClawPluginExtensionDirs(tempSshConfig.file, sandboxName, dir); + } finally { + tempSshConfig.cleanup(); + } +} + export const __test = { discoverFreshOpenClawPluginExtensionDirs, }; @@ -962,6 +990,7 @@ function buildStateFileRestoreInput( spec: StateFileSpec, backupContents: Buffer, mergeOpenClawConfig: boolean, + previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[], ): Buffer | null { if (!mergeOpenClawConfig) return backupContents; @@ -969,6 +998,7 @@ function buildStateFileRestoreInput( backupContents, dir, log: _log, + previousImagePluginInstalls, specPath: spec.path, sshArgs: sshArgs(configFile, sandboxName), }); @@ -986,6 +1016,7 @@ function restoreStateFile( backupPath: string, mergeOpenClawConfig = false, stateFileRestorePolicy?: StateFileRestorePolicy, + previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[], ): boolean { const localPath = path.join(backupPath, spec.path); if (!existsSync(localPath)) return true; @@ -1003,6 +1034,7 @@ function restoreStateFile( spec, backupContents, mergeOpenClawConfig, + previousImagePluginInstalls, ); if (input === null) return false; @@ -1045,6 +1077,22 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions = `backupSandboxState: agent=${agentName}, dir=${dir}, stateDirs=[${stateDirs.join(",")}], stateFiles=[${stateFiles.map((f) => f.path).join(",")}]`, ); + let openclawImagePluginInstalls: OpenClawImagePluginInstall[] | undefined; + if (agentName === "openclaw" && sb?.openclawImagePluginInstalls !== undefined) { + const provenance = parseOpenClawImagePluginInstalls(sb.openclawImagePluginInstalls, dir); + if (!provenance.ok) { + return { + success: false, + backedUpDirs: [], + failedDirs: [], + backedUpFiles: [], + failedFiles: [], + error: "registered OpenClaw image plugin provenance is invalid", + }; + } + openclawImagePluginInstalls = provenance.pluginInstalls; + } + // Validate user-supplied name and check for conflicts BEFORE creating any // files on disk. const existingBackups = listBackups(sandboxName); @@ -1109,6 +1157,7 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions = agentType: agentName, agentVersion: sb?.agentVersion || null, expectedVersion: agent.expectedVersion, + ...(openclawImagePluginInstalls !== undefined ? { openclawImagePluginInstalls } : {}), stateDirs, stateFiles, dir, @@ -1432,6 +1481,45 @@ export function restoreSandboxState( sandboxName: string, backupPath: string, options: RestoreOptions = {}, +): RestoreResult { + return restoreSandboxStateInternal(sandboxName, backupPath, options); +} + +export function restoreRecreatedSandboxState( + sandboxName: string, + backupPath: string, + options: RecreatedSandboxRestoreOptions, +): RestoreResult { + let freshOpenClawImagePluginInstalls: readonly OpenClawImagePluginInstall[] | undefined; + if (options.targetAgentType === "openclaw") { + const discovery = options.freshOpenClawImagePluginInstalls + ? parseOpenClawImagePluginInstalls( + options.freshOpenClawImagePluginInstalls, + "/sandbox/.openclaw", + ) + : discoverFreshOpenClawImagePluginInstalls(sandboxName); + if (!discovery.ok) { + return { + success: false, + restoredDirs: [], + failedDirs: ["extensions"], + restoredFiles: [], + failedFiles: [], + error: discovery.error, + }; + } + freshOpenClawImagePluginInstalls = discovery.pluginInstalls; + } + return restoreSandboxStateInternal(sandboxName, backupPath, { + freshOpenClawImagePluginInstalls, + stateFileRestorePolicy: options.stateFileRestorePolicy, + }); +} + +function restoreSandboxStateInternal( + sandboxName: string, + backupPath: string, + options: InternalRestoreOptions, ): RestoreResult { _log(`restoreSandboxState: sandbox=${sandboxName}, backupPath=${backupPath}`); const manifest = readManifest(backupPath); @@ -1496,41 +1584,70 @@ export function restoreSandboxState( const tempSshConfig = createTempSshConfig(sshConfig, "nemoclaw-state-"); const configFile = tempSshConfig.file; - let freshImageManagedExtensionDirs: string[] = []; + const freshOpenClawImagePluginInstalls = options.freshOpenClawImagePluginInstalls; + const previousOpenClawImagePluginInstalls = + freshOpenClawImagePluginInstalls !== undefined + ? manifest.openclawImagePluginInstalls + : undefined; try { const preserveManagedExtensions = shouldPreserveOpenClawManagedExtensions( manifest, dir, localDirs, ); - if (options.preserveFreshOpenClawPluginInstalls && preserveManagedExtensions) { - const discovery = discoverFreshOpenClawPluginExtensionDirs(configFile, sandboxName, dir); - if (!discovery.ok) { - _log(`FAILED: ${discovery.error}`); - return { - success: false, - restoredDirs, - failedDirs: [...localDirs], - restoredFiles, - failedFiles: localFiles.map((f) => f.path), - error: discovery.error, - }; - } - freshImageManagedExtensionDirs = discovery.extensionDirs; - _log(`Fresh image-managed OpenClaw extensions: [${discovery.extensionDirs.join(",")}]`); + const freshProjection = parseOpenClawImagePluginInstalls( + freshOpenClawImagePluginInstalls ?? [], + dir, + ); + const previousProjection = parseOpenClawImagePluginInstalls( + previousOpenClawImagePluginInstalls ?? [], + dir, + ); + if (!freshProjection.ok || !previousProjection.ok) { + let error = "OpenClaw image plugin provenance is invalid"; + if (!freshProjection.ok) error = freshProjection.error; + else if (!previousProjection.ok) error = previousProjection.error; + return { + success: false, + restoredDirs, + failedDirs: [...localDirs], + restoredFiles, + failedFiles: localFiles.map((f) => f.path), + error, + }; } - const managedExtensionDirs = preserveManagedExtensions + const freshImageManagedExtensionDirs = preserveManagedExtensions + ? freshProjection.extensionDirs + : []; + const previousImageManagedExtensionDirs = + preserveManagedExtensions && previousOpenClawImagePluginInstalls !== undefined + ? previousProjection.extensionDirs + : []; + if (freshOpenClawImagePluginInstalls !== undefined && preserveManagedExtensions) { + _log( + `Fresh image-managed OpenClaw extensions: [${freshImageManagedExtensionDirs.join(",")}]`, + ); + _log( + `Previous image-managed OpenClaw extensions: [${previousImageManagedExtensionDirs.join(",")}]`, + ); + } + const preservedManagedExtensionDirs = preserveManagedExtensions ? [ ...new Set([...OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS, ...freshImageManagedExtensionDirs]), ].sort() : []; + const restoreArchiveExcludedExtensionDirs = preserveManagedExtensions + ? [ + ...new Set([...preservedManagedExtensionDirs, ...previousImageManagedExtensionDirs]), + ].sort() + : []; if (localDirs.length > 0) { // Upload via tar pipe // NC-2227-04: Removed -h flag from restore as well — no symlink following. const tarResult = spawnSync( "tar", - buildRestoreTarArgs(backupPath, localDirs, managedExtensionDirs), + buildRestoreTarArgs(backupPath, localDirs, restoreArchiveExcludedExtensionDirs), { stdio: ["ignore", "pipe", "pipe"], timeout: 60000, @@ -1556,7 +1673,7 @@ export function restoreSandboxState( const rmCmd = buildRestoreCleanupCommand( dir, localDirs, - managedExtensionDirs, + preservedManagedExtensionDirs, new Set(freshImageManagedExtensionDirs), ); _log(`Cleaning target dirs before restore: ${rmCmd}`); @@ -1653,6 +1770,7 @@ export function restoreSandboxState( backupPath, shouldMergeOpenClawConfigStateFile(manifest.agentType, dir, spec), options.stateFileRestorePolicy, + previousOpenClawImagePluginInstalls, ) ) { restoredFiles.push(spec.path); diff --git a/test/e2e/fixtures/plugins/weather/package-lock.json b/test/e2e/fixtures/plugins/weather/package-lock.json index a3e1e00343..e58ef537b8 100644 --- a/test/e2e/fixtures/plugins/weather/package-lock.json +++ b/test/e2e/fixtures/plugins/weather/package-lock.json @@ -12,7 +12,7 @@ "typebox": "1.1.38" }, "devDependencies": { - "openclaw": "2026.6.10", + "openclaw": "2026.5.27", "typescript": "5.9.3" }, "engines": { @@ -23,63 +23,59 @@ } }, "node_modules/openclaw": { - "version": "2026.6.10", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.10.tgz", - "integrity": "sha512-LcooND2tBQw8A+kc1Ujltu3lg30bJ0w7XaeRy7eYzobb8BBdcW6DOGbwJL4vpj1vl9+gjRceOtlh5nh9OARcug==", + "version": "2026.5.27", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.27.tgz", + "integrity": "sha512-2N93zhdAo88KAbHt6T7KvYXf4s7XIkYXBgv1npYpn7e1Y9FvrtgtpsA38my9rtFW+70uXEojRPX5/OqnuDqJPw==", "dev": true, "hasInstallScript": true, "hasShrinkwrap": true, "license": "MIT", "dependencies": { "@agentclientprotocol/sdk": "0.22.1", - "@anthropic-ai/sdk": "0.100.1", "@clack/core": "1.3.1", "@clack/prompts": "1.4.0", - "@earendil-works/pi-tui": "0.78.0", - "@google/genai": "2.7.0", + "@earendil-works/pi-agent-core": "0.75.5", + "@earendil-works/pi-ai": "0.75.5", + "@earendil-works/pi-coding-agent": "0.75.5", + "@earendil-works/pi-tui": "0.75.5", + "@google/genai": "2.6.0", "@grammyjs/runner": "2.0.3", "@grammyjs/transformer-throttler": "1.2.1", - "@homebridge/ciao": "1.3.9", + "@homebridge/ciao": "1.3.8", "@lydell/node-pty": "1.2.0-beta.12", - "@mistralai/mistralai": "2.2.5", "@modelcontextprotocol/sdk": "1.29.0", "@mozilla/readability": "0.6.0", "@openclaw/fs-safe": "0.3.0", "@openclaw/proxyline": "0.3.3", "chalk": "5.6.2", "chokidar": "5.0.0", - "clawpdf": "0.3.0", "commander": "14.0.3", "croner": "10.0.1", - "diff": "9.0.0", "dotenv": "17.4.2", "express": "5.2.1", "file-type": "22.0.1", - "glob": "13.0.6", "grammy": "1.43.0", - "highlight.js": "11.11.1", - "hosted-git-info": "10.1.1", - "ignore": "7.0.5", + "ipaddr.js": "2.4.0", "jiti": "2.7.0", "json5": "2.2.3", "jszip": "3.10.1", "kysely": "0.29.2", "linkedom": "0.18.12", - "minimatch": "10.2.5", + "markdown-it": "14.1.1", "node-edge-tts": "1.2.10", - "openai": "6.39.1", - "partial-json": "0.1.7", + "openai": "6.39.0", + "pdfjs-dist": "5.7.284", "playwright-core": "1.60.0", - "proper-lockfile": "4.1.2", "qrcode": "1.5.4", - "quickjs-wasi": "3.0.0", - "rastermill": "0.3.1", - "tar": "7.5.16", + "quickjs-wasi": "2.2.0", + "rastermill": "0.3.0", + "tar": "7.5.15", + "tokenjuice": "0.7.1", "tree-sitter-bash": "0.25.1", "tslog": "4.10.2", - "typebox": "1.1.39", + "typebox": "1.1.38", "typescript": "6.0.3", - "undici": "8.5.0", + "undici": "8.3.0", "web-push": "3.6.7", "web-tree-sitter": "0.26.9", "ws": "8.21.0", @@ -107,9 +103,9 @@ } }, "node_modules/openclaw/node_modules/@anthropic-ai/sdk": { - "version": "0.100.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.100.1.tgz", - "integrity": "sha512-RANcEe7LpiLczkKGOwoXOTuFdPhuubS0i4xaAKOMpcqc55YO0mukgxppV7eygx3DXNjxWT6RYOLPyOy0aIAmwg==", + "version": "0.98.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.98.0.tgz", + "integrity": "sha512-N7aXtCvC5g6T1Y4V29lJjceu/zTkVkIZF0jdBvagr0TRFHuKeImffalGWEfqZKrvjH+IQbzJWw6TmSmUzrlMgg==", "dev": true, "license": "MIT", "dependencies": { @@ -128,10 +124,441 @@ } } }, + "node_modules/openclaw/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/openclaw/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1053.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1053.0.tgz", + "integrity": "sha512-I5dua8y1logE+Mx6r5kvI1tjM+XyC3H42KDCpEqmhrJfanor/x/AdOavyv3HnS4sBqUxx2IrjLP3ouEumjeTzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/credential-provider-node": "^3.972.44", + "@aws-sdk/eventstream-handler-node": "^3.972.17", + "@aws-sdk/middleware-eventstream": "^3.972.13", + "@aws-sdk/middleware-websocket": "^3.972.21", + "@aws-sdk/token-providers": "3.1053.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/node-http-handler": "^4.7.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/core": { + "version": "3.974.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.13.tgz", + "integrity": "sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@aws-sdk/xml-builder": "^3.972.25", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.3", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.39.tgz", + "integrity": "sha512-29wX9zpAvEt1vcj0psha+y6ygBHy2V/S72mp6e7q0KARLWXq+pwE/lR6qGkwknQvruh52lXvlqZIga8Hdxkucw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.41.tgz", + "integrity": "sha512-IA3CQTjtJkb6u1H4mE4936c8OPBMa9Jggtwe8U2Mqw/vvb/tZ5Ebd0mcZcX0uKWQhOyYo/+qNIwkV5Xh+FeJJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/node-http-handler": "^4.7.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.43.tgz", + "integrity": "sha512-4mzII+3mZEVXXE1xzrLQrCJL7/r62A63bA6SVzZoNL5rqCJghpf+xgGltVrIBBs0n+mOZBKrQl2tRREtvZ5l6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/credential-provider-env": "^3.972.39", + "@aws-sdk/credential-provider-http": "^3.972.41", + "@aws-sdk/credential-provider-login": "^3.972.43", + "@aws-sdk/credential-provider-process": "^3.972.39", + "@aws-sdk/credential-provider-sso": "^3.972.43", + "@aws-sdk/credential-provider-web-identity": "^3.972.43", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.43.tgz", + "integrity": "sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.44.tgz", + "integrity": "sha512-sDaBIT0yrNNIPfvlsiTCmANm07zKju+ipWODjEXgZlsjMeIJR3LVp7RDyAOzUoAsTbDfYKDWp+i5WrFiQP6rmQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.39", + "@aws-sdk/credential-provider-http": "^3.972.41", + "@aws-sdk/credential-provider-ini": "^3.972.43", + "@aws-sdk/credential-provider-process": "^3.972.39", + "@aws-sdk/credential-provider-sso": "^3.972.43", + "@aws-sdk/credential-provider-web-identity": "^3.972.43", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.39.tgz", + "integrity": "sha512-2k/amBifLd75eXNwgvPw/2lKYSQ3NhvHQgkVKVjfUq13/eJ3JRtHmznuFenn74OK3sSfp4SMy1YB2w+UVXoKqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.43.tgz", + "integrity": "sha512-LPc3+Y4vhH1T4x6CMqwCM6hk5+SRf/Lwmgm8INm95wxTtIRHcMwQUVkDzWu4Iw/RSncxYM2BC01OrYbxOPZvyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/token-providers": "3.1052.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.43.tgz", + "integrity": "sha512-wQtL34lUD/09VXjwAUo2T+I3aEXRDxMB3DKmTJL/Zj0Gi6sLDTrVhae1XVt01yzkquOWajI/sZW72JGDZ1ciTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.17.tgz", + "integrity": "sha512-WFwdNcjchKZr7jKYgGimUZO8sSKQF/le7GGqgeCzz/lHozInE6b0gFJ1YMr8NaIeAoWJwgtrF7RE4/qMgosAdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.13.tgz", + "integrity": "sha512-ECfsw7mf6G/sxNbKbGE3/h1xeIArY/yRI1IjDGYkLgDIankh+aDOtDRSr40LVlIHGL9+jEH1cVuxmbJ8NLL/1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.21.tgz", + "integrity": "sha512-yr+5+C7v9R55sAJ89A55Wrm7wIKPVn5cm6J3Hztnd5s/iwEUKxyJqCnIxJu4fVXgG9XBQD1Jc4rsWC1ozahJjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.11.tgz", + "integrity": "sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.28", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/fetch-http-handler": "^5.4.3", + "@smithy/node-http-handler": "^4.7.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.28.tgz", + "integrity": "sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/token-providers": { + "version": "3.1053.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1053.0.tgz", + "integrity": "sha512-laSwHLYMMrXQRl2mFDXszF43m/F4pKWyGr7hCLfJmV8rn8c6CnI/hp/bf/Gn7gLcjz0SY4evd7SBpqtnIhzA/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.13", + "@aws-sdk/nested-clients": "^3.997.11", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/types": { + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", + "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz", + "integrity": "sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.2", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/openclaw/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/openclaw/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "dev": true, "license": "MIT", "engines": { @@ -179,24 +606,51 @@ "node": ">= 20.12.0" } }, - "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { - "version": "0.78.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.78.0.tgz", - "integrity": "sha512-3a705FnsVVUhAyceShNB3kS2rpxcxLcx+hqB0u6MMMpHwQGbW+m++MqA6r7eOzq/8FLx5e3vDh38h/SVTk2qzw==", + "node_modules/openclaw/node_modules/@earendil-works/pi-agent-core": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.5.tgz", + "integrity": "sha512-LHygOgsW2pgXKb3IkXkOAeZPovHr9VF+EixgXVsDNuB4jmhEOXgshy/zksZ7slkUAx10OQ9W1Ed/2jsnhd1NqA==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "@earendil-works/pi-ai": "^0.75.5", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" }, "engines": { "node": ">=22.19.0" } }, - "node_modules/openclaw/node_modules/@google/genai": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.7.0.tgz", - "integrity": "sha512-tv0DRtcndt2oEhBYy+5mA0TaXH98+L1Gt0AP9unBfH7DP20KhB7+O3QqAN1Lz+laMARGTHS7BFQSNpLbl4gm1g==", + "node_modules/openclaw/node_modules/@earendil-works/pi-ai": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.5.tgz", + "integrity": "sha512-zf1F5kXk1pqZeFShXOqq9ibUk8QdtRoLCDPAjO+hj44e3EUs9/GFO2qnhTC5+JA2uwVCx+WCNe1PiCjlBYWm5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.1", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-ai/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -218,49 +672,145 @@ } } }, - "node_modules/openclaw/node_modules/@grammyjs/runner": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz", - "integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==", + "node_modules/openclaw/node_modules/@earendil-works/pi-ai/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", "dev": true, - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0" - }, - "engines": { - "node": ">=12.20.0 || >=14.13.1" + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" }, "peerDependencies": { - "grammy": "^1.13.1" + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } } }, - "node_modules/openclaw/node_modules/@grammyjs/transformer-throttler": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz", - "integrity": "sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==", + "node_modules/openclaw/node_modules/@earendil-works/pi-coding-agent": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.75.5.tgz", + "integrity": "sha512-O3CCQDYy28D4uwtP6zZkdEwzHN6X22v49Sb0+SZTC7x37V/YfmogrWPiaFoWeoc2hmdKhSATI7ZAK5bQbJG5NA==", "dev": true, "license": "MIT", "dependencies": { - "bottleneck": "^2.0.0" + "@earendil-works/pi-agent-core": "^0.75.5", + "@earendil-works/pi-ai": "^0.75.5", + "@earendil-works/pi-tui": "^0.75.5", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "typebox": "1.1.38", + "undici": "8.3.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" }, "engines": { - "node": "^12.20.0 || >=14.13.1" + "node": ">=22.19.0" }, - "peerDependencies": { - "grammy": "^1.0.0" + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.6" } }, - "node_modules/openclaw/node_modules/@grammyjs/types": { - "version": "3.27.3", - "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.27.3.tgz", - "integrity": "sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==", + "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.5.tgz", + "integrity": "sha512-LkXUM1/49pvzzeI39Y5wjBMlgafcCf67HCLhB9Z7yuXHy4XgT+VqxWcZVW5hBdhQsHZd0znjJotfGH1BzxMfiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/@google/genai": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz", + "integrity": "sha512-HjoW3mPuEn7pnuKABJl9VbDoWDSF4nbwYKYvYYor7YjPeDxrrBxHzu2d1Prcd+BAuC4w+85UP6y7ZdcrQAoO7g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@grammyjs/runner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz", + "integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0" + }, + "engines": { + "node": ">=12.20.0 || >=14.13.1" + }, + "peerDependencies": { + "grammy": "^1.13.1" + } + }, + "node_modules/openclaw/node_modules/@grammyjs/transformer-throttler": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz", + "integrity": "sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bottleneck": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + }, + "peerDependencies": { + "grammy": "^1.0.0" + } + }, + "node_modules/openclaw/node_modules/@grammyjs/types": { + "version": "3.27.3", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.27.3.tgz", + "integrity": "sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==", "dev": true, "license": "MIT" }, "node_modules/openclaw/node_modules/@homebridge/ciao": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.9.tgz", - "integrity": "sha512-TMy9zy173jDOpnFXDqL3BPIQn5lfcAkSsivYQatCCakoHk4fLGd7QjfAaNGYE3Ox+/ZI6Lq0e1gGcz1qdw/IbA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.8.tgz", + "integrity": "sha512-lNhpCsZVbdbjz2trFjQdzQ3cUIMZQMIMksi7wd3ntTIYgdaGLqT1Ms97DfVIJYHzRuduf56ISvgU8RRLTpK/ng==", "dev": true, "license": "MIT", "dependencies": { @@ -398,10 +948,200 @@ "win32" ] }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.6", + "@mariozechner/clipboard-darwin-universal": "0.3.6", + "@mariozechner/clipboard-darwin-x64": "0.3.6", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-musl": "0.3.6", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/openclaw/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/openclaw/node_modules/@mistralai/mistralai": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.5.tgz", - "integrity": "sha512-ATbWzKkNzNAZ+gtw9MI/c/ULTMG80tKUiRNIbQFfg4OP0uEZZpTfXZeBCNfs5Dq0uqMQ/tQWc4o6RRJQtMrpDA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -461,6 +1201,281 @@ "node": ">=14.0.0" } }, + "node_modules/openclaw/node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "dev": true, + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/openclaw/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/openclaw/node_modules/@openclaw/fs-safe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.3.0.tgz", @@ -488,85 +1503,141 @@ "undici": ">=8.3.0 <9" } }, - "node_modules/openclaw/node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/openclaw/node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "node_modules/openclaw/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0" }, - "node_modules/openclaw/node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "node_modules/openclaw/node_modules/@smithy/core": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.4.tgz", + "integrity": "sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/openclaw/node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "node_modules/openclaw/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.4.tgz", + "integrity": "sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/openclaw/node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "node_modules/openclaw/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.4.tgz", + "integrity": "sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { - "@protobufjs/aspromise": "^1.1.1" + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/openclaw/node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "node_modules/openclaw/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/openclaw/node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "node_modules/openclaw/node_modules/@smithy/node-http-handler": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.4.tgz", + "integrity": "sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/openclaw/node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "node_modules/openclaw/node_modules/@smithy/signature-v4": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.4.tgz", + "integrity": "sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.4", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/openclaw/node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "node_modules/openclaw/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/openclaw/node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "node_modules/openclaw/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/openclaw/node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "node_modules/openclaw/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } }, "node_modules/openclaw/node_modules/@stablelib/base64": { "version": "1.0.1", @@ -600,20 +1671,10 @@ "dev": true, "license": "MIT" }, - "node_modules/openclaw/node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, "node_modules/openclaw/node_modules/@types/retry": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", - "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true, "license": "MIT" }, @@ -715,6 +1776,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/openclaw/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/openclaw/node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -815,6 +1883,13 @@ "dev": true, "license": "MIT" }, + "node_modules/openclaw/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, "node_modules/openclaw/node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -932,19 +2007,6 @@ "node": ">=18" } }, - "node_modules/openclaw/node_modules/clawpdf": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/clawpdf/-/clawpdf-0.3.0.tgz", - "integrity": "sha512-41+3AnKk9yek2sm+/9XvUlDTN8Wi+ag7fmxZuqw+ySn4lqaf/fCgLeamqPLiXY4gVbizKEHGoTG/JrIIFNE2rw==", - "dev": true, - "license": "MIT", - "bin": { - "clawpdf": "dist/cli.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/openclaw/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1180,9 +2242,9 @@ } }, "node_modules/openclaw/node_modules/diff": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", - "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1351,9 +2413,9 @@ } }, "node_modules/openclaw/node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -1414,9 +2476,9 @@ } }, "node_modules/openclaw/node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", "dev": true, "license": "MIT", "engines": { @@ -1551,6 +2613,45 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/openclaw/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/openclaw/node_modules/fast-xml-parser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.0.tgz", + "integrity": "sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/openclaw/node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -1674,9 +2775,9 @@ } }, "node_modules/openclaw/node_modules/gaxios": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", - "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1784,9 +2885,9 @@ } }, "node_modules/openclaw/node_modules/google-auth-library": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", - "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1882,9 +2983,9 @@ } }, "node_modules/openclaw/node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1895,19 +2996,19 @@ } }, "node_modules/openclaw/node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">=12.0.0" + "node": "*" } }, "node_modules/openclaw/node_modules/hono": { - "version": "4.12.25", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", - "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", "dev": true, "license": "MIT", "engines": { @@ -1915,16 +3016,16 @@ } }, "node_modules/openclaw/node_modules/hosted-git-info": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", - "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^11.1.0" }, "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/openclaw/node_modules/html-escaper": { @@ -1998,6 +3099,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/openclaw/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/openclaw/node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2085,13 +3200,13 @@ } }, "node_modules/openclaw/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/openclaw/node_modules/is-fullwidth-code-point": { @@ -2277,6 +3392,16 @@ } } }, + "node_modules/openclaw/node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/openclaw/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2298,15 +3423,33 @@ "license": "Apache-2.0" }, "node_modules/openclaw/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, + "node_modules/openclaw/node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/openclaw/node_modules/marked": { "version": "15.0.12", "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", @@ -2330,6 +3473,13 @@ "node": ">= 0.4" } }, + "node_modules/openclaw/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/openclaw/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -2454,9 +3604,9 @@ } }, "node_modules/openclaw/node_modules/node-addon-api": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", - "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", "dev": true, "license": "MIT", "engines": { @@ -2580,9 +3730,9 @@ } }, "node_modules/openclaw/node_modules/openai": { - "version": "6.39.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.1.tgz", - "integrity": "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A==", + "version": "6.39.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.0.tgz", + "integrity": "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2688,6 +3838,22 @@ "node": ">=8" } }, + "node_modules/openclaw/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/openclaw/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2726,6 +3892,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/openclaw/node_modules/pdfjs-dist": { + "version": "5.7.284", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", + "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.100" + } + }, "node_modules/openclaw/node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -2789,24 +3968,13 @@ } }, "node_modules/openclaw/node_modules/protobufjs": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz", - "integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", + "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", "long": "^5.3.2" }, "engines": { @@ -2827,6 +3995,26 @@ "node": ">= 0.10" } }, + "node_modules/openclaw/node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/openclaw/node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/openclaw/node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -2933,9 +4121,9 @@ } }, "node_modules/openclaw/node_modules/quickjs-wasi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-3.0.0.tgz", - "integrity": "sha512-X7ouKC4ZVf9bXQ8rsE7+L6TeBbesejAJH61x16xRaGAQGfBHHRcniWgzJZZVtHc8rS9yVsY+Tvk8/usAosg4bg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", + "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", "dev": true, "license": "MIT" }, @@ -2950,16 +4138,16 @@ } }, "node_modules/openclaw/node_modules/rastermill": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.1.tgz", - "integrity": "sha512-CX4nij6+ZLHYIaojJNfLTr7W+AiH/IPJi6E9Aw1br2///1KZL2KBOHd68rkcLedc47MPvb4hhH+fzYeGFa4A/Q==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.0.tgz", + "integrity": "sha512-4g2i0I7M5sba//lFBh19Wi0hDGw8o+isnt/BtEyqQXIZaYclhcNBwL/Fw/6gDCp7aaLwQHADuUvyHCB0Oat5Vw==", "dev": true, "license": "MIT", "dependencies": { "@silvia-odwyer/photon-node": "0.3.4" }, "engines": { - "node": ">=22" + "node": ">=20" } }, "node_modules/openclaw/node_modules/raw-body": { @@ -3450,6 +4638,19 @@ "node": ">=8" } }, + "node_modules/openclaw/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/openclaw/node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", @@ -3468,9 +4669,9 @@ } }, "node_modules/openclaw/node_modules/tar": { - "version": "7.5.16", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", - "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3513,6 +4714,23 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/openclaw/node_modules/tokenjuice": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/tokenjuice/-/tokenjuice-0.7.1.tgz", + "integrity": "sha512-eO048hm9UcGHASjYkIWEij8QN68amGp+S1nJyo685qB1/ol+VGEYjPglcVPvCbJbZyFHvI+BBAMvOfnqYCtpsQ==", + "dev": true, + "license": "MIT", + "bin": { + "tokenjuice": "dist/cli/main.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/vincentkoc" + } + }, "node_modules/openclaw/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -3601,9 +4819,9 @@ } }, "node_modules/openclaw/node_modules/typebox": { - "version": "1.1.39", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.39.tgz", - "integrity": "sha512-vj0afVtOfLQvv0GR0VxVagYxsXN64btL7Z9XoaG0ZggH3mruMMkOO6hXdgMsjCY3shZgEvooAWVeznQVs5c43w==", + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", "dev": true, "license": "MIT" }, @@ -3621,6 +4839,13 @@ "node": ">=14.17" } }, + "node_modules/openclaw/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/openclaw/node_modules/uhyphen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", @@ -3642,22 +4867,15 @@ } }, "node_modules/openclaw/node_modules/undici": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", - "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", "dev": true, "license": "MIT", "engines": { "node": ">=22.19.0" } }, - "node_modules/openclaw/node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, - "license": "MIT" - }, "node_modules/openclaw/node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3810,6 +5028,22 @@ } } }, + "node_modules/openclaw/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/openclaw/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/test/e2e/fixtures/plugins/weather/package.json b/test/e2e/fixtures/plugins/weather/package.json index b6bc8612cd..9f0399b2ae 100644 --- a/test/e2e/fixtures/plugins/weather/package.json +++ b/test/e2e/fixtures/plugins/weather/package.json @@ -19,7 +19,7 @@ "minGatewayVersion": "2026.5.22" }, "build": { - "openclawVersion": "2026.6.10" + "openclawVersion": "2026.5.27" } }, "scripts": { @@ -29,7 +29,7 @@ "typebox": "1.1.38" }, "devDependencies": { - "openclaw": "2026.6.10", + "openclaw": "2026.5.27", "typescript": "5.9.3" }, "peerDependencies": { diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index b4a68a54b5..f22b3d9350 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -31,15 +32,8 @@ import { parseJsonFromText } from "./json-envelope.ts"; const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js"); -const CUSTOM_DOCKERFILE = path.join(REPO_ROOT, "Dockerfile.e2e-weather-plugin"); -const CUSTOM_PLUGIN_VERSION_SOURCE = path.join( - REPO_ROOT, - "Dockerfile.e2e-weather-plugin.version.ts", -); -const WEATHER_FIXTURE_PACKAGE_PATH = path.join( - REPO_ROOT, - "test/e2e/fixtures/plugins/weather/package.json", -); +const WEATHER_FIXTURE_DIR = path.join(REPO_ROOT, "test/e2e/fixtures/plugins/weather"); +const WEATHER_FIXTURE_PACKAGE_PATH = path.join(WEATHER_FIXTURE_DIR, "package.json"); const WEATHER_FIXTURE_PACKAGE = JSON.parse( fs.readFileSync(WEATHER_FIXTURE_PACKAGE_PATH, "utf8"), ) as { @@ -58,9 +52,14 @@ assert.match( /^\d+(?:\.\d+)+$/, "weather fixture must declare a canonical OpenClaw build version", ); -// Keep the dependency layer reproducible while the current managed Dockerfile -// upgrades its OpenClaw runtime to WEATHER_OPENCLAW_VERSION. The assertions in -// createCustomPluginDockerfile and the in-sandbox probe make that boundary explicit. +assert.equal( + WEATHER_OPENCLAW_VERSION, + "2026.5.27", + "weather fixture must match the OpenClaw runtime pinned by NemoClaw v0.0.71", +); +const NEMOCLAW_RELEASE_TAG = "v0.0.71"; +const NEMOCLAW_RELEASE_COMMIT = "e4b9111f5f0535c2fc3d6fbe8dc8dca101a6fdce"; +const NEMOCLAW_SOURCE_REPOSITORY = "https://github.com/NVIDIA/NemoClaw.git"; const SANDBOX_BASE_IMAGE_REF = "ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71"; const TOOL_DISCLOSURE_ENV_REFERENCE = "${NEMOCLAW_TOOL_DISCLOSURE}"; const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-plugin-exdev"; @@ -100,7 +99,7 @@ const EXDEV_PATTERNS = [ /cross-device link not permitted/i, ]; const liveTest = shouldRunLiveE2E() ? test : test.skip; -type WeatherFixtureVersion = "v1" | "v2"; +type WeatherFixtureVersion = "v1" | "v2" | "v3"; const GATEWAY_CATALOG_CALL_SOURCE = String.raw` import { Buffer } from "node:buffer"; @@ -383,16 +382,75 @@ test("OpenShell wrapper injects only the reviewed tmpfs config into sandbox crea expect(fs.existsSync(wrapper.directory)).toBe(false); }); -function writeCustomPluginVersion(version: WeatherFixtureVersion): void { +type CustomPluginBuildContext = { + sourceParentDir: string; + sourceRoot: string; + dockerfilePath: string; + versionSourcePath: string; + pluginDirPath: string; +}; + +function reserveCustomPluginBuildContext(): CustomPluginBuildContext { + const nonce = randomUUID(); + const sourceParentDir = path.join(os.tmpdir(), `nemoclaw-v0.0.71-weather-${nonce}`); + const sourceRoot = path.join(sourceParentDir, "NemoClaw"); + return { + sourceParentDir, + sourceRoot, + dockerfilePath: path.join(sourceRoot, `Dockerfile.e2e-weather-plugin-${nonce}`), + versionSourcePath: path.join(sourceRoot, `e2e-weather-plugin-version-${nonce}.ts`), + pluginDirPath: path.join(sourceRoot, `e2e-weather-plugin-${nonce}`), + }; +} + +test("custom plugin build paths are collision-safe and outside the checkout", () => { + const first = reserveCustomPluginBuildContext(); + const second = reserveCustomPluginBuildContext(); + expect(first.sourceParentDir).not.toBe(second.sourceParentDir); + expect(first.sourceParentDir.startsWith(`${REPO_ROOT}${path.sep}`)).toBe(false); + expect(second.sourceParentDir.startsWith(`${REPO_ROOT}${path.sep}`)).toBe(false); + expect(fs.existsSync(first.sourceParentDir)).toBe(false); + expect(fs.existsSync(second.sourceParentDir)).toBe(false); +}); + +function copyFixtureFileExclusive(source: string, target: string): void { + fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL); +} + +function stageWeatherPluginFixture(context: CustomPluginBuildContext): void { + fs.mkdirSync(context.pluginDirPath); + fs.mkdirSync(path.join(context.pluginDirPath, "src")); + for (const fileName of [ + "package.json", + "package-lock.json", + "tsconfig.json", + "openclaw.plugin.json", + ]) { + copyFixtureFileExclusive( + path.join(WEATHER_FIXTURE_DIR, fileName), + path.join(context.pluginDirPath, fileName), + ); + } + copyFixtureFileExclusive( + path.join(WEATHER_FIXTURE_DIR, "src", "index.ts"), + path.join(context.pluginDirPath, "src", "index.ts"), + ); +} + +function writeCustomPluginVersion( + versionSourcePath: string, + version: WeatherFixtureVersion, + exclusive = false, +): void { fs.writeFileSync( - CUSTOM_PLUGIN_VERSION_SOURCE, + versionSourcePath, `// Generated by the OpenClaw plugin lifecycle E2E.\nexport const WEATHER_FIXTURE_VERSION = ${JSON.stringify(version)};\n`, - "utf8", + { encoding: "utf8", flag: exclusive ? "wx" : "w" }, ); } -function createCustomPluginDockerfile(): () => void { - const sourceDockerfile = path.join(REPO_ROOT, "Dockerfile"); +function createCustomPluginDockerfile(context: CustomPluginBuildContext): void { + const sourceDockerfile = path.join(context.sourceRoot, "Dockerfile"); const source = fs.readFileSync(sourceDockerfile, "utf8"); const baseImageAnchor = "ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/sandbox-base:latest\n"; const runtimeAnchor = "FROM ${BASE_IMAGE}\n"; @@ -402,7 +460,7 @@ function createCustomPluginDockerfile(): () => void { expect(source.match(/^FROM \$\{BASE_IMAGE\}$/gm)?.length, "expected one runtime stage").toBe(1); expect( source.match(/^ARG OPENCLAW_VERSION=([0-9.]+)$/m)?.[1], - "weather fixture SDK must match the current managed runtime target", + "weather fixture SDK must match the v0.0.71 managed runtime target", ).toBe(WEATHER_OPENCLAW_VERSION); expect( WEATHER_FIXTURE_PACKAGE.devDependencies?.openclaw, @@ -412,16 +470,18 @@ function createCustomPluginDockerfile(): () => void { const runtime = source .replace(baseImageAnchor, `ARG BASE_IMAGE=${SANDBOX_BASE_IMAGE_REF}\n`) .replace(runtimeAnchor, "FROM ${BASE_IMAGE} AS nemoclaw-runtime\n"); + const pluginDirName = path.basename(context.pluginDirPath); + const versionSourceName = path.basename(context.versionSourcePath); const extension = String.raw` # Build the deterministic custom-plugin fixture used by this live contract. FROM builder AS weather-plugin-builder WORKDIR /opt/weather -COPY test/e2e/fixtures/plugins/weather/package.json test/e2e/fixtures/plugins/weather/package-lock.json test/e2e/fixtures/plugins/weather/tsconfig.json ./ +COPY ${pluginDirName}/package.json ${pluginDirName}/package-lock.json ${pluginDirName}/tsconfig.json ./ RUN npm ci --ignore-scripts --no-audit --no-fund -COPY test/e2e/fixtures/plugins/weather/openclaw.plugin.json ./ -COPY test/e2e/fixtures/plugins/weather/src/ ./src/ -COPY Dockerfile.e2e-weather-plugin.version.ts ./src/version.ts +COPY ${pluginDirName}/openclaw.plugin.json ./ +COPY ${pluginDirName}/src/ ./src/ +COPY ${versionSourceName} ./src/version.ts RUN npm run build \ && npm prune --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund \ && test ! -e node_modules/openclaw \ @@ -462,12 +522,12 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ && chown sandbox:sandbox /sandbox/.openclaw/.config-hash \ && chmod 660 /sandbox/.openclaw/.config-hash `; - writeCustomPluginVersion("v1"); - fs.writeFileSync(CUSTOM_DOCKERFILE, runtime.trimEnd() + extension, "utf8"); - return () => { - fs.rmSync(CUSTOM_DOCKERFILE, { force: true }); - fs.rmSync(CUSTOM_PLUGIN_VERSION_SOURCE, { force: true }); - }; + stageWeatherPluginFixture(context); + writeCustomPluginVersion(context.versionSourcePath, "v1", true); + fs.writeFileSync(context.dockerfilePath, runtime.trimEnd() + extension, { + encoding: "utf8", + flag: "wx", + }); } type WeatherPluginInspect = { @@ -633,6 +693,41 @@ printf 'tmpfs_mount=%s source=%s mount_device=%s tmp_device=%s\n' '${EXDEV_TMPFS return true; } +async function writeWorkspaceMarker(sandbox: SandboxClient, marker: string): Promise { + const markerPath = "/sandbox/.openclaw/workspace/plugin-lifecycle-marker.txt"; + const result = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript( + `mkdir -p -- /sandbox/.openclaw/workspace && printf %s ${shellQuote(marker)} > ${shellQuote(markerPath)}`, + ), + { + artifactName: "openclaw-plugin-write-workspace-marker", + env: liveEnv(), + timeoutMs: 30_000, + }, + ); + expect(result.exitCode, resultText(result)).toBe(0); +} + +async function assertWorkspaceMarker( + sandbox: SandboxClient, + phase: string, + marker: string, +): Promise { + const markerPath = "/sandbox/.openclaw/workspace/plugin-lifecycle-marker.txt"; + const result = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript(`cat -- ${shellQuote(markerPath)}`), + { + artifactName: `openclaw-plugin-workspace-marker-${phase}`, + env: liveEnv(), + timeoutMs: 30_000, + }, + ); + expect(result.exitCode, resultText(result)).toBe(0); + expect(normalizeSandboxStdoutFrames(result.stdout).trim()).toBe(marker); +} + const runtimeDepsReplacementProbeSource = `set -eu rm -rf /sandbox/.openclaw/plugin-runtime-deps/exdev-guard 2>/dev/null || true rm -rf ${EXDEV_TMPFS_SOURCE} @@ -710,8 +805,8 @@ const runtimeDepsReplacementProbe = trustedSandboxShellScript( ); liveTest( - "a custom OpenClaw plugin survives restart and rebuild without EXDEV failures (#6108)", - { timeout: ONBOARD_TIMEOUT_MS + REBUILD_TIMEOUT_MS + 15 * 60_000 }, + "a custom OpenClaw plugin survives restart, recreation, and rebuild without EXDEV failures (#6108)", + { timeout: ONBOARD_TIMEOUT_MS + REBUILD_TIMEOUT_MS + 25 * 60_000 }, async ({ artifacts, cleanup, host, sandbox, skip }) => { await artifacts.writeJson("target.json", { id: "openclaw-plugin-runtime-exdev", @@ -719,16 +814,19 @@ liveTest( boundary: "fresh-openclaw-sandbox-exec", regressionTargets: ["#6108", "#3513", "#3127"], contract: [ - "fresh OpenClaw sandbox onboards from a full managed custom-plugin Dockerfile", + "fresh OpenClaw sandbox onboards from the exact NemoClaw v0.0.71 source/runtime pair", "release-matched peer/dev dependencies prune private OpenClaw and link the host runtime", "gateway log, runtime inspection, tools.catalog, and tools.invoke prove weather/get_weather", - "custom-plugin v1 survives restart and a rebuilt v2 replaces it without backup rollback", + "custom-plugin v1 survives restart, recreation installs v2, and rebuild installs v3", + "workspace state survives both onboarding recreation and rebuild", `test-only driver config mounts tmpfs at ${EXDEV_TMPFS_MOUNT} without changing production policies`, "stock OpenClaw policy source bytes remain unchanged through onboard and rebuild", `sandbox proves ${EXDEV_TMPFS_SOURCE} and plugin-runtime-deps are distinct devices`, `legacy source-side staging fails with EXDEV across the same ${EXDEV_TMPFS_SOURCE} to plugin-runtime-deps boundary`, "OpenClaw-style target-side plugin runtime-deps replacement completes without EXDEV", ], + nemoclawSourceRelease: NEMOCLAW_RELEASE_TAG, + nemoclawSourceCommit: NEMOCLAW_RELEASE_COMMIT, sandboxBaseImageRef: SANDBOX_BASE_IMAGE_REF, openclawVersion: WEATHER_OPENCLAW_VERSION, }); @@ -807,8 +905,43 @@ liveTest( }), "OpenShell wrapper and explicit pinned components must pass onboard coherence preflight", ).toBe(true); - const removeCustomDockerfile = createCustomPluginDockerfile(); - cleanup.add("remove custom weather-plugin Dockerfile", removeCustomDockerfile); + const customPluginContext = reserveCustomPluginBuildContext(); + let removeCustomPluginContext = () => {}; + cleanup.add("remove v0.0.71 custom-plugin source worktree", () => removeCustomPluginContext()); + fs.mkdirSync(customPluginContext.sourceParentDir); + removeCustomPluginContext = () => + fs.rmSync(customPluginContext.sourceParentDir, { recursive: true, force: true }); + const cloneRelease = await host.command( + "git", + [ + "clone", + "--depth", + "1", + "--branch", + NEMOCLAW_RELEASE_TAG, + "--single-branch", + NEMOCLAW_SOURCE_REPOSITORY, + customPluginContext.sourceRoot, + ], + { + artifactName: "clone-nemoclaw-v0-0-71-plugin-source", + env: liveEnv(), + timeoutMs: 180_000, + }, + ); + expect(cloneRelease.exitCode, resultText(cloneRelease)).toBe(0); + const releaseHead = await host.command( + "git", + ["-C", customPluginContext.sourceRoot, "rev-parse", "HEAD"], + { + artifactName: "verify-nemoclaw-v0-0-71-plugin-source", + env: liveEnv(), + timeoutMs: 30_000, + }, + ); + expect(releaseHead.exitCode, resultText(releaseHead)).toBe(0); + expect(releaseHead.stdout.trim()).toBe(NEMOCLAW_RELEASE_COMMIT); + createCustomPluginDockerfile(customPluginContext); const sandboxEnv = withOpenShellWrapperEnv( liveEnv({ @@ -837,7 +970,7 @@ liveTest( "--agent", "openclaw", "--from", - CUSTOM_DOCKERFILE, + customPluginContext.dockerfilePath, ], { artifactName: "openclaw-plugin-exdev-onboard", @@ -867,10 +1000,46 @@ liveTest( const weatherAfterRestart = await assertWeatherPluginRuntime(sandbox, "after-restart", "v1"); expect(weatherAfterRestart.imageMarker).toBe(weatherAfterOnboard.imageMarker); + const workspaceMarker = `plugin-lifecycle-${randomUUID()}`; + await writeWorkspaceMarker(sandbox, workspaceMarker); + // Change an actual build-context input so rebuild must produce a distinct - // plugin artifact. Restore must preserve that fresh image-managed v2 + // plugin artifact. Onboarding recreation must preserve the fresh v2 // extension instead of replacing it with the backed-up v1 directory. - writeCustomPluginVersion("v2"); + writeCustomPluginVersion(customPluginContext.versionSourcePath, "v2"); + const recreate = await host.command( + "node", + [ + CLI_ENTRYPOINT, + "onboard", + "--fresh", + "--recreate-sandbox", + "--non-interactive", + "--yes", + "--yes-i-accept-third-party-software", + "--name", + SANDBOX_NAME, + "--agent", + "openclaw", + "--from", + customPluginContext.dockerfilePath, + ], + { + artifactName: "openclaw-weather-plugin-recreate", + env: sandboxEnv, + timeoutMs: ONBOARD_TIMEOUT_MS, + }, + ); + expect(recreate.exitCode, resultText(recreate)).toBe(0); + const tmpfsMountedAfterRecreate = await assertExdevTmpfsMounted(sandbox, "after-recreate"); + assertPolicySourcesUnchanged(policySourceSnapshot, "recreate"); + const weatherAfterRecreate = await assertWeatherPluginRuntime(sandbox, "after-recreate", "v2"); + expect(weatherAfterRecreate.imageMarker).not.toBe(weatherAfterOnboard.imageMarker); + await assertWorkspaceMarker(sandbox, "after-recreate", workspaceMarker); + + // A subsequent rebuild exercises the same semantic recreated-sandbox + // restore boundary with another fresh image artifact. + writeCustomPluginVersion(customPluginContext.versionSourcePath, "v3"); const rebuild = await host.command("node", [CLI_ENTRYPOINT, SANDBOX_NAME, "rebuild", "--yes"], { artifactName: "openclaw-weather-plugin-rebuild", env: sandboxEnv, @@ -879,8 +1048,9 @@ liveTest( expect(rebuild.exitCode, resultText(rebuild)).toBe(0); const tmpfsMountedAfterRebuild = await assertExdevTmpfsMounted(sandbox, "after-rebuild"); assertPolicySourcesUnchanged(policySourceSnapshot, "rebuild"); - const weatherAfterRebuild = await assertWeatherPluginRuntime(sandbox, "after-rebuild", "v2"); - expect(weatherAfterRebuild.imageMarker).not.toBe(weatherAfterOnboard.imageMarker); + const weatherAfterRebuild = await assertWeatherPluginRuntime(sandbox, "after-rebuild", "v3"); + expect(weatherAfterRebuild.imageMarker).not.toBe(weatherAfterRecreate.imageMarker); + await assertWorkspaceMarker(sandbox, "after-rebuild", workspaceMarker); const df = await sandbox.execShell( SANDBOX_NAME, @@ -916,6 +1086,7 @@ liveTest( id: "openclaw-plugin-runtime-exdev", onboardExitCode: onboard.exitCode, restartExitCode: restart.exitCode, + recreateExitCode: recreate.exitCode, rebuildExitCode: rebuild.exitCode, filesystemProbeExitCode: df.exitCode, runtimeDepsProbeExitCode: probe.exitCode, @@ -929,6 +1100,10 @@ liveTest( weatherAfterRestart.inspectLoaded && weatherAfterRestart.catalogToolIds.includes("get_weather") && weatherAfterRestart.toolInvoked, + weatherAfterRecreate: + weatherAfterRecreate.inspectLoaded && + weatherAfterRecreate.catalogToolIds.includes("get_weather") && + weatherAfterRecreate.toolInvoked, weatherAfterRebuild: weatherAfterRebuild.inspectLoaded && weatherAfterRebuild.catalogToolIds.includes("get_weather") && @@ -937,16 +1112,21 @@ liveTest( weatherAfterOnboard.imageMarker === weatherAfterRestart.imageMarker && weatherAfterOnboard.fixtureVersion === "v1" && weatherAfterRestart.fixtureVersion === "v1", - rebuiltV2ReplacedV1: - weatherAfterRebuild.imageMarker !== weatherAfterOnboard.imageMarker && - weatherAfterRebuild.fixtureVersion === "v2", + recreatedV2ReplacedV1: + weatherAfterRecreate.imageMarker !== weatherAfterOnboard.imageMarker && + weatherAfterRecreate.fixtureVersion === "v2", + rebuiltV3ReplacedV2: + weatherAfterRebuild.imageMarker !== weatherAfterRecreate.imageMarker && + weatherAfterRebuild.fixtureVersion === "v3", distinctDevices: /source_device=\d+ target_device=\d+/.test(probeText), sourceSideExdevSelfCheck: probeText.includes( "source-side staging failure self-check completed", ), noExdevSignature: !EXDEV_PATTERNS.some((pattern) => pattern.test(probeText)), successMarker: probeText.includes("runtime deps replacement completed"), - testOnlyTmpfsMounted: tmpfsMountedAfterOnboard && tmpfsMountedAfterRebuild, + workspaceStatePreserved: true, + testOnlyTmpfsMounted: + tmpfsMountedAfterOnboard && tmpfsMountedAfterRecreate && tmpfsMountedAfterRebuild, stockPolicySourcesUnchanged: true, }, }); diff --git a/test/e2e/support/e2e-workflow.test.ts b/test/e2e/support/e2e-workflow.test.ts index 0eae50fa27..2f52d178ab 100644 --- a/test/e2e/support/e2e-workflow.test.ts +++ b/test/e2e/support/e2e-workflow.test.ts @@ -650,6 +650,7 @@ describe("e2e workflow boundary", () => { expect(inventory.allowedJobs).toContain("openshell-gateway-auth-contract"); expect(inventory.allowedJobs).toContain("gateway-guard-recovery"); expect(inventory.allowedJobs).toContain("upgrade-stale-sandbox"); + expect(inventory.allowedJobs).toContain("openclaw-plugin-runtime-exdev"); expect(inventory.targetToJob.get("openshell-gateway-auth-contract")).toBe( "openshell-gateway-auth-contract", ); @@ -658,6 +659,9 @@ describe("e2e workflow boundary", () => { expect(inventory.targetToJob.get("credential-migration")).toBe("credential-migration"); expect(inventory.targetToJob.get("launchable-smoke")).toBe("launchable-smoke"); expect(inventory.targetToJob.get("gateway-guard-recovery")).toBe("gateway-guard-recovery"); + expect(inventory.targetToJob.get("openclaw-plugin-runtime-exdev")).toBe( + "openclaw-plugin-runtime-exdev", + ); expect( inventory.allowedJobs.every((job) => Object.keys((readWorkflow().jobs as Record) ?? {}).includes(job), diff --git a/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts b/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts new file mode 100644 index 0000000000..ec26ada097 --- /dev/null +++ b/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { PREPARE_E2E_ACTION } from "../../../tools/e2e/prepare-e2e-workflow-boundary.mts"; +import { UPLOAD_E2E_ARTIFACTS_ACTION } from "../../../tools/e2e/upload-e2e-artifacts-workflow-boundary.mts"; +import { + evaluateE2eWorkflowDispatchSelectors, + readFreeStandingJobsInventory, +} from "../../../tools/e2e/workflow-boundary.mts"; +import { readWorkflow } from "../../helpers/e2e-workflow-contract"; + +const JOB_ID = "openclaw-plugin-runtime-exdev"; +const CHECKOUT_ACTION = "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10"; +const SELECTOR_CONDITION = + "${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',openclaw-plugin-runtime-exdev,') || contains(format(',{0},', inputs.targets), ',openclaw-plugin-runtime-exdev,') }}"; + +type WorkflowStep = Record & { + name?: string; + uses?: string; + run?: string; + with?: Record; +}; + +type WorkflowJob = Record & { + env?: Record; + needs?: string | string[]; + permissions?: Record; + steps?: WorkflowStep[]; +}; + +type Workflow = { + on: { + schedule?: Array<{ cron?: string }>; + workflow_dispatch?: Record; + }; + jobs: Record; +}; + +function canonicalWorkflow(): Workflow { + return readWorkflow() as unknown as Workflow; +} + +describe("OpenClaw plugin runtime EXDEV workflow boundary", () => { + it("keeps the full-runtime plugin proof in the canonical scheduled lane", () => { + const workflow = canonicalWorkflow(); + const job = workflow.jobs[JOB_ID]; + expect(workflow.on.schedule).toContainEqual({ cron: "0 0 * * *" }); + expect(workflow.on.workflow_dispatch).toBeDefined(); + expect(job).toBeDefined(); + expect(job.needs).toBe("generate-matrix"); + expect(job.if).toBe(SELECTOR_CONDITION); + expect(job["runs-on"]).toBe("ubuntu-latest"); + expect(job.permissions).toEqual({ contents: "read" }); + expect(job["timeout-minutes"]).toBe(90); + expect(job.env).toMatchObject({ + E2E_JOB: "1", + E2E_TARGET_ID: JOB_ID, + E2E_ARTIFACT_DIR: "${{ github.workspace }}/e2e-artifacts/live/openclaw-plugin-runtime-exdev", + NEMOCLAW_CLI_BIN: "${{ github.workspace }}/bin/nemoclaw.js", + NEMOCLAW_RUN_LIVE_E2E: "1", + NEMOCLAW_NON_INTERACTIVE: "1", + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + NEMOCLAW_SANDBOX_NAME: "e2e-openclaw-plugin-exdev", + OPENSHELL_GATEWAY: "nemoclaw", + }); + expect(job.env).not.toHaveProperty("E2E_DEFAULT_ENABLED"); + expect(job.env).not.toHaveProperty("NVIDIA_INFERENCE_API_KEY"); + + const steps = job.steps ?? []; + expect(steps).toHaveLength(6); + expect(steps[0]).toEqual({ + uses: CHECKOUT_ACTION, + with: { "persist-credentials": false }, + }); + expect(steps[1]?.name).toBe("Authenticate to Docker Hub"); + expect(steps[2]).toEqual({ + name: "Prepare E2E workspace", + uses: PREPARE_E2E_ACTION, + }); + expect(steps[3]?.name).toBe( + "Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV live test", + ); + expect(steps[3]?.run).toContain("npx vitest run --project e2e-live"); + expect(steps[3]?.run).toContain("test/e2e/live/openclaw-plugin-runtime-exdev.test.ts"); + expect(steps[3]).not.toHaveProperty("env"); + expect(JSON.stringify(steps[3])).not.toContain("secrets."); + expect(steps[4]).toEqual({ + name: "Upload OpenClaw plugin runtime-deps EXDEV artifacts", + if: "always()", + uses: UPLOAD_E2E_ARTIFACTS_ACTION, + }); + expect(steps[5]).toEqual({ + name: "Clean up Docker auth", + if: "always()", + shell: "bash", + run: "bash .github/scripts/docker-auth-cleanup.sh", + }); + + expect(workflow.jobs["report-to-pr"]?.needs).toContain(JOB_ID); + expect(workflow.jobs.scorecard?.needs).toContain(JOB_ID); + }); + + it("keeps job and target selectors mapped to the default-enabled canonical job", () => { + const inventory = readFreeStandingJobsInventory(); + expect(inventory.allowedJobs).toContain(JOB_ID); + expect(inventory.explicitOnlyJobs).not.toContain(JOB_ID); + expect(inventory.targetToJob.get(JOB_ID)).toBe(JOB_ID); + expect(evaluateE2eWorkflowDispatchSelectors({ jobs: JOB_ID })).toMatchObject({ + valid: true, + liveTargetsRun: false, + selectedFreeStandingJobs: [JOB_ID], + registryTargets: [], + }); + expect(evaluateE2eWorkflowDispatchSelectors({ targets: JOB_ID })).toMatchObject({ + valid: true, + liveTargetsRun: false, + selectedFreeStandingJobs: [JOB_ID], + registryTargets: [], + }); + }); +}); diff --git a/test/e2e/support/upload-e2e-artifacts-workflow-boundary.test.ts b/test/e2e/support/upload-e2e-artifacts-workflow-boundary.test.ts index 3c67b3d878..d345dd66a1 100644 --- a/test/e2e/support/upload-e2e-artifacts-workflow-boundary.test.ts +++ b/test/e2e/support/upload-e2e-artifacts-workflow-boundary.test.ts @@ -77,7 +77,7 @@ function validateActionMutation(mutate: (action: MutableAction) => void): string } describe("upload-e2e-artifacts workflow boundary", () => { - it("binds one canonical uploader to all 73 E2E execution jobs", () => { + it("binds one canonical uploader to all 74 E2E execution jobs", () => { expect(validateUploadE2eArtifactsAction()).toEqual([]); expect(validateUploadE2eArtifactsInvocations(readWorkflow())).toEqual([]); }); @@ -177,8 +177,8 @@ describe("upload-e2e-artifacts workflow boundary", () => { expect(validateUploadE2eArtifactsInvocations(workflow)).toEqual( expect.arrayContaining([ - "upload-e2e-artifacts must cover exactly 73 live and E2E_JOB execution jobs", - "upload-e2e-artifacts must keep exactly 62 default callers", + "upload-e2e-artifacts must cover exactly 74 live and E2E_JOB execution jobs", + "upload-e2e-artifacts must keep exactly 63 default callers", ]), ); }); diff --git a/test/helpers/onboard-openshell-fixture.ts b/test/helpers/onboard-openshell-fixture.ts new file mode 100644 index 0000000000..44d6e836c7 --- /dev/null +++ b/test/helpers/onboard-openshell-fixture.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import path from "node:path"; + +function writeExecutable(target: string, contents: string): void { + fs.writeFileSync(target, contents, { mode: 0o755 }); +} + +export function writeOkOpenshell(fakeBin: string): void { + writeExecutable( + path.join(fakeBin, "openshell"), + '#!/usr/bin/env bash\nif [ "${1:-}" = sandbox ] && [ "${2:-}" = ssh-config ]; then printf "Host openshell-%s\\n HostName 127.0.0.1\\n User sandbox\\n" "${3:-sandbox}"; fi\nexit 0\n', + ); + writeExecutable( + path.join(fakeBin, "ssh"), + "#!/usr/bin/env bash\nprintf '%s\\n' '{\"version\":1,\"installRecords\":{}}'\n", + ); +} diff --git a/test/helpers/rebuild-flow-harness.ts b/test/helpers/rebuild-flow-harness.ts index 16324a73e1..93a6efe4e9 100644 --- a/test/helpers/rebuild-flow-harness.ts +++ b/test/helpers/rebuild-flow-harness.ts @@ -427,6 +427,7 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): failedDirs: [], failedFiles: [], manifest: { + agentType: overrides.agentName ?? "openclaw", backupPath: "/tmp/nemoclaw-rebuild-backup", timestamp: "2026-06-01T00:00:00.000Z", policyPresets: overrides.backupPolicyPresets ?? ["npm", "bad", "throw"], @@ -445,16 +446,18 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): : overrides.preDeleteLatestManifest) as ReturnType, ); vi.spyOn(sandboxState, "hasPositiveManagedImageEvidence").mockReturnValue(true); - const restoreSandboxStateSpy = vi.spyOn(sandboxState, "restoreSandboxState").mockImplementation( - overrides.restoreSandboxState ?? - (() => ({ - success: true, - restoredDirs: ["workspace"], - restoredFiles: ["user.md"], - failedDirs: [], - failedFiles: [], - })), - ); + const restoreSandboxStateSpy = vi + .spyOn(sandboxState, "restoreRecreatedSandboxState") + .mockImplementation( + overrides.restoreSandboxState ?? + (() => ({ + success: true, + restoredDirs: ["workspace"], + restoredFiles: ["user.md"], + failedDirs: [], + failedFiles: [], + })), + ); const runOpenshellSpy = vi.spyOn(openshellRuntime, "runOpenshell").mockImplementation((args) => { const argv = args as string[]; return argv[0] === "provider" && argv[1] === "get" diff --git a/test/helpers/rebuild-flow-lifecycle-cases.ts b/test/helpers/rebuild-flow-lifecycle-cases.ts index f4329c5a70..8a0c57c2c5 100644 --- a/test/helpers/rebuild-flow-lifecycle-cases.ts +++ b/test/helpers/rebuild-flow-lifecycle-cases.ts @@ -98,7 +98,7 @@ export function registerRebuildFlowLifecycleTests(): void { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", "/tmp/nemoclaw-rebuild-backup", - { preserveFreshOpenClawPluginInstalls: true }, + { targetAgentType: "openclaw" }, ); expect(harness.restoreMcpBridgesAfterRebuildSpy).toHaveBeenCalledWith("alpha", [mcpEntry]); expect(harness.removeSandboxRegistryEntryWithReceiptSpy).not.toHaveBeenCalled(); diff --git a/test/helpers/rebuild-flow-recovery-cases.ts b/test/helpers/rebuild-flow-recovery-cases.ts index 35963f7219..88057762be 100644 --- a/test/helpers/rebuild-flow-recovery-cases.ts +++ b/test/helpers/rebuild-flow-recovery-cases.ts @@ -31,7 +31,7 @@ export function registerRebuildFlowRecoveryTests(): void { expect(harness.restoreSandboxStateSpy).toHaveBeenCalledWith( "alpha", recoveryManifest.backupPath, - { preserveFreshOpenClawPluginInstalls: true }, + { targetAgentType: "openclaw" }, ); }); diff --git a/test/helpers/rebuild-flow-test-harness.ts b/test/helpers/rebuild-flow-test-harness.ts index 0a1e3e2713..b5785c3835 100644 --- a/test/helpers/rebuild-flow-test-harness.ts +++ b/test/helpers/rebuild-flow-test-harness.ts @@ -315,6 +315,10 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): failedDirs: [], failedFiles: [], manifest: { + agentType: + typeof overrides.sandboxEntry?.agent === "string" + ? overrides.sandboxEntry.agent + : "openclaw", backupPath: "/tmp/nemoclaw-rebuild-backup", timestamp: "2026-06-01T00:00:00.000Z", policyPresets: overrides.backupPolicyPresets ?? ["npm", "bad", "throw"], @@ -336,16 +340,18 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): vi.spyOn(sandboxState, "hasPositiveManagedImageEvidence").mockReturnValue( overrides.managedImageEvidence ?? true, ); - const restoreSandboxStateSpy = vi.spyOn(sandboxState, "restoreSandboxState").mockImplementation( - overrides.restoreSandboxState ?? - (() => ({ - success: true, - restoredDirs: ["workspace"], - restoredFiles: ["user.md"], - failedDirs: [], - failedFiles: [], - })), - ); + const restoreSandboxStateSpy = vi + .spyOn(sandboxState, "restoreRecreatedSandboxState") + .mockImplementation( + overrides.restoreSandboxState ?? + (() => ({ + success: true, + restoredDirs: ["workspace"], + restoredFiles: ["user.md"], + failedDirs: [], + failedFiles: [], + })), + ); const runOpenshellSpy = vi .spyOn(openshellRuntime, "runOpenshell") .mockImplementation((args: unknown) => { diff --git a/test/onboard-installer-restore-intent.test.ts b/test/onboard-installer-restore-intent.test.ts index b80e9af1d6..5cc9717627 100644 --- a/test/onboard-installer-restore-intent.test.ts +++ b/test/onboard-installer-restore-intent.test.ts @@ -8,19 +8,13 @@ import os from "node:os"; import path from "node:path"; import { describe, it } from "vitest"; +import { writeOkOpenshell } from "./helpers/onboard-openshell-fixture"; + const repoRoot = path.join(import.meta.dirname, ".."); const onboardScriptMocksPath = JSON.stringify( path.join(repoRoot, "test", "helpers", "onboard-script-mocks.cjs"), ); -function writeExecutable(target: string, contents: string) { - fs.writeFileSync(target, contents, { mode: 0o755 }); -} - -function writeOkOpenshell(fakeBin: string) { - writeExecutable(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n"); -} - describe("createSandbox installer restore intent", () => { it("non-interactive not-ready sandbox with installer restore intent skips the fresh backup, restores the pre-upgrade backup, and stays exec-usable for a workspace marker (#6114)", { timeout: 60_000, @@ -98,7 +92,7 @@ sandboxState.backupSandboxState = (name) => { manifest: { backupPath: "/tmp/fake-fresh-backup", timestamp: "2026-05-25T00:00:00Z" }, }; }; -sandboxState.restoreSandboxState = (name, backupPath) => { +sandboxState.restoreRecreatedSandboxState = (name, backupPath) => { events.push({ kind: "restore", name, backupPath }); return { success: true, diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 51d91ca5c8..332cf2f9e6 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -17,6 +17,7 @@ import { createInferenceRouteHelpers } from "../src/lib/onboard/inference-route. import { createLocalInferenceRouteApplier } from "../src/lib/onboard/local-inference-route.js"; import type { SetupInference, SetupInferenceDeps } from "../src/lib/onboard/setup-inference.js"; import { stageOptimizedSandboxBuildContext } from "../src/lib/sandbox/build-context.js"; +import { writeOkOpenshell } from "./helpers/onboard-openshell-fixture"; import { testTimeoutOptions } from "./helpers/timeouts"; import { createDirectCommandRouter, @@ -116,14 +117,6 @@ const onboardScriptMocksPath = JSON.stringify( path.join(repoRoot, "test", "helpers", "onboard-script-mocks.cjs"), ); -function writeExecutable(target: string, contents: string) { - fs.writeFileSync(target, contents, { mode: 0o755 }); -} - -function writeOkOpenshell(fakeBin: string) { - writeExecutable(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n"); -} - describe("onboard helpers", () => { it("adds host proxy variables to sandbox startup env args", () => { const envArgs = ["CHAT_UI_URL=http://127.0.0.1:18789"]; @@ -2786,8 +2779,8 @@ sandboxState.backupSandboxState = (name) => { manifest: { backupPath: "/tmp/fake-backup-path", timestamp: "2026-05-25T00:00:00Z" }, }; }; -sandboxState.restoreSandboxState = (name, backupPath) => { - events.push({ kind: "restore", name, backupPath }); +sandboxState.restoreRecreatedSandboxState = (name, backupPath, options) => { + events.push({ kind: "restore", name, backupPath, options }); return { success: true, restoredDirs: ["workspace", "skills"], @@ -2854,6 +2847,7 @@ const { createSandbox } = require(${onboardPath}); cmd?: string; name?: string; backupPath?: string; + options?: { targetAgentType?: string; freshOpenClawImagePluginInstalls?: unknown[] }; }>; const backupIndex = events.findIndex((e) => e.kind === "backup"); const deleteIndex = events.findIndex( @@ -2868,6 +2862,8 @@ const { createSandbox } = require(${onboardPath}); assert.equal(backupEvent?.name, "my-assistant", "backup target must match sandbox name"); const restoreEvent = events[restoreIndex]; assert.equal(restoreEvent?.backupPath, "/tmp/fake-backup-path", "restore must use backup path"); + assert.equal(restoreEvent?.options?.targetAgentType, "openclaw"); + assert.deepEqual(restoreEvent?.options?.freshOpenClawImagePluginInstalls, []); }); it("recreate-sandbox with NEMOCLAW_RECREATE_WITHOUT_BACKUP=1 skips backup", { @@ -2923,7 +2919,7 @@ sandboxState.backupSandboxState = () => { events.push({ kind: "backup" }); return { success: true, backedUpDirs: [], failedDirs: [], backedUpFiles: [], failedFiles: [] }; }; -sandboxState.restoreSandboxState = () => { +sandboxState.restoreRecreatedSandboxState = () => { events.push({ kind: "restore" }); return { success: true, restoredDirs: [], failedDirs: [], restoredFiles: [], failedFiles: [] }; }; @@ -2987,7 +2983,7 @@ const { createSandbox } = require(${onboardPath}); ); assert.ok( !events.some((e) => e.kind === "restore"), - "should not call restoreSandboxState when no backup occurred", + "should not call restoreRecreatedSandboxState when no backup occurred", ); }); @@ -3056,7 +3052,7 @@ sandboxState.backupSandboxState = (name) => { manifest: { backupPath: "/tmp/fake-backup-notready", timestamp: "2026-05-25T00:00:00Z" }, }; }; -sandboxState.restoreSandboxState = (name, backupPath) => { +sandboxState.restoreRecreatedSandboxState = (name, backupPath) => { events.push({ kind: "restore", name, backupPath }); return { success: true, diff --git a/test/registry.test.ts b/test/registry.test.ts index c9141bf860..e67fa0b11b 100644 --- a/test/registry.test.ts +++ b/test/registry.test.ts @@ -105,6 +105,46 @@ describe("registry", () => { }); }); + it("round-trips absent, known-empty, populated, and cloned image-plugin provenance", () => { + const weatherInstall = { + id: "weather", + installPath: "/sandbox/.openclaw/extensions/weather", + }; + registry.registerSandbox({ name: "legacy", agent: "openclaw" }); + registry.registerSandbox({ + name: "known-empty", + agent: "openclaw", + openclawImagePluginInstalls: [], + }); + registry.registerSandbox({ + name: "populated", + agent: "openclaw", + openclawImagePluginInstalls: [weatherInstall], + }); + registry.registerSandbox({ + ...registry.getSandbox("populated"), + name: "populated-clone", + }); + registry.registerSandbox({ + ...registry.getSandbox("known-empty"), + name: "known-empty-clone", + }); + + const data = JSON.parse(fs.readFileSync(regFile, "utf-8")).sandboxes; + expect(registry.getSandbox("legacy").openclawImagePluginInstalls).toBeUndefined(); + expect(registry.getSandbox("known-empty").openclawImagePluginInstalls).toEqual([]); + expect(registry.getSandbox("known-empty-clone").openclawImagePluginInstalls).toEqual([]); + expect(registry.getSandbox("populated").openclawImagePluginInstalls).toEqual([weatherInstall]); + expect(registry.getSandbox("populated-clone").openclawImagePluginInstalls).toEqual([ + weatherInstall, + ]); + expect(data.legacy.openclawImagePluginInstalls).toBeUndefined(); + expect(data["known-empty"].openclawImagePluginInstalls).toEqual([]); + expect(data["known-empty-clone"].openclawImagePluginInstalls).toEqual([]); + expect(data.populated.openclawImagePluginInstalls).toEqual([weatherInstall]); + expect(data["populated-clone"].openclawImagePluginInstalls).toEqual([weatherInstall]); + }); + it("preserves missing tool-disclosure state on reconstructed legacy rows", () => { registry.registerSandbox({ name: "legacy" }); diff --git a/test/snapshot-openclaw-managed-extensions.test.ts b/test/snapshot-openclaw-managed-extensions.test.ts index e26199750c..c5c1ab38d1 100644 --- a/test/snapshot-openclaw-managed-extensions.test.ts +++ b/test/snapshot-openclaw-managed-extensions.test.ts @@ -20,9 +20,9 @@ const loadedSandboxState = await import( pathToFileURL(path.join(REPO_ROOT, "src", "lib", "state", "sandbox.ts")).href ); assert.equal( - typeof loadedSandboxState.restoreSandboxState, + typeof loadedSandboxState.restoreRecreatedSandboxState, "function", - "Expected sandbox-state restore export to be available", + "Expected recreated-sandbox state restore export to be available", ); const sandboxState = loadedSandboxState as SandboxStateModule; const BACKUPS_ROOT = path.join(TMP_HOME, ".nemoclaw", "rebuild-backups"); @@ -31,7 +31,11 @@ function writeExecutable(filePath: string, source: string): void { fs.writeFileSync(filePath, source, { mode: 0o755 }); } -function writeBackup(sandboxName: string, dirName: string): { backupPath: string } { +function writeBackup( + sandboxName: string, + dirName: string, + openclawImagePluginInstalls?: Array<{ id: string; installPath: string }>, +): { backupPath: string } { const backupPath = path.join(BACKUPS_ROOT, sandboxName, dirName); fs.mkdirSync(backupPath, { recursive: true }); fs.writeFileSync( @@ -43,6 +47,7 @@ function writeBackup(sandboxName: string, dirName: string): { backupPath: string agentType: "openclaw", agentVersion: null, expectedVersion: null, + openclawImagePluginInstalls, stateDirs: ["extensions"], backedUpDirs: ["extensions"], dir: "/sandbox/.openclaw", @@ -100,10 +105,22 @@ beforeEach(() => { }); describe("OpenClaw managed extension snapshot restore", () => { - it.each([ - "sqlite", - "legacy", - ] as const)("preserves fresh built-in and custom image-managed OpenClaw extensions from the %s install index", (installIndexSource) => { + const pluginTransitions = [ + { name: "same-id update", previousPlugin: "weather", freshPlugin: "weather" }, + { name: "removal", previousPlugin: "weather", freshPlugin: null }, + { name: "rename", previousPlugin: "weather", freshPlugin: "forecast" }, + ] as const; + const installIndexCases = (["sqlite", "legacy"] as const).flatMap((installIndexSource) => + pluginTransitions.map((transition) => ({ installIndexSource, ...transition })), + ); + + it.each( + installIndexCases, + )("preserves fresh extensions and handles image-plugin $name from the $installIndexSource install index", ({ + installIndexSource, + previousPlugin, + freshPlugin, + }) => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-extension-restore-")); const oldPath = process.env.PATH; const oldOpenshell = process.env.NEMOCLAW_OPENSHELL_BIN; @@ -115,13 +132,13 @@ describe("OpenClaw managed extension snapshot restore", () => { const extensionsDir = path.join(openclawDir, "extensions"); const builtInManagedExtensions = "nemoclaw,diagnostics-otel,brave,discord,openclaw-weixin,slack,whatsapp,msteams".split(","); - const managedExtensions = [...builtInManagedExtensions, "weather"]; + const freshImagePlugins = freshPlugin ? [freshPlugin] : []; + const managedExtensions = [...builtInManagedExtensions, ...freshImagePlugins]; fs.mkdirSync(binDir, { recursive: true }); for (const extensionName of managedExtensions) { const extensionDir = path.join(extensionsDir, extensionName); fs.mkdirSync(extensionDir, { recursive: true }); - const marker = - extensionName === "weather" ? "fresh-weather-v2\n" : `fresh-${extensionName}\n`; + const marker = `fresh-${extensionName}\n`; fs.writeFileSync(path.join(extensionDir, "marker.txt"), marker); } fs.mkdirSync(path.join(extensionsDir, "stale-user-extension"), { recursive: true }); @@ -130,18 +147,26 @@ describe("OpenClaw managed extension snapshot restore", () => { freshRegistryPath, JSON.stringify({ version: 1, - installRecords: { - weather: { installPath: "/sandbox/.openclaw/extensions/weather" }, - }, + installRecords: Object.fromEntries( + freshImagePlugins.map((id) => [ + id, + { installPath: `/sandbox/.openclaw/extensions/${id}` }, + ]), + ), }), ); - const manifest = writeBackup("alpha", "2026-05-19T12-00-00-000Z"); + const manifest = writeBackup("alpha", "2026-05-19T12-00-00-000Z", [ + { + id: previousPlugin, + installPath: `/sandbox/.openclaw/extensions/${previousPlugin}`, + }, + ]); const backupExtensionsDir = path.join(manifest.backupPath, "extensions"); - for (const extensionName of managedExtensions) { + for (const extensionName of [...builtInManagedExtensions, previousPlugin]) { const extensionDir = path.join(backupExtensionsDir, extensionName); fs.mkdirSync(extensionDir, { recursive: true }); - const marker = extensionName === "weather" ? "old-weather-v1\n" : `old-${extensionName}\n`; + const marker = `old-${extensionName}\n`; fs.writeFileSync(path.join(extensionDir, "marker.txt"), marker); } fs.mkdirSync(path.join(backupExtensionsDir, "user-extension"), { recursive: true }); @@ -206,18 +231,19 @@ process.exit(0); process.env.NEMOCLAW_OPENSHELL_BIN = openshell; process.env.PATH = `${binDir}:${oldPath || ""}`; - const restore = sandboxState.restoreSandboxState("alpha", manifest.backupPath, { - preserveFreshOpenClawPluginInstalls: true, + const restore = sandboxState.restoreRecreatedSandboxState("alpha", manifest.backupPath, { + targetAgentType: "openclaw", }); expect(restore.success).toBe(true); expect(restore.restoredDirs).toEqual(["extensions"]); for (const extensionName of managedExtensions) { - const expectedMarker = - extensionName === "weather" ? "fresh-weather-v2\n" : `fresh-${extensionName}\n`; expect( fs.readFileSync(path.join(extensionsDir, extensionName, "marker.txt"), "utf-8"), - ).toBe(expectedMarker); + ).toBe(`fresh-${extensionName}\n`); } + expect(fs.existsSync(path.join(extensionsDir, previousPlugin))).toBe( + previousPlugin === freshPlugin, + ); expect(fs.existsSync(path.join(extensionsDir, "stale-user-extension"))).toBe(false); expect( fs.readFileSync(path.join(extensionsDir, "user-extension", "marker.txt"), "utf-8"), @@ -253,14 +279,19 @@ process.exit(0); }, }), ); - const rejected = sandboxState.restoreSandboxState("alpha", manifest.backupPath, { - preserveFreshOpenClawPluginInstalls: true, + const rejected = sandboxState.restoreRecreatedSandboxState("alpha", manifest.backupPath, { + targetAgentType: "openclaw", }); expect(rejected.success).toBe(false); expect(rejected.error).toBe("fresh OpenClaw plugin install registry failed validation"); - expect(fs.readFileSync(path.join(extensionsDir, "weather", "marker.txt"), "utf-8")).toBe( - "fresh-weather-v2\n", + expect(fs.existsSync(path.join(extensionsDir, previousPlugin))).toBe( + previousPlugin === freshPlugin, ); + for (const extensionName of managedExtensions) { + expect( + fs.readFileSync(path.join(extensionsDir, extensionName, "marker.txt"), "utf-8"), + ).toBe(`fresh-${extensionName}\n`); + } const commandsAfterRejectedRestore = fs .readFileSync(sshLog, "utf-8") .trim() diff --git a/test/snapshot-recovery-validation.test.ts b/test/snapshot-recovery-validation.test.ts index 1abaefeb72..0d60b0ffe3 100644 --- a/test/snapshot-recovery-validation.test.ts +++ b/test/snapshot-recovery-validation.test.ts @@ -81,6 +81,41 @@ describe("prepared rebuild backup recovery validation (#6114)", () => { }); }); + it("round-trips validated OpenClaw image-plugin provenance through recovery", () => { + const openclawImagePluginInstalls = [ + { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather" }, + { id: "npm-plugin", installPath: "/sandbox/.openclaw/npm/node_modules/npm-plugin" }, + ]; + writeBackup("alpha", "2026-07-01T06-50-42-045Z", { openclawImagePluginInstalls }); + const latest = sandboxState.getLatestBackup("alpha"); + + expect(latest?.openclawImagePluginInstalls).toEqual(openclawImagePluginInstalls); + expect(sandboxState.validateRebuildRecoveryManifest("alpha", "openclaw", latest!)).toEqual({ + ok: true, + manifest: expect.objectContaining({ openclawImagePluginInstalls }), + }); + }); + + it.each([ + ["a non-array value", { weather: "/sandbox/.openclaw/extensions/weather" }], + [ + "an unsafe plugin id", + [{ id: "../weather", installPath: "/sandbox/.openclaw/extensions/weather" }], + ], + ["a relative install path", [{ id: "weather", installPath: "extensions/weather" }]], + [ + "duplicate install paths", + [ + { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather" }, + { id: "weather-copy", installPath: "/sandbox/.openclaw/extensions/weather" }, + ], + ], + ])("rejects image-plugin provenance with %s", (_case, openclawImagePluginInstalls) => { + writeBackup("alpha", "2026-07-01T06-50-42-046Z", { openclawImagePluginInstalls }); + + expect(sandboxState.getLatestBackup("alpha")).toBeNull(); + }); + it("rejects a persisted manifest that disappears or becomes malformed after discovery", () => { const candidate = writeBackup("alpha", "2026-07-01T06-50-42-044Z", { agentVersion: "2026.5.27", diff --git a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts index ef42515dda..0b3914c63a 100644 --- a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts +++ b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts @@ -34,8 +34,8 @@ const CALLER_ALWAYS = "always()"; const MCP_SCANNED_UPLOAD_CONDITION = "${{ always() && steps.mcp_artifact_secret_scan.outcome == 'success' }}"; const TARGET_ID_PATTERN = /^[A-Za-z0-9_-]+$/; -const EXPECTED_UPLOAD_JOB_COUNT = 73; -const EXPECTED_DEFAULT_CALLER_COUNT = 62; +const EXPECTED_UPLOAD_JOB_COUNT = 74; +const EXPECTED_DEFAULT_CALLER_COUNT = 63; type WorkflowRecord = Record; type WorkflowStep = WorkflowRecord & { From 5c2f9f2be638e7b58102ace6fb1eda03bffc5663 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 15:20:50 -0700 Subject: [PATCH 32/46] test(e2e): allocate plugin context securely Signed-off-by: Aaron Erickson --- .../openclaw-plugin-runtime-exdev.test.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index f22b3d9350..063ff16b72 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -390,9 +390,9 @@ type CustomPluginBuildContext = { pluginDirPath: string; }; -function reserveCustomPluginBuildContext(): CustomPluginBuildContext { +function createCustomPluginBuildContext(): CustomPluginBuildContext { const nonce = randomUUID(); - const sourceParentDir = path.join(os.tmpdir(), `nemoclaw-v0.0.71-weather-${nonce}`); + const sourceParentDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-v0.0.71-weather-")); const sourceRoot = path.join(sourceParentDir, "NemoClaw"); return { sourceParentDir, @@ -404,13 +404,18 @@ function reserveCustomPluginBuildContext(): CustomPluginBuildContext { } test("custom plugin build paths are collision-safe and outside the checkout", () => { - const first = reserveCustomPluginBuildContext(); - const second = reserveCustomPluginBuildContext(); - expect(first.sourceParentDir).not.toBe(second.sourceParentDir); - expect(first.sourceParentDir.startsWith(`${REPO_ROOT}${path.sep}`)).toBe(false); - expect(second.sourceParentDir.startsWith(`${REPO_ROOT}${path.sep}`)).toBe(false); - expect(fs.existsSync(first.sourceParentDir)).toBe(false); - expect(fs.existsSync(second.sourceParentDir)).toBe(false); + const first = createCustomPluginBuildContext(); + const second = createCustomPluginBuildContext(); + try { + expect(first.sourceParentDir).not.toBe(second.sourceParentDir); + expect(first.sourceParentDir.startsWith(`${REPO_ROOT}${path.sep}`)).toBe(false); + expect(second.sourceParentDir.startsWith(`${REPO_ROOT}${path.sep}`)).toBe(false); + expect(fs.statSync(first.sourceParentDir).isDirectory()).toBe(true); + expect(fs.statSync(second.sourceParentDir).isDirectory()).toBe(true); + } finally { + fs.rmSync(first.sourceParentDir, { recursive: true, force: true }); + fs.rmSync(second.sourceParentDir, { recursive: true, force: true }); + } }); function copyFixtureFileExclusive(source: string, target: string): void { @@ -905,12 +910,10 @@ liveTest( }), "OpenShell wrapper and explicit pinned components must pass onboard coherence preflight", ).toBe(true); - const customPluginContext = reserveCustomPluginBuildContext(); - let removeCustomPluginContext = () => {}; - cleanup.add("remove v0.0.71 custom-plugin source worktree", () => removeCustomPluginContext()); - fs.mkdirSync(customPluginContext.sourceParentDir); - removeCustomPluginContext = () => - fs.rmSync(customPluginContext.sourceParentDir, { recursive: true, force: true }); + const customPluginContext = createCustomPluginBuildContext(); + cleanup.add("remove v0.0.71 custom-plugin source worktree", () => + fs.rmSync(customPluginContext.sourceParentDir, { recursive: true, force: true }), + ); const cloneRelease = await host.command( "git", [ From 8a08521bba36d281ef3f0efd03a052e55637cb3c Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 15:44:02 -0700 Subject: [PATCH 33/46] test(onboard): support plugin discovery in fixtures Signed-off-by: Aaron Erickson --- ci/test-file-size-budget.json | 2 +- test/onboard-custom-dockerfile.test.ts | 5 ++--- test/onboard-messaging.test.ts | 29 +++++++------------------- test/shellquote-sandbox.test.ts | 7 ++++--- 4 files changed, 15 insertions(+), 28 deletions(-) diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index 2b4cc768c4..71e08285ab 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -9,7 +9,7 @@ "test/generate-openclaw-config.test.ts": 1941, "test/install-preflight.test.ts": 3934, "test/nemoclaw-start.test.ts": 4827, - "test/onboard-messaging.test.ts": 2062, + "test/onboard-messaging.test.ts": 2049, "test/onboard-selection.test.ts": 5835, "test/onboard.test.ts": 4053, "test/policies.test.ts": 2332 diff --git a/test/onboard-custom-dockerfile.test.ts b/test/onboard-custom-dockerfile.test.ts index 3feda4419c..24cff22442 100644 --- a/test/onboard-custom-dockerfile.test.ts +++ b/test/onboard-custom-dockerfile.test.ts @@ -10,6 +10,7 @@ import path from "node:path"; import { describe, it } from "vitest"; import { createCustomBuildContextFilter } from "../src/lib/onboard/custom-build-context.js"; +import { writeOkOpenshell } from "./helpers/onboard-openshell-fixture"; import { testTimeoutOptions } from "./helpers/timeouts"; const repoRoot = path.join(import.meta.dirname, ".."); @@ -184,9 +185,7 @@ describe("onboard custom Dockerfile", () => { fs.writeFileSync(path.join(customBuildDir, "credentials.json"), "{}"); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const customDockerfilePath = JSON.stringify(path.join(customBuildDir, "Dockerfile")); diff --git a/test/onboard-messaging.test.ts b/test/onboard-messaging.test.ts index 445df67d00..c8693884f9 100644 --- a/test/onboard-messaging.test.ts +++ b/test/onboard-messaging.test.ts @@ -16,6 +16,7 @@ import { activeChannelsFromDockerfile, encodeTestMessagingPlan, } from "./helpers/messaging-plan-fixtures"; +import { writeOkOpenshell } from "./helpers/onboard-openshell-fixture"; type CommandEntry = { command: string; @@ -67,9 +68,7 @@ describe("onboard messaging", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -511,9 +510,7 @@ const { createSandbox } = require(${onboardPath}); ]); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -675,9 +672,7 @@ const { createSandbox } = require(${onboardPath}); const messagingPlanB64 = encodeTestMessagingPlan([{ channelId: "telegram", active: false }]); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -831,9 +826,7 @@ const { createSandbox } = require(${onboardPath}); const messagingPlanB64 = encodeTestMessagingPlan([{ channelId: "whatsapp", active: true }]); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -984,9 +977,7 @@ const { createSandbox } = require(${onboardPath}); const messagingPlanB64 = encodeTestMessagingPlan([{ channelId: "whatsapp", active: false }]); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -1309,9 +1300,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -1442,9 +1431,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); diff --git a/test/shellquote-sandbox.test.ts b/test/shellquote-sandbox.test.ts index ebe3569f51..741c7683df 100644 --- a/test/shellquote-sandbox.test.ts +++ b/test/shellquote-sandbox.test.ts @@ -8,6 +8,8 @@ import os from "os"; import path from "path"; import { describe, expect, it } from "vitest"; +import { writeOkOpenshell } from "./helpers/onboard-openshell-fixture"; + describe("sandboxName command hardening in onboard.js", () => { it("re-validates sandboxName at the createSandbox boundary", async () => { const onboardModule = await import("../src/lib/onboard.js"); @@ -41,9 +43,7 @@ describe("sandboxName command hardening in onboard.js", () => { const streamPath = sourceModule("sandbox", "create-stream.ts"); fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync(path.join(fakeBin, "openshell"), "#!/usr/bin/env bash\nexit 0\n", { - mode: 0o755, - }); + writeOkOpenshell(fakeBin); fs.writeFileSync( scriptPath, String.raw` @@ -57,6 +57,7 @@ for (const key of Object.keys(process.env)) { delete process.env[key]; } } +process.env.NEMOCLAW_OPENSHELL_BIN = ${JSON.stringify(path.join(fakeBin, "openshell"))}; const commands = []; const asText = (command) => Array.isArray(command) ? command.join(" ") : String(command); runner.run = (command, opts = {}) => { From a3a88c3c300b299562967addcd272c219aabd8f5 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 15:44:34 -0700 Subject: [PATCH 34/46] refactor(state): isolate OpenClaw plugin restore planning Signed-off-by: Aaron Erickson --- src/lib/onboard.ts | 8 +- src/lib/state/openclaw-plugin-restore.test.ts | 60 +++++++ src/lib/state/openclaw-plugin-restore.ts | 168 ++++++++++++++++++ .../sandbox-openclaw-plugin-restore.test.ts | 14 +- src/lib/state/sandbox.ts | 149 ++-------------- 5 files changed, 263 insertions(+), 136 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 4e372366ea..c9ecff64a1 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -540,6 +540,7 @@ const agentDefs = require("./agent/defs"); const gatewayState: typeof import("./state/gateway") = require("./state/gateway"); const notReadyRecreate: typeof import("./onboard/not-ready-recreate") = require("./onboard/not-ready-recreate"); +const openClawPluginRestore: typeof import("./state/openclaw-plugin-restore") = require("./state/openclaw-plugin-restore"); const sandboxState: typeof import("./state/sandbox") = require("./state/sandbox"); const validation: typeof import("./validation") = require("./validation"); const urlUtils: typeof import("./core/url-utils") = require("./core/url-utils"); @@ -2978,8 +2979,11 @@ async function createSandboxWithBaseImageResolution( preferredInferenceApi, }, { - discoverFreshOpenClawImagePluginInstalls: - sandboxState.discoverFreshOpenClawImagePluginInstalls, + discoverFreshOpenClawImagePluginInstalls: (name) => + openClawPluginRestore.discoverFreshOpenClawImagePluginInstalls(name, { + getSshConfig: sandboxState.getSshConfig, + sshArgs: sandboxState.sshArgs, + }), restoreRecreatedSandboxState: sandboxState.restoreRecreatedSandboxState, getDcodeSelectionDrift: (name, selectedProvider, selectedModel, selectedApi) => getDcodeSelectionDrift(name, selectedProvider, selectedModel, selectedApi, { diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts index 25d0036d02..e007aa1ea5 100644 --- a/src/lib/state/openclaw-plugin-restore.test.ts +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -12,6 +12,7 @@ import { buildFreshOpenClawPluginIndexSqliteReadCommand, parseFreshOpenClawPluginExtensionDirs, parseOpenClawImagePluginInstalls, + planOpenClawPluginRestore, } from "./openclaw-plugin-restore"; const OPENCLAW_DIR = "/sandbox/.openclaw"; @@ -247,3 +248,62 @@ describe("parseOpenClawImagePluginInstalls", () => { ); }); }); + +describe("planOpenClawPluginRestore", () => { + it("preserves fresh plugins while excluding removed previous plugins from the archive", () => { + const result = planOpenClawPluginRestore({ + agentType: "openclaw", + dir: OPENCLAW_DIR, + localDirs: ["extensions"], + freshImagePluginInstalls: [ + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + ], + previousImagePluginInstalls: [ + { id: "forecast", installPath: `${OPENCLAW_DIR}/extensions/forecast` }, + ], + }); + + expect(result.ok).toBe(true); + if (!result.ok) throw new Error(result.error); + expect(result.freshExtensionDirs).toEqual(["weather"]); + expect(result.previousExtensionDirs).toEqual(["forecast"]); + expect(result.preservedExtensionDirs).toEqual(expect.arrayContaining(["nemoclaw", "weather"])); + expect(result.preservedExtensionDirs).not.toContain("forecast"); + expect(result.archiveExcludedExtensionDirs).toEqual( + expect.arrayContaining(["forecast", "nemoclaw", "weather"]), + ); + expect(result.requiredFreshExtensionDirs).toEqual(["weather"]); + }); + + it("returns an empty extension plan when the backup does not contain extensions", () => { + expect( + planOpenClawPluginRestore({ + agentType: "openclaw", + dir: OPENCLAW_DIR, + localDirs: ["workspace"], + freshImagePluginInstalls: [ + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + ], + }), + ).toEqual({ + ok: true, + freshExtensionDirs: [], + previousExtensionDirs: [], + preservedExtensionDirs: [], + archiveExcludedExtensionDirs: [], + requiredFreshExtensionDirs: [], + }); + }); + + it("fails closed on invalid previous plugin provenance", () => { + expect( + planOpenClawPluginRestore({ + agentType: "openclaw", + dir: OPENCLAW_DIR, + localDirs: ["extensions"], + freshImagePluginInstalls: [], + previousImagePluginInstalls: [{ id: "weather", installPath: "extensions/weather" }], + }), + ).toEqual(expect.objectContaining({ ok: false })); + }); +}); diff --git a/src/lib/state/openclaw-plugin-restore.ts b/src/lib/state/openclaw-plugin-restore.ts index 3d5a444898..6ade627131 100644 --- a/src/lib/state/openclaw-plugin-restore.ts +++ b/src/lib/state/openclaw-plugin-restore.ts @@ -2,11 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 import path from "node:path"; +import { spawnSync } from "child_process"; import { isRecord } from "../core/json-types.js"; import { shellQuote } from "../core/shell-quote.js"; +import { createTempSshConfig } from "../sandbox/temp-ssh-config.js"; +import { + OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS, + shouldPreserveOpenClawManagedExtensions, +} from "./openclaw-managed-extensions.js"; const MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS = 128; +// The parser accepts at most 128 records with 4 KiB install paths, leaving +// ample room for IDs and metadata while bounding sandbox-controlled output. +const OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES = 1024 * 1024; // Bound sandbox-controlled registry strings before path normalization. const MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH = 4096; const MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS = 64; @@ -21,6 +30,22 @@ export interface OpenClawImagePluginInstall { readonly installPath: string; } +export interface OpenClawPluginDiscoveryDeps { + getSshConfig(sandboxName: string): string | null; + sshArgs(configFile: string, sandboxName: string): string[]; +} + +export type OpenClawPluginRestorePlanResult = + | { + ok: true; + freshExtensionDirs: string[]; + previousExtensionDirs: string[]; + preservedExtensionDirs: string[]; + archiveExcludedExtensionDirs: string[]; + requiredFreshExtensionDirs: string[]; + } + | { ok: false; error: string }; + const OPENCLAW_PLUGIN_INDEX_SQLITE_PY = [ "import json, sqlite3, sys, urllib.parse", 'uri = "file:" + urllib.parse.quote(sys.argv[1], safe="/") + "?mode=ro"', @@ -49,6 +74,99 @@ export function buildFreshOpenClawPluginIndexSqliteReadCommand(dir: string): str ].join("; "); } +function buildLegacyOpenClawPluginIndexReadCommand(dir: string): string { + const installIndexPath = `${dir.replace(/\/+$/, "")}/plugins/installs.json`; + const quotedInstallIndexPath = shellQuote(installIndexPath); + return [ + `src=${quotedInstallIndexPath}`, + '[ ! -e "$src" ] && exit 2', + '[ -f "$src" ] && [ ! -L "$src" ] || { echo "unsafe state file: $src" >&2; exit 10; }', + 'hardlink_count="$(find "$src" -maxdepth 0 -type f -links +1 -print 2>/dev/null | wc -l | tr -d " ")"', + '[ "${hardlink_count:-0}" = "0" ] || { echo "hard-linked state file rejected: $src" >&2; exit 11; }', + 'cat -- "$src"', + ].join("; "); +} + +function readFreshOpenClawPluginInstallIndex( + deps: OpenClawPluginDiscoveryDeps, + configFile: string, + sandboxName: string, + dir: string, +): ReturnType { + // OpenClaw 2026.6.10 moved install records into its shared SQLite state. + // Fall back only when that database is absent so a corrupt/incomplete + // canonical index cannot be masked by stale legacy JSON. + const sqliteResult = spawnSync( + "ssh", + [...deps.sshArgs(configFile, sandboxName), buildFreshOpenClawPluginIndexSqliteReadCommand(dir)], + { + stdio: ["ignore", "pipe", "pipe"], + timeout: 30000, + maxBuffer: OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES, + }, + ); + if (sqliteResult.status !== 2 || sqliteResult.error || sqliteResult.signal) return sqliteResult; + + return spawnSync( + "ssh", + [...deps.sshArgs(configFile, sandboxName), buildLegacyOpenClawPluginIndexReadCommand(dir)], + { + stdio: ["ignore", "pipe", "pipe"], + timeout: 30000, + maxBuffer: OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES, + }, + ); +} + +export function discoverFreshOpenClawPluginExtensionDirs( + deps: OpenClawPluginDiscoveryDeps, + configFile: string, + sandboxName: string, + dir: string, +): OpenClawManagedExtensionDiscoveryResult { + const result = readFreshOpenClawPluginInstallIndex(deps, configFile, sandboxName, dir); + if ( + result.stdout && + Buffer.byteLength(result.stdout) > OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES + ) { + return { ok: false, error: "fresh OpenClaw plugin install registry response too large" }; + } + if (result.status !== 0 || result.error || result.signal || !result.stdout) { + return { ok: false, error: "could not read fresh OpenClaw plugin install registry" }; + } + + let config: unknown; + try { + config = JSON.parse(result.stdout.toString("utf-8")) as unknown; + } catch { + return { + ok: false, + error: "fresh OpenClaw plugin install registry is not valid JSON", + }; + } + const parsed = parseFreshOpenClawPluginExtensionDirs(config, dir); + return parsed.ok + ? parsed + : { ok: false, error: "fresh OpenClaw plugin install registry failed validation" }; +} + +export function discoverFreshOpenClawImagePluginInstalls( + sandboxName: string, + deps: OpenClawPluginDiscoveryDeps, + dir = "/sandbox/.openclaw", +): OpenClawManagedExtensionDiscoveryResult { + const sshConfig = deps.getSshConfig(sandboxName); + if (!sshConfig) { + return { ok: false, error: "could not get SSH config for OpenClaw plugin discovery" }; + } + const tempSshConfig = createTempSshConfig(sshConfig, "nemoclaw-plugin-discovery-"); + try { + return discoverFreshOpenClawPluginExtensionDirs(deps, tempSshConfig.file, sandboxName, dir); + } finally { + tempSshConfig.cleanup(); + } +} + function isSafeOpenClawPluginInstallId(id: string): boolean { if (id.length === 0 || id.length > 256 || /[\u0000-\u001f\u007f]/.test(id)) return false; const slash = id.indexOf("/"); @@ -181,3 +299,53 @@ export function parseFreshOpenClawPluginExtensionDirs( .map((id) => [id, installs[id]] as const); return validateOpenClawImagePluginInstalls(entries, dir); } + +export function planOpenClawPluginRestore(options: { + agentType: string; + dir: string; + localDirs: readonly string[]; + freshImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; + previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; +}): OpenClawPluginRestorePlanResult { + const freshProjection = parseOpenClawImagePluginInstalls( + options.freshImagePluginInstalls ?? [], + options.dir, + ); + if (!freshProjection.ok) return freshProjection; + + const previousImagePluginInstalls = + options.freshImagePluginInstalls !== undefined + ? options.previousImagePluginInstalls + : undefined; + const previousProjection = parseOpenClawImagePluginInstalls( + previousImagePluginInstalls ?? [], + options.dir, + ); + if (!previousProjection.ok) return previousProjection; + + const preserveManagedExtensions = shouldPreserveOpenClawManagedExtensions( + { agentType: options.agentType }, + options.dir, + options.localDirs, + ); + const freshExtensionDirs = preserveManagedExtensions ? freshProjection.extensionDirs : []; + const previousExtensionDirs = + preserveManagedExtensions && previousImagePluginInstalls !== undefined + ? previousProjection.extensionDirs + : []; + const preservedExtensionDirs = preserveManagedExtensions + ? [...new Set([...OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS, ...freshExtensionDirs])].sort() + : []; + const archiveExcludedExtensionDirs = preserveManagedExtensions + ? [...new Set([...preservedExtensionDirs, ...previousExtensionDirs])].sort() + : []; + + return { + ok: true, + freshExtensionDirs, + previousExtensionDirs, + preservedExtensionDirs, + archiveExcludedExtensionDirs, + requiredFreshExtensionDirs: freshExtensionDirs, + }; +} diff --git a/src/lib/state/sandbox-openclaw-plugin-restore.test.ts b/src/lib/state/sandbox-openclaw-plugin-restore.test.ts index 979835544b..e5f8147937 100644 --- a/src/lib/state/sandbox-openclaw-plugin-restore.test.ts +++ b/src/lib/state/sandbox-openclaw-plugin-restore.test.ts @@ -10,7 +10,7 @@ vi.mock("child_process", async (importOriginal) => { return { ...actual, spawnSync: vi.fn() }; }); -import { __test } from "./sandbox"; +import { discoverFreshOpenClawPluginExtensionDirs } from "./openclaw-plugin-restore"; const OPENCLAW_DIR = "/sandbox/.openclaw"; const MAX_PLUGIN_REGISTRY_BYTES = 1024 * 1024; @@ -32,7 +32,11 @@ function spawnResult( } function discover() { - return __test.discoverFreshOpenClawPluginExtensionDirs( + return discoverFreshOpenClawPluginExtensionDirs( + { + getSshConfig: () => "unused", + sshArgs: (configFile, sandboxName) => ["-F", configFile, `openshell-${sandboxName}`], + }, "/tmp/ssh-config", "sandbox-one", OPENCLAW_DIR, @@ -59,7 +63,7 @@ describe("fresh OpenClaw plugin registry reads", () => { expect(parseSpy).not.toHaveBeenCalled(); expect(spawnSync).toHaveBeenCalledOnce(); expect(vi.mocked(spawnSync).mock.calls[0]?.[2]).toEqual( - expect.objectContaining({ maxBuffer: MAX_PLUGIN_REGISTRY_BYTES }), + expect.objectContaining({ maxBuffer: MAX_PLUGIN_REGISTRY_BYTES, timeout: 30000 }), ); }); @@ -94,7 +98,9 @@ describe("fresh OpenClaw plugin registry reads", () => { expect(parseSpy).not.toHaveBeenCalled(); expect(spawnSync).toHaveBeenCalledTimes(2); for (const call of vi.mocked(spawnSync).mock.calls) { - expect(call[2]).toEqual(expect.objectContaining({ maxBuffer: MAX_PLUGIN_REGISTRY_BYTES })); + expect(call[2]).toEqual( + expect.objectContaining({ maxBuffer: MAX_PLUGIN_REGISTRY_BYTES, timeout: 30000 }), + ); } }); diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 65f218bcd6..3f73d35e80 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -42,15 +42,12 @@ import { buildRestoreCleanupCommand, buildRestoreTarArgs, isAllowedStateSymlink, - OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS, - shouldPreserveOpenClawManagedExtensions, } from "./openclaw-managed-extensions.js"; import { - buildFreshOpenClawPluginIndexSqliteReadCommand, + discoverFreshOpenClawImagePluginInstalls, type OpenClawImagePluginInstall, - type OpenClawManagedExtensionDiscoveryResult, - parseFreshOpenClawPluginExtensionDirs, parseOpenClawImagePluginInstalls, + planOpenClawPluginRestore, } from "./openclaw-plugin-restore.js"; import type { CustomPolicyEntry } from "./registry.js"; import * as registry from "./registry.js"; @@ -62,9 +59,6 @@ const HOME_DIR = path.resolve(process.env.HOME || os.homedir()); const REBUILD_BACKUPS_DIR = path.join(HOME_DIR, ".nemoclaw", "rebuild-backups"); const MANIFEST_VERSION = 1; -// The parser accepts at most 128 records with 4 KiB install paths, leaving -// ample room for IDs and metadata while bounding sandbox-controlled output. -const OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES = 1024 * 1024; function parseJson(text: string): T { return JSON.parse(text); @@ -675,88 +669,6 @@ function existingBackupDirs(backupPath: string, dirNames: string[]): string[] { return existing; } -function readFreshOpenClawPluginInstallIndex( - configFile: string, - sandboxName: string, - dir: string, -): ReturnType { - // OpenClaw 2026.6.10 moved install records into its shared SQLite state. - // Fall back only when that database is absent so a corrupt/incomplete - // canonical index cannot be masked by stale legacy JSON. - const sqliteResult = spawnSync( - "ssh", - [...sshArgs(configFile, sandboxName), buildFreshOpenClawPluginIndexSqliteReadCommand(dir)], - { - stdio: ["ignore", "pipe", "pipe"], - timeout: 30000, - maxBuffer: OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES, - }, - ); - if (sqliteResult.status !== 2 || sqliteResult.error || sqliteResult.signal) return sqliteResult; - - const legacySpec: StateFileSpec = { path: "plugins/installs.json", strategy: "copy" }; - return spawnSync( - "ssh", - [...sshArgs(configFile, sandboxName), buildStateFileBackupCommand(dir, legacySpec)], - { - stdio: ["ignore", "pipe", "pipe"], - timeout: 30000, - maxBuffer: OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES, - }, - ); -} - -function discoverFreshOpenClawPluginExtensionDirs( - configFile: string, - sandboxName: string, - dir: string, -): OpenClawManagedExtensionDiscoveryResult { - const result = readFreshOpenClawPluginInstallIndex(configFile, sandboxName, dir); - if ( - result.stdout && - Buffer.byteLength(result.stdout) > OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES - ) { - return { ok: false, error: "fresh OpenClaw plugin install registry response too large" }; - } - if (result.status !== 0 || result.error || result.signal || !result.stdout) { - return { ok: false, error: "could not read fresh OpenClaw plugin install registry" }; - } - - let config: unknown; - try { - config = JSON.parse(result.stdout.toString("utf-8")) as unknown; - } catch { - return { - ok: false, - error: "fresh OpenClaw plugin install registry is not valid JSON", - }; - } - const parsed = parseFreshOpenClawPluginExtensionDirs(config, dir); - return parsed.ok - ? parsed - : { ok: false, error: "fresh OpenClaw plugin install registry failed validation" }; -} - -export function discoverFreshOpenClawImagePluginInstalls( - sandboxName: string, - dir = "/sandbox/.openclaw", -): OpenClawManagedExtensionDiscoveryResult { - const sshConfig = getSshConfig(sandboxName); - if (!sshConfig) { - return { ok: false, error: "could not get SSH config for OpenClaw plugin discovery" }; - } - const tempSshConfig = createTempSshConfig(sshConfig, "nemoclaw-plugin-discovery-"); - try { - return discoverFreshOpenClawPluginExtensionDirs(tempSshConfig.file, sandboxName, dir); - } finally { - tempSshConfig.cleanup(); - } -} - -export const __test = { - discoverFreshOpenClawPluginExtensionDirs, -}; - function normalizeStateFileSpec(spec: AgentStateFile | StateFileSpec): StateFileSpec | null { const normalized = normalizeStateFilePath(spec.path); if (!normalized) return null; @@ -1497,7 +1409,7 @@ export function restoreRecreatedSandboxState( options.freshOpenClawImagePluginInstalls, "/sandbox/.openclaw", ) - : discoverFreshOpenClawImagePluginInstalls(sandboxName); + : discoverFreshOpenClawImagePluginInstalls(sandboxName, { getSshConfig, sshArgs }); if (!discovery.ok) { return { success: false, @@ -1590,64 +1502,41 @@ function restoreSandboxStateInternal( ? manifest.openclawImagePluginInstalls : undefined; try { - const preserveManagedExtensions = shouldPreserveOpenClawManagedExtensions( - manifest, + const pluginRestorePlan = planOpenClawPluginRestore({ + agentType: manifest.agentType, dir, localDirs, - ); - const freshProjection = parseOpenClawImagePluginInstalls( - freshOpenClawImagePluginInstalls ?? [], - dir, - ); - const previousProjection = parseOpenClawImagePluginInstalls( - previousOpenClawImagePluginInstalls ?? [], - dir, - ); - if (!freshProjection.ok || !previousProjection.ok) { - let error = "OpenClaw image plugin provenance is invalid"; - if (!freshProjection.ok) error = freshProjection.error; - else if (!previousProjection.ok) error = previousProjection.error; + freshImagePluginInstalls: freshOpenClawImagePluginInstalls, + previousImagePluginInstalls: previousOpenClawImagePluginInstalls, + }); + if (!pluginRestorePlan.ok) { return { success: false, restoredDirs, failedDirs: [...localDirs], restoredFiles, failedFiles: localFiles.map((f) => f.path), - error, + error: pluginRestorePlan.error, }; } - const freshImageManagedExtensionDirs = preserveManagedExtensions - ? freshProjection.extensionDirs - : []; - const previousImageManagedExtensionDirs = - preserveManagedExtensions && previousOpenClawImagePluginInstalls !== undefined - ? previousProjection.extensionDirs - : []; - if (freshOpenClawImagePluginInstalls !== undefined && preserveManagedExtensions) { + if ( + freshOpenClawImagePluginInstalls !== undefined && + pluginRestorePlan.preservedExtensionDirs.length > 0 + ) { _log( - `Fresh image-managed OpenClaw extensions: [${freshImageManagedExtensionDirs.join(",")}]`, + `Fresh image-managed OpenClaw extensions: [${pluginRestorePlan.freshExtensionDirs.join(",")}]`, ); _log( - `Previous image-managed OpenClaw extensions: [${previousImageManagedExtensionDirs.join(",")}]`, + `Previous image-managed OpenClaw extensions: [${pluginRestorePlan.previousExtensionDirs.join(",")}]`, ); } - const preservedManagedExtensionDirs = preserveManagedExtensions - ? [ - ...new Set([...OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS, ...freshImageManagedExtensionDirs]), - ].sort() - : []; - const restoreArchiveExcludedExtensionDirs = preserveManagedExtensions - ? [ - ...new Set([...preservedManagedExtensionDirs, ...previousImageManagedExtensionDirs]), - ].sort() - : []; if (localDirs.length > 0) { // Upload via tar pipe // NC-2227-04: Removed -h flag from restore as well — no symlink following. const tarResult = spawnSync( "tar", - buildRestoreTarArgs(backupPath, localDirs, restoreArchiveExcludedExtensionDirs), + buildRestoreTarArgs(backupPath, localDirs, pluginRestorePlan.archiveExcludedExtensionDirs), { stdio: ["ignore", "pipe", "pipe"], timeout: 60000, @@ -1673,8 +1562,8 @@ function restoreSandboxStateInternal( const rmCmd = buildRestoreCleanupCommand( dir, localDirs, - preservedManagedExtensionDirs, - new Set(freshImageManagedExtensionDirs), + pluginRestorePlan.preservedExtensionDirs, + new Set(pluginRestorePlan.requiredFreshExtensionDirs), ); _log(`Cleaning target dirs before restore: ${rmCmd}`); const rmResult = spawnSync("ssh", [...sshArgs(configFile, sandboxName), rmCmd], { From fab9a6343be2bc8637c9023d0b77de5d32df8196 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 15:47:00 -0700 Subject: [PATCH 35/46] fix(ci): keep onboard adapter net-neutral Signed-off-by: Aaron Erickson --- src/lib/onboard.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index c9ecff64a1..581ae3a470 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2980,10 +2980,7 @@ async function createSandboxWithBaseImageResolution( }, { discoverFreshOpenClawImagePluginInstalls: (name) => - openClawPluginRestore.discoverFreshOpenClawImagePluginInstalls(name, { - getSshConfig: sandboxState.getSshConfig, - sshArgs: sandboxState.sshArgs, - }), + openClawPluginRestore.discoverFreshOpenClawImagePluginInstalls(name, sandboxState), restoreRecreatedSandboxState: sandboxState.restoreRecreatedSandboxState, getDcodeSelectionDrift: (name, selectedProvider, selectedModel, selectedApi) => getDcodeSelectionDrift(name, selectedProvider, selectedModel, selectedApi, { From 753deeb85a92e2098b0154aeb9504d8052ed99e8 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 6 Jul 2026 15:48:59 -0700 Subject: [PATCH 36/46] test(state): keep restore planning assertion linear Signed-off-by: Aaron Erickson --- src/lib/state/openclaw-plugin-restore.test.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts index e007aa1ea5..a15f5bea55 100644 --- a/src/lib/state/openclaw-plugin-restore.test.ts +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -8,6 +8,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; +import { OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS } from "./openclaw-managed-extensions"; import { buildFreshOpenClawPluginIndexSqliteReadCommand, parseFreshOpenClawPluginExtensionDirs, @@ -263,16 +264,17 @@ describe("planOpenClawPluginRestore", () => { ], }); - expect(result.ok).toBe(true); - if (!result.ok) throw new Error(result.error); - expect(result.freshExtensionDirs).toEqual(["weather"]); - expect(result.previousExtensionDirs).toEqual(["forecast"]); - expect(result.preservedExtensionDirs).toEqual(expect.arrayContaining(["nemoclaw", "weather"])); - expect(result.preservedExtensionDirs).not.toContain("forecast"); - expect(result.archiveExcludedExtensionDirs).toEqual( - expect.arrayContaining(["forecast", "nemoclaw", "weather"]), - ); - expect(result.requiredFreshExtensionDirs).toEqual(["weather"]); + const preservedExtensionDirs = [ + ...new Set([...OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS, "weather"]), + ].sort(); + expect(result).toEqual({ + ok: true, + freshExtensionDirs: ["weather"], + previousExtensionDirs: ["forecast"], + preservedExtensionDirs, + archiveExcludedExtensionDirs: [...preservedExtensionDirs, "forecast"].sort(), + requiredFreshExtensionDirs: ["weather"], + }); }); it("returns an empty extension plan when the backup does not contain extensions", () => { From 458b92db8209b862b8cbb880fa3fdc27fe38f67d Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 6 Jul 2026 16:40:09 -0700 Subject: [PATCH 37/46] fix(state): reconcile custom image plugin provenance Signed-off-by: Apurv Kumaria --- docs/deployment/install-openclaw-plugins.mdx | 24 +- .../sandbox/rebuild-backup-phase.test.ts | 101 ++++++++ .../actions/sandbox/rebuild-backup-phase.ts | 51 +++- .../sandbox/rebuild-prepared-recovery.ts | 27 ++- src/lib/onboard.ts | 23 +- .../created-sandbox-finalization.test.ts | 59 +++++ .../onboard/created-sandbox-finalization.ts | 23 +- src/lib/onboard/not-ready-recreate.test.ts | 122 +++++++++- src/lib/onboard/not-ready-recreate.ts | 75 +++++- .../sandbox-backup-on-recreate.test.ts | 40 ++++ src/lib/onboard/sandbox-backup-on-recreate.ts | 30 ++- src/lib/onboard/sandbox-registration.test.ts | 9 +- src/lib/onboard/sandbox-registration.ts | 1 + src/lib/state/openclaw-config-merge.test.ts | 220 ++++++++++++++++-- src/lib/state/openclaw-config-merge.ts | 189 +++++++++++---- .../openclaw-config-restore-input.test.ts | 48 ++++ .../state/openclaw-config-restore-input.ts | 27 ++- src/lib/state/openclaw-plugin-restore.test.ts | 200 +++++++++++++--- src/lib/state/openclaw-plugin-restore.ts | 182 ++++++++++++--- src/lib/state/registry.ts | 5 +- src/lib/state/sandbox.ts | 85 ++++++- test/helpers/rebuild-flow-recovery-cases.ts | 33 +++ test/helpers/rebuild-flow-test-harness.ts | 16 ++ test/registry.test.ts | 1 + ...apshot-openclaw-managed-extensions.test.ts | 19 +- test/snapshot-recovery-validation.test.ts | 65 +++++- test/snapshot.test.ts | 31 ++- 27 files changed, 1533 insertions(+), 173 deletions(-) diff --git a/docs/deployment/install-openclaw-plugins.mdx b/docs/deployment/install-openclaw-plugins.mdx index 39394b15fc..8185fdcf27 100644 --- a/docs/deployment/install-openclaw-plugins.mdx +++ b/docs/deployment/install-openclaw-plugins.mdx @@ -193,8 +193,28 @@ NemoClaw's live regression also verifies that the running gateway's tool catalog Keep the source checkout and Dockerfile at the recorded path if you plan to run `nemoclaw weather-agent rebuild --yes`. This source-based workflow can reproduce the plugin during that rebuild, but it is not the durable managed-plugin lifecycle proposed in issue #5998. -During rebuild, NemoClaw validates plugin install IDs and paths recorded by the freshly built image, preserves those fresh extension directories, and restores backup-only extension state around them. -That reconciliation keeps a rebuilt plugin update instead of replacing it with the pre-rebuild copy from the state backup. + + +The provenance safeguards below start in NemoClaw `v0.0.76`. +The `v0.0.71` worked example demonstrates custom-image construction but does not provide these lifecycle guards. +To rely on them, use NemoClaw `v0.0.76` or later and match its CLI, source checkout, base image, and pinned OpenClaw version. + + +Starting with `v0.0.76`, custom-image onboarding records validated plugin IDs and canonical install paths from OpenClaw's install index. +It derives direct extension-directory ownership from those paths and records exact configured load paths owned by linked `path` installs. +Use `openclaw plugins install` as shown above; manually copied, unregistered extension directories are outside this provenance contract. +During rebuild or warm recreation, the new image remains authoritative for those image-owned plugins: updated plugins keep the fresh image copy, removed plugins stay removed, and renamed plugins do not restore their old IDs. +NemoClaw continues to restore backup-only user plugins and their configuration. +Before rebuild or the default state-preserving warm recreation path deletes an existing custom-image sandbox, NemoClaw requires complete, validated provenance from the registry or selected backup. +If that previous provenance is missing or invalid, the operation stops with the existing sandbox and backup untouched. +Setting `NEMOCLAW_RECREATE_WITHOUT_BACKUP=1` deliberately bypasses the warm-recreation backup and provenance guard; use it only after making an independent backup and accepting loss of the current sandbox state. +After initial onboarding or replacement creation, NemoClaw validates the fresh image again before it restores state or publishes registry metadata. +If fresh discovery or restore-time reconciliation fails, NemoClaw does not publish the fresh replacement metadata. +Direct onboarding or recreation leaves the created sandbox unregistered; rebuild preserves the backup, restores retry metadata, and prints manual recovery commands. +Preserve any reported backup, then inspect and remove an unregistered sandbox with OpenShell before you retry the original onboarding command. +If the fresh image itself fails validation, correct its plugin install records or linked load paths before you retry onboarding. +Custom-image sandboxes created before `v0.0.76` cannot enter the state-preserving path automatically when they lack recorded provenance. +Keep the older sandbox intact, onboard the current release under a different name, and migrate its state manually. Rerun the plugin inspection and HTTP invocation after a gateway restart or sandbox rebuild to verify that the plugin remains available. If the plugin imports runtime packages, keep those packages in `dependencies` rather than `devDependencies` so the prune step preserves them. diff --git a/src/lib/actions/sandbox/rebuild-backup-phase.test.ts b/src/lib/actions/sandbox/rebuild-backup-phase.test.ts index 36c5f2efcf..4d931bf593 100644 --- a/src/lib/actions/sandbox/rebuild-backup-phase.test.ts +++ b/src/lib/actions/sandbox/rebuild-backup-phase.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from "vitest"; import { normalizeRebuildWebSearchPolicyPresets, + type RebuildBackupPhaseInput, runRebuildBackupPhase, } from "./rebuild-backup-phase"; @@ -85,3 +86,103 @@ describe("rebuild web-search policy normalization", () => { expect(result?.sessionPolicyPresets).toEqual([]); }); }); + +describe("custom OpenClaw plugin provenance rebuild guard (#6108)", () => { + const completeMarkedManifest = { + agentType: "openclaw", + dir: "/sandbox/.openclaw", + backupPath: "/tmp/custom-openclaw-backup", + reconcileOpenClawImagePluginProvenance: true, + openclawImagePluginInstalls: [], + } as never; + + function customOpenClawInput(overrides: Record = {}): RebuildBackupPhaseInput { + return { + sandboxName: "custom-openclaw", + sandboxEntry: { + name: "custom-openclaw", + agent: "openclaw", + fromDockerfile: "/tmp/Dockerfile.custom", + }, + staleRecovery: false, + preparedRecoveryManifest: null, + messagingPlan: null, + webSearchConfig: null, + log: vi.fn(), + bail: (message: string): never => { + throw new Error(message); + }, + relockShieldsIfNeeded: vi.fn(() => true), + ...overrides, + } as RebuildBackupPhaseInput; + } + + it("blocks a live custom image with missing registry provenance before backup", () => { + const backupStateForRebuild = vi.fn(); + const input = customOpenClawInput(); + const errorLog = vi.spyOn(console, "error").mockImplementation(() => undefined); + + expect(() => runRebuildBackupPhase(input, backupStateForRebuild)).toThrow( + "Custom-image OpenClaw plugin provenance is unavailable.", + ); + + expect(backupStateForRebuild).not.toHaveBeenCalled(); + expect(input.relockShieldsIfNeeded).toHaveBeenCalledWith(true); + expect(errorLog).toHaveBeenCalledWith(expect.stringContaining("new sandbox name")); + expect(errorLog).not.toHaveBeenCalledWith( + expect.stringContaining("NEMOCLAW_RECREATE_WITHOUT_BACKUP"), + ); + errorLog.mockRestore(); + }); + + it("uses a marked prepared manifest when registry provenance is missing", () => { + const backupStateForRebuild = vi.fn(); + const input = customOpenClawInput({ preparedRecoveryManifest: completeMarkedManifest }); + + const result = runRebuildBackupPhase(input, backupStateForRebuild); + + expect(result?.backupManifest).toBe(completeMarkedManifest); + expect(backupStateForRebuild).not.toHaveBeenCalled(); + }); + + it("blocks an unmarked legacy prepared manifest before deletion", () => { + const backupStateForRebuild = vi.fn(); + const input = customOpenClawInput({ + preparedRecoveryManifest: { + agentType: "openclaw", + dir: "/sandbox/.openclaw", + backupPath: "/tmp/legacy-custom-openclaw-backup", + openclawImagePluginInstalls: [], + }, + }); + + expect(() => runRebuildBackupPhase(input, backupStateForRebuild)).toThrow( + "Custom-image OpenClaw plugin provenance is unavailable.", + ); + + expect(backupStateForRebuild).not.toHaveBeenCalled(); + }); + + it("revalidates a newly generated backup manifest before deletion", () => { + const backupStateForRebuild = vi.fn(() => ({ + agentType: "openclaw", + dir: "/sandbox/.openclaw", + backupPath: "/tmp/incomplete-custom-openclaw-backup", + reconcileOpenClawImagePluginProvenance: true, + })); + const input = customOpenClawInput({ + sandboxEntry: { + name: "custom-openclaw", + agent: "openclaw", + fromDockerfile: "/tmp/Dockerfile.custom", + openclawImagePluginInstalls: [], + }, + }); + + expect(() => runRebuildBackupPhase(input, backupStateForRebuild as never)).toThrow( + "Custom-image OpenClaw plugin provenance is unavailable.", + ); + + expect(backupStateForRebuild).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/lib/actions/sandbox/rebuild-backup-phase.ts b/src/lib/actions/sandbox/rebuild-backup-phase.ts index 6117e55d06..ce43ec799c 100644 --- a/src/lib/actions/sandbox/rebuild-backup-phase.ts +++ b/src/lib/actions/sandbox/rebuild-backup-phase.ts @@ -6,6 +6,8 @@ import type { SandboxMessagingPlan } from "../../messaging"; import { mergeRebuildMessagingPolicyPresets } from "../../onboard/messaging-policy-presets"; import { resolveRecreatePolicyPresets } from "../../onboard/policy-preset-persistence"; import { isStaleBuiltinWebSearchPolicyPreset } from "../../onboard/policy-selection"; +import { hasCompleteOpenClawImagePluginProvenance } from "../../state/openclaw-plugin-restore"; +import { hasAuthoritativeOpenClawImagePluginProvenance } from "../../state/sandbox"; import type { RebuildBail, RebuildLog } from "./rebuild-credential-preflight"; import { backupSandboxStateForRebuild, type RebuildSandboxEntry } from "./rebuild-flow-helpers"; @@ -32,6 +34,18 @@ export interface RebuildBackupPhaseResult { sessionPolicyPresets: string[] | null; } +function bailForUnsafeOpenClawPluginProvenance(input: RebuildBackupPhaseInput): never { + console.error( + " Custom-image OpenClaw plugin provenance is missing or invalid; rebuild cannot safely distinguish image-owned plugins from user state.", + ); + console.error(" The sandbox is untouched — no data was lost."); + console.error( + " To preserve state, onboard the custom image under a new sandbox name and manually migrate only user-owned state.", + ); + input.relockShieldsIfNeeded(!input.staleRecovery); + return input.bail("Custom-image OpenClaw plugin provenance is unavailable."); +} + /** Align built-in web-search egress with the durable provider selection. */ export function normalizeRebuildWebSearchPolicyPresets( presets: readonly string[], @@ -66,10 +80,35 @@ export function normalizeRebuildWebSearchPolicyPresets( export function runRebuildBackupPhase( input: RebuildBackupPhaseInput, + backupStateForRebuild: typeof backupSandboxStateForRebuild = backupSandboxStateForRebuild, ): RebuildBackupPhaseResult | null { + const customOpenClaw = + Boolean(input.sandboxEntry.fromDockerfile) && + (!input.sandboxEntry.agent || input.sandboxEntry.agent === "openclaw"); + const preparedRecoveryManifest = input.preparedRecoveryManifest; + const hasPreparedRecovery = preparedRecoveryManifest !== null; + const preparedRecoveryIsAuthoritative = + preparedRecoveryManifest !== null && + hasAuthoritativeOpenClawImagePluginProvenance(preparedRecoveryManifest); + const restoresCustomOpenClawState = + customOpenClaw && (!input.staleRecovery || hasPreparedRecovery); + if ( + (hasPreparedRecovery && + preparedRecoveryManifest?.reconcileOpenClawImagePluginProvenance === true && + !preparedRecoveryIsAuthoritative) || + (restoresCustomOpenClawState && + !preparedRecoveryIsAuthoritative && + (hasPreparedRecovery || + !hasCompleteOpenClawImagePluginProvenance( + input.sandboxEntry.openclawImagePluginInstalls, + "/sandbox/.openclaw", + ))) + ) { + return bailForUnsafeOpenClawPluginProvenance(input); + } const backupManifest = - input.preparedRecoveryManifest ?? - backupSandboxStateForRebuild( + preparedRecoveryManifest ?? + backupStateForRebuild( input.sandboxName, input.sandboxEntry, input.staleRecovery, @@ -78,6 +117,14 @@ export function runRebuildBackupPhase( input.bail, ); if (backupManifest === undefined) return null; + if ( + backupManifest && + (backupManifest.reconcileOpenClawImagePluginProvenance === true || + restoresCustomOpenClawState) && + !hasAuthoritativeOpenClawImagePluginProvenance(backupManifest) + ) { + return bailForUnsafeOpenClawPluginProvenance(input); + } const registryPolicyPresets = Array.isArray(input.sandboxEntry.policies) ? input.sandboxEntry.policies.filter( diff --git a/src/lib/actions/sandbox/rebuild-prepared-recovery.ts b/src/lib/actions/sandbox/rebuild-prepared-recovery.ts index 6ababf3eed..6821644a6a 100644 --- a/src/lib/actions/sandbox/rebuild-prepared-recovery.ts +++ b/src/lib/actions/sandbox/rebuild-prepared-recovery.ts @@ -26,6 +26,13 @@ function failPreparedRecoveryPreDelete( return bail(errorMessage); } +function registryEntryWithoutOpenClawPluginProvenance( + entry: RebuildSandboxEntry, +): Omit { + const { openclawImagePluginInstalls: _provenance, ...rest } = entry; + return rest; +} + export function validatePreparedRecoveryManifest( sandboxName: string, sandboxEntry: RebuildSandboxEntry, @@ -45,7 +52,10 @@ export function validatePreparedRecoveryManifest( bail(`Invalid recovery manifest: ${validation.reason}`); return null; } - if (!sandboxState.hasPositiveManagedImageEvidence(sandboxEntry)) { + if ( + !sandboxState.hasAuthoritativeOpenClawImagePluginProvenance(validation.manifest) && + !sandboxState.hasPositiveManagedImageEvidence(sandboxEntry) + ) { console.error(""); console.error( ` ${_RD}Recovery preflight failed:${R} registry has no NemoClaw-managed image fingerprint.`, @@ -81,7 +91,15 @@ export function revalidatePreparedRecoveryBeforeDelete( bail, ); } - if (!isDeepStrictEqual(currentEntry, initialEntry)) { + const authoritativePluginProvenance = + sandboxState.hasAuthoritativeOpenClawImagePluginProvenance(candidate); + const registryConfigurationMatches = authoritativePluginProvenance + ? isDeepStrictEqual( + registryEntryWithoutOpenClawPluginProvenance(currentEntry), + registryEntryWithoutOpenClawPluginProvenance(initialEntry), + ) + : isDeepStrictEqual(currentEntry, initialEntry); + if (!registryConfigurationMatches) { return failPreparedRecoveryPreDelete( "registered sandbox configuration changed during preflight", "Recovery registry configuration changed during preflight.", @@ -114,7 +132,10 @@ export function revalidatePreparedRecoveryBeforeDelete( bail, ); } - if (!sandboxState.hasPositiveManagedImageEvidence(currentEntry)) { + if ( + !sandboxState.hasAuthoritativeOpenClawImagePluginProvenance(validation.manifest) && + !sandboxState.hasPositiveManagedImageEvidence(currentEntry) + ) { return failPreparedRecoveryPreDelete( "registry no longer has a NemoClaw-managed image fingerprint", "Recovery registry entry has no NemoClaw-managed image fingerprint.", diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 581ae3a470..fcea8cfa9c 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2395,10 +2395,14 @@ async function createSandboxWithBaseImageResolution( let pendingStateRestore: BackupResult | null = null; let pendingStateRestoreBackupPath: string | null = null; let notReadyRecreateInProgress = false; + const requireOpenClawImagePluginProvenance = + Boolean(fromDockerfile) && getRequestedSandboxAgentName(agent) === "openclaw"; pendingStateRestoreBackupPath = notReadyRecreate.selectPreUpgradeBackupForCreate({ liveExists, hasExistingRegistryEntry: existingEntry !== null, + existingSandboxEntry: existingEntry, + requireOpenClawImagePluginProvenance, sandboxName, note, }); @@ -2548,7 +2552,12 @@ async function createSandboxWithBaseImageResolution( } } else { notReadyRecreateInProgress = true; - const outcome = notReadyRecreate.resolveNotReadyOutcome(sandboxName, note); + const outcome = notReadyRecreate.resolveNotReadyOutcome( + sandboxName, + note, + existingEntry, + requireOpenClawImagePluginProvenance, + ); if (outcome.kind === "blocked") { for (const hint of outcome.hints) console.error(hint); process.exit(1); @@ -2607,7 +2616,11 @@ async function createSandboxWithBaseImageResolution( console.log(` Messaging credential(s) rotated: ${rotatedNames}`); console.log(" Rebuilding sandbox to propagate new credentials to the L7 proxy..."); if (!shouldSkipPreRecreateBackup(process.env)) { - const result = backupSandboxBeforeRecreate({ sandboxName }); + const result = backupSandboxBeforeRecreate({ + sandboxName, + sandboxEntry: existingEntry, + requireOpenClawImagePluginProvenance, + }); if (!result.ok) { console.error( " Set NEMOCLAW_RECREATE_WITHOUT_BACKUP=1 to recreate without preserving state.", @@ -2669,7 +2682,11 @@ async function createSandboxWithBaseImageResolution( !shouldSkipPreRecreateBackup(process.env) ) { note(" Backing up workspace state before recreating sandbox..."); - const result = backupSandboxBeforeRecreate({ sandboxName }); + const result = backupSandboxBeforeRecreate({ + sandboxName, + sandboxEntry: existingEntry, + requireOpenClawImagePluginProvenance, + }); if (!result.ok) { console.error( " Set NEMOCLAW_RECREATE_WITHOUT_BACKUP=1 to recreate without preserving state.", diff --git a/src/lib/onboard/created-sandbox-finalization.test.ts b/src/lib/onboard/created-sandbox-finalization.test.ts index 903e85db13..dc6ca020e8 100644 --- a/src/lib/onboard/created-sandbox-finalization.test.ts +++ b/src/lib/onboard/created-sandbox-finalization.test.ts @@ -402,6 +402,7 @@ describe("created OpenClaw sandbox finalization", () => { { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: [], }, ]; @@ -531,5 +532,63 @@ describe("created OpenClaw sandbox finalization", () => { expect(error).toHaveBeenCalledWith( " State was not restored and registry metadata was not updated.", ); + expect(error).toHaveBeenCalledWith(' openshell sandbox delete "openclaw"'); + expect(error).toHaveBeenCalledWith( + " Then rerun the original `nemoclaw onboard --from ` command.", + ); + expect(error).toHaveBeenCalledWith(" Manual recovery: /tmp/openclaw-backup"); + }); + + it("does not register after a marked backup provenance mismatch", () => { + const register = vi.fn(); + const error = vi.fn(); + + expect(() => + finalizeCreatedSandbox( + { + sandboxName: "openclaw", + restoreBackupPath: "/tmp/openclaw-backup", + preUpgradeBackup: false, + targetAgentType: "openclaw", + validateManagedDcode: false, + provider: "compatible-endpoint", + model: "demo", + preferredInferenceApi: "openai-completions", + }, + { + discoverFreshOpenClawImagePluginInstalls: () => ({ + ok: true, + extensionDirs: ["weather"], + pluginInstalls, + }), + restoreRecreatedSandboxState: () => ({ + success: false, + restoredDirs: [], + failedDirs: ["manifest"], + restoredFiles: [], + failedFiles: [], + error: sandboxState.OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR, + }), + getDcodeSelectionDrift: vi.fn(), + register, + note: vi.fn(), + error, + exitProcess: (code): never => { + throw new Error(`exit ${code}`); + }, + }, + ), + ).toThrow("exit 1"); + + expect(register).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith(expect.stringContaining("future rebuild would be unsafe")); + expect(error).toHaveBeenCalledWith( + expect.stringContaining(sandboxState.OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR), + ); + expect(error).toHaveBeenCalledWith(' openshell sandbox delete "openclaw"'); + expect(error).toHaveBeenCalledWith( + " Then rerun the original `nemoclaw onboard --from ` command.", + ); + expect(error).toHaveBeenCalledWith(" Manual recovery: /tmp/openclaw-backup"); }); }); diff --git a/src/lib/onboard/created-sandbox-finalization.ts b/src/lib/onboard/created-sandbox-finalization.ts index ec61b2ac50..e711009658 100644 --- a/src/lib/onboard/created-sandbox-finalization.ts +++ b/src/lib/onboard/created-sandbox-finalization.ts @@ -6,7 +6,11 @@ import type { OpenClawImagePluginInstall, OpenClawManagedExtensionDiscoveryResult, } from "../state/openclaw-plugin-restore"; -import type { RecreatedSandboxRestoreOptions, RestoreResult } from "../state/sandbox"; +import { + OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR, + type RecreatedSandboxRestoreOptions, + type RestoreResult, +} from "../state/sandbox"; import type { SelectionDrift } from "./selection-drift"; export type CreatedSandboxFinalizationOptions = { @@ -54,6 +58,10 @@ export function finalizeCreatedSandbox( ` OpenClaw image plugin discovery failed for sandbox '${options.sandboxName}': ${discovery.error}`, ); deps.error(" State was not restored and registry metadata was not updated."); + deps.error(" Remove the unregistered sandbox before retrying:"); + deps.error(` openshell sandbox delete ${JSON.stringify(options.sandboxName)}`); + deps.error(" Then rerun the original `nemoclaw onboard --from ` command."); + if (options.restoreBackupPath) deps.error(` Manual recovery: ${options.restoreBackupPath}`); return deps.exitProcess(1); } freshOpenClawImagePluginInstalls = discovery.pluginInstalls; @@ -83,6 +91,19 @@ export function finalizeCreatedSandbox( ` ✓ State restored (${restore.restoredDirs.length} directories, ${restore.restoredFiles.length} files)`, ); } else { + if (restore.error === OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR) { + deps.error( + ` OpenClaw image plugin provenance validation failed for sandbox '${options.sandboxName}': ${restore.error}`, + ); + deps.error( + " The sandbox still exists, but registry metadata was not updated because a future rebuild would be unsafe.", + ); + deps.error(" Remove the unregistered sandbox before retrying:"); + deps.error(` openshell sandbox delete ${JSON.stringify(options.sandboxName)}`); + deps.error(" Then rerun the original `nemoclaw onboard --from ` command."); + deps.error(` Manual recovery: ${options.restoreBackupPath}`); + return deps.exitProcess(1); + } // Source-of-truth review: // - Invalid state: a fresh sandbox exists after an external workspace copy fails. // - Boundary: restore.success owns copy completeness; live validation owns route integrity. diff --git a/src/lib/onboard/not-ready-recreate.test.ts b/src/lib/onboard/not-ready-recreate.test.ts index 311bfc495b..5180c52671 100644 --- a/src/lib/onboard/not-ready-recreate.test.ts +++ b/src/lib/onboard/not-ready-recreate.test.ts @@ -11,6 +11,7 @@ import { NotReadySandboxError, resolveNotReadyOutcome, selectPreUpgradeBackupForCreate, + UnsafeCustomImagePluginBackupError, } from "./not-ready-recreate"; const BACKUP_PATH = "/home/user/.nemoclaw/rebuild-backups/my-assistant/2026-07-01T06-50-40-925Z"; @@ -124,7 +125,7 @@ describe("selectPreUpgradeBackupForCreate", () => { process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; getLatestBackupSpy.mockReturnValue({ backupPath: BACKUP_PATH, - } as ReturnType); + } as unknown as ReturnType); expect( selectPreUpgradeBackupForCreate({ liveExists: false, @@ -138,6 +139,78 @@ describe("selectPreUpgradeBackupForCreate", () => { ); }); + it("blocks a legacy custom OpenClaw backup before installer recreation (#6108)", () => { + process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; + getLatestBackupSpy.mockReturnValue({ + agentType: "openclaw", + dir: "/sandbox/.openclaw", + backupPath: BACKUP_PATH, + openclawImagePluginInstalls: [], + } as unknown as ReturnType); + + expect(() => + selectPreUpgradeBackupForCreate({ + liveExists: false, + hasExistingRegistryEntry: true, + existingSandboxEntry: { + name: "my-assistant", + agent: "openclaw", + fromDockerfile: "/tmp/Dockerfile.custom", + }, + sandboxName: "my-assistant", + note, + }), + ).toThrow(UnsafeCustomImagePluginBackupError); + + expect(note).not.toHaveBeenCalled(); + }); + + it("accepts an authoritative custom OpenClaw backup for installer recreation (#6108)", () => { + process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; + getLatestBackupSpy.mockReturnValue({ + agentType: "openclaw", + dir: "/sandbox/.openclaw", + backupPath: BACKUP_PATH, + reconcileOpenClawImagePluginProvenance: true, + openclawImagePluginInstalls: [], + } as unknown as ReturnType); + + expect( + selectPreUpgradeBackupForCreate({ + liveExists: false, + hasExistingRegistryEntry: true, + existingSandboxEntry: { + name: "my-assistant", + agent: "openclaw", + fromDockerfile: "/tmp/Dockerfile.custom", + }, + sandboxName: "my-assistant", + note, + }), + ).toBe(BACKUP_PATH); + }); + + it("blocks custom OpenClaw installer recreation when no valid backup is readable (#6108)", () => { + process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; + getLatestBackupSpy.mockReturnValue(null); + + expect(() => + selectPreUpgradeBackupForCreate({ + liveExists: false, + hasExistingRegistryEntry: true, + existingSandboxEntry: { + name: "my-assistant", + agent: "openclaw", + fromDockerfile: "/tmp/Dockerfile.custom", + }, + sandboxName: "my-assistant", + note, + }), + ).toThrow(UnsafeCustomImagePluginBackupError); + + expect(note).not.toHaveBeenCalled(); + }); + it("returns null and notes fresh-state recreate when installer restore intent finds no backup", () => { process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; getLatestBackupSpy.mockReturnValue(null); @@ -202,7 +275,7 @@ describe("applyNonInteractiveNotReadyDecision", () => { process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; getLatestBackupSpy.mockReturnValue({ backupPath: BACKUP_PATH, - } as ReturnType); + } as unknown as ReturnType); expect(applyNonInteractiveNotReadyDecision("my-assistant", note)).toBe(BACKUP_PATH); expect(note).toHaveBeenCalledWith( expect.stringMatching(/recreating and restoring pre-upgrade backup/), @@ -249,12 +322,55 @@ describe("resolveNotReadyOutcome", () => { process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; getLatestBackupSpy.mockReturnValue({ backupPath: BACKUP_PATH, - } as ReturnType); + } as unknown as ReturnType); expect(resolveNotReadyOutcome("my-assistant", note)).toEqual({ kind: "proceed", restoreBackupPath: BACKUP_PATH, }); }); + + it("blocks live not-ready custom OpenClaw recreation with a legacy backup (#6108)", () => { + process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; + getLatestBackupSpy.mockReturnValue({ + agentType: "openclaw", + dir: "/sandbox/.openclaw", + backupPath: BACKUP_PATH, + openclawImagePluginInstalls: [], + } as unknown as ReturnType); + + const outcome = resolveNotReadyOutcome("my-assistant", note, { + name: "my-assistant", + agent: "openclaw", + fromDockerfile: "/tmp/Dockerfile.custom", + }); + + expect(outcome.kind).toBe("blocked"); + expect(outcome).toMatchObject({ + hints: expect.arrayContaining([expect.stringContaining("lacks verified plugin provenance")]), + }); + expect(note).not.toHaveBeenCalled(); + }); + + it("blocks an orphan sandbox when the requested target is custom OpenClaw (#6108)", () => { + process.env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE = "1"; + getLatestBackupSpy.mockReturnValue({ + agentType: "openclaw", + dir: "/sandbox/.openclaw", + backupPath: BACKUP_PATH, + openclawImagePluginInstalls: [], + } as unknown as ReturnType); + + const outcome = resolveNotReadyOutcome("orphan", note, null, true); + + expect(outcome.kind).toBe("blocked"); + expect(outcome).toMatchObject({ + hints: expect.arrayContaining([ + expect.stringContaining("new sandbox name"), + expect.stringContaining("NEMOCLAW_RECREATE_WITHOUT_BACKUP=1"), + ]), + }); + expect(note).not.toHaveBeenCalled(); + }); }); describe("installerRestoreOnRecreateFromEnv", () => { diff --git a/src/lib/onboard/not-ready-recreate.ts b/src/lib/onboard/not-ready-recreate.ts index e45e5c6f90..580c946498 100644 --- a/src/lib/onboard/not-ready-recreate.ts +++ b/src/lib/onboard/not-ready-recreate.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import type { SandboxEntry } from "../state/registry"; import * as sandboxState from "../state/sandbox"; export interface NotReadyRecreateInput { @@ -52,6 +53,47 @@ export class NotReadySandboxError extends Error { } } +export class UnsafeCustomImagePluginBackupError extends Error { + readonly hints: readonly string[]; + + constructor(sandboxName: string, backupPath: string | null) { + super(`Custom-image backup for '${sandboxName}' lacks verified OpenClaw plugin provenance.`); + this.name = "UnsafeCustomImagePluginBackupError"; + this.hints = [ + ` The pre-upgrade backup for custom OpenClaw sandbox '${sandboxName}' lacks verified plugin provenance.`, + " Automatic recreation is blocked before delete so image-owned plugins cannot be restored as user state.", + " The sandbox and backup are untouched — no data was lost.", + backupPath + ? ` Recover manually from: ${backupPath}` + : " No valid pre-upgrade backup was found; inspect the existing sandbox before manual recovery.", + " To preserve state, onboard the custom image under a new sandbox name and manually migrate only user-owned state.", + " Or, after taking an independent manual backup, explicitly accept destructive same-name recreation with NEMOCLAW_RECREATE_WITHOUT_BACKUP=1.", + ]; + } +} + +function assertNotReadyBackupPluginProvenance( + sandboxName: string, + backup: sandboxState.SnapshotEntry | null, + entry: SandboxEntry | null, + requireOpenClawImagePluginProvenance: boolean, +): void { + const customOpenClaw = + requireOpenClawImagePluginProvenance || + (Boolean(entry?.fromDockerfile) && (!entry?.agent || entry.agent === "openclaw")); + if (!backup) { + if (customOpenClaw) throw new UnsafeCustomImagePluginBackupError(sandboxName, null); + return; + } + const markedCustomImageBackup = backup.reconcileOpenClawImagePluginProvenance === true; + if ( + (customOpenClaw || markedCustomImageBackup) && + !sandboxState.hasAuthoritativeOpenClawImagePluginProvenance(backup) + ) { + throw new UnsafeCustomImagePluginBackupError(sandboxName, backup.backupPath); + } +} + export function installerRestoreOnRecreateFromEnv(env: NodeJS.ProcessEnv): boolean { return env.NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE === "1"; } @@ -59,6 +101,8 @@ export function installerRestoreOnRecreateFromEnv(env: NodeJS.ProcessEnv): boole export interface PreUpgradeBackupSelectInput { liveExists: boolean; hasExistingRegistryEntry: boolean; + existingSandboxEntry?: SandboxEntry | null; + requireOpenClawImagePluginProvenance?: boolean; sandboxName: string; note: (message: string) => void; } @@ -102,6 +146,12 @@ export function selectPreUpgradeBackupForCreate(input: PreUpgradeBackupSelectInp return null; } const latest = sandboxState.getLatestBackup(input.sandboxName); + assertNotReadyBackupPluginProvenance( + input.sandboxName, + latest, + input.existingSandboxEntry ?? null, + input.requireOpenClawImagePluginProvenance === true, + ); if (latest?.backupPath) { input.note( ` Found pre-upgrade backup for '${input.sandboxName}'; it will be restored after recreation.`, @@ -123,9 +173,19 @@ export function selectPreUpgradeBackupForCreate(input: PreUpgradeBackupSelectInp export function applyNonInteractiveNotReadyDecision( sandboxName: string, note: (message: string) => void, + existingSandboxEntry: SandboxEntry | null = null, + requireOpenClawImagePluginProvenance = false, ): string | null { const installerRestoreOnRecreate = installerRestoreOnRecreateFromEnv(process.env); const latest = installerRestoreOnRecreate ? sandboxState.getLatestBackup(sandboxName) : null; + if (installerRestoreOnRecreate) { + assertNotReadyBackupPluginProvenance( + sandboxName, + latest, + existingSandboxEntry, + requireOpenClawImagePluginProvenance, + ); + } const decision = decideNonInteractiveNotReadyAction({ sandboxName, installerRestoreOnRecreate, @@ -156,14 +216,25 @@ export type NonInteractiveNotReadyOutcome = export function resolveNotReadyOutcome( sandboxName: string, note: (message: string) => void, + existingSandboxEntry: SandboxEntry | null = null, + requireOpenClawImagePluginProvenance = false, ): NonInteractiveNotReadyOutcome { try { return { kind: "proceed", - restoreBackupPath: applyNonInteractiveNotReadyDecision(sandboxName, note), + restoreBackupPath: applyNonInteractiveNotReadyDecision( + sandboxName, + note, + existingSandboxEntry, + requireOpenClawImagePluginProvenance, + ), }; } catch (error) { - if (!(error instanceof NotReadySandboxError)) throw error; + if ( + !(error instanceof NotReadySandboxError) && + !(error instanceof UnsafeCustomImagePluginBackupError) + ) + throw error; return { kind: "blocked", hints: error.hints }; } } diff --git a/src/lib/onboard/sandbox-backup-on-recreate.test.ts b/src/lib/onboard/sandbox-backup-on-recreate.test.ts index 6442d16985..060e944383 100644 --- a/src/lib/onboard/sandbox-backup-on-recreate.test.ts +++ b/src/lib/onboard/sandbox-backup-on-recreate.test.ts @@ -41,6 +41,46 @@ describe("backupSandboxBeforeRecreate", () => { expect(log).toHaveBeenCalledWith(expect.stringContaining("State backed up")); }); + it("rejects an unmarked custom OpenClaw backup before recreate deletion (#6108)", () => { + const errorLog = vi.fn(); + const result = backupSandboxBeforeRecreate({ + sandboxName: "my-assistant", + sandboxEntry: { + name: "my-assistant", + agent: "openclaw", + fromDockerfile: "/tmp/Dockerfile.custom", + }, + backupImpl: () => makeBackup(), + log: vi.fn(), + errorLog, + }); + + expect(result.ok).toBe(false); + expect(result.failureKind).toBe("plugin-provenance"); + expect(errorLog).toHaveBeenCalledWith( + expect.stringContaining("aborting recreate before delete"), + ); + }); + + it("rejects an unmarked backup for an orphan custom OpenClaw target (#6108)", () => { + const errorLog = vi.fn(); + const result = backupSandboxBeforeRecreate({ + sandboxName: "orphan", + sandboxEntry: null, + requireOpenClawImagePluginProvenance: true, + backupImpl: () => makeBackup(), + log: vi.fn(), + errorLog, + }); + + expect(result.ok).toBe(false); + expect(result.failureKind).toBe("plugin-provenance"); + expect(errorLog).toHaveBeenCalledWith(expect.stringContaining("new name")); + expect(errorLog).toHaveBeenCalledWith( + expect.stringContaining("NEMOCLAW_RECREATE_WITHOUT_BACKUP=1"), + ); + }); + it("returns ok:false with failureKind=partial when some entries failed", () => { const backup = makeBackup({ success: false, diff --git a/src/lib/onboard/sandbox-backup-on-recreate.ts b/src/lib/onboard/sandbox-backup-on-recreate.ts index d475f4a3bc..bbbfda4dd9 100644 --- a/src/lib/onboard/sandbox-backup-on-recreate.ts +++ b/src/lib/onboard/sandbox-backup-on-recreate.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import type { SandboxEntry } from "../state/registry"; import type { BackupResult } from "../state/sandbox"; import * as sandboxState from "../state/sandbox"; @@ -8,12 +9,19 @@ export type SandboxBackupImpl = (sandboxName: string) => BackupResult; export interface PreRecreateBackupOptions { sandboxName: string; + sandboxEntry?: SandboxEntry | null; + requireOpenClawImagePluginProvenance?: boolean; backupImpl?: SandboxBackupImpl; log?: (msg: string) => void; errorLog?: (msg: string) => void; } -export type PreRecreateBackupFailureKind = "none" | "partial" | "empty" | "threw"; +export type PreRecreateBackupFailureKind = + | "none" + | "partial" + | "empty" + | "threw" + | "plugin-provenance"; export interface PreRecreateBackupResult { ok: boolean; @@ -28,9 +36,29 @@ export function backupSandboxBeforeRecreate( const log = opts.log ?? ((m: string) => console.log(m)); const errorLog = opts.errorLog ?? ((m: string) => console.error(m)); const backupImpl = opts.backupImpl ?? sandboxState.backupSandboxState; + const sandboxEntry = opts.sandboxEntry ?? null; + const customOpenClaw = + opts.requireOpenClawImagePluginProvenance === true || + (Boolean(sandboxEntry?.fromDockerfile) && + (!sandboxEntry?.agent || sandboxEntry.agent === "openclaw")); try { const backup = backupImpl(opts.sandboxName); if (backup.success && backup.manifest?.backupPath) { + if ( + (customOpenClaw || backup.manifest.reconcileOpenClawImagePluginProvenance === true) && + !sandboxState.hasAuthoritativeOpenClawImagePluginProvenance(backup.manifest) + ) { + errorLog( + " Custom-image OpenClaw plugin provenance is missing; aborting recreate before delete.", + ); + errorLog( + " Keep the sandbox and backup untouched; onboard under a new name and manually migrate user-owned state.", + ); + errorLog( + " Or take an independent manual backup, then explicitly accept destructive recreation with NEMOCLAW_RECREATE_WITHOUT_BACKUP=1.", + ); + return { ok: false, backup, failureKind: "plugin-provenance" }; + } log( ` ✓ State backed up (${backup.backedUpDirs.length} directories, ${backup.backedUpFiles.length} files)`, ); diff --git a/src/lib/onboard/sandbox-registration.test.ts b/src/lib/onboard/sandbox-registration.test.ts index 4d56e25afd..1851b372be 100644 --- a/src/lib/onboard/sandbox-registration.test.ts +++ b/src/lib/onboard/sandbox-registration.test.ts @@ -27,7 +27,11 @@ describe("buildCreatedSandboxRegistryEntry", () => { plan: { sandboxName: "demo" }, }; const openclawImagePluginInstalls = [ - { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather" }, + { + id: "weather", + installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: ["/opt/weather-plugin"], + }, ]; const entry = buildCreatedSandboxRegistryEntry({ @@ -92,6 +96,9 @@ describe("buildCreatedSandboxRegistryEntry", () => { expect(entry.nemoclawVersion).toBeTruthy(); expect(entry.openclawImagePluginInstalls).not.toBe(openclawImagePluginInstalls); expect(entry.openclawImagePluginInstalls?.[0]).not.toBe(openclawImagePluginInstalls[0]); + expect(entry.openclawImagePluginInstalls?.[0]?.loadPaths).not.toBe( + openclawImagePluginInstalls[0]?.loadPaths, + ); expect(entry.messaging).toBe(plannedMessagingState); const rawEntry = entry as unknown as Record; expect(rawEntry.messagingChannels).toBeUndefined(); diff --git a/src/lib/onboard/sandbox-registration.ts b/src/lib/onboard/sandbox-registration.ts index cdd4b33759..b3760d37f6 100644 --- a/src/lib/onboard/sandbox-registration.ts +++ b/src/lib/onboard/sandbox-registration.ts @@ -117,6 +117,7 @@ export function buildCreatedSandboxRegistryEntry( ? { openclawImagePluginInstalls: input.openclawImagePluginInstalls.map((install) => ({ ...install, + ...(install.loadPaths !== undefined ? { loadPaths: [...install.loadPaths] } : {}), })), } : {}), diff --git a/src/lib/state/openclaw-config-merge.test.ts b/src/lib/state/openclaw-config-merge.test.ts index 4aee0c686c..e6710ec4e2 100644 --- a/src/lib/state/openclaw-config-merge.test.ts +++ b/src/lib/state/openclaw-config-merge.test.ts @@ -17,6 +17,10 @@ function pluginConfig( return { plugins: { entries, installs, load: { paths } } }; } +function imageInstall(id: string, installPath: string, loadPaths: string[] = []) { + return { id, installPath, loadPaths }; +} + describe("mergeOpenClawRestoredConfig", () => { it("keeps rebuilt runtime-owned config while restoring durable backup-only settings", () => { const merged = mergeOpenClawRestoredConfig( @@ -398,26 +402,47 @@ describe("mergeOpenClawRestoredConfig", () => { expect(merged.plugins.entries.customPlugin).toEqual({ enabled: true }); }); - it("does not resurrect an image plugin removed by the fresh image", () => { + it("removes all prior image-owned config while preserving user-owned plugin state", () => { const merged = mergeOpenClawRestoredConfig( - pluginConfig( - { weather: { enabled: true, config: { revision: "v1" } } }, - { weather: { installPath: WEATHER_V1_PATH } }, - [WEATHER_V1_PATH], - ), - pluginConfig({}, {}, []), - { previousImagePluginInstalls: [{ id: "weather", installPath: WEATHER_V1_PATH }] }, - ) as { - plugins: { - entries: Record; - installs: Record; - load?: { paths?: string[] }; - }; - }; + { + channels: { + weather: { token: "stale-image-channel" }, + "user-channel": { room: "keep" }, + }, + plugins: { + allow: ["weather", "user-plugin"], + deny: ["weather", "user-denied"], + entries: { + weather: { enabled: true, config: { revision: "v1" } }, + "user-plugin": { enabled: true }, + }, + installs: { + weather: { installPath: WEATHER_V1_PATH }, + "user-plugin": { installPath: USER_PLUGIN_PATH }, + }, + load: { paths: [WEATHER_V1_PATH, USER_PLUGIN_PATH] }, + slots: { memory: "weather", contextEngine: "user-plugin" }, + }, + }, + { channels: {}, plugins: { allow: [], deny: [], entries: {}, load: { paths: [] } } }, + { + freshImagePluginInstalls: [], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH, [WEATHER_V1_PATH])], + }, + ); - expect(merged.plugins.entries.weather).toBeUndefined(); - expect(merged.plugins.installs.weather).toBeUndefined(); - expect(merged.plugins.load).toBeUndefined(); + expect(merged).toMatchObject({ + channels: { "user-channel": { room: "keep" } }, + plugins: { + allow: ["user-plugin"], + deny: ["user-denied"], + entries: { "user-plugin": { enabled: true } }, + load: { paths: [USER_PLUGIN_PATH] }, + slots: { contextEngine: "user-plugin" }, + }, + }); + expect((merged as { channels: Record }).channels.weather).toBeUndefined(); + expect((merged as { plugins: Record }).plugins.installs).toBeUndefined(); }); it("keeps only the fresh plugin when an image plugin is renamed", () => { @@ -432,12 +457,14 @@ describe("mergeOpenClawRestoredConfig", () => { { "weather-v2": { installPath: WEATHER_V2_PATH } }, [WEATHER_V2_PATH], ), - { previousImagePluginInstalls: [{ id: "weather", installPath: WEATHER_V1_PATH }] }, + { + freshImagePluginInstalls: [imageInstall("weather-v2", WEATHER_V2_PATH, [WEATHER_V2_PATH])], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH, [WEATHER_V1_PATH])], + }, ) as { plugins: Record }; expect(merged.plugins).toEqual({ entries: { "weather-v2": { enabled: true, config: { revision: "v2" } } }, - installs: { "weather-v2": { installPath: WEATHER_V2_PATH } }, load: { paths: [WEATHER_V2_PATH] }, }); }); @@ -456,23 +483,168 @@ describe("mergeOpenClawRestoredConfig", () => { [WEATHER_V1_PATH, USER_PLUGIN_PATH], ), pluginConfig({}, {}, []), - { previousImagePluginInstalls: [{ id: "weather", installPath: WEATHER_V1_PATH }] }, + { + freshImagePluginInstalls: [], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH, [WEATHER_V1_PATH])], + }, ) as { plugins: Record }; expect(merged.plugins).toEqual({ entries: { "user-plugin": { enabled: true, config: { owner: "user" } } }, - installs: { "user-plugin": { installPath: USER_PLUGIN_PATH } }, load: { paths: [USER_PLUGIN_PATH] }, }); }); - it("keeps legacy backup-only plugins when no image provenance was recorded", () => { + it("does not treat a non-linked install path as an owned load path", () => { + const merged = mergeOpenClawRestoredConfig( + pluginConfig({ weather: { enabled: true } }, { weather: { installPath: WEATHER_V1_PATH } }, [ + WEATHER_V1_PATH, + ]), + pluginConfig({}, {}, []), + { + freshImagePluginInstalls: [], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH)], + }, + ) as { plugins: Record }; + + expect(merged.plugins).toEqual({ entries: {}, load: { paths: [WEATHER_V1_PATH] } }); + }); + + it("preserves retained allow and deny state when the fresh image omits those lists", () => { + const merged = mergeOpenClawRestoredConfig( + { + channels: { weather: { token: "stale" } }, + plugins: { + allow: ["weather", "user-plugin"], + deny: ["weather", "user-denied"], + entries: { weather: { enabled: false }, "user-plugin": { enabled: true } }, + slots: { memory: "weather" }, + }, + }, + { channels: {}, plugins: { entries: { weather: { enabled: true } } } }, + { + freshImagePluginInstalls: [imageInstall("weather", WEATHER_V2_PATH)], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH)], + }, + ) as { channels: Record; plugins: Record }; + + expect(merged.channels.weather).toBeUndefined(); + expect(merged.plugins).toMatchObject({ + allow: ["weather", "user-plugin"], + deny: ["weather", "user-denied"], + entries: { weather: { enabled: true }, "user-plugin": { enabled: true } }, + }); + expect(merged.plugins.slots).toBeUndefined(); + }); + + it("lets an explicit fresh allowlist own prior image IDs while retaining user IDs", () => { + const merged = mergeOpenClawRestoredConfig( + { plugins: { allow: ["weather", "user-plugin"], entries: {} } }, + { plugins: { allow: ["weather"], entries: { weather: { enabled: true } } } }, + { + freshImagePluginInstalls: [imageInstall("weather", WEATHER_V2_PATH)], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH)], + }, + ) as { plugins: Record }; + + expect(merged.plugins.allow).toEqual(["weather", "user-plugin"]); + }); + + it("lets a fresh allowlist override stale backup deny entries", () => { + const merged = mergeOpenClawRestoredConfig( + { plugins: { deny: ["weather", "user-denied"], entries: {} } }, + { plugins: { allow: ["weather"], entries: { weather: { enabled: true } } } }, + { + freshImagePluginInstalls: [imageInstall("weather", WEATHER_V2_PATH)], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH)], + }, + ) as { plugins: Record }; + + expect(merged.plugins.allow).toEqual(["weather"]); + expect(merged.plugins.deny).toEqual(["user-denied"]); + }); + + it("lets a fresh denylist override stale backup allow entries", () => { + const merged = mergeOpenClawRestoredConfig( + { plugins: { allow: ["weather", "user-plugin"], entries: {} } }, + { plugins: { deny: ["weather"], entries: { weather: { enabled: true } } } }, + { + freshImagePluginInstalls: [imageInstall("weather", WEATHER_V2_PATH)], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH)], + }, + ) as { plugins: Record }; + + expect(merged.plugins.allow).toEqual(["user-plugin"]); + expect(merged.plugins.deny).toEqual(["weather"]); + }); + + it("lets an explicit fresh denylist own prior image IDs while retaining user IDs", () => { + const merged = mergeOpenClawRestoredConfig( + { plugins: { deny: ["weather", "user-denied"], entries: {} } }, + { plugins: { deny: [], entries: { weather: { enabled: true } } } }, + { + freshImagePluginInstalls: [imageInstall("weather", WEATHER_V2_PATH)], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH)], + }, + ) as { plugins: Record }; + + expect(merged.plugins.deny).toEqual(["user-denied"]); + }); + + it("does not merge stale same-ID channel fallback into the fresh image channel", () => { + const merged = mergeOpenClawRestoredConfig( + { + channels: { weather: { enabled: false, token: "stale" } }, + plugins: { entries: { weather: { enabled: false } } }, + }, + { + channels: { weather: { enabled: true } }, + plugins: { entries: { weather: { enabled: true } } }, + }, + { + freshImagePluginInstalls: [imageInstall("weather", WEATHER_V2_PATH)], + previousImagePluginInstalls: [imageInstall("weather", WEATHER_V1_PATH)], + }, + ) as { channels: Record }; + + expect(merged.channels.weather).toEqual({ enabled: true }); + }); + + it("keeps legacy backup-only plugins but drops transient install records", () => { const backup = pluginConfig( { weather: { enabled: true, config: { revision: "v1" } } }, { weather: { installPath: WEATHER_V1_PATH } }, [WEATHER_V1_PATH], ); - expect(mergeOpenClawRestoredConfig(backup, pluginConfig({}, {}, []))).toMatchObject(backup); + expect(mergeOpenClawRestoredConfig(backup, pluginConfig({}, {}, []))).toEqual({ + plugins: { + entries: { weather: { enabled: true, config: { revision: "v1" } } }, + load: { paths: [WEATHER_V1_PATH] }, + }, + }); + }); + + it("fails closed when reconciliation receives incomplete or one-sided provenance", () => { + expect(() => + mergeOpenClawRestoredConfig(pluginConfig({}, {}, []), pluginConfig({}, {}, []), { + previousImagePluginInstalls: [ + { id: "weather", installPath: WEATHER_V1_PATH, loadPaths: undefined }, + ], + }), + ).toThrow("Complete previous and fresh OpenClaw image plugin provenance is required"); + expect(() => + mergeOpenClawRestoredConfig(pluginConfig({}, {}, []), pluginConfig({}, {}, []), { + freshImagePluginInstalls: [], + }), + ).toThrow("Complete previous and fresh OpenClaw image plugin provenance is required"); + expect(() => + mergeOpenClawRestoredConfig(pluginConfig({}, {}, []), pluginConfig({}, {}, []), { + freshImagePluginInstalls: [], + previousImagePluginInstalls: [ + { id: "weather", installPath: WEATHER_V1_PATH, loadPaths: undefined }, + ], + }), + ).toThrow("missing explicit load paths"); }); }); diff --git a/src/lib/state/openclaw-config-merge.ts b/src/lib/state/openclaw-config-merge.ts index d3357416f3..4155ddca4d 100644 --- a/src/lib/state/openclaw-config-merge.ts +++ b/src/lib/state/openclaw-config-merge.ts @@ -74,7 +74,11 @@ function mergeJsonObjects( return merged; } -function mergeOpenClawChannels(backupChannels: unknown, currentChannels: unknown): unknown { +function mergeOpenClawChannels( + backupChannels: unknown, + currentChannels: unknown, + previousImagePluginIds?: ReadonlySet, +): unknown { if (!isPlainJsonObject(backupChannels)) return cloneJson(currentChannels); const merged: Record = isPlainJsonObject(currentChannels) @@ -90,7 +94,7 @@ function mergeOpenClawChannels(backupChannels: unknown, currentChannels: unknown continue; } - if (MANAGED_OPENCLAW_CHANNELS.has(key)) { + if (MANAGED_OPENCLAW_CHANNELS.has(key) || previousImagePluginIds?.has(key)) { // Freshly generated channel blocks carry current OpenShell placeholder // revisions and current start/stop/add/remove state. Never resurrect a // managed channel that the fresh config omitted, and never overwrite a @@ -135,13 +139,8 @@ function mergeOpenClawEntryMap( function mergeOpenClawPluginLoad( backupLoad: unknown, currentLoad: unknown, - previousImagePluginInstallPaths?: ReadonlySet, -): unknown { - if (!previousImagePluginInstallPaths) { - if (!isPlainJsonObject(backupLoad)) return cloneJson(currentLoad); - if (!isPlainJsonObject(currentLoad)) return cloneJson(backupLoad); - return mergeJsonObjects(currentLoad, backupLoad); - } + previousImagePluginLoadPaths?: ReadonlySet, +): Record | undefined { const backup = isPlainJsonObject(backupLoad) ? backupLoad : {}; const current = isPlainJsonObject(currentLoad) ? currentLoad : {}; const merged = mergeJsonObjects(current, backup); @@ -151,13 +150,84 @@ function mergeOpenClawPluginLoad( const backupPaths = Array.isArray(backup.paths) ? backup.paths.filter( (entry): entry is string => - typeof entry === "string" && !previousImagePluginInstallPaths.has(entry), + typeof entry === "string" && !previousImagePluginLoadPaths?.has(entry), ) : []; const paths = [...new Set([...currentPaths, ...backupPaths])]; - if (paths.length > 0) merged.paths = paths; + if (Array.isArray(current.paths) || Array.isArray(backup.paths)) merged.paths = paths; else delete merged.paths; - return merged; + return Object.keys(merged).length > 0 ? merged : undefined; +} + +function stringArray(value: unknown): string[] | undefined { + return Array.isArray(value) + ? value.filter((entry): entry is string => typeof entry === "string") + : undefined; +} + +function mergeOpenClawPluginIdList( + backupValue: unknown, + currentValue: unknown, + previousImagePluginIds?: ReadonlySet, + currentOppositeIds: ReadonlySet = new Set(), +): string[] | undefined { + const backup = stringArray(backupValue); + const current = stringArray(currentValue); + if (!backup && !current) return undefined; + return [ + ...new Set([ + ...(current ?? []), + ...(backup ?? []).filter( + (id) => !previousImagePluginIds?.has(id) && !currentOppositeIds.has(id), + ), + ]), + ]; +} + +function mergeOpenClawPluginSlots( + backupSlots: unknown, + currentSlots: unknown, + previousImagePluginIds?: ReadonlySet, +): Record | undefined { + if (!isPlainJsonObject(backupSlots) && !isPlainJsonObject(currentSlots)) return undefined; + const merged: Record = {}; + if (isPlainJsonObject(backupSlots)) { + for (const [key, value] of Object.entries(backupSlots)) { + if (typeof value === "string" && previousImagePluginIds?.has(value)) continue; + merged[key] = cloneJson(value); + } + } + if (isPlainJsonObject(currentSlots)) Object.assign(merged, cloneJson(currentSlots)); + return Object.keys(merged).length > 0 ? merged : undefined; +} + +type OpenClawImagePluginOwnership = { + ids?: ReadonlySet; + loadPaths?: ReadonlySet; +}; + +function imagePluginOwnership( + installs?: readonly OpenClawImagePluginInstall[], +): OpenClawImagePluginOwnership { + if (installs === undefined) return {}; + const ids = new Set(); + const loadPaths = new Set(); + for (const install of installs) { + if (!Array.isArray(install.loadPaths)) { + throw new Error("OpenClaw image plugin provenance is missing explicit load paths"); + } + ids.add(install.id); + for (const loadPath of install.loadPaths) loadPaths.add(loadPath); + } + return { ids, loadPaths }; +} + +function removedImagePluginIds( + previous: OpenClawImagePluginOwnership, + fresh: OpenClawImagePluginOwnership, +): ReadonlySet | undefined { + if (!previous.ids) return undefined; + return new Set([...previous.ids].filter((id) => !fresh.ids?.has(id))); } function mergeOpenClawTools(backupTools: unknown, currentTools: unknown): unknown { @@ -305,43 +375,54 @@ function mergeOpenClawModels(backupModels: unknown, currentModels: unknown): unk function mergeOpenClawPlugins( backupPlugins: unknown, currentPlugins: unknown, - previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[], -): unknown { - if (!isPlainJsonObject(backupPlugins)) return cloneJson(currentPlugins); + previousOwnership: OpenClawImagePluginOwnership, + freshOwnership: OpenClawImagePluginOwnership, +): Record | undefined { + if (!isPlainJsonObject(backupPlugins) && !isPlainJsonObject(currentPlugins)) return undefined; + const backup = isPlainJsonObject(backupPlugins) ? backupPlugins : {}; const current = isPlainJsonObject(currentPlugins) ? currentPlugins : {}; - const previousImagePluginIds = previousImagePluginInstalls - ? new Set(previousImagePluginInstalls.map((install) => install.id)) - : undefined; - const previousImagePluginInstallPaths = previousImagePluginInstalls - ? new Set(previousImagePluginInstalls.map((install) => install.installPath)) - : undefined; - - const merged = mergeJsonObjects(current, backupPlugins); - const entries = mergeOpenClawEntryMap( - backupPlugins.entries, - current.entries, - previousImagePluginIds, - ); + const merged = mergeJsonObjects(current, backup); + // OpenClaw injects install records only into transient command snapshots. Its + // durable ledger is separate, so never write this synthetic map to openclaw.json. + delete merged.installs; + const entries = mergeOpenClawEntryMap(backup.entries, current.entries, previousOwnership.ids); if (entries) merged.entries = entries; - const installs = mergeOpenClawEntryMap( - backupPlugins.installs, - current.installs, - previousImagePluginIds, + else delete merged.entries; + + const currentAllow = stringArray(current.allow); + const currentDeny = stringArray(current.deny); + const removedIds = removedImagePluginIds(previousOwnership, freshOwnership); + const backupAllowOwnedIds = currentAllow ? previousOwnership.ids : removedIds; + const backupDenyOwnedIds = currentDeny ? previousOwnership.ids : removedIds; + const allow = mergeOpenClawPluginIdList( + backup.allow, + current.allow, + backupAllowOwnedIds, + new Set(currentDeny ?? []), ); - if (installs) merged.installs = installs; - if (previousImagePluginInstalls !== undefined) { - const load = mergeOpenClawPluginLoad( - backupPlugins.load, - current.load, - previousImagePluginInstallPaths, - ); - if (isPlainJsonObject(load) && Object.keys(load).length === 0) delete merged.load; - else merged.load = load; - } - return merged; + if (allow && allow.length > 0) merged.allow = allow; + else delete merged.allow; + const deny = mergeOpenClawPluginIdList( + backup.deny, + current.deny, + backupDenyOwnedIds, + new Set(currentAllow ?? []), + ); + if (deny && deny.length > 0) merged.deny = deny; + else delete merged.deny; + + const slots = mergeOpenClawPluginSlots(backup.slots, current.slots, previousOwnership.ids); + if (slots) merged.slots = slots; + else delete merged.slots; + + const load = mergeOpenClawPluginLoad(backup.load, current.load, previousOwnership.loadPaths); + if (load) merged.load = load; + else delete merged.load; + return Object.keys(merged).length > 0 ? merged : undefined; } export interface OpenClawConfigMergeOptions { + freshImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; } @@ -350,9 +431,18 @@ export function mergeOpenClawRestoredConfig( currentConfig: unknown, options: OpenClawConfigMergeOptions = {}, ): unknown { - if (!isPlainJsonObject(backedUpConfig)) return cloneJson(currentConfig ?? backedUpConfig); - if (!isPlainJsonObject(currentConfig)) return cloneJson(backedUpConfig); + if (!isPlainJsonObject(backedUpConfig) || !isPlainJsonObject(currentConfig)) { + throw new Error("OpenClaw selective config merge requires JSON objects"); + } + if ( + (options.previousImagePluginInstalls === undefined) !== + (options.freshImagePluginInstalls === undefined) + ) { + throw new Error("Complete previous and fresh OpenClaw image plugin provenance is required"); + } + const previousOwnership = imagePluginOwnership(options.previousImagePluginInstalls); + const freshOwnership = imagePluginOwnership(options.freshImagePluginInstalls); const merged = mergeJsonObjects(currentConfig, backedUpConfig); for (const key of OPENCLAW_CONFIG_RESTORE_OWNERSHIP.runtimeSections) { @@ -360,12 +450,17 @@ export function mergeOpenClawRestoredConfig( else delete merged[key]; } - merged.channels = mergeOpenClawChannels(backedUpConfig.channels, currentConfig.channels); + merged.channels = mergeOpenClawChannels( + backedUpConfig.channels, + currentConfig.channels, + previousOwnership.ids, + ); merged.models = mergeOpenClawModels(backedUpConfig.models, currentConfig.models); merged.plugins = mergeOpenClawPlugins( backedUpConfig.plugins, currentConfig.plugins, - options.previousImagePluginInstalls, + previousOwnership, + freshOwnership, ); merged.tools = mergeOpenClawTools(backedUpConfig.tools, currentConfig.tools); diff --git a/src/lib/state/openclaw-config-restore-input.test.ts b/src/lib/state/openclaw-config-restore-input.test.ts index dbe6493f34..910fa16b72 100644 --- a/src/lib/state/openclaw-config-restore-input.test.ts +++ b/src/lib/state/openclaw-config-restore-input.test.ts @@ -74,4 +74,52 @@ describe("buildOpenClawConfigRestoreInput", () => { expect(result.error).toContain("refusing unsafe wholesale backup restore"); } }); + + it("reconciles complete plugin provenance without persisting transient installs", () => { + const result = buildOpenClawConfigRestoreInput( + bufferJson({ + plugins: { + entries: { weather: { enabled: true } }, + installs: { weather: { installPath: "/sandbox/.openclaw/extensions/weather" } }, + }, + }), + bufferJson({ plugins: { entries: {} } }), + { + freshImagePluginInstalls: [], + previousImagePluginInstalls: [ + { + id: "weather", + installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: [], + }, + ], + }, + ); + + expect(result.ok).toBe(true); + expect(result.ok ? JSON.parse(result.input.toString("utf8")) : null).toEqual({ + plugins: { entries: {} }, + }); + }); + + it("fails closed on incomplete plugin provenance", () => { + const result = buildOpenClawConfigRestoreInput( + bufferJson({ plugins: { entries: {} } }), + bufferJson({ plugins: { entries: {} } }), + { + freshImagePluginInstalls: [], + previousImagePluginInstalls: [ + { + id: "weather", + installPath: "/sandbox/.openclaw/extensions/weather", + }, + ], + }, + ); + + expect(result).toMatchObject({ + ok: false, + error: expect.stringContaining("missing explicit load paths"), + }); + }); }); diff --git a/src/lib/state/openclaw-config-restore-input.ts b/src/lib/state/openclaw-config-restore-input.ts index 4dd3b278d5..00b8439176 100644 --- a/src/lib/state/openclaw-config-restore-input.ts +++ b/src/lib/state/openclaw-config-restore-input.ts @@ -8,7 +8,10 @@ import { mergeOpenClawRestoredConfig, type OpenClawConfigMergeOptions, } from "./openclaw-config-merge.js"; -import type { OpenClawImagePluginInstall } from "./openclaw-plugin-restore.js"; +import { + hasCompleteOpenClawImagePluginProvenance, + type OpenClawImagePluginInstall, +} from "./openclaw-plugin-restore.js"; export interface OpenClawConfigStateFileSpec { path: string; @@ -50,6 +53,7 @@ export type OpenClawConfigRestoreInputResult = export interface OpenClawConfigRestoreFromSandboxOptions { backupContents: Buffer; dir: string; + freshImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; log?: (message: string) => void; previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[]; specPath: string; @@ -120,14 +124,33 @@ export function buildOpenClawConfigRestoreInput( export function buildOpenClawConfigRestoreInputFromSandbox({ backupContents, dir, + freshImagePluginInstalls, log = () => {}, previousImagePluginInstalls, specPath, sshArgs, }: OpenClawConfigRestoreFromSandboxOptions): OpenClawConfigRestoreInputResult { + if ((previousImagePluginInstalls === undefined) !== (freshImagePluginInstalls === undefined)) { + return { + ok: false, + error: "Complete previous and fresh OpenClaw image plugin provenance is required", + }; + } + if ( + freshImagePluginInstalls !== undefined && + !hasCompleteOpenClawImagePluginProvenance(freshImagePluginInstalls, dir) + ) { + return { ok: false, error: "Fresh OpenClaw image plugin provenance is incomplete" }; + } + if ( + previousImagePluginInstalls !== undefined && + !hasCompleteOpenClawImagePluginProvenance(previousImagePluginInstalls, dir) + ) { + return { ok: false, error: "OpenClaw image plugin provenance is incomplete" }; + } return buildOpenClawConfigRestoreInput( backupContents, readCurrentOpenClawConfig(sshArgs, dir, specPath, log), - { previousImagePluginInstalls }, + { freshImagePluginInstalls, previousImagePluginInstalls }, ); } diff --git a/src/lib/state/openclaw-plugin-restore.test.ts b/src/lib/state/openclaw-plugin-restore.test.ts index a15f5bea55..3a087b1e6c 100644 --- a/src/lib/state/openclaw-plugin-restore.test.ts +++ b/src/lib/state/openclaw-plugin-restore.test.ts @@ -11,6 +11,7 @@ import { describe, expect, it } from "vitest"; import { OPENCLAW_IMAGE_MANAGED_EXTENSION_DIRS } from "./openclaw-managed-extensions"; import { buildFreshOpenClawPluginIndexSqliteReadCommand, + hasCompleteOpenClawImagePluginProvenance, parseFreshOpenClawPluginExtensionDirs, parseOpenClawImagePluginInstalls, planOpenClawPluginRestore, @@ -19,7 +20,18 @@ import { const OPENCLAW_DIR = "/sandbox/.openclaw"; function install(installPath: string): Record { - return { source: "path", installPath }; + return { source: "npm", installPath }; +} + +function pathInstall(installPath: string, sourcePath: string): Record { + return { source: "path", sourcePath, installPath }; +} + +function writeOpenClawConfig(root: string, loadPaths: string[] = []): void { + fs.writeFileSync( + path.join(root, "openclaw.json"), + JSON.stringify({ plugins: { load: { paths: loadPaths } } }), + ); } const CREATE_PLUGIN_INDEX_SQLITE_PY = [ @@ -44,13 +56,14 @@ describe("buildFreshOpenClawPluginIndexSqliteReadCommand", () => { const dbPath = path.join(root, "state", "openclaw.sqlite"); const records = { weather: install(`${OPENCLAW_DIR}/extensions/weather`) }; createPluginIndexDatabase(dbPath, records); + writeOpenClawConfig(root); const stdout = execFileSync( "bash", ["-c", buildFreshOpenClawPluginIndexSqliteReadCommand(root)], { encoding: "utf8" }, ); - expect(JSON.parse(stdout)).toEqual({ version: 1, installRecords: records }); + expect(JSON.parse(stdout)).toEqual({ version: 1, installRecords: records, loadPaths: [] }); } finally { fs.rmSync(root, { recursive: true, force: true }); } @@ -60,6 +73,7 @@ describe("buildFreshOpenClawPluginIndexSqliteReadCommand", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-plugin-index-")); try { createPluginIndexDatabase(path.join(root, "state", "openclaw.sqlite"), null); + writeOpenClawConfig(root); expect(() => execFileSync("bash", ["-c", buildFreshOpenClawPluginIndexSqliteReadCommand(root)]), ).toThrow(); @@ -96,6 +110,21 @@ describe("buildFreshOpenClawPluginIndexSqliteReadCommand", () => { fs.rmSync(root, { recursive: true, force: true }); } }); + + it("rejects a symlinked OpenClaw config used for load-path ownership", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-plugin-index-")); + try { + createPluginIndexDatabase(path.join(root, "state", "openclaw.sqlite"), {}); + fs.symlinkSync(path.join(root, "missing-openclaw.json"), path.join(root, "openclaw.json")); + const result = spawnSync("bash", [ + "-c", + buildFreshOpenClawPluginIndexSqliteReadCommand(root), + ]); + expect(result.status).toBe(10); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); }); describe("parseFreshOpenClawPluginExtensionDirs", () => { @@ -108,6 +137,7 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { weather: install(`${OPENCLAW_DIR}/extensions/weather`), nemoclaw: install(`${OPENCLAW_DIR}/extensions/nemoclaw`), }, + loadPaths: [], }, OPENCLAW_DIR, ), @@ -115,8 +145,8 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { ok: true, extensionDirs: ["nemoclaw", "weather"], pluginInstalls: [ - { id: "nemoclaw", installPath: `${OPENCLAW_DIR}/extensions/nemoclaw` }, - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { id: "nemoclaw", installPath: `${OPENCLAW_DIR}/extensions/nemoclaw`, loadPaths: [] }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, ], }); }); @@ -130,25 +160,36 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { "@scope/weather": install(`${OPENCLAW_DIR}/extensions/scope__weather`), diagnostics: install(`${OPENCLAW_DIR}/npm/node_modules/diagnostics`), "foo+bar": install(`${OPENCLAW_DIR}/extensions/foo+bar`), - "my plugin": install(`${OPENCLAW_DIR}/extensions/my plugin`), }, + loadPaths: [], }, OPENCLAW_DIR, ), ).toEqual({ ok: true, - extensionDirs: ["foo+bar", "my plugin", "scope__weather"], + extensionDirs: ["foo+bar", "scope__weather"], pluginInstalls: expect.arrayContaining([ - { id: "@scope/weather", installPath: `${OPENCLAW_DIR}/extensions/scope__weather` }, - { id: "diagnostics", installPath: `${OPENCLAW_DIR}/npm/node_modules/diagnostics` }, - { id: "foo+bar", installPath: `${OPENCLAW_DIR}/extensions/foo+bar` }, - { id: "my plugin", installPath: `${OPENCLAW_DIR}/extensions/my plugin` }, + { + id: "@scope/weather", + installPath: `${OPENCLAW_DIR}/extensions/scope__weather`, + loadPaths: [], + }, + { + id: "diagnostics", + installPath: `${OPENCLAW_DIR}/npm/node_modules/diagnostics`, + loadPaths: [], + }, + { id: "foo+bar", installPath: `${OPENCLAW_DIR}/extensions/foo+bar`, loadPaths: [] }, ]), }); }); it.each([ ["a traversal ID", { "../weather": install(`${OPENCLAW_DIR}/extensions/../weather`) }], + [ + "an ID containing whitespace", + { "my plugin": install(`${OPENCLAW_DIR}/extensions/my-plugin`) }, + ], ["a nested install path", { weather: install(`${OPENCLAW_DIR}/extensions/nested/weather`) }], ["a noncanonical install path", { weather: install(`${OPENCLAW_DIR}/extensions/../weather`) }], ["an oversized install path", { weather: install(`/${"a".repeat(4096)}`) }], @@ -160,7 +201,7 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { ["non-object metadata", { weather: "invalid" }], ])("rejects %s", (_label, installs) => { const result = parseFreshOpenClawPluginExtensionDirs( - { version: 1, installRecords: installs }, + { version: 1, installRecords: installs, loadPaths: [] }, OPENCLAW_DIR, ); expect(result).toEqual( @@ -179,7 +220,10 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { }), ); expect( - parseFreshOpenClawPluginExtensionDirs({ version: 1, installRecords: installs }, OPENCLAW_DIR), + parseFreshOpenClawPluginExtensionDirs( + { version: 1, installRecords: installs, loadPaths: [] }, + OPENCLAW_DIR, + ), ).toEqual({ ok: false, error: "fresh OpenClaw registry has too many plugin installs (129)", @@ -190,15 +234,71 @@ describe("parseFreshOpenClawPluginExtensionDirs", () => { const installPath = `/${Array.from({ length: 64 }, () => "a").join("/")}`; expect( parseFreshOpenClawPluginExtensionDirs( - { version: 1, installRecords: { weather: install(installPath) } }, + { version: 1, installRecords: { weather: install(installPath) }, loadPaths: [] }, OPENCLAW_DIR, ), ).toEqual({ ok: true, extensionDirs: [], - pluginInstalls: [{ id: "weather", installPath }], + pluginInstalls: [{ id: "weather", installPath, loadPaths: [] }], }); }); + + it("captures only configured load paths owned by linked path installs", () => { + const linkedPath = "/opt/weather-linked"; + expect( + parseFreshOpenClawPluginExtensionDirs( + { + version: 1, + installRecords: { + linked: pathInstall(linkedPath, linkedPath), + copied: pathInstall(`${OPENCLAW_DIR}/extensions/copied`, "/opt/copied"), + }, + loadPaths: [linkedPath, linkedPath, "./user-owned", "~/user-owned"], + }, + OPENCLAW_DIR, + ), + ).toEqual({ + ok: true, + extensionDirs: ["copied"], + pluginInstalls: [ + { id: "copied", installPath: `${OPENCLAW_DIR}/extensions/copied`, loadPaths: [] }, + { id: "linked", installPath: linkedPath, loadPaths: [linkedPath] }, + ], + }); + }); + + it.each([ + ["missing load paths", { version: 1, installRecords: {} }], + [ + "control character in configured load path", + { version: 1, installRecords: {}, loadPaths: ["~/plug\u0000in"] }, + ], + [ + "unsupported install source", + { + version: 1, + installRecords: { + weather: { source: "unknown", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + }, + loadPaths: [], + }, + ], + [ + "relative path-install source", + { + version: 1, + installRecords: { + weather: pathInstall(`${OPENCLAW_DIR}/extensions/weather`, "relative/weather"), + }, + loadPaths: [], + }, + ], + ])("rejects %s", (_label, index) => { + expect(parseFreshOpenClawPluginExtensionDirs(index, OPENCLAW_DIR)).toEqual( + expect.objectContaining({ ok: false }), + ); + }); }); describe("parseOpenClawImagePluginInstalls", () => { @@ -211,8 +311,12 @@ describe("parseOpenClawImagePluginInstalls", () => { expect( parseOpenClawImagePluginInstalls( [ - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, - { id: "npm-tool", installPath: `${OPENCLAW_DIR}/npm/node_modules/npm-tool` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, + { + id: "npm-tool", + installPath: `${OPENCLAW_DIR}/npm/node_modules/npm-tool`, + loadPaths: [], + }, ], OPENCLAW_DIR, ), @@ -220,34 +324,72 @@ describe("parseOpenClawImagePluginInstalls", () => { ok: true, extensionDirs: ["weather"], pluginInstalls: [ - { id: "npm-tool", installPath: `${OPENCLAW_DIR}/npm/node_modules/npm-tool` }, - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { + id: "npm-tool", + installPath: `${OPENCLAW_DIR}/npm/node_modules/npm-tool`, + loadPaths: [], + }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, ], }); }); it.each([ - ["unsafe ID", [{ id: "../weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }]], + [ + "unsafe ID", + [{ id: "../weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }], + ], + [ + "prototype-pollution ID", + [{ id: "__proto__", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }], + ], + [ + "constructor prototype-pollution ID", + [{ id: "constructor", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }], + ], + [ + "prototype key ID", + [{ id: "prototype", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }], + ], + [ + "control character in path", + [{ id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weath\u0000er`, loadPaths: [] }], + ], + [ + "missing explicit load paths", + [{ id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }], + ], [ "duplicate ID", [ - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/forecast` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/forecast`, loadPaths: [] }, ], ], [ "duplicate install path", [ - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, - { id: "forecast", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, + { id: "forecast", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, ], ], - ["relative path", [{ id: "weather", installPath: "extensions/weather" }]], + ["relative path", [{ id: "weather", installPath: "extensions/weather", loadPaths: [] }]], ])("rejects %s", (_label, provenance) => { expect(parseOpenClawImagePluginInstalls(provenance, OPENCLAW_DIR)).toEqual( expect.objectContaining({ ok: false }), ); }); + + it("distinguishes complete empty provenance from missing legacy provenance", () => { + expect(hasCompleteOpenClawImagePluginProvenance([], OPENCLAW_DIR)).toBe(true); + expect(hasCompleteOpenClawImagePluginProvenance(undefined, OPENCLAW_DIR)).toBe(false); + expect( + hasCompleteOpenClawImagePluginProvenance( + [{ id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }], + OPENCLAW_DIR, + ), + ).toBe(false); + }); }); describe("planOpenClawPluginRestore", () => { @@ -257,10 +399,10 @@ describe("planOpenClawPluginRestore", () => { dir: OPENCLAW_DIR, localDirs: ["extensions"], freshImagePluginInstalls: [ - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, ], previousImagePluginInstalls: [ - { id: "forecast", installPath: `${OPENCLAW_DIR}/extensions/forecast` }, + { id: "forecast", installPath: `${OPENCLAW_DIR}/extensions/forecast`, loadPaths: [] }, ], }); @@ -284,7 +426,7 @@ describe("planOpenClawPluginRestore", () => { dir: OPENCLAW_DIR, localDirs: ["workspace"], freshImagePluginInstalls: [ - { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather` }, + { id: "weather", installPath: `${OPENCLAW_DIR}/extensions/weather`, loadPaths: [] }, ], }), ).toEqual({ @@ -304,7 +446,9 @@ describe("planOpenClawPluginRestore", () => { dir: OPENCLAW_DIR, localDirs: ["extensions"], freshImagePluginInstalls: [], - previousImagePluginInstalls: [{ id: "weather", installPath: "extensions/weather" }], + previousImagePluginInstalls: [ + { id: "weather", installPath: "extensions/weather", loadPaths: [] }, + ], }), ).toEqual(expect.objectContaining({ ok: false })); }); diff --git a/src/lib/state/openclaw-plugin-restore.ts b/src/lib/state/openclaw-plugin-restore.ts index 6ade627131..2032d7dd19 100644 --- a/src/lib/state/openclaw-plugin-restore.ts +++ b/src/lib/state/openclaw-plugin-restore.ts @@ -13,6 +13,7 @@ import { } from "./openclaw-managed-extensions.js"; const MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS = 128; +const MAX_OPENCLAW_CONFIGURED_PLUGIN_LOAD_PATHS = 512; // The parser accepts at most 128 records with 4 KiB install paths, leaving // ample room for IDs and metadata while bounding sandbox-controlled output. const OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES = 1024 * 1024; @@ -20,6 +21,17 @@ const OPENCLAW_PLUGIN_INSTALL_REGISTRY_MAX_BYTES = 1024 * 1024; const MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH = 4096; const MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS = 64; const OPENCLAW_EXTENSION_GLOB_CHARS = ["/", "\\", "*", "?", "[", "]"] as const; +const OPENCLAW_PLUGIN_INSTALL_SOURCES = new Set([ + "archive", + "clawhub", + "git", + "marketplace", + "npm", + "path", +]); +const OPENCLAW_PLUGIN_ID_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._+~-]*$/; +const FORBIDDEN_OPENCLAW_PLUGIN_IDS = new Set(["__proto__", "constructor", "prototype"]); +const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/; export type OpenClawManagedExtensionDiscoveryResult = | { ok: true; extensionDirs: string[]; pluginInstalls: OpenClawImagePluginInstall[] } @@ -28,8 +40,14 @@ export type OpenClawManagedExtensionDiscoveryResult = export interface OpenClawImagePluginInstall { readonly id: string; readonly installPath: string; + /** Exact configured load paths owned by this image install; durable validation requires it. */ + readonly loadPaths?: readonly string[]; } +export type CompleteOpenClawImagePluginInstall = Omit & { + readonly loadPaths: readonly string[]; +}; + export interface OpenClawPluginDiscoveryDeps { getSshConfig(sandboxName: string): string | null; sshArgs(configFile: string, sandboxName: string): string[]; @@ -56,34 +74,61 @@ const OPENCLAW_PLUGIN_INDEX_SQLITE_PY = [ " row = conn.execute(\"SELECT install_records_json FROM installed_plugin_index WHERE index_key = 'installed-plugin-index'\").fetchone()", " if not row or not row[0]: raise SystemExit(12)", " records = json.loads(row[0])", - " print(json.dumps({'version': 1, 'installRecords': records}, separators=(',', ':')))", + " with open(sys.argv[2], 'r', encoding='utf-8') as config_file: config = json.load(config_file)", + " plugins = config.get('plugins') if isinstance(config, dict) else None", + " load = plugins.get('load') if isinstance(plugins, dict) else None", + " load_paths = load.get('paths', []) if isinstance(load, dict) else []", + " print(json.dumps({'version': 1, 'installRecords': records, 'loadPaths': load_paths}, separators=(',', ':')))", "finally:", " conn.close()", ].join("\n"); +const OPENCLAW_PLUGIN_INDEX_LEGACY_PY = [ + "import json, sys", + "with open(sys.argv[1], 'r', encoding='utf-8') as index_file: index = json.load(index_file)", + "with open(sys.argv[2], 'r', encoding='utf-8') as config_file: config = json.load(config_file)", + "plugins = config.get('plugins') if isinstance(config, dict) else None", + "load = plugins.get('load') if isinstance(plugins, dict) else None", + "load_paths = load.get('paths', []) if isinstance(load, dict) else []", + "if not isinstance(index, dict): raise SystemExit(12)", + "index['loadPaths'] = load_paths", + "print(json.dumps(index, separators=(',', ':')))", +].join("\n"); + +function buildSafeRegularFileReadGuard(variable: string, missingStatus: number): string[] { + return [ + `[ -e "$${variable}" ] || [ -L "$${variable}" ] || exit ${missingStatus}`, + `[ -f "$${variable}" ] && [ ! -L "$${variable}" ] || { echo "unsafe state file: $${variable}" >&2; exit 10; }`, + `${variable}_hardlinks="$(find "$${variable}" -maxdepth 0 -type f -links +1 -print 2>/dev/null | wc -l | tr -d " ")"`, + `[ "\${${variable}_hardlinks:-0}" = "0" ] || { echo "hard-linked state file rejected: $${variable}" >&2; exit 11; }`, + ]; +} + export function buildFreshOpenClawPluginIndexSqliteReadCommand(dir: string): string { const sqlitePath = `${dir.replace(/\/+$/, "")}/state/openclaw.sqlite`; + const configPath = `${dir.replace(/\/+$/, "")}/openclaw.json`; const quotedSqlitePath = shellQuote(sqlitePath); + const quotedConfigPath = shellQuote(configPath); return [ `db=${quotedSqlitePath}`, - '[ -e "$db" ] || [ -L "$db" ] || exit 2', - '[ -f "$db" ] && [ ! -L "$db" ] || { echo "unsafe OpenClaw state database: $db" >&2; exit 10; }', - 'hardlink_count="$(find "$db" -maxdepth 0 -type f -links +1 -print 2>/dev/null | wc -l | tr -d " ")"', - '[ "${hardlink_count:-0}" = "0" ] || { echo "hard-linked OpenClaw state database rejected: $db" >&2; exit 11; }', - `python3 -c ${shellQuote(OPENCLAW_PLUGIN_INDEX_SQLITE_PY)} "$db"`, + `cfg=${quotedConfigPath}`, + ...buildSafeRegularFileReadGuard("db", 2), + ...buildSafeRegularFileReadGuard("cfg", 12), + `python3 -c ${shellQuote(OPENCLAW_PLUGIN_INDEX_SQLITE_PY)} "$db" "$cfg"`, ].join("; "); } function buildLegacyOpenClawPluginIndexReadCommand(dir: string): string { const installIndexPath = `${dir.replace(/\/+$/, "")}/plugins/installs.json`; + const configPath = `${dir.replace(/\/+$/, "")}/openclaw.json`; const quotedInstallIndexPath = shellQuote(installIndexPath); + const quotedConfigPath = shellQuote(configPath); return [ `src=${quotedInstallIndexPath}`, - '[ ! -e "$src" ] && exit 2', - '[ -f "$src" ] && [ ! -L "$src" ] || { echo "unsafe state file: $src" >&2; exit 10; }', - 'hardlink_count="$(find "$src" -maxdepth 0 -type f -links +1 -print 2>/dev/null | wc -l | tr -d " ")"', - '[ "${hardlink_count:-0}" = "0" ] || { echo "hard-linked state file rejected: $src" >&2; exit 11; }', - 'cat -- "$src"', + `cfg=${quotedConfigPath}`, + ...buildSafeRegularFileReadGuard("src", 2), + ...buildSafeRegularFileReadGuard("cfg", 12), + `python3 -c ${shellQuote(OPENCLAW_PLUGIN_INDEX_LEGACY_PY)} "$src" "$cfg"`, ].join("; "); } @@ -168,10 +213,18 @@ export function discoverFreshOpenClawImagePluginInstalls( } function isSafeOpenClawPluginInstallId(id: string): boolean { - if (id.length === 0 || id.length > 256 || /[\u0000-\u001f\u007f]/.test(id)) return false; + if (id.length === 0 || id.length > 256 || FORBIDDEN_OPENCLAW_PLUGIN_IDS.has(id)) return false; const slash = id.indexOf("/"); - if (slash === -1) return true; - return id.startsWith("@") && slash > 1 && slash === id.lastIndexOf("/") && slash < id.length - 1; + if (slash === -1) return OPENCLAW_PLUGIN_ID_SEGMENT.test(id); + return ( + id.startsWith("@") && + slash > 1 && + slash === id.lastIndexOf("/") && + slash < id.length - 1 && + !FORBIDDEN_OPENCLAW_PLUGIN_IDS.has(id.slice(slash + 1)) && + OPENCLAW_PLUGIN_ID_SEGMENT.test(id.slice(1, slash)) && + OPENCLAW_PLUGIN_ID_SEGMENT.test(id.slice(slash + 1)) + ); } export function isSafeOpenClawExtensionDirName(name: string): boolean { @@ -185,21 +238,42 @@ export function isSafeOpenClawExtensionDirName(name: string): boolean { ); } -function validateOpenClawImagePluginInstall( +function validateCanonicalAbsolutePath(value: unknown): value is string { + return ( + typeof value === "string" && + value.length > 0 && + value.length <= MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH && + !CONTROL_CHARACTERS.test(value) && + value.split("/").filter(Boolean).length <= MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS && + path.posix.isAbsolute(value) && + path.posix.normalize(value) === value + ); +} + +function validateConfiguredLoadPath(value: unknown): value is string { + return ( + typeof value === "string" && + value.length > 0 && + value.length <= MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH && + !CONTROL_CHARACTERS.test(value) + ); +} + +function validateDurableOpenClawImagePluginInstall( id: unknown, installPath: unknown, -): OpenClawImagePluginInstall | null { + loadPaths: unknown, +): CompleteOpenClawImagePluginInstall | null { if (typeof id !== "string" || !isSafeOpenClawPluginInstallId(id)) return null; + if (!validateCanonicalAbsolutePath(installPath)) return null; if ( - typeof installPath !== "string" || - installPath.length > MAX_OPENCLAW_PLUGIN_INSTALL_PATH_LENGTH || - installPath.split("/").filter(Boolean).length > MAX_OPENCLAW_PLUGIN_INSTALL_PATH_SEGMENTS || - !path.posix.isAbsolute(installPath) || - path.posix.normalize(installPath) !== installPath - ) { + !Array.isArray(loadPaths) || + loadPaths.length > 1 || + !loadPaths.every(validateCanonicalAbsolutePath) || + new Set(loadPaths).size !== loadPaths.length + ) return null; - } - return { id, installPath }; + return { id, installPath, loadPaths: [...loadPaths] }; } function extensionDirForInstall( @@ -229,7 +303,7 @@ function extensionDirForInstall( } function validateOpenClawImagePluginInstalls( - entries: readonly (readonly [string, unknown])[], + entries: readonly (readonly [string, unknown, unknown])[], dir: string, ): OpenClawManagedExtensionDiscoveryResult { if (entries.length > MAX_OPENCLAW_IMAGE_MANAGED_PLUGIN_INSTALLS) { @@ -241,15 +315,19 @@ function validateOpenClawImagePluginInstalls( const ids = new Set(); const installPaths = new Set(); + const loadPaths = new Set(); const extensionDirs = new Set(); const pluginInstalls: OpenClawImagePluginInstall[] = []; - for (const [id, metadata] of entries) { - const installPath = isRecord(metadata) ? metadata.installPath : undefined; - const install = validateOpenClawImagePluginInstall(id, installPath); + for (const [id, installPath, durableLoadPaths] of entries) { + const install = validateDurableOpenClawImagePluginInstall(id, installPath, durableLoadPaths); if (!install) { return { ok: false, error: `fresh OpenClaw plugin install metadata is invalid: ${id}` }; } - if (ids.has(install.id) || installPaths.has(install.installPath)) { + if ( + ids.has(install.id) || + installPaths.has(install.installPath) || + install.loadPaths?.some((loadPath) => loadPaths.has(loadPath)) + ) { return { ok: false, error: `fresh OpenClaw plugin install provenance is duplicated: ${id}` }; } const projected = extensionDirForInstall(install, dir); @@ -259,6 +337,7 @@ function validateOpenClawImagePluginInstalls( } ids.add(install.id); installPaths.add(install.installPath); + for (const loadPath of install.loadPaths ?? []) loadPaths.add(loadPath); pluginInstalls.push(install); if (projected.extensionDir) extensionDirs.add(projected.extensionDir); } @@ -277,11 +356,23 @@ export function parseOpenClawImagePluginInstalls( return { ok: false, error: "OpenClaw image plugin provenance is invalid" }; } return validateOpenClawImagePluginInstalls( - value.map((entry) => [isRecord(entry) && typeof entry.id === "string" ? entry.id : "", entry]), + value.map((entry) => [ + isRecord(entry) && typeof entry.id === "string" ? entry.id : "", + isRecord(entry) ? entry.installPath : undefined, + isRecord(entry) ? entry.loadPaths : undefined, + ]), dir, ); } +/** True only for explicit, fully validated provenance; an explicit empty array is complete. */ +export function hasCompleteOpenClawImagePluginProvenance( + value: unknown, + dir: string, +): value is readonly CompleteOpenClawImagePluginInstall[] { + return Array.isArray(value) && parseOpenClawImagePluginInstalls(value, dir).ok; +} + export function parseFreshOpenClawPluginExtensionDirs( registryIndex: unknown, dir: string, @@ -294,9 +385,38 @@ export function parseFreshOpenClawPluginExtensionDirs( return { ok: false, error: "fresh OpenClaw plugin install records are invalid" }; } + const configuredLoadPaths = registryIndex.loadPaths; + if ( + !Array.isArray(configuredLoadPaths) || + configuredLoadPaths.length > MAX_OPENCLAW_CONFIGURED_PLUGIN_LOAD_PATHS || + !configuredLoadPaths.every(validateConfiguredLoadPath) + ) { + return { ok: false, error: "fresh OpenClaw configured plugin load paths are invalid" }; + } + const configuredLoadPathSet = new Set(configuredLoadPaths); + const entries = Object.keys(installs) .sort() - .map((id) => [id, installs[id]] as const); + .map((id) => { + const metadata = installs[id]; + if (!isRecord(metadata) || !OPENCLAW_PLUGIN_INSTALL_SOURCES.has(String(metadata.source))) { + return [id, undefined, undefined] as const; + } + if ( + metadata.sourcePath !== undefined && + !validateCanonicalAbsolutePath(metadata.sourcePath) + ) { + return [id, undefined, undefined] as const; + } + if (metadata.source === "path" && !validateCanonicalAbsolutePath(metadata.sourcePath)) { + return [id, undefined, undefined] as const; + } + const loadPaths = + metadata.source === "path" && configuredLoadPathSet.has(String(metadata.sourcePath)) + ? [String(metadata.sourcePath)] + : []; + return [id, metadata.installPath, loadPaths] as const; + }); return validateOpenClawImagePluginInstalls(entries, dir); } diff --git a/src/lib/state/registry.ts b/src/lib/state/registry.ts index 9ad26ae540..1f5aaa013e 100644 --- a/src/lib/state/registry.ts +++ b/src/lib/state/registry.ts @@ -494,7 +494,10 @@ export function registerSandbox(entry: SandboxEntry): void { agent: entry.agent || null, agentVersion: entry.agentVersion || null, openclawImagePluginInstalls: Array.isArray(entry.openclawImagePluginInstalls) - ? entry.openclawImagePluginInstalls.map((install) => ({ ...install })) + ? entry.openclawImagePluginInstalls.map((install) => ({ + ...install, + ...(install.loadPaths !== undefined ? { loadPaths: [...install.loadPaths] } : {}), + })) : undefined, nemoclawVersion: entry.nemoclawVersion || null, fromDockerfile: entry.fromDockerfile || null, diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 3f73d35e80..fdb37310c1 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -45,6 +45,7 @@ import { } from "./openclaw-managed-extensions.js"; import { discoverFreshOpenClawImagePluginInstalls, + hasCompleteOpenClawImagePluginProvenance, type OpenClawImagePluginInstall, parseOpenClawImagePluginInstalls, planOpenClawPluginRestore, @@ -59,6 +60,8 @@ const HOME_DIR = path.resolve(process.env.HOME || os.homedir()); const REBUILD_BACKUPS_DIR = path.join(HOME_DIR, ".nemoclaw", "rebuild-backups"); const MANIFEST_VERSION = 1; +export const OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR = + "custom-image OpenClaw plugin provenance is missing or invalid"; function parseJson(text: string): T { return JSON.parse(text); @@ -75,6 +78,8 @@ export interface RebuildManifest { expectedVersion: string | null; /** Fresh-image plugin baseline captured before user state was restored. */ openclawImagePluginInstalls?: OpenClawImagePluginInstall[]; + /** The plugin baseline is authoritative and must be reconciled during recreation. */ + reconcileOpenClawImagePluginProvenance?: boolean; stateDirs: string[]; /** Directories verified as safe to restore. Absent on older manifests. */ backedUpDirs?: string[]; @@ -217,6 +222,31 @@ function isCustomPolicyEntryArray(value: unknown): value is CustomPolicyEntry[] ); } +function cloneOpenClawImagePluginInstalls( + installs: readonly OpenClawImagePluginInstall[], +): OpenClawImagePluginInstall[] { + return installs.map((install) => ({ + ...install, + ...(install.loadPaths !== undefined ? { loadPaths: [...install.loadPaths] } : {}), + })); +} + +export function hasAuthoritativeOpenClawImagePluginProvenance(value: { + agentType?: unknown; + dir?: unknown; + writableDir?: unknown; + openclawImagePluginInstalls?: unknown; + reconcileOpenClawImagePluginProvenance?: unknown; +}): boolean { + const dir = typeof value.dir === "string" ? value.dir : value.writableDir; + return ( + value.agentType === "openclaw" && + typeof dir === "string" && + value.reconcileOpenClawImagePluginProvenance === true && + hasCompleteOpenClawImagePluginProvenance(value.openclawImagePluginInstalls, dir) + ); +} + function isRebuildManifest(value: unknown): value is RebuildManifest { if (!isRecord(value) || !isStateDirArray(value.stateDirs)) return false; const dir = typeof value.dir === "string" ? value.dir : value.writableDir; @@ -231,6 +261,10 @@ function isRebuildManifest(value: unknown): value is RebuildManifest { typeof dir === "string" && (value.openclawImagePluginInstalls === undefined || parseOpenClawImagePluginInstalls(value.openclawImagePluginInstalls, dir).ok) && + (value.reconcileOpenClawImagePluginProvenance === undefined || + typeof value.reconcileOpenClawImagePluginProvenance === "boolean") && + (value.reconcileOpenClawImagePluginProvenance !== true || + hasAuthoritativeOpenClawImagePluginProvenance(value)) && typeof value.backupPath === "string" && (value.stateFiles === undefined || (Array.isArray(value.stateFiles) && value.stateFiles.every(isStateFileSpec))) && @@ -902,6 +936,7 @@ function buildStateFileRestoreInput( spec: StateFileSpec, backupContents: Buffer, mergeOpenClawConfig: boolean, + freshImagePluginInstalls?: readonly OpenClawImagePluginInstall[], previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[], ): Buffer | null { if (!mergeOpenClawConfig) return backupContents; @@ -909,6 +944,7 @@ function buildStateFileRestoreInput( const result = buildOpenClawConfigRestoreInputFromSandbox({ backupContents, dir, + freshImagePluginInstalls, log: _log, previousImagePluginInstalls, specPath: spec.path, @@ -928,6 +964,7 @@ function restoreStateFile( backupPath: string, mergeOpenClawConfig = false, stateFileRestorePolicy?: StateFileRestorePolicy, + freshImagePluginInstalls?: readonly OpenClawImagePluginInstall[], previousImagePluginInstalls?: readonly OpenClawImagePluginInstall[], ): boolean { const localPath = path.join(backupPath, spec.path); @@ -946,6 +983,7 @@ function restoreStateFile( spec, backupContents, mergeOpenClawConfig, + freshImagePluginInstalls, previousImagePluginInstalls, ); if (input === null) return false; @@ -989,9 +1027,14 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions = `backupSandboxState: agent=${agentName}, dir=${dir}, stateDirs=[${stateDirs.join(",")}], stateFiles=[${stateFiles.map((f) => f.path).join(",")}]`, ); + const reconcileOpenClawImagePluginProvenance = + agentName === "openclaw" && Boolean(sb?.fromDockerfile); let openclawImagePluginInstalls: OpenClawImagePluginInstall[] | undefined; - if (agentName === "openclaw" && sb?.openclawImagePluginInstalls !== undefined) { - const provenance = parseOpenClawImagePluginInstalls(sb.openclawImagePluginInstalls, dir); + if ( + agentName === "openclaw" && + (reconcileOpenClawImagePluginProvenance || sb?.openclawImagePluginInstalls !== undefined) + ) { + const provenance = parseOpenClawImagePluginInstalls(sb?.openclawImagePluginInstalls, dir); if (!provenance.ok) { return { success: false, @@ -999,10 +1042,10 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions = failedDirs: [], backedUpFiles: [], failedFiles: [], - error: "registered OpenClaw image plugin provenance is invalid", + error: "registered OpenClaw image plugin provenance is missing or invalid", }; } - openclawImagePluginInstalls = provenance.pluginInstalls; + openclawImagePluginInstalls = cloneOpenClawImagePluginInstalls(provenance.pluginInstalls); } // Validate user-supplied name and check for conflicts BEFORE creating any @@ -1070,6 +1113,9 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions = agentVersion: sb?.agentVersion || null, expectedVersion: agent.expectedVersion, ...(openclawImagePluginInstalls !== undefined ? { openclawImagePluginInstalls } : {}), + ...(reconcileOpenClawImagePluginProvenance + ? { reconcileOpenClawImagePluginProvenance: true } + : {}), stateDirs, stateFiles, dir, @@ -1437,12 +1483,16 @@ function restoreSandboxStateInternal( const manifest = readManifest(backupPath); if (!manifest) { _log("FAILED: Could not read rebuild-manifest.json"); + const provenanceError = hasInvalidMarkedOpenClawPluginProvenance(backupPath) + ? OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR + : undefined; return { success: false, restoredDirs: [], failedDirs: ["manifest"], restoredFiles: [], failedFiles: [], + ...(provenanceError ? { error: provenanceError } : {}), }; } @@ -1516,7 +1566,10 @@ function restoreSandboxStateInternal( failedDirs: [...localDirs], restoredFiles, failedFiles: localFiles.map((f) => f.path), - error: pluginRestorePlan.error, + error: + manifest.reconcileOpenClawImagePluginProvenance === true + ? OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR + : pluginRestorePlan.error, }; } if ( @@ -1659,6 +1712,7 @@ function restoreSandboxStateInternal( backupPath, shouldMergeOpenClawConfigStateFile(manifest.agentType, dir, spec), options.stateFileRestorePolicy, + freshOpenClawImagePluginInstalls, previousOpenClawImagePluginInstalls, ) ) { @@ -1692,11 +1746,28 @@ function writeManifest(backupPath: string, manifest: RebuildManifest): void { chmodSync(manifestPath, 0o600); } -function readManifest(backupPath: string): RebuildManifest | null { +function readManifestPayload(backupPath: string): unknown | null { const manifestPath = path.join(backupPath, "rebuild-manifest.json"); if (!existsSync(manifestPath)) return null; try { - const parsed = parseJson(readFileSync(manifestPath, "utf-8")); + return parseJson(readFileSync(manifestPath, "utf-8")); + } catch { + return null; + } +} + +function hasInvalidMarkedOpenClawPluginProvenance(backupPath: string): boolean { + const parsed = readManifestPayload(backupPath); + return ( + isRecord(parsed) && + parsed.reconcileOpenClawImagePluginProvenance === true && + !hasAuthoritativeOpenClawImagePluginProvenance(parsed) + ); +} + +function readManifest(backupPath: string): RebuildManifest | null { + try { + const parsed = readManifestPayload(backupPath); if (!isRebuildManifest(parsed)) return null; const manifest = parsed as RebuildManifest & { dir?: string; writableDir?: string }; const dir = manifest.dir ?? manifest.writableDir; diff --git a/test/helpers/rebuild-flow-recovery-cases.ts b/test/helpers/rebuild-flow-recovery-cases.ts index 88057762be..2ad5034fed 100644 --- a/test/helpers/rebuild-flow-recovery-cases.ts +++ b/test/helpers/rebuild-flow-recovery-cases.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import path from "node:path"; + import { describe, expect, it } from "vitest"; import { makeActiveTeamsMessagingPlan, @@ -35,6 +37,37 @@ export function registerRebuildFlowRecoveryTests(): void { ); }); + it("uses marked manifest provenance when the custom-image registry baseline is missing (#6108)", async () => { + const customDockerfile = path.join(process.cwd(), "Dockerfile"); + const recoveryManifest = { + ...makePreparedRecoveryManifest(), + reconcileOpenClawImagePluginProvenance: true, + openclawImagePluginInstalls: [], + }; + const harness = createRebuildFlowHarness({ + sandboxEntry: { + fromDockerfile: customDockerfile, + nemoclawVersion: null, + openclawImagePluginInstalls: undefined, + }, + preDeleteLatestManifest: recoveryManifest, + managedImageEvidence: false, + }); + + await expect( + harness.rebuildSandbox("alpha", ["--yes"], { + throwOnError: true, + recoveryManifest, + }), + ).resolves.toBeUndefined(); + + expect(harness.backupSandboxStateSpy).not.toHaveBeenCalled(); + expect(harness.runOpenshellSpy).toHaveBeenCalledWith( + ["sandbox", "delete", "alpha"], + expect.objectContaining({ ignoreError: true }), + ); + }); + it("rejects a mismatched prepared manifest before deleting the sandbox (#6114)", async () => { const harness = createRebuildFlowHarness({ recoveryManifestValidation: () => ({ diff --git a/test/helpers/rebuild-flow-test-harness.ts b/test/helpers/rebuild-flow-test-harness.ts index b5785c3835..dc377873a5 100644 --- a/test/helpers/rebuild-flow-test-harness.ts +++ b/test/helpers/rebuild-flow-test-harness.ts @@ -179,6 +179,12 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): vi.spyOn(onboardSession, "acquireOnboardLock").mockReturnValue({ acquired: true }); const markStepFailedSpy = installTerminalStepFailureMock(onboardSession, session); session.sandboxName = overrides.sessionSandboxName ?? session.sandboxName; + const modelsCustomOpenClawImage = + typeof overrides.sandboxEntry?.fromDockerfile === "string" && + (!overrides.sandboxEntry.agent || overrides.sandboxEntry.agent === "openclaw"); + const customOpenClawPluginProvenance = modelsCustomOpenClawImage + ? { openclawImagePluginInstalls: [] } + : {}; const currentSandboxEntry = { name: "alpha", provider: "ollama-local", @@ -191,6 +197,7 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): dashboardPort: 18789, gatewayName: "nemoclaw", gatewayPort: 8080, + ...customOpenClawPluginProvenance, ...(overrides.sandboxEntry ?? {}), }; const readCurrentSandboxEntry = () => structuredClone(currentSandboxEntry); @@ -319,9 +326,18 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): typeof overrides.sandboxEntry?.agent === "string" ? overrides.sandboxEntry.agent : "openclaw", + dir: "/sandbox/.openclaw", backupPath: "/tmp/nemoclaw-rebuild-backup", timestamp: "2026-06-01T00:00:00.000Z", policyPresets: overrides.backupPolicyPresets ?? ["npm", "bad", "throw"], + ...(modelsCustomOpenClawImage + ? { + reconcileOpenClawImagePluginProvenance: true, + openclawImagePluginInstalls: structuredClone( + currentSandboxEntry.openclawImagePluginInstalls, + ), + } + : {}), }, }; }); diff --git a/test/registry.test.ts b/test/registry.test.ts index e67fa0b11b..f615df2289 100644 --- a/test/registry.test.ts +++ b/test/registry.test.ts @@ -109,6 +109,7 @@ describe("registry", () => { const weatherInstall = { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: [], }; registry.registerSandbox({ name: "legacy", agent: "openclaw" }); registry.registerSandbox({ diff --git a/test/snapshot-openclaw-managed-extensions.test.ts b/test/snapshot-openclaw-managed-extensions.test.ts index c5c1ab38d1..dfe5e8c20d 100644 --- a/test/snapshot-openclaw-managed-extensions.test.ts +++ b/test/snapshot-openclaw-managed-extensions.test.ts @@ -34,7 +34,11 @@ function writeExecutable(filePath: string, source: string): void { function writeBackup( sandboxName: string, dirName: string, - openclawImagePluginInstalls?: Array<{ id: string; installPath: string }>, + openclawImagePluginInstalls?: Array<{ + id: string; + installPath: string; + loadPaths: string[]; + }>, ): { backupPath: string } { const backupPath = path.join(BACKUPS_ROOT, sandboxName, dirName); fs.mkdirSync(backupPath, { recursive: true }); @@ -147,10 +151,15 @@ describe("OpenClaw managed extension snapshot restore", () => { freshRegistryPath, JSON.stringify({ version: 1, + loadPaths: [], installRecords: Object.fromEntries( freshImagePlugins.map((id) => [ id, - { installPath: `/sandbox/.openclaw/extensions/${id}` }, + { + source: "path", + sourcePath: `/sandbox/.openclaw/extensions/${id}`, + installPath: `/sandbox/.openclaw/extensions/${id}`, + }, ]), ), }), @@ -160,6 +169,7 @@ describe("OpenClaw managed extension snapshot restore", () => { { id: previousPlugin, installPath: `/sandbox/.openclaw/extensions/${previousPlugin}`, + loadPaths: [], }, ]); const backupExtensionsDir = path.join(manifest.backupPath, "extensions"); @@ -199,7 +209,7 @@ if (cmd.includes("installed_plugin_index") && cmd.includes("state/openclaw.sqlit if (installIndexSource === "sqlite") process.stdout.write(fs.readFileSync(${JSON.stringify(freshRegistryPath)})); process.exit(installIndexSource === "sqlite" ? 0 : 2); } -if (cmd.includes("plugins/installs.json") && cmd.includes("cat --")) { +if (cmd.includes("plugins/installs.json") && cmd.includes("python3 -c")) { if (installIndexSource === "legacy") process.stdout.write(fs.readFileSync(${JSON.stringify(freshRegistryPath)})); process.exit(installIndexSource === "legacy" ? 0 : 2); } @@ -272,8 +282,11 @@ process.exit(0); freshRegistryPath, JSON.stringify({ version: 1, + loadPaths: [], installRecords: { "\u001b[31m../weather": { + source: "path", + sourcePath: "/sandbox/.openclaw/extensions/../weather", installPath: "/sandbox/.openclaw/extensions/../weather", }, }, diff --git a/test/snapshot-recovery-validation.test.ts b/test/snapshot-recovery-validation.test.ts index 0d60b0ffe3..2de49e1341 100644 --- a/test/snapshot-recovery-validation.test.ts +++ b/test/snapshot-recovery-validation.test.ts @@ -83,16 +83,48 @@ describe("prepared rebuild backup recovery validation (#6114)", () => { it("round-trips validated OpenClaw image-plugin provenance through recovery", () => { const openclawImagePluginInstalls = [ - { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather" }, - { id: "npm-plugin", installPath: "/sandbox/.openclaw/npm/node_modules/npm-plugin" }, + { + id: "weather", + installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: [], + }, + { + id: "npm-plugin", + installPath: "/sandbox/.openclaw/npm/node_modules/npm-plugin", + loadPaths: [], + }, ]; - writeBackup("alpha", "2026-07-01T06-50-42-045Z", { openclawImagePluginInstalls }); + writeBackup("alpha", "2026-07-01T06-50-42-045Z", { + reconcileOpenClawImagePluginProvenance: true, + openclawImagePluginInstalls, + }); const latest = sandboxState.getLatestBackup("alpha"); expect(latest?.openclawImagePluginInstalls).toEqual(openclawImagePluginInstalls); + expect(latest?.reconcileOpenClawImagePluginProvenance).toBe(true); expect(sandboxState.validateRebuildRecoveryManifest("alpha", "openclaw", latest!)).toEqual({ ok: true, - manifest: expect.objectContaining({ openclawImagePluginInstalls }), + manifest: expect.objectContaining({ + reconcileOpenClawImagePluginProvenance: true, + openclawImagePluginInstalls, + }), + }); + }); + + it("rejects a marked manifest without explicit image-plugin provenance", () => { + const manifest = writeBackup("alpha", "2026-07-01T06-50-42-045Z", { + reconcileOpenClawImagePluginProvenance: true, + }); + + expect(sandboxState.getLatestBackup("alpha")).toBeNull(); + expect( + sandboxState.restoreRecreatedSandboxState("alpha", String(manifest.backupPath), { + targetAgentType: "openclaw", + freshOpenClawImagePluginInstalls: [], + }), + ).toMatchObject({ + success: false, + error: sandboxState.OPENCLAW_IMAGE_PLUGIN_PROVENANCE_RESTORE_ERROR, }); }); @@ -100,14 +132,31 @@ describe("prepared rebuild backup recovery validation (#6114)", () => { ["a non-array value", { weather: "/sandbox/.openclaw/extensions/weather" }], [ "an unsafe plugin id", - [{ id: "../weather", installPath: "/sandbox/.openclaw/extensions/weather" }], + [ + { + id: "../weather", + installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: [], + }, + ], + ], + [ + "a relative install path", + [{ id: "weather", installPath: "extensions/weather", loadPaths: [] }], ], - ["a relative install path", [{ id: "weather", installPath: "extensions/weather" }]], [ "duplicate install paths", [ - { id: "weather", installPath: "/sandbox/.openclaw/extensions/weather" }, - { id: "weather-copy", installPath: "/sandbox/.openclaw/extensions/weather" }, + { + id: "weather", + installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: [], + }, + { + id: "weather-copy", + installPath: "/sandbox/.openclaw/extensions/weather", + loadPaths: [], + }, ], ], ])("rejects image-plugin provenance with %s", (_case, openclawImagePluginInstalls) => { diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts index 20a541a169..cacddad140 100644 --- a/test/snapshot.test.ts +++ b/test/snapshot.test.ts @@ -79,7 +79,11 @@ beforeEach(() => { function writeExecutable(filePath: string, source: string): void { fs.writeFileSync(filePath, source, { mode: 0o755 }); } -function writeAgentRegistry(sandboxName: string, agent: string | null): void { +function writeAgentRegistry( + sandboxName: string, + agent: string | null, + overrides: Record = {}, +): void { fs.mkdirSync(path.join(TMP_HOME, ".nemoclaw"), { recursive: true }); fs.writeFileSync( path.join(TMP_HOME, ".nemoclaw", "sandboxes.json"), @@ -93,14 +97,15 @@ function writeAgentRegistry(sandboxName: string, agent: string | null): void { gpuEnabled: false, policies: [], agent, + ...overrides, }, }, }), ); } -function writeOpenClawRegistry(sandboxName: string): void { - writeAgentRegistry(sandboxName, null); +function writeOpenClawRegistry(sandboxName: string, overrides: Record = {}): void { + writeAgentRegistry(sandboxName, null, overrides); } function writeFakeOpenshell(binDir: string): string { const openshell = path.join(binDir, "openshell"); @@ -435,6 +440,19 @@ describe("parseRestoreArgs", () => { }); describe("sandbox directory backup semantics", () => { + it("rejects a custom OpenClaw backup with missing image-plugin provenance (#6108)", () => { + writeOpenClawRegistry("custom-openclaw", { + fromDockerfile: "/tmp/Dockerfile.custom", + }); + + const backup = sandboxState.backupSandboxState("custom-openclaw"); + + expect(backup.success).toBe(false); + expect(backup.manifest).toBeUndefined(); + expect(backup.error).toBe("registered OpenClaw image plugin provenance is missing or invalid"); + expect(fs.existsSync(path.join(BACKUPS_ROOT, "custom-openclaw"))).toBe(false); + }); + it("treats empty state directories as backed up when tar exits cleanly", () => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-empty-dirs-")); const oldPath = process.env.PATH; @@ -479,7 +497,10 @@ process.exit(0); `, ); - writeOpenClawRegistry("alpha"); + writeOpenClawRegistry("alpha", { + fromDockerfile: "/tmp/Dockerfile.custom", + openclawImagePluginInstalls: [], + }); process.env.NEMOCLAW_OPENSHELL_BIN = openshell; process.env.PATH = `${binDir}${path.delimiter}${oldPath || ""}`; @@ -488,6 +509,8 @@ process.exit(0); expect(backup.failedDirs).toEqual([]); expect(backup.backedUpDirs).toEqual(existingDirs); expect(backup.manifest?.backedUpDirs).toEqual(existingDirs); + expect(backup.manifest?.reconcileOpenClawImagePluginProvenance).toBe(true); + expect(backup.manifest?.openclawImagePluginInstalls).toEqual([]); } finally { if (oldOpenshell === undefined) { delete process.env.NEMOCLAW_OPENSHELL_BIN; From 362730b3291ea43c877da32fb81aaa7bdeac5b5c Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 6 Jul 2026 16:40:43 -0700 Subject: [PATCH 38/46] test(e2e): verify release-matched plugin lifecycle Signed-off-by: Apurv Kumaria --- .github/workflows/e2e.yaml | 6 +- .github/workflows/regression-e2e.yaml | 6 +- .../openclaw-plugin-runtime-exdev.test.ts | 296 +++++++++++++++--- ...in-runtime-exdev-workflow-boundary.test.ts | 2 +- test/regression-e2e-workflow.test.ts | 2 + 5 files changed, 259 insertions(+), 53 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 73603aae1f..00832e5178 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -3671,9 +3671,9 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # Cold-cache custom-image onboarding, recreation, rebuild, and EXDEV proof - # can exceed an hour, so this lane keeps a bounded 90-minute budget. - timeout-minutes: 90 + # Three bounded 25-minute onboards plus the 20-minute rebuild and 15-minute + # Vitest buffer need 110 minutes; allow 20 more for setup and teardown. + timeout-minutes: 130 env: E2E_JOB: "1" E2E_TARGET_ID: "openclaw-plugin-runtime-exdev" diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index df173657b2..5687c6da71 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -280,9 +280,9 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # Cold-cache custom-image onboarding, gateway restart, rebuild, and EXDEV - # proof can exceed 45 minutes, so this lane keeps a bounded 75-minute budget. - timeout-minutes: 75 + # Three bounded 25-minute onboards plus the 20-minute rebuild and 15-minute + # Vitest buffer need 110 minutes; allow 20 more for setup and teardown. + timeout-minutes: 130 steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 063ff16b72..6c4d83f332 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -59,6 +59,8 @@ assert.equal( ); const NEMOCLAW_RELEASE_TAG = "v0.0.71"; const NEMOCLAW_RELEASE_COMMIT = "e4b9111f5f0535c2fc3d6fbe8dc8dca101a6fdce"; +const NEMOCLAW_RELEASE_OPENSHELL_VERSION = "0.0.71"; +const CURRENT_OPENSHELL_VERSION = "0.0.72"; const NEMOCLAW_SOURCE_REPOSITORY = "https://github.com/NVIDIA/NemoClaw.git"; const SANDBOX_BASE_IMAGE_REF = "ghcr.io/nvidia/nemoclaw/sandbox-base:v0.0.71"; const TOOL_DISCLOSURE_ENV_REFERENCE = "${NEMOCLAW_TOOL_DISCLOSURE}"; @@ -254,20 +256,45 @@ function resolvePinnedOpenShellComponents(openshellPath: string): PinnedOpenShel async function installAndResolvePinnedOpenShell( host: HostCliClient, + installScriptPath: string, + artifactLabel: string, + expectedVersion?: string, ): Promise { - const install = await host.command( - "bash", - [path.join(REPO_ROOT, "scripts", "install-openshell.sh")], - { - artifactName: "install-pinned-openshell-for-exdev-wrapper", - env: liveEnv(), - timeoutMs: 5 * 60_000, - }, - ); + const install = await host.command("bash", [installScriptPath], { + artifactName: `install-${artifactLabel}-openshell-for-exdev-wrapper`, + env: liveEnv(), + timeoutMs: 5 * 60_000, + }); expect(install.exitCode, resultText(install)).toBe(0); const resolved = resolveOpenshell(); expect(resolved, "pinned OpenShell installer did not leave an executable CLI").not.toBeNull(); - return resolvePinnedOpenShellComponents(resolved as string); + const components = resolvePinnedOpenShellComponents(resolved as string); + const version = await host.command(components.cli, ["--version"], { + artifactName: `verify-${artifactLabel}-openshell-version`, + env: liveEnv(), + timeoutMs: 30_000, + }); + expect(version.exitCode, resultText(version)).toBe(0); + if (expectedVersion) { + expect(resultText(version)).toMatch( + new RegExp(`\\b${expectedVersion.replaceAll(".", "\\.")}\\b`), + ); + } + return components; +} + +async function stopOpenShellGatewayBeforeVersionSwitch( + host: HostCliClient, + artifactLabel: string, + env: NodeJS.ProcessEnv = liveEnv(), +): Promise { + const openshellPath = resolveOpenshell(); + if (!openshellPath) return; + await host.command(openshellPath, ["gateway", "stop", "-g", "nemoclaw"], { + artifactName: `stop-${artifactLabel}-openshell-gateway-before-version-switch`, + env, + timeoutMs: 60_000, + }); } type PolicySourceSnapshot = ReadonlyArray<{ policyPath: string; bytes: Buffer }>; @@ -385,6 +412,7 @@ test("OpenShell wrapper injects only the reviewed tmpfs config into sandbox crea type CustomPluginBuildContext = { sourceParentDir: string; sourceRoot: string; + cliEntrypoint: string; dockerfilePath: string; versionSourcePath: string; pluginDirPath: string; @@ -397,6 +425,7 @@ function createCustomPluginBuildContext(): CustomPluginBuildContext { return { sourceParentDir, sourceRoot, + cliEntrypoint: path.join(sourceRoot, "bin", "nemoclaw.js"), dockerfilePath: path.join(sourceRoot, `Dockerfile.e2e-weather-plugin-${nonce}`), versionSourcePath: path.join(sourceRoot, `e2e-weather-plugin-version-${nonce}.ts`), pluginDirPath: path.join(sourceRoot, `e2e-weather-plugin-${nonce}`), @@ -535,6 +564,64 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \ }); } +async function buildAndVerifyTaggedCli( + host: HostCliClient, + context: CustomPluginBuildContext, +): Promise { + const workingDirectory = await host.command( + "node", + ["-e", "process.stdout.write(process.cwd())"], + { + artifactName: "verify-v0-0-71-nemoclaw-cli-working-directory", + cwd: context.sourceRoot, + env: liveEnv(), + timeoutMs: 30_000, + }, + ); + expect(workingDirectory.exitCode, resultText(workingDirectory)).toBe(0); + expect(workingDirectory.stdout).toBe(context.sourceRoot); + + const install = await host.command("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { + artifactName: "install-v0-0-71-nemoclaw-cli", + cwd: context.sourceRoot, + env: liveEnv(), + timeoutMs: 10 * 60_000, + }); + expect(install.exitCode, resultText(install)).toBe(0); + const build = await host.command("npm", ["run", "build:cli"], { + artifactName: "build-v0-0-71-nemoclaw-cli", + cwd: context.sourceRoot, + env: liveEnv(), + timeoutMs: 5 * 60_000, + }); + expect(build.exitCode, resultText(build)).toBe(0); + + const version = await host.command("node", [context.cliEntrypoint, "--version"], { + artifactName: "version-v0-0-71-nemoclaw-cli", + cwd: context.sourceRoot, + env: liveEnv(), + timeoutMs: 30_000, + }); + expect(version.exitCode, resultText(version)).toBe(0); + expect(resultText(version)).toMatch(/\bv0\.0\.71\b/); + const help = await host.command("node", [context.cliEntrypoint, "onboard", "--help"], { + artifactName: "help-v0-0-71-nemoclaw-cli", + cwd: context.sourceRoot, + env: liveEnv(), + timeoutMs: 30_000, + }); + expect(help.exitCode, resultText(help)).toBe(0); + for (const option of [ + "--from", + "--fresh", + "--name", + "--no-gpu", + "--yes-i-accept-third-party-software", + ]) { + expect(resultText(help)).toContain(option); + } +} + type WeatherPluginInspect = { plugin?: { id?: unknown; status?: unknown; toolNames?: unknown }; tools?: Array<{ names?: unknown }>; @@ -811,7 +898,7 @@ const runtimeDepsReplacementProbe = trustedSandboxShellScript( liveTest( "a custom OpenClaw plugin survives restart, recreation, and rebuild without EXDEV failures (#6108)", - { timeout: ONBOARD_TIMEOUT_MS + REBUILD_TIMEOUT_MS + 25 * 60_000 }, + { timeout: ONBOARD_TIMEOUT_MS * 3 + REBUILD_TIMEOUT_MS + 15 * 60_000 }, async ({ artifacts, cleanup, host, sandbox, skip }) => { await artifacts.writeJson("target.json", { id: "openclaw-plugin-runtime-exdev", @@ -819,7 +906,9 @@ liveTest( boundary: "fresh-openclaw-sandbox-exec", regressionTargets: ["#6108", "#3513", "#3127"], contract: [ - "fresh OpenClaw sandbox onboards from the exact NemoClaw v0.0.71 source/runtime pair", + "the exact NemoClaw v0.0.71 checkout installs, builds, and reports its tagged CLI version", + "the tagged CLI uses OpenShell 0.0.71 with matching source, base image, and OpenClaw runtime", + "the current CLI reinstalls OpenShell 0.0.72 before current lifecycle coverage", "release-matched peer/dev dependencies prune private OpenClaw and link the host runtime", "gateway log, runtime inspection, tools.catalog, and tools.invoke prove weather/get_weather", "custom-plugin v1 survives restart, recreation installs v2, and rebuild installs v3", @@ -832,6 +921,8 @@ liveTest( ], nemoclawSourceRelease: NEMOCLAW_RELEASE_TAG, nemoclawSourceCommit: NEMOCLAW_RELEASE_COMMIT, + taggedOpenshellVersion: NEMOCLAW_RELEASE_OPENSHELL_VERSION, + currentOpenshellVersion: CURRENT_OPENSHELL_VERSION, sandboxBaseImageRef: SANDBOX_BASE_IMAGE_REF, openclawVersion: WEATHER_OPENCLAW_VERSION, }); @@ -889,27 +980,6 @@ liveTest( ); const policySourceSnapshot = snapshotPolicySources(); - const pinnedOpenshell = await installAndResolvePinnedOpenShell(host); - expect( - hasRequiredOpenshellMessagingFeatures({ - openshellBin: pinnedOpenshell.cli, - gatewayBin: pinnedOpenshell.gateway, - sandboxBin: pinnedOpenshell.sandbox, - }), - "canonical pinned OpenShell components must pass coherence preflight before delegation", - ).toBe(true); - const openshellWrapper = createOpenShellTmpfsWrapper(pinnedOpenshell.cli); - cleanup.add("remove EXDEV OpenShell PATH wrapper", openshellWrapper.remove); - expect( - hasRequiredOpenshellMessagingFeatures({ - openshellBin: openshellWrapper.executable, - gatewayBin: pinnedOpenshell.gateway, - sandboxBin: pinnedOpenshell.sandbox, - allowExternalGatewayBin: true, - allowExternalSandboxBin: true, - }), - "OpenShell wrapper and explicit pinned components must pass onboard coherence preflight", - ).toBe(true); const customPluginContext = createCustomPluginBuildContext(); cleanup.add("remove v0.0.71 custom-plugin source worktree", () => fs.rmSync(customPluginContext.sourceParentDir, { recursive: true, force: true }), @@ -945,23 +1015,148 @@ liveTest( expect(releaseHead.exitCode, resultText(releaseHead)).toBe(0); expect(releaseHead.stdout.trim()).toBe(NEMOCLAW_RELEASE_COMMIT); createCustomPluginDockerfile(customPluginContext); + await buildAndVerifyTaggedCli(host, customPluginContext); + + await stopOpenShellGatewayBeforeVersionSwitch(host, "existing"); + const taggedPinnedOpenshell = await installAndResolvePinnedOpenShell( + host, + path.join(customPluginContext.sourceRoot, "scripts", "install-openshell.sh"), + "v0-0-71", + NEMOCLAW_RELEASE_OPENSHELL_VERSION, + ); + expect( + hasRequiredOpenshellMessagingFeatures({ + openshellBin: taggedPinnedOpenshell.cli, + gatewayBin: taggedPinnedOpenshell.gateway, + sandboxBin: taggedPinnedOpenshell.sandbox, + }), + "v0.0.71 OpenShell components must pass coherence preflight before delegation", + ).toBe(true); + const taggedOpenShellWrapper = createOpenShellTmpfsWrapper(taggedPinnedOpenshell.cli); + cleanup.add("remove v0.0.71 EXDEV OpenShell PATH wrapper", taggedOpenShellWrapper.remove); + expect( + hasRequiredOpenshellMessagingFeatures({ + openshellBin: taggedOpenShellWrapper.executable, + gatewayBin: taggedPinnedOpenshell.gateway, + sandboxBin: taggedPinnedOpenshell.sandbox, + allowExternalGatewayBin: true, + allowExternalSandboxBin: true, + }), + "v0.0.71 OpenShell wrapper and components must pass onboard coherence preflight", + ).toBe(true); - const sandboxEnv = withOpenShellWrapperEnv( - liveEnv({ - COMPATIBLE_API_KEY: "nemoclaw-exdev-dummy-key", - NEMOCLAW_ENDPOINT_URL: "http://host.openshell.internal:65535/v1", - NEMOCLAW_MODEL: "nemoclaw-exdev-probe", - NEMOCLAW_PROVIDER_KEY: "nemoclaw-exdev-dummy-key", - NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, - NEMOCLAW_SANDBOX_BASE_IMAGE_REF: SANDBOX_BASE_IMAGE_REF, - NEMOCLAW_POLICY_MODE: "skip", - NEMOCLAW_PREFERRED_API: "openai-completions", - NEMOCLAW_PROVIDER: "custom", + const deploymentEnv = liveEnv({ + COMPATIBLE_API_KEY: "nemoclaw-exdev-dummy-key", + NEMOCLAW_ENDPOINT_URL: "http://host.openshell.internal:65535/v1", + NEMOCLAW_MODEL: "nemoclaw-exdev-probe", + NEMOCLAW_PROVIDER_KEY: "nemoclaw-exdev-dummy-key", + NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, + NEMOCLAW_SANDBOX_BASE_IMAGE_REF: SANDBOX_BASE_IMAGE_REF, + NEMOCLAW_POLICY_MODE: "skip", + NEMOCLAW_PREFERRED_API: "openai-completions", + NEMOCLAW_PROVIDER: "custom", + }); + const taggedSandboxEnv = withOpenShellWrapperEnv( + deploymentEnv, + taggedOpenShellWrapper, + taggedPinnedOpenshell, + ); + + const taggedOnboard = await host.command( + "node", + [ + customPluginContext.cliEntrypoint, + "onboard", + "--fresh", + "--non-interactive", + "--yes-i-accept-third-party-software", + "--no-gpu", + "--name", + SANDBOX_NAME, + "--from", + customPluginContext.dockerfilePath, + ], + { + artifactName: "v0-0-71-openclaw-plugin-onboard", + cwd: customPluginContext.sourceRoot, + env: taggedSandboxEnv, + timeoutMs: ONBOARD_TIMEOUT_MS, + }, + ); + const taggedOnboardText = resultText(taggedOnboard); + expect(taggedOnboard.exitCode, taggedOnboardText).toBe(0); + expect(taggedOnboardText).toContain("Deployment verified"); + const taggedRuntimeVersion = await sandbox.exec(SANDBOX_NAME, ["openclaw", "--version"], { + artifactName: "v0-0-71-openclaw-version", + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }); + expect(taggedRuntimeVersion.exitCode, resultText(taggedRuntimeVersion)).toBe(0); + expect(resultText(taggedRuntimeVersion)).toContain(WEATHER_OPENCLAW_VERSION); + const taggedPlugin = await sandbox.execShell( + SANDBOX_NAME, + trustedSandboxShellScript("HOME=/sandbox openclaw plugins inspect weather --runtime --json"), + { + artifactName: "v0-0-71-weather-plugin-inspect", + env: liveEnv(), + timeoutMs: PROBE_TIMEOUT_MS, + }, + ); + expect(taggedPlugin.exitCode, resultText(taggedPlugin)).toBe(0); + const taggedPluginInspect = parseJsonFromText( + normalizeSandboxStdoutFrames(taggedPlugin.stdout), + ) as WeatherPluginInspect; + expect(taggedPluginInspect.plugin?.id).toBe("weather"); + expect(taggedPluginInspect.plugin?.status).toBe("loaded"); + expect(taggedPluginInspect.plugin?.toolNames).toContain("get_weather"); + const taggedDestroy = await host.command( + "node", + [customPluginContext.cliEntrypoint, SANDBOX_NAME, "destroy", "--yes"], + { + artifactName: "v0-0-71-openclaw-plugin-destroy", + cwd: customPluginContext.sourceRoot, + env: taggedSandboxEnv, + timeoutMs: 120_000, + }, + ); + expect(taggedDestroy.exitCode, resultText(taggedDestroy)).toBe(0); + await ignoreCleanupError(() => + sandbox.openshell(["sandbox", "delete", SANDBOX_NAME], { + artifactName: "v0-0-71-openclaw-plugin-delete-fallback", + env: taggedSandboxEnv, + timeoutMs: 60_000, }), - openshellWrapper, - pinnedOpenshell, ); + await stopOpenShellGatewayBeforeVersionSwitch(host, "v0-0-71", taggedSandboxEnv); + const pinnedOpenshell = await installAndResolvePinnedOpenShell( + host, + path.join(REPO_ROOT, "scripts", "install-openshell.sh"), + "current", + CURRENT_OPENSHELL_VERSION, + ); + expect( + hasRequiredOpenshellMessagingFeatures({ + openshellBin: pinnedOpenshell.cli, + gatewayBin: pinnedOpenshell.gateway, + sandboxBin: pinnedOpenshell.sandbox, + }), + "current pinned OpenShell components must pass coherence preflight before delegation", + ).toBe(true); + const openshellWrapper = createOpenShellTmpfsWrapper(pinnedOpenshell.cli); + cleanup.add("remove current EXDEV OpenShell PATH wrapper", openshellWrapper.remove); + expect( + hasRequiredOpenshellMessagingFeatures({ + openshellBin: openshellWrapper.executable, + gatewayBin: pinnedOpenshell.gateway, + sandboxBin: pinnedOpenshell.sandbox, + allowExternalGatewayBin: true, + allowExternalSandboxBin: true, + }), + "current OpenShell wrapper and components must pass onboard coherence preflight", + ).toBe(true); + const sandboxEnv = withOpenShellWrapperEnv(deploymentEnv, openshellWrapper, pinnedOpenshell); + const onboard = await host.command( "node", [ @@ -1087,6 +1282,8 @@ liveTest( await artifacts.writeJson("target-result.json", { id: "openclaw-plugin-runtime-exdev", + taggedOnboardExitCode: taggedOnboard.exitCode, + taggedDestroyExitCode: taggedDestroy.exitCode, onboardExitCode: onboard.exitCode, restartExitCode: restart.exitCode, recreateExitCode: recreate.exitCode, @@ -1095,6 +1292,13 @@ liveTest( runtimeDepsProbeExitCode: probe.exitCode, testOnlyTmpfsSource: EXDEV_TMPFS_SOURCE, assertions: { + taggedReleaseRuntimeMatched: + resultText(taggedRuntimeVersion).includes(WEATHER_OPENCLAW_VERSION), + taggedReleasePluginLoaded: + taggedPluginInspect.plugin?.id === "weather" && + taggedPluginInspect.plugin?.status === "loaded" && + Array.isArray(taggedPluginInspect.plugin?.toolNames) && + taggedPluginInspect.plugin.toolNames.includes("get_weather"), weatherAfterOnboard: weatherAfterOnboard.inspectLoaded && weatherAfterOnboard.catalogToolIds.includes("get_weather") && diff --git a/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts b/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts index ec26ada097..11e8bee7f1 100644 --- a/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts +++ b/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts @@ -53,7 +53,7 @@ describe("OpenClaw plugin runtime EXDEV workflow boundary", () => { expect(job.if).toBe(SELECTOR_CONDITION); expect(job["runs-on"]).toBe("ubuntu-latest"); expect(job.permissions).toEqual({ contents: "read" }); - expect(job["timeout-minutes"]).toBe(90); + expect(job["timeout-minutes"]).toBe(130); expect(job.env).toMatchObject({ E2E_JOB: "1", E2E_TARGET_ID: JOB_ID, diff --git a/test/regression-e2e-workflow.test.ts b/test/regression-e2e-workflow.test.ts index 7b93a0d649..ed7b1179dd 100644 --- a/test/regression-e2e-workflow.test.ts +++ b/test/regression-e2e-workflow.test.ts @@ -20,6 +20,7 @@ type RegressionWorkflow = { { permissions?: Record; steps?: WorkflowStep[]; + "timeout-minutes"?: number; } >; }; @@ -86,6 +87,7 @@ describe("Regression E2E workflow contract", () => { const serializedJob = JSON.stringify(job); expect(job?.permissions).toEqual({ contents: "read" }); + expect(job?.["timeout-minutes"]).toBe(130); expect(checkoutStep?.uses).toMatch(FULL_SHA_ACTION); expect(checkoutStep?.with?.["persist-credentials"]).toBe(false); expect(setupNodeStep?.uses).toMatch(FULL_SHA_ACTION); From d0dd1a4c99f309fdff3647250950f918a1d156f7 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 6 Jul 2026 16:56:20 -0700 Subject: [PATCH 39/46] fix(onboard): scope plugin provenance to custom images Signed-off-by: Apurv Kumaria --- src/lib/onboard.ts | 40 ++++-------- .../created-sandbox-finalization.test.ts | 36 +++++++++++ .../onboard/created-sandbox-finalization.ts | 3 +- .../sandbox-recreate-protection.test.ts | 63 +++++++++++++++++++ .../onboard/sandbox-recreate-protection.ts | 59 +++++++++++++++++ test/onboard.test.ts | 2 +- 6 files changed, 172 insertions(+), 31 deletions(-) create mode 100644 src/lib/onboard/sandbox-recreate-protection.test.ts create mode 100644 src/lib/onboard/sandbox-recreate-protection.ts diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index fcea8cfa9c..7edf4d9d79 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -539,7 +539,6 @@ const agentOnboard = require("./agent/onboard"); const agentDefs = require("./agent/defs"); const gatewayState: typeof import("./state/gateway") = require("./state/gateway"); -const notReadyRecreate: typeof import("./onboard/not-ready-recreate") = require("./onboard/not-ready-recreate"); const openClawPluginRestore: typeof import("./state/openclaw-plugin-restore") = require("./state/openclaw-plugin-restore"); const sandboxState: typeof import("./state/sandbox") = require("./state/sandbox"); const validation: typeof import("./validation") = require("./validation"); @@ -597,16 +596,14 @@ import { printMessagingProviderMissing, printSwapCreationFailed, } from "./onboard/preflight-messages"; -import { - backupSandboxBeforeRecreate, - shouldSkipPreRecreateBackup, -} from "./onboard/sandbox-backup-on-recreate"; +import { shouldSkipPreRecreateBackup } from "./onboard/sandbox-backup-on-recreate"; import { getResumeSandboxGpuOverrides, resolveSandboxGpuConfig, type SandboxGpuConfig, type SandboxGpuFlag, } from "./onboard/sandbox-gpu-mode"; +import { createSandboxRecreateProtection } from "./onboard/sandbox-recreate-protection"; import type { SelectionDrift } from "./onboard/selection-drift"; import { formatOnboardConfigSummary, formatSandboxBuildEstimateNote } from "./onboard/summary"; import type { @@ -2393,19 +2390,16 @@ async function createSandboxWithBaseImageResolution( const sandboxWasLiveDefault = liveExists && wasSandboxDefault(registry.getDefault(), sandboxName); let pendingStateRestore: BackupResult | null = null; - let pendingStateRestoreBackupPath: string | null = null; let notReadyRecreateInProgress = false; - const requireOpenClawImagePluginProvenance = + const customOpenClawImage = Boolean(fromDockerfile) && getRequestedSandboxAgentName(agent) === "openclaw"; - - pendingStateRestoreBackupPath = notReadyRecreate.selectPreUpgradeBackupForCreate({ - liveExists, - hasExistingRegistryEntry: existingEntry !== null, - existingSandboxEntry: existingEntry, - requireOpenClawImagePluginProvenance, + const recreateProtection = createSandboxRecreateProtection({ sandboxName, + sandboxEntry: existingEntry, + customOpenClawImage, note, }); + let pendingStateRestoreBackupPath = recreateProtection.selectPreUpgradeBackup(liveExists); if (liveExists) { const existingSandboxState = getSandboxReuseState(sandboxName); @@ -2552,12 +2546,7 @@ async function createSandboxWithBaseImageResolution( } } else { notReadyRecreateInProgress = true; - const outcome = notReadyRecreate.resolveNotReadyOutcome( - sandboxName, - note, - existingEntry, - requireOpenClawImagePluginProvenance, - ); + const outcome = recreateProtection.resolveNotReadyOutcome(); if (outcome.kind === "blocked") { for (const hint of outcome.hints) console.error(hint); process.exit(1); @@ -2616,11 +2605,7 @@ async function createSandboxWithBaseImageResolution( console.log(` Messaging credential(s) rotated: ${rotatedNames}`); console.log(" Rebuilding sandbox to propagate new credentials to the L7 proxy..."); if (!shouldSkipPreRecreateBackup(process.env)) { - const result = backupSandboxBeforeRecreate({ - sandboxName, - sandboxEntry: existingEntry, - requireOpenClawImagePluginProvenance, - }); + const result = recreateProtection.backup(); if (!result.ok) { console.error( " Set NEMOCLAW_RECREATE_WITHOUT_BACKUP=1 to recreate without preserving state.", @@ -2682,11 +2667,7 @@ async function createSandboxWithBaseImageResolution( !shouldSkipPreRecreateBackup(process.env) ) { note(" Backing up workspace state before recreating sandbox..."); - const result = backupSandboxBeforeRecreate({ - sandboxName, - sandboxEntry: existingEntry, - requireOpenClawImagePluginProvenance, - }); + const result = recreateProtection.backup(); if (!result.ok) { console.error( " Set NEMOCLAW_RECREATE_WITHOUT_BACKUP=1 to recreate without preserving state.", @@ -2990,6 +2971,7 @@ async function createSandboxWithBaseImageResolution( restoreBackupPath, preUpgradeBackup: pendingStateRestoreBackupPath !== null, targetAgentType: agent?.name ?? "openclaw", + discoverOpenClawImagePluginInstalls: customOpenClawImage, validateManagedDcode: isManagedDcodeAgent, provider, model, diff --git a/src/lib/onboard/created-sandbox-finalization.test.ts b/src/lib/onboard/created-sandbox-finalization.test.ts index dc6ca020e8..48d083c7a9 100644 --- a/src/lib/onboard/created-sandbox-finalization.test.ts +++ b/src/lib/onboard/created-sandbox-finalization.test.ts @@ -406,6 +406,38 @@ describe("created OpenClaw sandbox finalization", () => { }, ]; + it("skips image-plugin discovery for a managed OpenClaw image", () => { + const discoverFreshOpenClawImagePluginInstalls = vi.fn(); + const register = vi.fn(); + + finalizeCreatedSandbox( + { + sandboxName: "openclaw", + restoreBackupPath: null, + preUpgradeBackup: false, + targetAgentType: "openclaw", + validateManagedDcode: false, + provider: "compatible-endpoint", + model: "demo", + preferredInferenceApi: "openai-completions", + }, + { + discoverFreshOpenClawImagePluginInstalls, + restoreRecreatedSandboxState: vi.fn(), + getDcodeSelectionDrift: vi.fn(), + register, + note: vi.fn(), + error: vi.fn(), + exitProcess: (code) => { + throw new Error(`unexpected exit ${code}`); + }, + }, + ); + + expect(discoverFreshOpenClawImagePluginInstalls).not.toHaveBeenCalled(); + expect(register).toHaveBeenCalledWith(undefined); + }); + it("captures and registers a fresh image plugin baseline without a restore", () => { const order: string[] = []; const restoreRecreatedSandboxState = vi.fn(); @@ -417,6 +449,7 @@ describe("created OpenClaw sandbox finalization", () => { restoreBackupPath: null, preUpgradeBackup: false, targetAgentType: "openclaw", + discoverOpenClawImagePluginInstalls: true, validateManagedDcode: false, provider: "compatible-endpoint", model: "demo", @@ -463,6 +496,7 @@ describe("created OpenClaw sandbox finalization", () => { restoreBackupPath: "/tmp/openclaw-backup", preUpgradeBackup: false, targetAgentType: "openclaw", + discoverOpenClawImagePluginInstalls: true, validateManagedDcode: false, provider: "compatible-endpoint", model: "demo", @@ -504,6 +538,7 @@ describe("created OpenClaw sandbox finalization", () => { restoreBackupPath: "/tmp/openclaw-backup", preUpgradeBackup: false, targetAgentType: "openclaw", + discoverOpenClawImagePluginInstalls: true, validateManagedDcode: false, provider: "compatible-endpoint", model: "demo", @@ -550,6 +585,7 @@ describe("created OpenClaw sandbox finalization", () => { restoreBackupPath: "/tmp/openclaw-backup", preUpgradeBackup: false, targetAgentType: "openclaw", + discoverOpenClawImagePluginInstalls: true, validateManagedDcode: false, provider: "compatible-endpoint", model: "demo", diff --git a/src/lib/onboard/created-sandbox-finalization.ts b/src/lib/onboard/created-sandbox-finalization.ts index e711009658..1c72953ec4 100644 --- a/src/lib/onboard/created-sandbox-finalization.ts +++ b/src/lib/onboard/created-sandbox-finalization.ts @@ -18,6 +18,7 @@ export type CreatedSandboxFinalizationOptions = { restoreBackupPath: string | null; preUpgradeBackup: boolean; targetAgentType: string; + discoverOpenClawImagePluginInstalls?: boolean; validateManagedDcode: boolean; provider: string; model: string; @@ -51,7 +52,7 @@ export function finalizeCreatedSandbox( deps: CreatedSandboxFinalizationDeps, ): void { let freshOpenClawImagePluginInstalls: readonly OpenClawImagePluginInstall[] | undefined; - if (options.targetAgentType === "openclaw") { + if (options.discoverOpenClawImagePluginInstalls === true) { const discovery = deps.discoverFreshOpenClawImagePluginInstalls(options.sandboxName); if (!discovery.ok) { deps.error( diff --git a/src/lib/onboard/sandbox-recreate-protection.test.ts b/src/lib/onboard/sandbox-recreate-protection.test.ts new file mode 100644 index 0000000000..6da8b2b806 --- /dev/null +++ b/src/lib/onboard/sandbox-recreate-protection.test.ts @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; +import { createSandboxRecreateProtection } from "./sandbox-recreate-protection"; + +describe("createSandboxRecreateProtection", () => { + it("forwards one custom-image protection context to every recreation path (#6108)", () => { + const note = vi.fn(); + const sandboxEntry = { + name: "my-assistant", + agent: "openclaw" as const, + fromDockerfile: "/tmp/Dockerfile.custom", + }; + const selectPreUpgradeBackupForCreate = vi.fn(() => "/tmp/backup"); + const resolveNotReadyOutcome = vi.fn(() => ({ + kind: "proceed" as const, + restoreBackupPath: "/tmp/backup", + })); + const backupResult = { + ok: true, + backup: null, + failureKind: "none" as const, + }; + const backupSandboxBeforeRecreate = vi.fn(() => backupResult); + const protection = createSandboxRecreateProtection( + { + sandboxName: "my-assistant", + sandboxEntry, + customOpenClawImage: true, + note, + }, + { + selectPreUpgradeBackupForCreate, + resolveNotReadyOutcome, + backupSandboxBeforeRecreate, + }, + ); + + expect(protection.selectPreUpgradeBackup(true)).toBe("/tmp/backup"); + expect(selectPreUpgradeBackupForCreate).toHaveBeenCalledWith({ + liveExists: true, + hasExistingRegistryEntry: true, + existingSandboxEntry: sandboxEntry, + requireOpenClawImagePluginProvenance: true, + sandboxName: "my-assistant", + note, + }); + + expect(protection.resolveNotReadyOutcome()).toEqual({ + kind: "proceed", + restoreBackupPath: "/tmp/backup", + }); + expect(resolveNotReadyOutcome).toHaveBeenCalledWith("my-assistant", note, sandboxEntry, true); + + expect(protection.backup()).toBe(backupResult); + expect(backupSandboxBeforeRecreate).toHaveBeenCalledWith({ + sandboxName: "my-assistant", + sandboxEntry, + requireOpenClawImagePluginProvenance: true, + }); + }); +}); diff --git a/src/lib/onboard/sandbox-recreate-protection.ts b/src/lib/onboard/sandbox-recreate-protection.ts new file mode 100644 index 0000000000..94df005ab2 --- /dev/null +++ b/src/lib/onboard/sandbox-recreate-protection.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { SandboxEntry } from "../state/registry"; +import * as notReadyRecreate from "./not-ready-recreate"; +import { + backupSandboxBeforeRecreate, + type PreRecreateBackupResult, +} from "./sandbox-backup-on-recreate"; + +export interface SandboxRecreateProtectionOptions { + sandboxName: string; + sandboxEntry: SandboxEntry | null; + customOpenClawImage: boolean; + note(message: string): void; +} + +interface SandboxRecreateProtectionDeps { + selectPreUpgradeBackupForCreate: typeof notReadyRecreate.selectPreUpgradeBackupForCreate; + resolveNotReadyOutcome: typeof notReadyRecreate.resolveNotReadyOutcome; + backupSandboxBeforeRecreate: typeof backupSandboxBeforeRecreate; +} + +const defaultDeps: SandboxRecreateProtectionDeps = { + selectPreUpgradeBackupForCreate: notReadyRecreate.selectPreUpgradeBackupForCreate, + resolveNotReadyOutcome: notReadyRecreate.resolveNotReadyOutcome, + backupSandboxBeforeRecreate, +}; + +/** Bind the shared state-preservation checks used by every onboard recreation path. */ +export function createSandboxRecreateProtection( + options: SandboxRecreateProtectionOptions, + deps: SandboxRecreateProtectionDeps = defaultDeps, +) { + const { sandboxName, sandboxEntry, customOpenClawImage, note } = options; + + return { + selectPreUpgradeBackup(liveExists: boolean): string | null { + return deps.selectPreUpgradeBackupForCreate({ + liveExists, + hasExistingRegistryEntry: sandboxEntry !== null, + existingSandboxEntry: sandboxEntry, + requireOpenClawImagePluginProvenance: customOpenClawImage, + sandboxName, + note, + }); + }, + resolveNotReadyOutcome(): notReadyRecreate.NonInteractiveNotReadyOutcome { + return deps.resolveNotReadyOutcome(sandboxName, note, sandboxEntry, customOpenClawImage); + }, + backup(): PreRecreateBackupResult { + return deps.backupSandboxBeforeRecreate({ + sandboxName, + sandboxEntry, + requireOpenClawImagePluginProvenance: customOpenClawImage, + }); + }, + }; +} diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 332cf2f9e6..b92a6e3355 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -2863,7 +2863,7 @@ const { createSandbox } = require(${onboardPath}); const restoreEvent = events[restoreIndex]; assert.equal(restoreEvent?.backupPath, "/tmp/fake-backup-path", "restore must use backup path"); assert.equal(restoreEvent?.options?.targetAgentType, "openclaw"); - assert.deepEqual(restoreEvent?.options?.freshOpenClawImagePluginInstalls, []); + assert.equal(restoreEvent?.options?.freshOpenClawImagePluginInstalls, undefined); }); it("recreate-sandbox with NEMOCLAW_RECREATE_WITHOUT_BACKUP=1 skips backup", { From a4d503141f8cb6c2f74da45200efd755f7e4c259 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 6 Jul 2026 16:59:35 -0700 Subject: [PATCH 40/46] test(e2e): require pinned openshell version Signed-off-by: Apurv Kumaria --- test/e2e/live/openclaw-plugin-runtime-exdev.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index 6c4d83f332..d997b720de 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -258,7 +258,7 @@ async function installAndResolvePinnedOpenShell( host: HostCliClient, installScriptPath: string, artifactLabel: string, - expectedVersion?: string, + expectedVersion: string, ): Promise { const install = await host.command("bash", [installScriptPath], { artifactName: `install-${artifactLabel}-openshell-for-exdev-wrapper`, @@ -275,11 +275,9 @@ async function installAndResolvePinnedOpenShell( timeoutMs: 30_000, }); expect(version.exitCode, resultText(version)).toBe(0); - if (expectedVersion) { - expect(resultText(version)).toMatch( - new RegExp(`\\b${expectedVersion.replaceAll(".", "\\.")}\\b`), - ); - } + expect(resultText(version)).toMatch( + new RegExp(`\\b${expectedVersion.replaceAll(".", "\\.")}\\b`), + ); return components; } From 1cc994f94ab68da70e3704ebe9f52853335e4a7b Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 6 Jul 2026 17:17:50 -0700 Subject: [PATCH 41/46] test(onboard): model empty image plugin registry Signed-off-by: Apurv Kumaria --- test/onboard-custom-dockerfile.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/onboard-custom-dockerfile.test.ts b/test/onboard-custom-dockerfile.test.ts index 24cff22442..bcd6b641d5 100644 --- a/test/onboard-custom-dockerfile.test.ts +++ b/test/onboard-custom-dockerfile.test.ts @@ -200,6 +200,20 @@ const { EventEmitter } = require("node:events"); const fs = require("node:fs"); const path = require("node:path"); +const originalSpawnSync = childProcess.spawnSync; +childProcess.spawnSync = (command, args, options) => { + const normalized = _n([command, ...(Array.isArray(args) ? args : [])]); + if (command === "ssh" && normalized.includes("installed_plugin_index")) { + return { + status: 0, + signal: null, + stdout: Buffer.from(JSON.stringify({ version: 1, installRecords: {}, loadPaths: [] })), + stderr: Buffer.alloc(0), + }; + } + return originalSpawnSync(command, args, options); +}; + const commands = []; let hasExtraFileAtSpawn = false; let stagedIgnoredFilesAtSpawn = null; From 592939e8ea0c402168078d0ee53d97b603a156ff Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 6 Jul 2026 17:18:44 -0700 Subject: [PATCH 42/46] test(e2e): isolate release plugin fixture credentials Signed-off-by: Apurv Kumaria --- .github/workflows/e2e.yaml | 20 ++++++++++- ...eather-plugin-fixture-dependency-review.md | 3 +- .../openclaw-plugin-runtime-exdev.test.ts | 20 ++--------- ...in-runtime-exdev-workflow-boundary.test.ts | 36 ++++++++++++++----- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 00832e5178..121c9dbb25 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -3691,13 +3691,31 @@ jobs: - *dockerhub-auth + - name: Pre-pull release-matched Docker Hub builder image + shell: bash + run: | + set -euo pipefail + docker pull node:22-trixie-slim@sha256:2d9f5c76c8f4dd36e8f253bee5d828a83a6c09f36188f0b0414325232e0b175d + + - name: Remove Docker auth before release-pinned fixture + if: always() + shell: bash + run: | + set -euo pipefail + bash .github/scripts/docker-auth-cleanup.sh + - name: Prepare E2E workspace uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@50281ee84c4a6fc759da95ea28fc0b7d9c378a28 - name: Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV live test run: | set -euo pipefail - npx vitest run --project e2e-live \ + test -n "${DOCKER_CONFIG:-}" + test ! -e "${DOCKER_CONFIG}" + test -z "${DOCKERHUB_USERNAME:-}" + test -z "${DOCKERHUB_TOKEN:-}" + env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN \ + npx vitest run --project e2e-live \ test/e2e/live/openclaw-plugin-runtime-exdev.test.ts \ --silent=false --reporter=default diff --git a/docs/security/e2e-weather-plugin-fixture-dependency-review.md b/docs/security/e2e-weather-plugin-fixture-dependency-review.md index 2cf6d98a5c..d6804ce314 100644 --- a/docs/security/e2e-weather-plugin-fixture-dependency-review.md +++ b/docs/security/e2e-weather-plugin-fixture-dependency-review.md @@ -29,7 +29,8 @@ The release-matched `openclaw@2026.5.27` development graph currently has known a - The committed npm lockfile records registry integrity for the resolved dependency graph. - Every fixture install uses `npm ci --ignore-scripts`; the Docker build also uses `--no-audit --no-fund` and prunes development and peer dependencies before staging the plugin. - The image build fails if a private `node_modules/openclaw` remains, then verifies that OpenClaw creates the expected link to the stock global runtime. -- The GitHub Actions job has read-only `contents` permission, uses full-SHA-pinned actions, disables checkout credential persistence, and receives no repository secrets. +- The GitHub Actions job has read-only `contents` permission, uses full-SHA-pinned actions, and disables checkout credential persistence. Trusted runs use Docker Hub credentials only to pre-pull the digest-pinned builder image; the workflow then removes Docker auth, and the release-pinned fixture execution receives no repository secrets or Docker credential environment variables. +- The historical NemoClaw `v0.0.71` exercise uses that tagged CLI's onboarding preflight as its OpenShell compatibility boundary instead of applying current-release capability markers to the older stack. - The lane is isolated to deterministic test data and uploads only its path-scoped E2E artifact directory. ## Advisory Audit diff --git a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts index d997b720de..f9ce965b6d 100644 --- a/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts +++ b/test/e2e/live/openclaw-plugin-runtime-exdev.test.ts @@ -1022,26 +1022,10 @@ liveTest( "v0-0-71", NEMOCLAW_RELEASE_OPENSHELL_VERSION, ); - expect( - hasRequiredOpenshellMessagingFeatures({ - openshellBin: taggedPinnedOpenshell.cli, - gatewayBin: taggedPinnedOpenshell.gateway, - sandboxBin: taggedPinnedOpenshell.sandbox, - }), - "v0.0.71 OpenShell components must pass coherence preflight before delegation", - ).toBe(true); + // OpenShell 0.0.71 predates the current 0.0.72 MCP capability marker. + // The tagged CLI's own onboarding preflight below owns this compatibility check. const taggedOpenShellWrapper = createOpenShellTmpfsWrapper(taggedPinnedOpenshell.cli); cleanup.add("remove v0.0.71 EXDEV OpenShell PATH wrapper", taggedOpenShellWrapper.remove); - expect( - hasRequiredOpenshellMessagingFeatures({ - openshellBin: taggedOpenShellWrapper.executable, - gatewayBin: taggedPinnedOpenshell.gateway, - sandboxBin: taggedPinnedOpenshell.sandbox, - allowExternalGatewayBin: true, - allowExternalSandboxBin: true, - }), - "v0.0.71 OpenShell wrapper and components must pass onboard coherence preflight", - ).toBe(true); const deploymentEnv = liveEnv({ COMPATIBLE_API_KEY: "nemoclaw-exdev-dummy-key", diff --git a/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts b/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts index 11e8bee7f1..49fb453fb0 100644 --- a/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts +++ b/test/e2e/support/openclaw-plugin-runtime-exdev-workflow-boundary.test.ts @@ -13,6 +13,8 @@ import { readWorkflow } from "../../helpers/e2e-workflow-contract"; const JOB_ID = "openclaw-plugin-runtime-exdev"; const CHECKOUT_ACTION = "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10"; +const RELEASE_BUILDER_IMAGE = + "node:22-trixie-slim@sha256:2d9f5c76c8f4dd36e8f253bee5d828a83a6c09f36188f0b0414325232e0b175d"; const SELECTOR_CONDITION = "${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',openclaw-plugin-runtime-exdev,') || contains(format(',{0},', inputs.targets), ',openclaw-plugin-runtime-exdev,') }}"; @@ -69,29 +71,47 @@ describe("OpenClaw plugin runtime EXDEV workflow boundary", () => { expect(job.env).not.toHaveProperty("NVIDIA_INFERENCE_API_KEY"); const steps = job.steps ?? []; - expect(steps).toHaveLength(6); + expect(steps).toHaveLength(8); expect(steps[0]).toEqual({ uses: CHECKOUT_ACTION, with: { "persist-credentials": false }, }); expect(steps[1]?.name).toBe("Authenticate to Docker Hub"); expect(steps[2]).toEqual({ + name: "Pre-pull release-matched Docker Hub builder image", + shell: "bash", + run: `set -euo pipefail\ndocker pull ${RELEASE_BUILDER_IMAGE}\n`, + }); + expect(steps[3]).toEqual({ + name: "Remove Docker auth before release-pinned fixture", + if: "always()", + shell: "bash", + run: "set -euo pipefail\n" + "bash .github/scripts/docker-auth-cleanup.sh\n", + }); + expect(steps[4]).toEqual({ name: "Prepare E2E workspace", uses: PREPARE_E2E_ACTION, }); - expect(steps[3]?.name).toBe( + expect(steps[5]?.name).toBe( "Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV live test", ); - expect(steps[3]?.run).toContain("npx vitest run --project e2e-live"); - expect(steps[3]?.run).toContain("test/e2e/live/openclaw-plugin-runtime-exdev.test.ts"); - expect(steps[3]).not.toHaveProperty("env"); - expect(JSON.stringify(steps[3])).not.toContain("secrets."); - expect(steps[4]).toEqual({ + expect(steps[5]?.run).toContain('test -n "${DOCKER_CONFIG:-}"'); + expect(steps[5]?.run).toContain('test ! -e "${DOCKER_CONFIG}"'); + expect(steps[5]?.run).toContain('test -z "${DOCKERHUB_USERNAME:-}"'); + expect(steps[5]?.run).toContain('test -z "${DOCKERHUB_TOKEN:-}"'); + expect(steps[5]?.run).toContain( + "env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN", + ); + expect(steps[5]?.run).toContain("npx vitest run --project e2e-live"); + expect(steps[5]?.run).toContain("test/e2e/live/openclaw-plugin-runtime-exdev.test.ts"); + expect(steps[5]).not.toHaveProperty("env"); + expect(JSON.stringify(steps[5])).not.toContain("secrets."); + expect(steps[6]).toEqual({ name: "Upload OpenClaw plugin runtime-deps EXDEV artifacts", if: "always()", uses: UPLOAD_E2E_ARTIFACTS_ACTION, }); - expect(steps[5]).toEqual({ + expect(steps[7]).toEqual({ name: "Clean up Docker auth", if: "always()", shell: "bash", From f95e167cd6131efdaf2f4f055559b346f334324e Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 6 Jul 2026 17:19:08 -0700 Subject: [PATCH 43/46] chore(onboard): mark runtime diagnosis removal Signed-off-by: Apurv Kumaria --- src/lib/onboard/custom-openclaw-runtime-diagnosis.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts b/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts index cafa54a08e..b4089f6d76 100644 --- a/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts +++ b/src/lib/onboard/custom-openclaw-runtime-diagnosis.ts @@ -37,8 +37,9 @@ const OPENCLAW_RUNTIME_PROBE = * earlier would require reliable static analysis of arbitrary multi-stage * Dockerfiles or a new build-time image contract. The focused classifier tests * prevent false positives, and the live custom-plugin E2E proves the supported - * full-runtime path. Remove this runtime fallback when #5998 supplies the - * managed plugin lifecycle or a build-time contract validator replaces it. + * full-runtime path. + * REMOVE-WHEN: #5998 supplies the managed plugin lifecycle, or a build-time + * image contract validator replaces this runtime fallback. */ export function shouldDiagnoseCustomOpenClawRuntime( From ad6627251ceebd8461a1626a84494bda5c50168a Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Tue, 7 Jul 2026 12:00:44 -0700 Subject: [PATCH 44/46] docs(rebuild): clarify custom-image recovery Signed-off-by: Apurv Kumaria --- docs/get-started/quickstart.mdx | 3 ++- docs/manage-sandboxes/lifecycle.mdx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 5a98ebf47c..b27c61738d 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -122,7 +122,8 @@ After the host upgrade, it runs `nemoclaw upgrade-sandboxes --auto` to rebuild s Successful recovery completes the existing-sandbox upgrade and skips generic onboarding, so the installer does not create an extra sandbox or ask for a new provider credential. For pre-fingerprint OpenClaw and Hermes registry entries, the installer asks you to confirm that every listed sandbox used a NemoClaw-managed image before it permits recovery onto the current managed image. In non-interactive runs, set `NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE` to the exact JSON array of names printed by the installer, such as `["my-assistant","preserve-hermes"]`, only after you verify every named sandbox used a managed image. -Registry entries with recorded custom-image evidence remain blocked from automatic recreation. +Legacy managed-image confirmation never overrides recorded custom-image evidence. +A custom OpenClaw sandbox can be recovered only when the selected validated backup independently carries complete authoritative image-plugin provenance; otherwise recovery stops before deletion. If any backup is skipped or fails, the installer exits with a nonzero status before it changes the gateway. If an automatic rebuild fails or a non-Ready recovery is blocked or fails, the installer exits with a nonzero status and does not start generic onboarding. diff --git a/docs/manage-sandboxes/lifecycle.mdx b/docs/manage-sandboxes/lifecycle.mdx index 551f3a5fcc..13d1f315f5 100644 --- a/docs/manage-sandboxes/lifecycle.mdx +++ b/docs/manage-sandboxes/lifecycle.mdx @@ -272,7 +272,8 @@ After the host CLI and OpenShell update, the installer runs `$$nemoclaw upgrade- If an existing sandbox is not Ready, the automatic path requires a validated latest backup whose sandbox and agent identity match the registry and positive evidence that NemoClaw managed the image. For a listed pre-fingerprint OpenClaw or Hermes registry entry, you can provide that evidence through the installer's explicit managed-image confirmation. In a non-interactive run, set `NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE` to the exact JSON array of names printed by the installer, such as `["my-assistant","preserve-hermes"]`, only after you verify every named sandbox used a managed image. -Recorded custom-image evidence remains blocked from automatic recreation. +Legacy managed-image confirmation never overrides recorded custom-image evidence. +A custom OpenClaw sandbox can be recovered only when the selected validated backup independently carries complete authoritative image-plugin provenance; otherwise recovery stops before deletion. The installer attempts every eligible recovery, exits with a nonzero status if any recovery fails, and skips generic onboarding after successful recovery. For manual upgrade flows, create a snapshot first and then run the update or rebuild command you need: From 1b979779453a5af4eac778b89e345c9093103ba6 Mon Sep 17 00:00:00 2001 From: cjagwani Date: Wed, 8 Jul 2026 19:21:00 +0000 Subject: [PATCH 45/46] test(ci): ratchet merged image test size Signed-off-by: cjagwani --- ci/test-file-size-budget.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index edb852d722..700f372988 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -7,6 +7,7 @@ "src/lib/onboard/preflight.test.ts": 1904, "test/generate-openclaw-config.test.ts": 1941, "test/install-preflight.test.ts": 3934, + "test/langchain-deepagents-code-image.test.ts": 1501, "test/nemoclaw-start.test.ts": 4826, "test/onboard-messaging.test.ts": 2049, "test/onboard-selection.test.ts": 4774, From e4c712ec57439455fbc919b174744a55ae671d88 Mon Sep 17 00:00:00 2001 From: cjagwani Date: Wed, 8 Jul 2026 19:22:41 +0000 Subject: [PATCH 46/46] test(ci): keep merged image test within ceiling Signed-off-by: cjagwani --- ci/test-file-size-budget.json | 1 - test/langchain-deepagents-code-image.test.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index 700f372988..edb852d722 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -7,7 +7,6 @@ "src/lib/onboard/preflight.test.ts": 1904, "test/generate-openclaw-config.test.ts": 1941, "test/install-preflight.test.ts": 3934, - "test/langchain-deepagents-code-image.test.ts": 1501, "test/nemoclaw-start.test.ts": 4826, "test/onboard-messaging.test.ts": 2049, "test/onboard-selection.test.ts": 4774, diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 44112c4cb5..a9d4bcfaf3 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -37,7 +37,6 @@ function fakePrivateKeyBlock(type = "", newline = "\\n"): string { const label = type ? `${type} PRIVATE KEY-----` : "PRIVATE KEY-----"; return `-----BEGIN ${label} ${newline}opaque-test-body${newline}-----END ${label}`; } - const repoRoot = path.resolve(import.meta.dirname, ".."); const agentDir = path.join(repoRoot, "agents", "langchain-deepagents-code"); const tuiStartupCheckPath = path.join(