diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index 43126e85bf9..d6340d124e8 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -21,7 +21,7 @@ on: jobs: description: >- Comma-separated regression job names to run (empty = all). - Valid: dashboard-remote-bind-e2e,gateway-health-honest-e2e,gateway-drift-preflight-e2e,openshell-version-pin-e2e,onboard-inference-smoke-e2e,model-router-provider-routed-inference-e2e + Valid: dashboard-remote-bind-e2e,gateway-health-honest-e2e,gateway-drift-preflight-e2e,openshell-version-pin-e2e,onboard-inference-smoke-e2e,model-router-provider-routed-inference-e2e,openclaw-plugin-runtime-exdev-e2e required: false type: string default: "" @@ -50,6 +50,7 @@ jobs: openshell_version_pin: ${{ steps.select.outputs.openshell_version_pin }} onboard_inference_smoke: ${{ steps.select.outputs.onboard_inference_smoke }} model_router_provider_routed_inference: ${{ steps.select.outputs.model_router_provider_routed_inference }} + openclaw_plugin_runtime_exdev: ${{ steps.select.outputs.openclaw_plugin_runtime_exdev }} steps: - id: select env: @@ -101,6 +102,12 @@ jobs: echo "model_router_provider_routed_inference=false" >> "$GITHUB_OUTPUT" fi + if [ -z "$normalized" ] || includes_job "openclaw-plugin-runtime-exdev-e2e"; then + echo "openclaw_plugin_runtime_exdev=true" >> "$GITHUB_OUTPUT" + else + echo "openclaw_plugin_runtime_exdev=false" >> "$GITHUB_OUTPUT" + fi + dashboard-remote-bind-e2e: needs: select_regression_jobs if: >- @@ -290,3 +297,37 @@ jobs: /tmp/nemoclaw-e2e-model-router-health.log /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 + # 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. + openclaw-plugin-runtime-exdev-e2e: + needs: select_regression_jobs + if: >- + github.repository == 'NVIDIA/NemoClaw' && + needs.select_regression_jobs.outputs.openclaw_plugin_runtime_exdev == 'true' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run OpenClaw plugin runtime-deps EXDEV E2E test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + run: bash test/e2e/test-openclaw-plugin-runtime-exdev.sh + + - name: Upload OpenClaw plugin runtime-deps EXDEV logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: openclaw-plugin-runtime-exdev-logs + path: | + /tmp/nemoclaw-e2e-openclaw-plugin-exdev-onboard.log + /tmp/nemoclaw-e2e-openclaw-plugin-exdev-agent.log + /tmp/nemoclaw-e2e-openclaw-plugin-exdev-df.log + if-no-files-found: ignore diff --git a/test/e2e/docs/parity-map.yaml b/test/e2e/docs/parity-map.yaml index efa7932f8bd..4da0e2435a4 100644 --- a/test/e2e/docs/parity-map.yaml +++ b/test/e2e/docs/parity-map.yaml @@ -5680,6 +5680,136 @@ scripts: reason: live legacy behavior requires non-deterministic infrastructure; retained for bucket parity tracking owner: e2e-maintainers secret_requirement: WeChat test credentials + - legacy: 'M-WA0: channels add whatsapp registered QR-only channel' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA0: channels add whatsapp failed or did not register channel' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA1: Unexpected WhatsApp bridge provider exists in gateway' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA1: WhatsApp QR-only channel creates no bridge provider' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA2: registry.messagingChannels contains whatsapp after channel add' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA2: registry.messagingChannels missing whatsapp after channel add ($(registry_field messagingChannels))' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA3: WhatsApp policy preset applied before rebuild' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA3: WhatsApp policy preset missing expected endpoints before rebuild' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA4: Rebuild completed after WhatsApp channel add' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA4: Rebuild failed after WhatsApp channel add' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA5: WhatsApp policy preset survived rebuild with Node binary scope' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA5: WhatsApp policy preset missing expected endpoints/binaries after rebuild' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA6: Sandbox ''$SANDBOX_NAME'' is Ready after WhatsApp rebuild' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA6: Sandbox ''$SANDBOX_NAME'' not Ready after WhatsApp rebuild (list: ${sandbox_list:0:200})' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA7a: WhatsApp credential-like env var found in sandbox environment' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA7a: No WhatsApp credential-like env var present in sandbox environment' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA7b: WhatsApp credential placeholder found in sandbox process list' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA7b: No WhatsApp credential placeholder present in sandbox process list' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA7c: WhatsApp host credential material found on sandbox filesystem: ${sandbox_fs_wa}' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA7c: No WhatsApp host credential material found on sandbox filesystem' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA8: WhatsApp account is enabled in openclaw.json' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA8: WhatsApp account missing or disabled in openclaw.json (${whatsapp_account_json:0:200})' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA8a: WhatsApp health monitor is disabled for unpaired QR session' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA8a: WhatsApp health monitor is not disabled (${whatsapp_account_json:0:200})' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA9: WhatsApp config has no token/auth/session provider placeholders' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support + - legacy: 'M-WA9: WhatsApp config contains secret-like fields: ${whatsapp_secret_fields}' + status: deferred + reason: live WhatsApp QR-only behavior requires non-deterministic infrastructure; retained for bucket parity tracking + owner: e2e-maintainers + runner_requirement: sandbox runner with WhatsApp QR channel support test-network-policy.sh: scenario: ubuntu-repo-cloud-openclaw status: migrated @@ -6850,6 +6980,76 @@ scripts: reason: legacy assertion is obsolete or negative cleanup behavior after scenario migration reviewer: e2e-maintainers approved_at: '2026-05-13' + test-openclaw-plugin-runtime-exdev.sh: + scenario: '' + status: deferred + bucket: openclaw-plugin-runtime-exdev + assertions: + - legacy: Docker is running + status: deferred + reason: regression guard validates #3513 before migration to scenario framework + owner: e2e-maintainers + runner_requirement: sandbox runner with Docker and NemoClaw/OpenShell CLIs + - legacy: Docker is not running + status: deferred + reason: prerequisite failure for #3513 regression guard + owner: e2e-maintainers + runner_requirement: sandbox runner with Docker and NemoClaw/OpenShell CLIs + - legacy: NVIDIA_API_KEY is set + status: deferred + reason: regression guard validates #3513 with live provider credentials before scenario migration + owner: e2e-maintainers + runner_requirement: sandbox runner with NVIDIA_API_KEY + - legacy: NVIDIA_API_KEY is required and must start with nvapi- + status: deferred + reason: prerequisite failure for #3513 regression guard + owner: e2e-maintainers + runner_requirement: sandbox runner with NVIDIA_API_KEY + - legacy: 'nemoclaw is available: $(nemoclaw --version 2>/dev/null || echo unknown)' + status: deferred + reason: regression guard validates checkout install before scenario migration + owner: e2e-maintainers + runner_requirement: local CLI build with installer + - legacy: nemoclaw not found after install + status: deferred + reason: prerequisite failure for #3513 regression guard + owner: e2e-maintainers + runner_requirement: local CLI build with installer + - legacy: fresh sandbox onboard completed + status: deferred + reason: regression guard validates #3513 on a fresh sandbox before scenario migration + owner: e2e-maintainers + runner_requirement: sandbox runner with Docker and OpenShell + - legacy: fresh sandbox onboard failed (exit ${onboard_rc}); see ${ONBOARD_LOG} + status: deferred + reason: setup failure evidence for #3513 regression guard + owner: e2e-maintainers + runner_requirement: sandbox runner with Docker and OpenShell + - legacy: 'OpenClaw-style plugin runtime deps replacement hit #3513 EXDEV failure' + status: deferred + reason: executable red assertion for #3513 regression guard + owner: e2e-maintainers + runner_requirement: sandbox runner with cross-filesystem /dev/shm and /sandbox paths + - legacy: runtime deps replacement exited ${agent_rc}; see ${AGENT_LOG} + status: deferred + reason: runtime failure evidence for #3513 regression guard + owner: e2e-maintainers + runner_requirement: sandbox runner with OpenShell exec + - legacy: OpenClaw-style plugin runtime-deps replacement completed across filesystems + status: deferred + reason: expected green assertion after #3513 fix before migration to scenario framework + owner: e2e-maintainers + runner_requirement: sandbox runner with cross-filesystem /dev/shm and /sandbox paths + - legacy: runtime deps replacement exited 0 but success marker was missing; see ${AGENT_LOG} + status: deferred + reason: runtime success-marker failure evidence for #3513 regression guard + owner: e2e-maintainers + runner_requirement: sandbox runner with OpenShell exec + - legacy: OpenClaw plugin runtime-deps EXDEV guard passed + status: deferred + reason: expected green summary after #3513 fix before migration to scenario framework + owner: e2e-maintainers + runner_requirement: sandbox runner with OpenShell exec test-openshell-gateway-upgrade.sh: scenario: ubuntu-repo-cloud-openclaw status: migrated diff --git a/test/e2e/test-openclaw-plugin-runtime-exdev.sh b/test/e2e/test-openclaw-plugin-runtime-exdev.sh new file mode 100755 index 00000000000..55554501491 --- /dev/null +++ b/test/e2e/test-openclaw-plugin-runtime-exdev.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Coverage guard for #3513 / #3127 — a fresh sandbox must be able to run the +# first OpenClaw CLI invocation without bundled plugin runtime-deps failing on +# EXDEV cross-device rename. + +set -uo pipefail + +PASS=0 +FAIL=0 +TOTAL=0 + +pass() { + ((PASS++)) + ((TOTAL++)) + echo " OK: $1" +} +fail() { + ((FAIL++)) + ((TOTAL++)) + echo " ERROR: $1" +} +section() { + echo "" + printf '\033[1;36m=== %s ===\033[0m\n' "$1" +} +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "${SCRIPT_DIR}/../.." && pwd)" +SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-openclaw-plugin-exdev}" +ONBOARD_LOG="${E2E_OPENCLAW_PLUGIN_EXDEV_ONBOARD_LOG:-/tmp/nemoclaw-e2e-openclaw-plugin-exdev-onboard.log}" +AGENT_LOG="${E2E_OPENCLAW_PLUGIN_EXDEV_AGENT_LOG:-/tmp/nemoclaw-e2e-openclaw-plugin-exdev-agent.log}" +DF_LOG="${E2E_OPENCLAW_PLUGIN_EXDEV_DF_LOG:-/tmp/nemoclaw-e2e-openclaw-plugin-exdev-df.log}" +TIMEOUT_CMD="${TIMEOUT_CMD:-timeout}" + +# shellcheck source=test/e2e/lib/install-path-refresh.sh +. "${SCRIPT_DIR}/lib/install-path-refresh.sh" +# shellcheck source=test/e2e/lib/sandbox-teardown.sh +. "${SCRIPT_DIR}/lib/sandbox-teardown.sh" +register_sandbox_for_teardown "$SANDBOX_NAME" + +redact_file() { + local file="$1" + [ -f "$file" ] || return 0 + python3 - "$file" <<'PY' +import os, sys +path = sys.argv[1] +secrets = [os.environ.get("NVIDIA_API_KEY", ""), os.environ.get("NEMOCLAW_PROVIDER_KEY", "")] +text = open(path, "r", errors="replace").read() +for secret in filter(None, secrets): + text = text.replace(secret, "") +open(path, "w").write(text) +PY +} + +redact_logs() { + redact_file "$ONBOARD_LOG" + redact_file "$AGENT_LOG" + redact_file "$DF_LOG" +} +trap redact_logs EXIT + +section "Prerequisites" +if docker info >/dev/null 2>&1; then + pass "Docker is running" +else + fail "Docker is not running" + exit 1 +fi + +if [ -n "${NVIDIA_API_KEY:-}" ] && [[ "${NVIDIA_API_KEY}" == nvapi-* ]]; then + pass "NVIDIA_API_KEY is set" +else + fail "NVIDIA_API_KEY is required and must start with nvapi-" + exit 1 +fi + +section "Install NemoClaw from checkout" +if ! command -v nemoclaw >/dev/null 2>&1; then + NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ + bash "${REPO}/install.sh" --non-interactive --yes-i-accept-third-party-software >"$ONBOARD_LOG" 2>&1 || true + nemoclaw_refresh_install_env +fi + +if command -v nemoclaw >/dev/null 2>&1; then + pass "nemoclaw is available: $(nemoclaw --version 2>/dev/null || echo unknown)" +else + fail "nemoclaw not found after install" + exit 1 +fi + +section "Fresh sandbox onboard" +rm -f "$HOME/.nemoclaw/onboard.lock" 2>/dev/null || true +nemoclaw "$SANDBOX_NAME" destroy --yes >/dev/null 2>&1 || true + +python3 - "${REPO}" <<'PY' +import sys +from pathlib import Path +repo = Path(sys.argv[1]) +policy_paths = [ + repo / "agents/openclaw/policy-permissive.yaml", + repo / "nemoclaw-blueprint/policies/openclaw-sandbox.yaml", + repo / "nemoclaw-blueprint/policies/openclaw-sandbox-permissive.yaml", +] +for path in policy_paths: + text = path.read_text() + needle = " read_write:\n - /tmp\n" + if needle not in text: + raise SystemExit(f"could not find read_write /tmp anchor in {path}") + additions = "" + for entry in ["/dev", "/dev/shm"]: + if f" - {entry}\n" not in text: + additions += f" - {entry}\n" + if additions: + path.write_text(text.replace(needle, needle + additions, 1)) +PY +env \ + NEMOCLAW_PROVIDER_KEY="$NVIDIA_API_KEY" \ + NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" \ + NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ + NEMOCLAW_POLICY_MODE="skip" \ + NEMOCLAW_PROVIDER="build" \ + NVIDIA_API_KEY="$NVIDIA_API_KEY" \ + "$TIMEOUT_CMD" 1500 nemoclaw onboard --fresh --non-interactive --yes-i-accept-third-party-software --agent openclaw --from "$REPO/Dockerfile" \ + >"$ONBOARD_LOG" 2>&1 +onboard_rc=$? +redact_logs +if [ "$onboard_rc" -eq 0 ]; then + pass "fresh sandbox onboard completed" +else + fail "fresh sandbox onboard failed (exit ${onboard_rc}); see ${ONBOARD_LOG}" + exit 1 +fi + +section "Filesystem layout evidence" +openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc 'df -PT / /tmp /dev/shm /sandbox /sandbox/.openclaw/plugin-runtime-deps 2>&1' \ + >"$DF_LOG" 2>&1 || true +redact_logs +info "Filesystem layout captured in ${DF_LOG}" + +section "Bundled plugin runtime-deps cross-device replacement" +agent_rc=0 +# Reproduce the precise #3513 failure mode without depending on OpenClaw's +# broader CLI temp/log initialization: the vulnerable helper copies dependency +# contents into a staging dir adjacent to the source and then renameSyncs that +# staged node_modules dir into the final plugin-runtime-deps target. When source +# is on tmpfs (/dev/shm) and target is under /sandbox, unfixed code throws EXDEV. +remote_script_b64=$( + cat <<'REMOTE' | base64 | tr -d '\n' +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 +printf 'ok\n' >/dev/shm/nemoclaw-exdev-source/package.txt +node --input-type=module - <<'NODE' +import fs from 'node:fs'; +import path from 'node:path'; +function replaceNodeModulesDir(targetDir, sourceDir) { + const parentDir = path.dirname(sourceDir); + fs.mkdirSync(path.dirname(targetDir), { recursive: true }); + const tempDir = fs.mkdtempSync(path.join(parentDir, '.openclaw-runtime-deps-copy-')); + const stagedDir = path.join(tempDir, 'node_modules'); + try { + fs.cpSync(sourceDir, stagedDir, { recursive: true }); + fs.rmSync(targetDir, { recursive: true, force: true }); + fs.renameSync(stagedDir, targetDir); + } finally { + try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {} + } +} +replaceNodeModulesDir('/sandbox/.openclaw/plugin-runtime-deps/exdev-guard/node_modules', '/dev/shm/nemoclaw-exdev-source'); +console.log('runtime deps replacement completed'); +NODE +REMOTE +) +remote_cmd="printf '%s' '${remote_script_b64}' | base64 -d > /tmp/nemoclaw-exdev-guard.sh && sh /tmp/nemoclaw-exdev-guard.sh" +"$TIMEOUT_CMD" 60 openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc "$remote_cmd" \ + >"$AGENT_LOG" 2>&1 || agent_rc=$? +redact_logs + +if grep -qiE 'EXDEV: cross-device link not permitted|cross-device link not permitted' "$AGENT_LOG"; then + fail "OpenClaw-style plugin runtime deps replacement hit #3513 EXDEV failure" + info "Runtime-deps log excerpt: $(grep -iE 'EXDEV|cross-device link not permitted' "$AGENT_LOG" | head -5 | tr '\n' ' ')" + exit 1 +fi + +if [ "$agent_rc" -ne 0 ]; then + fail "runtime deps replacement exited ${agent_rc}; see ${AGENT_LOG}" + exit 1 +fi + +if grep -q 'runtime deps replacement completed' "$AGENT_LOG"; then + pass "OpenClaw-style plugin runtime-deps replacement completed across filesystems" +else + fail "runtime deps replacement exited 0 but success marker was missing; see ${AGENT_LOG}" + exit 1 +fi + +section "Summary" +if [ "$FAIL" -eq 0 ]; then + pass "OpenClaw plugin runtime-deps EXDEV guard passed" + exit 0 +fi +exit 1