diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e89f0c2c5e7..d1986b00ec78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai - Require admin scope for node device token management [AI]. (#81067) Thanks @pgondhi987. - Restrict chat sender allowlist matching [AI]. (#80898) Thanks @pgondhi987. - Sessions: redact persisted tool result detail metadata before writing transcripts so diagnostic secrets do not survive tool output redaction. (#80444) Thanks @nimbleenigma. +- Codex runtime: allow the official installed `@openclaw/codex` package to use its private task-runtime SDK helper, fixing `MODULE_NOT_FOUND` during migrated OpenAI/Codex beta runs. - Codex migration: make Enter activate the highlighted checkbox row before continuing, so `Skip for now` and bulk-selection rows work even when planned items start preselected. - fix: harden safe-bin argument validation [AI]. (#80999) Thanks @pgondhi987. - fix: scan plugin runtime entries during install [AI]. (#80998) Thanks @pgondhi987. diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index 0ac128acd11f..c1e280a064fd 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -244,6 +244,28 @@ function writePluginEntry(root: string, relativePath: string) { return pluginEntry; } +function writeInstalledPluginEntry(params: { + installRoot: string; + packageName: string; + entry?: string; +}) { + const entry = params.entry ?? "dist/index.js"; + const packageRoot = path.join( + params.installRoot, + "node_modules", + ...params.packageName.split("/"), + ); + const pluginEntry = path.join(packageRoot, entry); + mkdirSafeDir(path.dirname(pluginEntry)); + fs.writeFileSync( + path.join(packageRoot, "package.json"), + JSON.stringify({ name: params.packageName, type: "module" }, null, 2), + "utf-8", + ); + fs.writeFileSync(pluginEntry, 'export const plugin = "installed";\n', "utf-8"); + return { packageRoot, pluginEntry }; +} + function createUserInstalledPluginSdkAliasFixture() { const { fixture, sourcePluginEntryPath, sourceRootAlias, sourceChannelRuntimePath } = createPluginSdkAliasTargetFixture(); @@ -631,16 +653,15 @@ describe("plugin sdk alias helpers", () => { expect(subpaths).toEqual(["core", "qa-channel", "qa-channel-protocol", "qa-lab", "qa-runtime"]); }); - it("adds the non-QA private Codex task runtime subpath only for bundled Codex", () => { + it("adds the non-QA private Codex task runtime subpath only for trusted Codex plugins", () => { const fixture = createPluginSdkAliasFixture({ packageExports: { "./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" }, }, }); - fs.writeFileSync( + fs.rmSync( path.join(fixture.root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"), - JSON.stringify(["codex-native-task-runtime", "qa-runtime"], null, 2), - "utf-8", + { force: true }, ); fs.writeFileSync( path.join(fixture.root, "src", "plugin-sdk", "codex-native-task-runtime.ts"), @@ -660,6 +681,25 @@ describe("plugin sdk alias helpers", () => { fixture.root, bundledPluginFile("demo", "src/index.ts"), ); + const { packageRoot: installedCodexRoot, pluginEntry: installedCodexEntry } = + writeInstalledPluginEntry({ + installRoot: path.join(makeTempDir(), ".openclaw", "npm"), + packageName: "@openclaw/codex", + }); + const { packageRoot: installedOtherRoot, pluginEntry: installedOtherEntry } = + writeInstalledPluginEntry({ + installRoot: path.join(makeTempDir(), ".openclaw", "npm"), + packageName: "@openclaw/demo", + }); + const shadowCodexRoot = path.join(makeTempDir(), ".openclaw", "extensions", "codex-shadow"); + const shadowCodexEntry = path.join(shadowCodexRoot, "dist", "index.js"); + mkdirSafeDir(path.dirname(shadowCodexEntry)); + fs.writeFileSync( + path.join(shadowCodexRoot, "package.json"), + JSON.stringify({ name: "@openclaw/codex", type: "module" }, null, 2), + "utf-8", + ); + fs.writeFileSync(shadowCodexEntry, 'export const plugin = "shadow";\n', "utf-8"); const codexSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () => listPluginSdkExportedSubpaths({ @@ -671,9 +711,36 @@ describe("plugin sdk alias helpers", () => { modulePath: sourceOtherEntry, }), ); + const installedCodexSubpaths = withCwd(installedCodexRoot, () => + withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () => + listPluginSdkExportedSubpaths({ + modulePath: installedCodexEntry, + argv1: path.join(fixture.root, "openclaw.mjs"), + }), + ), + ); + const installedOtherSubpaths = withCwd(installedOtherRoot, () => + withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () => + listPluginSdkExportedSubpaths({ + modulePath: installedOtherEntry, + argv1: path.join(fixture.root, "openclaw.mjs"), + }), + ), + ); + const shadowCodexSubpaths = withCwd(shadowCodexRoot, () => + withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () => + listPluginSdkExportedSubpaths({ + modulePath: shadowCodexEntry, + argv1: path.join(fixture.root, "openclaw.mjs"), + }), + ), + ); expect(codexSubpaths).toEqual(["codex-native-task-runtime", "core"]); + expect(installedCodexSubpaths).toEqual(["codex-native-task-runtime", "core"]); expect(otherSubpaths).toEqual(["core"]); + expect(installedOtherSubpaths).toEqual(["core"]); + expect(shadowCodexSubpaths).toEqual(["core"]); }); it("does not reuse a non-private cached subpath list after private qa gets enabled", () => { @@ -844,7 +911,7 @@ describe("plugin sdk alias helpers", () => { ); }); - it("aliases non-QA private plugin-sdk subpaths for bundled runtime source loading", () => { + it("aliases non-QA private plugin-sdk subpaths for trusted Codex runtime loading", () => { const fixture = createPluginSdkAliasFixture({ packageExports: { "./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" }, @@ -857,18 +924,30 @@ describe("plugin sdk alias helpers", () => { "plugin-sdk", "codex-native-task-runtime.ts", ); + const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs"); + const distCodexNativeTaskRuntimePath = path.join( + fixture.root, + "dist", + "plugin-sdk", + "codex-native-task-runtime.js", + ); const sourceQaRuntimePath = path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts"); fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8"); - fs.writeFileSync( + fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8"); + fs.rmSync( path.join(fixture.root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"), - JSON.stringify(["codex-native-task-runtime", "qa-runtime"], null, 2), - "utf-8", + { force: true }, ); fs.writeFileSync( sourceCodexNativeTaskRuntimePath, "export const codexNativeTaskRuntime = true;\n", "utf-8", ); + fs.writeFileSync( + distCodexNativeTaskRuntimePath, + "export const codexNativeTaskRuntime = true;\n", + "utf-8", + ); fs.writeFileSync(sourceQaRuntimePath, "export const qaRuntime = true;\n", "utf-8"); const sourcePluginEntry = writePluginEntry( fixture.root, @@ -878,6 +957,25 @@ describe("plugin sdk alias helpers", () => { fixture.root, bundledPluginFile("demo", "src/index.ts"), ); + const { packageRoot: installedCodexRoot, pluginEntry: installedCodexEntry } = + writeInstalledPluginEntry({ + installRoot: path.join(makeTempDir(), ".openclaw", "npm"), + packageName: "@openclaw/codex", + }); + const { packageRoot: installedOtherRoot, pluginEntry: installedOtherEntry } = + writeInstalledPluginEntry({ + installRoot: path.join(makeTempDir(), ".openclaw", "npm"), + packageName: "@openclaw/demo", + }); + const shadowCodexRoot = path.join(makeTempDir(), ".openclaw", "extensions", "codex-shadow"); + const shadowCodexEntry = path.join(shadowCodexRoot, "dist", "index.js"); + mkdirSafeDir(path.dirname(shadowCodexEntry)); + fs.writeFileSync( + path.join(shadowCodexRoot, "package.json"), + JSON.stringify({ name: "@openclaw/codex", type: "module" }, null, 2), + "utf-8", + ); + fs.writeFileSync(shadowCodexEntry, 'export const plugin = "shadow";\n', "utf-8"); const aliases = withEnv( { OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, @@ -887,6 +985,36 @@ describe("plugin sdk alias helpers", () => { { OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () => buildPluginLoaderAliasMap(sourceOtherPluginEntry), ); + const installedAliases = withCwd(installedCodexRoot, () => + withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () => + buildPluginLoaderAliasMap( + installedCodexEntry, + path.join(fixture.root, "openclaw.mjs"), + undefined, + "dist", + ), + ), + ); + const shadowCodexAliases = withCwd(shadowCodexRoot, () => + withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () => + buildPluginLoaderAliasMap( + shadowCodexEntry, + path.join(fixture.root, "openclaw.mjs"), + undefined, + "dist", + ), + ), + ); + const installedOtherAliases = withCwd(installedOtherRoot, () => + withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () => + buildPluginLoaderAliasMap( + installedOtherEntry, + path.join(fixture.root, "openclaw.mjs"), + undefined, + "dist", + ), + ), + ); expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe( fs.realpathSync(sourceRootAlias), @@ -894,8 +1022,13 @@ describe("plugin sdk alias helpers", () => { expect(fs.realpathSync(aliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? "")).toBe( fs.realpathSync(sourceCodexNativeTaskRuntimePath), ); + expect( + fs.realpathSync(installedAliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? ""), + ).toBe(fs.realpathSync(distCodexNativeTaskRuntimePath)); expect(aliases["openclaw/plugin-sdk/qa-runtime"]).toBeUndefined(); expect(otherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined(); + expect(installedOtherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined(); + expect(shadowCodexAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined(); }); it("applies explicit dist resolution to plugin-sdk subpath aliases too", () => { diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index e82457370c97..9106da45b803 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -265,6 +265,7 @@ const cachedPluginSdkScopedAliasMaps = new PluginLruCache MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES, ); const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const; +const OFFICIAL_CODEX_PLUGIN_PACKAGE_NAME = "@openclaw/codex"; const CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH = "codex-native-task-runtime"; const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [ ".ts", @@ -309,10 +310,14 @@ function readPrivateLocalOnlyPluginSdkSubpaths(packageRoot: string): string[] { const parsed = tryReadJsonSync( path.join(packageRoot, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"), ); - if (!Array.isArray(parsed)) { - return []; - } - return parsed.filter((subpath): subpath is string => isSafePluginSdkSubpathSegment(subpath)); + return [ + ...new Set([ + CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH, + ...(Array.isArray(parsed) + ? parsed.filter((subpath): subpath is string => isSafePluginSdkSubpathSegment(subpath)) + : []), + ]), + ]; } function readBundledPluginPackageName(packageJsonPath: string): string | null { @@ -467,6 +472,40 @@ function isBundledCodexPluginModulePath(params: { packageRoot: string; modulePat ); } +function isOfficialInstalledCodexPluginPackageRoot(packageRoot: string) { + const segments = path.resolve(packageRoot).split(path.sep).filter(Boolean); + const last = segments.at(-1); + const scope = segments.at(-2); + const nodeModules = segments.at(-3); + return last === "codex" && scope === "@openclaw" && nodeModules === "node_modules"; +} + +function isOfficialInstalledCodexPluginModulePath(params: { modulePath: string }) { + let cursor = path.dirname(path.resolve(params.modulePath)); + for (let depth = 0; depth < 12; depth += 1) { + const packageJson = tryReadJsonSync<{ name?: unknown }>(path.join(cursor, "package.json")); + if (packageJson) { + return ( + packageJson.name === OFFICIAL_CODEX_PLUGIN_PACKAGE_NAME && + isOfficialInstalledCodexPluginPackageRoot(cursor) + ); + } + const parent = path.dirname(cursor); + if (parent === cursor) { + break; + } + cursor = parent; + } + return false; +} + +function isTrustedCodexPluginModulePath(params: { packageRoot: string; modulePath: string }) { + return ( + isBundledCodexPluginModulePath(params) || + isOfficialInstalledCodexPluginModulePath({ modulePath: params.modulePath }) + ); +} + function shouldIncludePrivateLocalOnlyPluginSdkSubpath(params: { packageRoot: string; modulePath: string; @@ -475,7 +514,7 @@ function shouldIncludePrivateLocalOnlyPluginSdkSubpath(params: { return ( shouldIncludePrivateLocalOnlyPluginSdkSubpaths() || (params.subpath === CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH && - isBundledCodexPluginModulePath({ + isTrustedCodexPluginModulePath({ packageRoot: params.packageRoot, modulePath: params.modulePath, })) @@ -535,7 +574,7 @@ export function listPluginSdkExportedSubpaths( if (!packageRoot) { return []; } - const includeCodexPrivateRuntime = isBundledCodexPluginModulePath({ packageRoot, modulePath }); + const includeCodexPrivateRuntime = isTrustedCodexPluginModulePath({ packageRoot, modulePath }); const cacheKey = `${packageRoot}::privateQa=${shouldIncludePrivateLocalOnlyPluginSdkSubpaths() ? "1" : "0"}::codexPrivate=${includeCodexPrivateRuntime ? "1" : "0"}`; const cached = cachedPluginSdkExportedSubpaths.get(cacheKey); if (cached) { @@ -573,7 +612,7 @@ export function resolvePluginSdkScopedAliasMap( isProduction: process.env.NODE_ENV === "production", pluginSdkResolution: params.pluginSdkResolution, }); - const includeCodexPrivateRuntime = isBundledCodexPluginModulePath({ packageRoot, modulePath }); + const includeCodexPrivateRuntime = isTrustedCodexPluginModulePath({ packageRoot, modulePath }); const cacheKey = `${packageRoot}::${orderedKinds.join(",")}::privateQa=${shouldIncludePrivateLocalOnlyPluginSdkSubpaths() ? "1" : "0"}::codexPrivate=${includeCodexPrivateRuntime ? "1" : "0"}`; const cached = cachedPluginSdkScopedAliasMaps.get(cacheKey); if (cached) {