From 406f06dcc552b127d19f7e41629ea51d46751f78 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Apr 2026 12:26:42 +0900 Subject: [PATCH] fix: preserve linked install unsafe flag and baseline regressions --- CHANGELOG.md | 1 + extensions/telegram/src/doctor-contract.ts | 2 +- extensions/whatsapp/contract-api.ts | 31 +++++++++++++++---- src/channels/plugins/contract-surfaces.ts | 32 +++++++++++++++----- src/channels/plugins/setup-wizard-helpers.ts | 8 ++++- src/plugins/discovery.ts | 29 ++++++++++++++++++ 6 files changed, 87 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e9c778707d..be1e9414b05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ Docs: https://docs.openclaw.ai - Providers/OpenAI Codex: add forward-compat `openai-codex/gpt-5.4-mini` synthesis across provider runtime, model catalog, and model listing so Codex mini works before bundled Pi catalog updates land. - Plugins/marketplace: block remote marketplace symlink escapes without rewriting ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit. - Plugins/Kimi Coding: keep native Anthropic tool payloads on the Kimi coding endpoint while still parsing tagged tool-call text on the response path, so tool calls execute again instead of echoing raw markup. (#60391) Thanks @Eric-Guo. +- Plugins/install: preserve `--dangerously-force-unsafe-install` across linked plugin probes and linked hook-pack fallback probes so local `--link` installs honor the documented unsafe override. (#60624) Thanks @JerrettDavis. ## 2026.4.2 diff --git a/extensions/telegram/src/doctor-contract.ts b/extensions/telegram/src/doctor-contract.ts index 3cada065691..d135e05c323 100644 --- a/extensions/telegram/src/doctor-contract.ts +++ b/extensions/telegram/src/doctor-contract.ts @@ -183,7 +183,7 @@ export function normalizeCompatibilityConfig({ } } - if (!changed) { + if (!changed && changes.length === 0) { return { config: cfg, changes: [] }; } return { diff --git a/extensions/whatsapp/contract-api.ts b/extensions/whatsapp/contract-api.ts index 0c7c76a67d6..c93d0815763 100644 --- a/extensions/whatsapp/contract-api.ts +++ b/extensions/whatsapp/contract-api.ts @@ -12,12 +12,31 @@ export const unsupportedSecretRefSurfacePatterns = [ "channels.whatsapp.accounts.*.creds.json", ] as const; -export { canonicalizeLegacySessionKey, isLegacyGroupSessionKey } from "./src/session-contract.js"; -export { createWhatsAppPollFixture, expectWhatsAppPollSent } from "./src/outbound-test-support.js"; -export { whatsappCommandPolicy } from "./src/command-policy.js"; -export { resolveLegacyGroupSessionKey } from "./src/group-session-contract.js"; -export { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./src/normalize-target.js"; -export { __testing as whatsappAccessControlTesting } from "./src/inbound/access-control.js"; +import { whatsappCommandPolicy as whatsappCommandPolicyImpl } from "./src/command-policy.js"; +import { resolveLegacyGroupSessionKey as resolveLegacyGroupSessionKeyImpl } from "./src/group-session-contract.js"; +import { __testing as whatsappAccessControlTestingImpl } from "./src/inbound/access-control.js"; +import { + isWhatsAppGroupJid as isWhatsAppGroupJidImpl, + normalizeWhatsAppTarget as normalizeWhatsAppTargetImpl, +} from "./src/normalize-target.js"; +import { + createWhatsAppPollFixture as createWhatsAppPollFixtureImpl, + expectWhatsAppPollSent as expectWhatsAppPollSentImpl, +} from "./src/outbound-test-support.js"; +import { + canonicalizeLegacySessionKey as canonicalizeLegacySessionKeyImpl, + isLegacyGroupSessionKey as isLegacyGroupSessionKeyImpl, +} from "./src/session-contract.js"; + +export const canonicalizeLegacySessionKey = canonicalizeLegacySessionKeyImpl; +export const createWhatsAppPollFixture = createWhatsAppPollFixtureImpl; +export const expectWhatsAppPollSent = expectWhatsAppPollSentImpl; +export const isLegacyGroupSessionKey = isLegacyGroupSessionKeyImpl; +export const isWhatsAppGroupJid = isWhatsAppGroupJidImpl; +export const normalizeWhatsAppTarget = normalizeWhatsAppTargetImpl; +export const resolveLegacyGroupSessionKey = resolveLegacyGroupSessionKeyImpl; +export const whatsappAccessControlTesting = whatsappAccessControlTestingImpl; +export const whatsappCommandPolicy = whatsappCommandPolicyImpl; export function collectUnsupportedSecretRefConfigCandidates( raw: unknown, diff --git a/src/channels/plugins/contract-surfaces.ts b/src/channels/plugins/contract-surfaces.ts index 7e738be42cd..18d65cdb0e9 100644 --- a/src/channels/plugins/contract-surfaces.ts +++ b/src/channels/plugins/contract-surfaces.ts @@ -46,17 +46,29 @@ function createModuleLoader() { const loadModule = createModuleLoader(); -function resolveContractSurfaceModulePath(rootDir: string | undefined): string | null { +function resolveContractSurfaceModulePaths(rootDir: string | undefined): string[] { if (typeof rootDir !== "string" || rootDir.length === 0) { - return null; + return []; } + const modulePaths: string[] = []; for (const basename of CONTRACT_SURFACE_BASENAMES) { const modulePath = path.join(rootDir, basename); - if (fs.existsSync(modulePath)) { - return modulePath; + if (!fs.existsSync(modulePath)) { + continue; } + const compiledDistModulePath = modulePath.replace( + `${path.sep}dist-runtime${path.sep}`, + `${path.sep}dist${path.sep}`, + ); + // Prefer the compiled dist module over the dist-runtime shim so Jiti sees + // the full named export surface instead of only local wrapper exports. + if (compiledDistModulePath !== modulePath && fs.existsSync(compiledDistModulePath)) { + modulePaths.push(compiledDistModulePath); + continue; + } + modulePaths.push(modulePath); } - return null; + return modulePaths; } function loadBundledChannelContractSurfaces(): unknown[] { @@ -79,14 +91,18 @@ function loadBundledChannelContractSurfaceEntries(): Array<{ if (manifest.origin !== "bundled" || manifest.channels.length === 0) { continue; } - const modulePath = resolveContractSurfaceModulePath(manifest.rootDir); - if (!modulePath) { + const modulePaths = resolveContractSurfaceModulePaths(manifest.rootDir); + if (modulePaths.length === 0) { continue; } try { + const surface = Object.assign( + {}, + ...modulePaths.map((modulePath) => loadModule(modulePath)(modulePath) as object), + ); surfaces.push({ pluginId: manifest.id, - surface: loadModule(modulePath)(modulePath), + surface, }); } catch { continue; diff --git a/src/channels/plugins/setup-wizard-helpers.ts b/src/channels/plugins/setup-wizard-helpers.ts index 7024cddad73..179a597f0e8 100644 --- a/src/channels/plugins/setup-wizard-helpers.ts +++ b/src/channels/plugins/setup-wizard-helpers.ts @@ -897,8 +897,14 @@ function patchConfigForScopedAccount(params: { ensureEnabled: boolean; }): OpenClawConfig { const { cfg, channel, accountId, patch, ensureEnabled } = params; + const channelConfig = cfg.channels?.[channel] as + | { accounts?: Record } + | undefined; + const hasExistingAccounts = Boolean( + channelConfig?.accounts && Object.keys(channelConfig.accounts).length > 0, + ); const seededCfg = - accountId === DEFAULT_ACCOUNT_ID + accountId === DEFAULT_ACCOUNT_ID || hasExistingAccounts ? cfg : moveSingleAccountChannelSectionToDefaultAccount({ cfg, diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index 531b0051ecd..145f1ed2cb1 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -567,11 +567,20 @@ function discoverInDirectory(params: { candidates: PluginCandidate[]; diagnostics: PluginDiagnostic[]; seen: Set; + recurseDirectories?: boolean; skipDirectories?: Set; + visitedDirectories?: Set; }) { if (!fs.existsSync(params.dir)) { return; } + const resolvedDir = safeRealpathSync(params.dir) ?? path.resolve(params.dir); + if (params.recurseDirectories) { + if (params.visitedDirectories?.has(resolvedDir)) { + return; + } + params.visitedDirectories?.add(resolvedDir); + } let entries: fs.Dirent[] = []; try { entries = fs.readdirSync(params.dir, { withFileTypes: true }); @@ -695,6 +704,14 @@ function discoverInDirectory(params: { manifest, packageDir: fullPath, }); + continue; + } + + if (params.recurseDirectories) { + discoverInDirectory({ + ...params, + dir: fullPath, + }); } } } @@ -894,6 +911,18 @@ export function discoverOpenClawPlugins(params: { }); } if (roots.workspace && workspaceRoot) { + discoverInDirectory({ + dir: workspaceRoot, + origin: "workspace", + ownershipUid: params.ownershipUid, + workspaceDir: workspaceRoot, + candidates, + diagnostics, + seen, + recurseDirectories: true, + skipDirectories: new Set([".openclaw"]), + visitedDirectories: new Set(), + }); discoverInDirectory({ dir: roots.workspace, origin: "workspace",