diff --git a/extensions/browser/src/browser/trash.ts b/extensions/browser/src/browser/trash.ts index a76def77c24..080d0d9ca45 100644 --- a/extensions/browser/src/browser/trash.ts +++ b/extensions/browser/src/browser/trash.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; const TRASH_DESTINATION_COLLISION_CODES = new Set(["EEXIST", "ENOTEMPTY", "ERR_FS_CP_EEXIST"]); const TRASH_DESTINATION_RETRY_LIMIT = 4; @@ -23,7 +24,7 @@ function isSameOrChildPath(candidate: string, parent: string): boolean { } function resolveAllowedTrashRoots(): string[] { - const roots = [os.homedir(), os.tmpdir()].map((root) => { + const roots = [os.homedir(), resolvePreferredOpenClawTmpDir()].map((root) => { try { return path.resolve(fs.realpathSync.native(root)); } catch { diff --git a/extensions/signal/src/install-signal-cli.ts b/extensions/signal/src/install-signal-cli.ts index 81d8df6136a..f15d7b5a2b1 100644 --- a/extensions/signal/src/install-signal-cli.ts +++ b/extensions/signal/src/install-signal-cli.ts @@ -247,21 +247,32 @@ async function installSignalCliViaBrew(runtime: RuntimeEnv): Promise { const apiUrl = "https://api.github.com/repos/AsamK/signal-cli/releases/latest"; - const response = await fetch(apiUrl, { - headers: { - "User-Agent": "openclaw", - Accept: "application/vnd.github+json", + const { response, release } = await fetchWithSsrFGuard({ + url: apiUrl, + maxRedirects: 5, + requireHttps: true, + capture: false, + auditContext: "signal-cli-release-info", + init: { + headers: { + "User-Agent": "openclaw", + Accept: "application/vnd.github+json", + }, }, }); - if (!response.ok) { - return { - ok: false, - error: `Failed to fetch release info (${response.status})`, - }; + let payload: ReleaseResponse; + try { + if (!response.ok) { + return { + ok: false, + error: `Failed to fetch release info (${response.status})`, + }; + } + payload = (await response.json()) as ReleaseResponse; + } finally { + await release(); } - - const payload = (await response.json()) as ReleaseResponse; const version = payload.tag_name?.replace(/^v/, "") ?? "unknown"; const assets = payload.assets ?? []; const asset = pickAsset(assets, process.platform, process.arch); diff --git a/scripts/test-built-plugin-singleton.mjs b/scripts/test-built-plugin-singleton.mjs index 082e148477e..6944f340fbe 100644 --- a/scripts/test-built-plugin-singleton.mjs +++ b/scripts/test-built-plugin-singleton.mjs @@ -135,9 +135,10 @@ const record = registry.plugins.find((entry) => entry.id === pluginId); assert.ok(record, "smoke plugin missing from registry"); assert.equal(record.status, "loaded", record.error ?? "smoke plugin failed to load"); -assert.deepEqual(getPluginCommandSpecs(), [ - { name: "pair", description: "Pair a device", acceptsArgs: true }, -]); +assert.deepEqual( + getPluginCommandSpecs().filter((command) => command.name === "pair"), + [{ name: "pair", description: "Pair a device", acceptsArgs: true }], +); const match = matchPluginCommand("/pair now"); assert.ok(match, "canonical built command registry did not receive the command"); diff --git a/src/commands/doctor-bundled-plugin-runtime-deps.ts b/src/commands/doctor-bundled-plugin-runtime-deps.ts index aed670c37d9..2ec040d4f55 100644 --- a/src/commands/doctor-bundled-plugin-runtime-deps.ts +++ b/src/commands/doctor-bundled-plugin-runtime-deps.ts @@ -1,7 +1,9 @@ +import fs from "node:fs"; import path from "node:path"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; +import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import { createBundledRuntimeDepsWritableInstallSpecs, repairBundledRuntimeDepsInstallRootAsync, @@ -9,13 +11,98 @@ import { scanBundledPluginRuntimeDeps, type BundledRuntimeDepsInstallParams, } from "../plugins/bundled-runtime-deps.js"; +import { normalizePluginsConfig } from "../plugins/config-state.js"; import { resolveEffectivePluginIds } from "../plugins/effective-plugin-ids.js"; +import { passesManifestOwnerBasePolicy } from "../plugins/manifest-owner-policy.js"; import type { RuntimeEnv } from "../runtime.js"; import { note } from "../terminal/note.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; const RUNTIME_DEPS_INSTALL_HEARTBEAT_MS = 15_000; +function filterPluginIdsPresentInBundledTree( + bundledPluginsDir: string, + pluginIds: readonly string[], +): string[] | undefined { + const present = pluginIds.filter((pluginId) => { + if (path.basename(pluginId) !== pluginId) { + return false; + } + return fs.existsSync(path.join(bundledPluginsDir, pluginId)); + }); + return present.length > 0 ? present : undefined; +} + +function collectPackagedRuntimeDepsRepairPluginIds(params: { + bundledPluginsDir: string; + config: OpenClawConfig; + includeConfiguredChannels?: boolean; +}): string[] { + if (!fs.existsSync(params.bundledPluginsDir)) { + return []; + } + const plugins = normalizePluginsConfig(params.config.plugins); + const ids = new Set(); + for (const entry of fs.readdirSync(params.bundledPluginsDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const pluginDir = path.join(params.bundledPluginsDir, entry.name); + let manifest: Record; + try { + manifest = JSON.parse( + fs.readFileSync(path.join(pluginDir, "openclaw.plugin.json"), "utf-8"), + ) as Record; + } catch { + continue; + } + const pluginId = typeof manifest.id === "string" && manifest.id ? manifest.id : entry.name; + if ( + !passesManifestOwnerBasePolicy({ + plugin: { id: pluginId }, + normalizedConfig: plugins, + allowRestrictiveAllowlistBypass: true, + }) + ) { + continue; + } + if (plugins.allow.includes(pluginId) || plugins.entries[pluginId]?.enabled === true) { + ids.add(pluginId); + continue; + } + const channels = Array.isArray(manifest.channels) + ? manifest.channels.filter((channel): channel is string => typeof channel === "string") + : []; + if ( + channels.some((channelId) => { + const channelConfig = (params.config.channels as Record | undefined)?.[ + channelId + ]; + if (!channelConfig || typeof channelConfig !== "object" || Array.isArray(channelConfig)) { + return false; + } + if ((channelConfig as { enabled?: unknown }).enabled === false) { + return false; + } + return ( + (channelConfig as { enabled?: unknown }).enabled === true || + params.includeConfiguredChannels === true + ); + }) + ) { + ids.add(pluginId); + continue; + } + const providers = Array.isArray(manifest.providers) + ? manifest.providers.filter((provider): provider is string => typeof provider === "string") + : []; + if (manifest.enabledByDefault === true && providers.length === 0 && channels.length === 0) { + ids.add(pluginId); + } + } + return [...ids].toSorted((left, right) => left.localeCompare(right)); +} + function formatElapsedMs(elapsedMs: number): string { if (elapsedMs < 1000) { return `${elapsedMs}ms`; @@ -56,13 +143,23 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: { const env = params.env ?? process.env; const bundledPluginsDir = path.join(packageRoot, "dist", "extensions"); const effectivePluginIds = params.config - ? resolveEffectivePluginIds({ - config: params.config, - env: { - ...env, - OPENCLAW_BUNDLED_PLUGINS_DIR: bundledPluginsDir, - }, - }) + ? resolveBundledPluginsDir({ ...env, OPENCLAW_BUNDLED_PLUGINS_DIR: bundledPluginsDir }) === + bundledPluginsDir + ? filterPluginIdsPresentInBundledTree( + bundledPluginsDir, + resolveEffectivePluginIds({ + config: params.config, + env: { + ...env, + OPENCLAW_BUNDLED_PLUGINS_DIR: bundledPluginsDir, + }, + }), + ) + : collectPackagedRuntimeDepsRepairPluginIds({ + bundledPluginsDir, + config: params.config, + includeConfiguredChannels: params.includeConfiguredChannels, + }) : undefined; const { deps, missing, conflicts } = scanBundledPluginRuntimeDeps({ packageRoot, diff --git a/src/commands/doctor-plugin-manifests.test.ts b/src/commands/doctor-plugin-manifests.test.ts index 510f3ab6896..fe579b998bc 100644 --- a/src/commands/doctor-plugin-manifests.test.ts +++ b/src/commands/doctor-plugin-manifests.test.ts @@ -90,8 +90,8 @@ describe("doctor plugin manifest legacy contract repair", () => { const migrations = collectLegacyPluginManifestContractMigrations({ env: { ...process.env, - OPENCLAW_BUNDLED_PLUGINS_DIR: pluginsRoot, }, + manifestRoots: [pluginsRoot], }); expect(migrations).toHaveLength(1); @@ -119,8 +119,8 @@ describe("doctor plugin manifest legacy contract repair", () => { await maybeRepairLegacyPluginManifestContracts({ env: { ...process.env, - OPENCLAW_BUNDLED_PLUGINS_DIR: pluginsRoot, }, + manifestRoots: [pluginsRoot], runtime: createRuntime(), prompter: createPrompter(), note: vi.fn(), @@ -156,8 +156,8 @@ describe("doctor plugin manifest legacy contract repair", () => { const migrations = collectLegacyPluginManifestContractMigrations({ env: { ...process.env, - OPENCLAW_BUNDLED_PLUGINS_DIR: pluginsRoot, }, + manifestRoots: [pluginsRoot], }); expect(migrations).toHaveLength(1); diff --git a/src/commands/doctor-plugin-manifests.ts b/src/commands/doctor-plugin-manifests.ts index f86d8e3ccb7..7eb088eeb31 100644 --- a/src/commands/doctor-plugin-manifests.ts +++ b/src/commands/doctor-plugin-manifests.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import path from "node:path"; import { z } from "zod"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -81,10 +82,35 @@ function buildLegacyManifestContractMigration(params: { export function collectLegacyPluginManifestContractMigrations(params?: { env?: NodeJS.ProcessEnv; + manifestRoots?: string[]; }): LegacyManifestContractMigration[] { const seen = new Set(); const migrations: LegacyManifestContractMigration[] = []; + for (const root of params?.manifestRoots ?? []) { + if (!fs.existsSync(root)) { + continue; + } + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const manifestPath = path.join(root, entry.name, "openclaw.plugin.json"); + if (seen.has(manifestPath)) { + continue; + } + seen.add(manifestPath); + const raw = readManifestJson(manifestPath); + if (!raw) { + continue; + } + const migration = buildLegacyManifestContractMigration({ manifestPath, raw }); + if (migration) { + migrations.push(migration); + } + } + } + for (const plugin of loadPluginManifestRegistry({ cache: false, ...(params?.env ? { env: params.env } : {}), @@ -111,13 +137,15 @@ export function collectLegacyPluginManifestContractMigrations(params?: { export async function maybeRepairLegacyPluginManifestContracts(params: { env?: NodeJS.ProcessEnv; + manifestRoots?: string[]; runtime: RuntimeEnv; prompter: DoctorPrompter; note?: typeof note; }): Promise { - const migrations = collectLegacyPluginManifestContractMigrations( - params.env ? { env: params.env } : undefined, - ); + const migrations = collectLegacyPluginManifestContractMigrations({ + ...(params.env ? { env: params.env } : {}), + ...(params.manifestRoots ? { manifestRoots: params.manifestRoots } : {}), + }); if (migrations.length === 0) { return; }