From dadf0005ec3a53ad5798d3487db9c24d603009d0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 21:29:14 -0700 Subject: [PATCH] fix(plugins): alias bundled public surfaces in source loaders --- CHANGELOG.md | 1 + src/plugins/sdk-alias.test.ts | 74 ++++++++++++++++++++ src/plugins/sdk-alias.ts | 126 ++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b79c297b700..0c15945205a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/session status: keep semantic `session_status({ sessionKey: "current" })` on the live run session even before that run has a persisted session-store entry, instead of falling back to the sandbox policy key. Thanks @vincentkoc. +- QA/Slack: resolve bundled official plugin public-surface package aliases during source-mode QA runs, so release Slack live validation can load `@openclaw/slack/api.js` without workspace symlinks. Thanks @vincentkoc. - Codex: pass the live run session key into app-server dynamic tools when sandbox policy uses a separate session key, so `session_status({ sessionKey: "current" })` reports the active run instead of the sandbox policy key. Thanks @vincentkoc. - Plugins/tools: mark manifest-optional sibling tools as optional even when they come from a shared non-optional factory, so cached/status/MCP metadata keeps opt-in tool policy accurate. Thanks @vincentkoc. - Matrix: keep `streaming.progress.toolProgress` scoped to progress draft mode, so partial and quiet Matrix previews do not lose tool progress unless `streaming.preview.toolProgress` is disabled. Thanks @vincentkoc. diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index e769a30fe8d..f828ceaaf32 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -198,6 +198,39 @@ function createPluginSdkAliasTargetFixture(params?: { }; } +function createBundledPluginPackagePublicSurfaceAliasFixture() { + const fixture = createPluginSdkAliasTargetFixture(); + const extensionRoot = path.join(fixture.fixture.root, bundledPluginRoot("slack")); + const distExtensionRoot = path.join(fixture.fixture.root, "dist", "extensions", "slack"); + mkdirSafeDir(extensionRoot); + mkdirSafeDir(distExtensionRoot); + fs.writeFileSync( + path.join(extensionRoot, "package.json"), + JSON.stringify({ name: "@openclaw/slack", type: "module" }, null, 2), + "utf-8", + ); + const sourceApiPath = path.join(extensionRoot, "api.ts"); + const sourceRuntimeApiPath = path.join(extensionRoot, "runtime-api.ts"); + const distApiPath = path.join(distExtensionRoot, "api.js"); + const distRuntimeApiPath = path.join(distExtensionRoot, "runtime-api.js"); + fs.writeFileSync(sourceApiPath, "export const slackApi = 'source';\n", "utf-8"); + fs.writeFileSync(sourceRuntimeApiPath, "export const slackRuntimeApi = 'source';\n", "utf-8"); + fs.writeFileSync(distApiPath, "export const slackApi = 'dist';\n", "utf-8"); + fs.writeFileSync(distRuntimeApiPath, "export const slackRuntimeApi = 'dist';\n", "utf-8"); + fs.writeFileSync( + path.join(extensionRoot, "internal.ts"), + "export const internal = true;\n", + "utf-8", + ); + return { + ...fixture, + distApiPath, + distRuntimeApiPath, + sourceApiPath, + sourceRuntimeApiPath, + }; +} + function writePluginEntry(root: string, relativePath: string) { const pluginEntry = path.join(root, relativePath); fs.mkdirSync(path.dirname(pluginEntry), { recursive: true }); @@ -777,6 +810,47 @@ describe("plugin sdk alias helpers", () => { }); }); + it("aliases bundled plugin package public surfaces for source plugin transforms", () => { + const { fixture, sourceApiPath, sourceRuntimeApiPath } = + createBundledPluginPackagePublicSurfaceAliasFixture(); + const sourcePluginEntry = writePluginEntry( + fixture.root, + bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"), + ); + + const aliases = withEnv({ NODE_ENV: undefined }, () => + buildPluginLoaderAliasMap(sourcePluginEntry), + ); + + expect(fs.realpathSync(aliases["@openclaw/slack/api.js"] ?? "")).toBe( + fs.realpathSync(sourceApiPath), + ); + expect(fs.realpathSync(aliases["@openclaw/slack/runtime-api.js"] ?? "")).toBe( + fs.realpathSync(sourceRuntimeApiPath), + ); + expect(aliases["@openclaw/slack/internal.js"]).toBeUndefined(); + }); + + it("aliases bundled plugin package public surfaces to dist when dist resolution is requested", () => { + const { fixture, distApiPath, distRuntimeApiPath } = + createBundledPluginPackagePublicSurfaceAliasFixture(); + const sourcePluginEntry = writePluginEntry( + fixture.root, + bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"), + ); + + const aliases = withEnv({ NODE_ENV: undefined }, () => + buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"), + ); + + expect(fs.realpathSync(aliases["@openclaw/slack/api.js"] ?? "")).toBe( + fs.realpathSync(distApiPath), + ); + expect(fs.realpathSync(aliases["@openclaw/slack/runtime-api.js"] ?? "")).toBe( + fs.realpathSync(distRuntimeApiPath), + ); + }); + it("falls back to source plugin-sdk subpath aliases when dist chunks are stale", () => { const fixture = createPluginSdkAliasFixture({ srcFile: "provider-entry.ts", diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 71b778b4ae6..b2410a44882 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -274,6 +274,7 @@ const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [ ".cts", ".cjs", ] as const; +const BUNDLED_PLUGIN_PUBLIC_SURFACE_SOURCE_PATTERN = /^(?:api|runtime-api|test-api|.+-api)$/u; const JS_STATIC_RELATIVE_DEPENDENCY_PATTERN = /(?:\bfrom\s*["']|\bimport\s*\(\s*["']|\brequire\s*\(\s*["'])(\.{1,2}\/[^"']+)["']/g; @@ -320,6 +321,125 @@ function readPrivateLocalOnlyPluginSdkSubpaths(packageRoot: string): string[] { } } +function readBundledPluginPackageName(packageJsonPath: string): string | null { + try { + const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown }; + const name = typeof parsed.name === "string" ? parsed.name.trim() : ""; + return name.startsWith("@openclaw/") ? name : null; + } catch { + return null; + } +} + +function listBundledPluginPublicSurfaceSourceBasenames(extensionSourceRoot: string): string[] { + try { + return fs + .readdirSync(extensionSourceRoot, { withFileTypes: true }) + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .flatMap((fileName) => { + const ext = PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS.find((candidateExt) => + fileName.endsWith(candidateExt), + ); + if (!ext) { + return []; + } + const basename = fileName.slice(0, -ext.length); + return BUNDLED_PLUGIN_PUBLIC_SURFACE_SOURCE_PATTERN.test(basename) ? [basename] : []; + }) + .toSorted(); + } catch { + return []; + } +} + +function resolveBundledPluginPublicSurfaceAliasTarget(params: { + packageRoot: string; + dirName: string; + basename: string; + orderedKinds: PluginSdkAliasCandidateKind[]; +}): string | null { + for (const kind of params.orderedKinds) { + if (kind === "dist") { + const candidate = path.join( + params.packageRoot, + "dist", + "extensions", + params.dirName, + `${params.basename}.js`, + ); + if (fs.existsSync(candidate)) { + return candidate; + } + continue; + } + for (const ext of PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS) { + const candidate = path.join( + params.packageRoot, + "extensions", + params.dirName, + `${params.basename}${ext}`, + ); + if (fs.existsSync(candidate)) { + return candidate; + } + } + } + return null; +} + +function resolveBundledPluginPackagePublicSurfaceAliasMap(params: { + modulePath: string; + argv1?: string; + moduleUrl?: string; + pluginSdkResolution: PluginSdkResolutionPreference; +}): Record { + const packageRoot = resolveLoaderPluginSdkPackageRoot(params); + if (!packageRoot) { + return {}; + } + const extensionsRoot = path.join(packageRoot, "extensions"); + let extensionDirs: fs.Dirent[]; + try { + extensionDirs = fs.readdirSync(extensionsRoot, { withFileTypes: true }); + } catch { + return {}; + } + const orderedKinds = resolvePluginSdkAliasCandidateOrder({ + modulePath: params.modulePath, + isProduction: process.env.NODE_ENV === "production", + pluginSdkResolution: params.pluginSdkResolution, + }); + const aliasMap: Record = {}; + for (const entry of extensionDirs) { + if (!entry.isDirectory()) { + continue; + } + const dirName = entry.name; + const packageName = readBundledPluginPackageName( + path.join(extensionsRoot, dirName, "package.json"), + ); + if (!packageName) { + continue; + } + for (const basename of listBundledPluginPublicSurfaceSourceBasenames( + path.join(extensionsRoot, dirName), + )) { + const target = resolveBundledPluginPublicSurfaceAliasTarget({ + packageRoot, + dirName, + basename, + orderedKinds, + }); + if (!target) { + continue; + } + aliasMap[`${packageName}/${basename}.js`] = normalizeJitiAliasTargetPath(target); + } + } + return aliasMap; +} + function shouldIncludePrivateLocalOnlyPluginSdkSubpaths() { return process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI === "1"; } @@ -626,6 +746,12 @@ export function buildPluginLoaderAliasMap( ...(extensionApiAlias ? { "openclaw/extension-api": normalizeJitiAliasTargetPath(extensionApiAlias) } : {}), + ...resolveBundledPluginPackagePublicSurfaceAliasMap({ + modulePath, + argv1, + moduleUrl, + pluginSdkResolution, + }), ...(pluginSdkAlias ? Object.fromEntries( PLUGIN_SDK_PACKAGE_NAMES.map((packageName) => [