mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
Trust installed Codex for its private task runtime (#81206)
* fix(codex): trust installed codex task runtime * fix(codex): keep private runtime alias packaged
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -265,6 +265,7 @@ const cachedPluginSdkScopedAliasMaps = new PluginLruCache<Record<string, string>
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user