#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugins-e2e" OPENCLAW_PLUGINS_E2E_IMAGE)" docker_e2e_build_or_reuse "$IMAGE_NAME" plugins DOCKER_ENV_ARGS=(-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0) for env_name in \ OPENCLAW_PLUGINS_E2E_CLAWHUB \ OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC \ OPENCLAW_PLUGINS_E2E_CLAWHUB_ID \ OPENCLAW_CLAWHUB_URL \ CLAWHUB_URL \ OPENCLAW_CLAWHUB_TOKEN \ CLAWHUB_TOKEN \ CLAWHUB_AUTH_TOKEN; do env_value="${!env_name:-}" if [[ -n "$env_value" && "$env_value" != "undefined" && "$env_value" != "null" ]]; then DOCKER_ENV_ARGS+=(-e "$env_name") fi done echo "Running plugins Docker E2E..." RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-plugins-run.XXXXXX")" if ! docker run --rm "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" bash -s >"$RUN_LOG" 2>&1 <<'EOF' set -euo pipefail if [ -f dist/index.mjs ]; then OPENCLAW_ENTRY="dist/index.mjs" elif [ -f dist/index.js ]; then OPENCLAW_ENTRY="dist/index.js" else echo "Missing dist/index.(m)js (build output):" ls -la dist || true exit 1 fi export OPENCLAW_ENTRY home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX") export HOME="$home_dir" BUNDLED_PLUGIN_ROOT_DIR="extensions" OPENCLAW_PLUGIN_HOME="$HOME/.openclaw/$BUNDLED_PLUGIN_ROOT_DIR" record_fixture_plugin_trust() { local plugin_id="$1" local plugin_root="$2" local enabled="$3" node - <<'NODE' "$plugin_id" "$plugin_root" "$enabled" const fs = require("node:fs"); const path = require("node:path"); const pluginId = process.argv[2]; const pluginRoot = process.argv[3]; const enabled = process.argv[4] === "1"; const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {}; const plugins = (config.plugins ??= {}); const entries = (plugins.entries ??= {}); entries[pluginId] = { ...(entries[pluginId] ?? {}), enabled }; delete plugins.installs; plugins.allow = Array.from(new Set([...(plugins.allow ?? []), pluginId])).sort(); fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); const ledgerPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const ledger = fs.existsSync(ledgerPath) ? JSON.parse(fs.readFileSync(ledgerPath, "utf8")) : { version: 1, warning: "DO NOT EDIT. This file is generated by OpenClaw plugin install/update/uninstall commands. Use `openclaw plugins install/update/uninstall` instead.", records: {}, }; ledger.updatedAtMs = Date.now(); ledger.records ??= {}; ledger.records[pluginId] = { ...(ledger.records[pluginId] ?? {}), source: "path", installPath: pluginRoot, sourcePath: pluginRoot, }; fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); fs.writeFileSync(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, "utf8"); NODE } run_logged() { local label="$1" shift local log_file="/tmp/openclaw-plugins-e2e-${label}.log" if ! "$@" >"$log_file" 2>&1; then cat "$log_file" exit 1 fi } write_fixture_plugin() { local dir="$1" local id="$2" local version="$3" local method="$4" local name="$5" mkdir -p "$dir" cat > "$dir/package.json" < "$dir/index.js" < ({ ok: true })); }, }; JS cat > "$dir/openclaw.plugin.json" <<'JSON' { "id": "placeholder", "configSchema": { "type": "object", "properties": {} } } JSON node - <<'NODE' "$dir/openclaw.plugin.json" "$id" const fs = require("node:fs"); const file = process.argv[2]; const id = process.argv[3]; const parsed = JSON.parse(fs.readFileSync(file, "utf8")); parsed.id = id; fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`); NODE } demo_plugin_id="demo-plugin" demo_plugin_root="$OPENCLAW_PLUGIN_HOME/$demo_plugin_id" mkdir -p "$demo_plugin_root" cat > "$demo_plugin_root/index.js" <<'JS' module.exports = { id: "demo-plugin", name: "Demo Plugin", description: "Docker E2E demo plugin", register(api) { api.registerTool(() => null, { name: "demo_tool" }); api.registerGatewayMethod("demo.ping", async () => ({ ok: true })); api.registerCli(() => {}, { commands: ["demo"] }); api.registerService({ id: "demo-service", start: () => {} }); }, }; JS cat > "$demo_plugin_root/openclaw.plugin.json" <<'JSON' { "id": "demo-plugin", "configSchema": { "type": "object", "properties": {} } } JSON record_fixture_plugin_trust "$demo_plugin_id" "$demo_plugin_root" 1 node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins.json node "$OPENCLAW_ENTRY" plugins inspect demo-plugin --json > /tmp/plugins-inspect.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/plugins.json", "utf8")); const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-inspect.json", "utf8")); const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin"); if (!plugin) throw new Error("plugin not found"); if (plugin.status !== "loaded") { throw new Error(`unexpected status: ${plugin.status}`); } const assertIncludes = (list, value, label) => { if (!Array.isArray(list) || !list.includes(value)) { throw new Error(`${label} missing: ${value}`); } }; const inspectToolNames = Array.isArray(inspect.tools) ? inspect.tools.flatMap((entry) => (Array.isArray(entry?.names) ? entry.names : [])) : []; assertIncludes(inspectToolNames, "demo_tool", "tool"); assertIncludes(inspect.gatewayMethods, "demo.ping", "gateway method"); assertIncludes(inspect.cliCommands, "demo", "cli command"); assertIncludes(inspect.services, "demo-service", "service"); const diagErrors = (data.diagnostics || []).filter((diag) => diag.level === "error"); if (diagErrors.length > 0) { throw new Error(`diagnostics errors: ${diagErrors.map((diag) => diag.message).join("; ")}`); } console.log("ok"); NODE echo "Testing tgz install flow..." pack_dir="$(mktemp -d "/tmp/openclaw-plugin-pack.XXXXXX")" mkdir -p "$pack_dir/package" cat > "$pack_dir/package/package.json" <<'JSON' { "name": "@openclaw/demo-plugin-tgz", "version": "0.0.1", "openclaw": { "extensions": ["./index.js"] } } JSON cat > "$pack_dir/package/index.js" <<'JS' module.exports = { id: "demo-plugin-tgz", name: "Demo Plugin TGZ", register(api) { api.registerGatewayMethod("demo.tgz", async () => ({ ok: true })); }, }; JS cat > "$pack_dir/package/openclaw.plugin.json" <<'JSON' { "id": "demo-plugin-tgz", "configSchema": { "type": "object", "properties": {} } } JSON tar -czf /tmp/demo-plugin-tgz.tgz -C "$pack_dir" package run_logged install-tgz node "$OPENCLAW_ENTRY" plugins install /tmp/demo-plugin-tgz.tgz node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins2.json node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-tgz --json > /tmp/plugins2-inspect.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/plugins2.json", "utf8")); const inspect = JSON.parse(fs.readFileSync("/tmp/plugins2-inspect.json", "utf8")); const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin-tgz"); if (!plugin) throw new Error("tgz plugin not found"); if (plugin.status !== "loaded") { throw new Error(`unexpected status: ${plugin.status}`); } if (!Array.isArray(inspect.gatewayMethods) || !inspect.gatewayMethods.includes("demo.tgz")) { throw new Error("expected gateway method demo.tgz"); } console.log("ok"); NODE echo "Testing install from local folder (plugins.load.paths)..." dir_plugin="$(mktemp -d "/tmp/openclaw-plugin-dir.XXXXXX")" cat > "$dir_plugin/package.json" <<'JSON' { "name": "@openclaw/demo-plugin-dir", "version": "0.0.1", "openclaw": { "extensions": ["./index.js"] } } JSON cat > "$dir_plugin/index.js" <<'JS' module.exports = { id: "demo-plugin-dir", name: "Demo Plugin DIR", register(api) { api.registerGatewayMethod("demo.dir", async () => ({ ok: true })); }, }; JS cat > "$dir_plugin/openclaw.plugin.json" <<'JSON' { "id": "demo-plugin-dir", "configSchema": { "type": "object", "properties": {} } } JSON run_logged install-dir node "$OPENCLAW_ENTRY" plugins install "$dir_plugin" node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins3.json node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-dir --json > /tmp/plugins3-inspect.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/plugins3.json", "utf8")); const inspect = JSON.parse(fs.readFileSync("/tmp/plugins3-inspect.json", "utf8")); const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin-dir"); if (!plugin) throw new Error("dir plugin not found"); if (plugin.status !== "loaded") { throw new Error(`unexpected status: ${plugin.status}`); } if (!Array.isArray(inspect.gatewayMethods) || !inspect.gatewayMethods.includes("demo.dir")) { throw new Error("expected gateway method demo.dir"); } console.log("ok"); NODE echo "Testing install from npm spec (file:)..." file_pack_dir="$(mktemp -d "/tmp/openclaw-plugin-filepack.XXXXXX")" mkdir -p "$file_pack_dir/package" cat > "$file_pack_dir/package/package.json" <<'JSON' { "name": "@openclaw/demo-plugin-file", "version": "0.0.1", "openclaw": { "extensions": ["./index.js"] } } JSON cat > "$file_pack_dir/package/index.js" <<'JS' module.exports = { id: "demo-plugin-file", name: "Demo Plugin FILE", register(api) { api.registerGatewayMethod("demo.file", async () => ({ ok: true })); }, }; JS cat > "$file_pack_dir/package/openclaw.plugin.json" <<'JSON' { "id": "demo-plugin-file", "configSchema": { "type": "object", "properties": {} } } JSON run_logged install-file node "$OPENCLAW_ENTRY" plugins install "file:$file_pack_dir/package" node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins4.json node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-file --json > /tmp/plugins4-inspect.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/plugins4.json", "utf8")); const inspect = JSON.parse(fs.readFileSync("/tmp/plugins4-inspect.json", "utf8")); const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin-file"); if (!plugin) throw new Error("file plugin not found"); if (plugin.status !== "loaded") { throw new Error(`unexpected status: ${plugin.status}`); } if (!Array.isArray(inspect.gatewayMethods) || !inspect.gatewayMethods.includes("demo.file")) { throw new Error("expected gateway method demo.file"); } console.log("ok"); NODE echo "Testing Claude bundle enable and inspect flow..." bundle_plugin_id="claude-bundle-e2e" bundle_root="$OPENCLAW_PLUGIN_HOME/$bundle_plugin_id" mkdir -p "$bundle_root/.claude-plugin" "$bundle_root/commands" cat > "$bundle_root/.claude-plugin/plugin.json" <<'JSON' { "name": "claude-bundle-e2e" } JSON cat > "$bundle_root/commands/office-hours.md" <<'MD' --- description: Help with architecture and rollout planning --- Act as an engineering advisor. Focus on: $ARGUMENTS MD record_fixture_plugin_trust "$bundle_plugin_id" "$bundle_root" 0 node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-bundle-disabled.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/plugins-bundle-disabled.json", "utf8")); const plugin = (data.plugins || []).find((entry) => entry.id === "claude-bundle-e2e"); if (!plugin) throw new Error("Claude bundle plugin not found"); if (plugin.status !== "disabled") { throw new Error(`expected disabled bundle before enable, got ${plugin.status}`); } console.log("ok"); NODE run_logged enable-claude-bundle node "$OPENCLAW_ENTRY" plugins enable claude-bundle-e2e node "$OPENCLAW_ENTRY" plugins inspect claude-bundle-e2e --json > /tmp/plugins-bundle-inspect.json node - <<'NODE' const fs = require("node:fs"); const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-bundle-inspect.json", "utf8")); if (inspect.plugin?.bundleFormat !== "claude") { throw new Error(`expected Claude bundle format, got ${inspect.plugin?.bundleFormat}`); } if (inspect.plugin?.enabled !== true || inspect.plugin?.status !== "loaded") { throw new Error( `expected enabled loaded Claude bundle, got enabled=${inspect.plugin?.enabled} status=${inspect.plugin?.status}`, ); } console.log("ok"); NODE echo "Testing plugin install visible after explicit restart..." slash_install_dir="$(mktemp -d "/tmp/openclaw-plugin-slash-install.XXXXXX")" cat > "$slash_install_dir/package.json" <<'JSON' { "name": "@openclaw/slash-install-plugin", "version": "0.0.1", "openclaw": { "extensions": ["./index.js"] } } JSON cat > "$slash_install_dir/index.js" <<'JS' module.exports = { id: "slash-install-plugin", name: "Slash Install Plugin", register(api) { api.registerGatewayMethod("demo.slash.install", async () => ({ ok: true })); }, }; JS cat > "$slash_install_dir/openclaw.plugin.json" <<'JSON' { "id": "slash-install-plugin", "configSchema": { "type": "object", "properties": {} } } JSON run_logged install-slash-plugin node "$OPENCLAW_ENTRY" plugins install "$slash_install_dir" node "$OPENCLAW_ENTRY" plugins inspect slash-install-plugin --json > /tmp/plugin-command-install-show.json node - <<'NODE' const fs = require("node:fs"); const inspect = JSON.parse(fs.readFileSync("/tmp/plugin-command-install-show.json", "utf8")); if (inspect.plugin?.status !== "loaded") { throw new Error(`expected loaded status after install, got ${inspect.plugin?.status}`); } if (inspect.plugin?.enabled !== true) { throw new Error(`expected enabled status after install, got ${inspect.plugin?.enabled}`); } if (!inspect.gatewayMethods.includes("demo.slash.install")) { throw new Error(`expected installed gateway method, got ${inspect.gatewayMethods.join(", ")}`); } console.log("ok"); NODE echo "Testing marketplace install and update flows..." marketplace_root="$HOME/.claude/plugins/marketplaces/fixture-marketplace" mkdir -p "$HOME/.claude/plugins" "$marketplace_root/.claude-plugin" write_fixture_plugin \ "$marketplace_root/plugins/marketplace-shortcut" \ "marketplace-shortcut" \ "0.0.1" \ "demo.marketplace.shortcut.v1" \ "Marketplace Shortcut" write_fixture_plugin \ "$marketplace_root/plugins/marketplace-direct" \ "marketplace-direct" \ "0.0.1" \ "demo.marketplace.direct.v1" \ "Marketplace Direct" cat > "$marketplace_root/.claude-plugin/marketplace.json" <<'JSON' { "name": "Fixture Marketplace", "version": "1.0.0", "plugins": [ { "name": "marketplace-shortcut", "version": "0.0.1", "description": "Shortcut install fixture", "source": "./plugins/marketplace-shortcut" }, { "name": "marketplace-direct", "version": "0.0.1", "description": "Explicit marketplace fixture", "source": { "type": "path", "path": "./plugins/marketplace-direct" } } ] } JSON cat > "$HOME/.claude/plugins/known_marketplaces.json" < /tmp/marketplace-list.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/marketplace-list.json", "utf8")); const names = (data.plugins || []).map((entry) => entry.name).sort(); if (data.name !== "Fixture Marketplace") { throw new Error(`unexpected marketplace name: ${data.name}`); } if (!names.includes("marketplace-shortcut") || !names.includes("marketplace-direct")) { throw new Error(`unexpected marketplace plugins: ${names.join(", ")}`); } console.log("ok"); NODE run_logged install-marketplace-shortcut node "$OPENCLAW_ENTRY" plugins install marketplace-shortcut@claude-fixtures run_logged install-marketplace-direct node "$OPENCLAW_ENTRY" plugins install marketplace-direct --marketplace claude-fixtures node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-marketplace.json node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --json > /tmp/plugins-marketplace-shortcut-inspect.json node "$OPENCLAW_ENTRY" plugins inspect marketplace-direct --json > /tmp/plugins-marketplace-direct-inspect.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace.json", "utf8")); const shortcutInspect = JSON.parse( fs.readFileSync("/tmp/plugins-marketplace-shortcut-inspect.json", "utf8"), ); const directInspect = JSON.parse( fs.readFileSync("/tmp/plugins-marketplace-direct-inspect.json", "utf8"), ); const getPlugin = (id) => { const plugin = (data.plugins || []).find((entry) => entry.id === id); if (!plugin) throw new Error(`plugin not found: ${id}`); if (plugin.status !== "loaded") { throw new Error(`unexpected status for ${id}: ${plugin.status}`); } return plugin; }; const shortcut = getPlugin("marketplace-shortcut"); const direct = getPlugin("marketplace-direct"); if (shortcut.version !== "0.0.1") { throw new Error(`unexpected shortcut version: ${shortcut.version}`); } if (direct.version !== "0.0.1") { throw new Error(`unexpected direct version: ${direct.version}`); } if (!shortcutInspect.gatewayMethods.includes("demo.marketplace.shortcut.v1")) { throw new Error("expected marketplace shortcut gateway method"); } if (!directInspect.gatewayMethods.includes("demo.marketplace.direct.v1")) { throw new Error("expected marketplace direct gateway method"); } console.log("ok"); NODE node - <<'NODE' const fs = require("node:fs"); const path = require("node:path"); const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const index = JSON.parse(fs.readFileSync(indexPath, "utf8")); for (const id of ["marketplace-shortcut", "marketplace-direct"]) { const record = index.installRecords?.[id]; if (!record) throw new Error(`missing install record for ${id}`); if (record.source !== "marketplace") { throw new Error(`unexpected source for ${id}: ${record.source}`); } if (record.marketplaceSource !== "claude-fixtures") { throw new Error(`unexpected marketplace source for ${id}: ${record.marketplaceSource}`); } if (record.marketplacePlugin !== id) { throw new Error(`unexpected marketplace plugin for ${id}: ${record.marketplacePlugin}`); } } console.log("ok"); NODE write_fixture_plugin \ "$marketplace_root/plugins/marketplace-shortcut" \ "marketplace-shortcut" \ "0.0.2" \ "demo.marketplace.shortcut.v2" \ "Marketplace Shortcut" run_logged update-marketplace-shortcut-dry-run node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut --dry-run run_logged update-marketplace-shortcut node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-marketplace-updated.json node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --json > /tmp/plugins-marketplace-updated-inspect.json node - <<'NODE' const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace-updated.json", "utf8")); const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace-updated-inspect.json", "utf8")); const plugin = (data.plugins || []).find((entry) => entry.id === "marketplace-shortcut"); if (!plugin) throw new Error("updated marketplace plugin not found"); if (plugin.version !== "0.0.2") { throw new Error(`unexpected updated version: ${plugin.version}`); } if (!inspect.gatewayMethods.includes("demo.marketplace.shortcut.v2")) { throw new Error(`expected updated gateway method, got ${inspect.gatewayMethods.join(", ")}`); } console.log("ok"); NODE if [ "${OPENCLAW_PLUGINS_E2E_CLAWHUB:-1}" = "0" ]; then echo "Skipping ClawHub plugin install and uninstall (OPENCLAW_PLUGINS_E2E_CLAWHUB=0)." else echo "Testing ClawHub plugin install and uninstall..." CLAWHUB_PLUGIN_SPEC="${OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC:-clawhub:openclaw-now4real}" CLAWHUB_PLUGIN_ID="${OPENCLAW_PLUGINS_E2E_CLAWHUB_ID:-now4real}" export CLAWHUB_PLUGIN_SPEC CLAWHUB_PLUGIN_ID node - <<'NODE' const spec = process.env.CLAWHUB_PLUGIN_SPEC; if (!spec?.startsWith("clawhub:")) { throw new Error(`expected clawhub: spec, got ${spec}`); } const parsePackageName = (rawSpec) => { const value = rawSpec.slice("clawhub:".length).trim(); const slashIndex = value.lastIndexOf("/"); const atIndex = value.lastIndexOf("@"); return atIndex > 0 && atIndex > slashIndex ? value.slice(0, atIndex) : value; }; const packageName = parsePackageName(spec); const baseUrl = (process.env.OPENCLAW_CLAWHUB_URL || process.env.CLAWHUB_URL || "https://clawhub.ai") .replace(/\/+$/, ""); const token = process.env.OPENCLAW_CLAWHUB_TOKEN || process.env.CLAWHUB_TOKEN || process.env.CLAWHUB_AUTH_TOKEN || ""; const response = await fetch(`${baseUrl}/api/v1/packages/${encodeURIComponent(packageName)}`, { headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error(`ClawHub package preflight failed for ${packageName}: ${response.status} ${body}`); } const detail = await response.json(); const family = detail.package?.family; if (family !== "code-plugin" && family !== "bundle-plugin") { throw new Error(`ClawHub package ${packageName} is not installable as a plugin: ${family}`); } if (detail.package?.runtimeId && detail.package.runtimeId !== process.env.CLAWHUB_PLUGIN_ID) { throw new Error( `ClawHub package ${packageName} runtimeId ${detail.package.runtimeId} does not match expected ${process.env.CLAWHUB_PLUGIN_ID}`, ); } console.log(`Using ClawHub package ${packageName} (${family}).`); NODE run_logged install-clawhub node "$OPENCLAW_ENTRY" plugins install "$CLAWHUB_PLUGIN_SPEC" node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-clawhub-installed.json node "$OPENCLAW_ENTRY" plugins inspect "$CLAWHUB_PLUGIN_ID" --json > /tmp/plugins-clawhub-inspect.json node - <<'NODE' const fs = require("node:fs"); const path = require("node:path"); const pluginId = process.env.CLAWHUB_PLUGIN_ID; const spec = process.env.CLAWHUB_PLUGIN_SPEC; const parsePackageName = (rawSpec) => { const value = rawSpec.slice("clawhub:".length).trim(); const slashIndex = value.lastIndexOf("/"); const atIndex = value.lastIndexOf("@"); return atIndex > 0 && atIndex > slashIndex ? value.slice(0, atIndex) : value; }; const packageName = parsePackageName(spec); const list = JSON.parse(fs.readFileSync("/tmp/plugins-clawhub-installed.json", "utf8")); const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-clawhub-inspect.json", "utf8")); const plugin = (list.plugins || []).find((entry) => entry.id === pluginId); if (!plugin) throw new Error(`ClawHub plugin not found after install: ${pluginId}`); if (plugin.status !== "loaded") { throw new Error(`unexpected ClawHub plugin status for ${pluginId}: ${plugin.status}`); } if (inspect.plugin?.id !== pluginId) { throw new Error(`unexpected ClawHub inspect plugin id: ${inspect.plugin?.id}`); } const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const index = JSON.parse(fs.readFileSync(indexPath, "utf8")); const record = index.installRecords?.[pluginId]; if (!record) throw new Error(`missing ClawHub install record for ${pluginId}`); if (record.source !== "clawhub") { throw new Error(`unexpected ClawHub install source for ${pluginId}: ${record.source}`); } if (record.clawhubPackage !== packageName) { throw new Error( `unexpected ClawHub package for ${pluginId}: ${record.clawhubPackage}, expected ${packageName}`, ); } if (record.clawhubFamily !== "code-plugin" && record.clawhubFamily !== "bundle-plugin") { throw new Error(`unexpected ClawHub family for ${pluginId}: ${record.clawhubFamily}`); } if (typeof record.installPath !== "string" || record.installPath.length === 0) { throw new Error(`missing ClawHub install path for ${pluginId}`); } const installPath = record.installPath.replace(/^~(?=$|\/)/, process.env.HOME); const extensionsRoot = path.join(process.env.HOME, ".openclaw", "extensions"); if (!installPath.startsWith(`${extensionsRoot}${path.sep}`)) { throw new Error(`ClawHub install path is outside managed extensions root: ${installPath}`); } if (!fs.existsSync(installPath)) { throw new Error(`ClawHub install path missing on disk: ${installPath}`); } fs.writeFileSync("/tmp/plugins-clawhub-install-path.txt", installPath, "utf8"); console.log("ok"); NODE run_logged uninstall-clawhub node "$OPENCLAW_ENTRY" plugins uninstall "$CLAWHUB_PLUGIN_SPEC" --force node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-clawhub-uninstalled.json node - <<'NODE' const fs = require("node:fs"); const path = require("node:path"); const pluginId = process.env.CLAWHUB_PLUGIN_ID; const installPath = fs.readFileSync("/tmp/plugins-clawhub-install-path.txt", "utf8").trim(); const list = JSON.parse(fs.readFileSync("/tmp/plugins-clawhub-uninstalled.json", "utf8")); if ((list.plugins || []).some((entry) => entry.id === pluginId)) { throw new Error(`ClawHub plugin still listed after uninstall: ${pluginId}`); } const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const index = fs.existsSync(indexPath) ? JSON.parse(fs.readFileSync(indexPath, "utf8")) : {}; if (index.installRecords?.[pluginId]) { throw new Error(`ClawHub install record still present after uninstall: ${pluginId}`); } const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {}; if (config.plugins?.entries?.[pluginId]) { throw new Error(`ClawHub config entry still present after uninstall: ${pluginId}`); } if ((config.plugins?.allow || []).includes(pluginId)) { throw new Error(`ClawHub allowlist entry still present after uninstall: ${pluginId}`); } if ((config.plugins?.deny || []).includes(pluginId)) { throw new Error(`ClawHub denylist entry still present after uninstall: ${pluginId}`); } if (fs.existsSync(installPath)) { throw new Error(`ClawHub managed install directory still exists after uninstall: ${installPath}`); } console.log("ok"); NODE fi EOF then cat "$RUN_LOG" rm -f "$RUN_LOG" exit 1 fi rm -f "$RUN_LOG" echo "OK"