Files
openclaw/extensions/codex/src/app-server/computer-use-cache.ts
clawsweeper[bot] 1b6393c51e fix (Computer Use) Stabilize Codex Computer Use readiness (#103331)
* fix (Computer Use) Stabilize Codex Computer Use readiness

Co-authored-by: Ben Badejo <188106718+bdjben@users.noreply.github.com>

* fix (Computer Use) Stabilize Codex Computer Use readiness

* fix (Computer Use) Stabilize Codex Computer Use readiness

* fix (Computer Use) Stabilize Codex Computer Use readiness

* fix(clawsweeper): address review for automerge-openclaw-openclaw-103331 (1)

* fix (Computer Use) Stabilize Codex Computer Use readiness

---------

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Ben Badejo <188106718+bdjben@users.noreply.github.com>
2026-07-11 09:42:27 -05:00

185 lines
5.8 KiB
TypeScript

/** Shared Computer Use plugin cache reconciliation for isolated Codex homes. */
import fs from "node:fs/promises";
import path from "node:path";
import type { ResolvedCodexComputerUseConfig } from "./config.js";
import {
resolveFirstExistingMacOSDesktopCodexBundledMarketplacePath,
resolveMacOSDesktopCodexBundledMarketplaceCandidates,
} from "./desktop-app-paths.js";
export type CodexComputerUsePluginCacheRepairResult =
| {
status: "disabled" | "explicit_marketplace" | "independent" | "source_missing" | "shared";
changed: boolean;
message: string;
cachePath?: string;
targetPath?: string;
version?: string;
removedStaleVersions: string[];
warnings: string[];
}
| {
status: "failed";
changed: false;
message: string;
removedStaleVersions: string[];
warnings: string[];
};
export const DEFAULT_CODEX_COMPUTER_USE_BUNDLED_MARKETPLACE_PATH =
resolveMacOSDesktopCodexBundledMarketplaceCandidates("darwin")[0] ?? "";
const DEFAULT_BUNDLED_MARKETPLACE_NAME = "openai-bundled";
export async function ensureCodexComputerUseSharedPluginCache(params: {
codexHome: string;
config: ResolvedCodexComputerUseConfig;
bundledMarketplacePath?: string;
bundledMarketplacePathCandidates?: readonly string[];
}): Promise<CodexComputerUsePluginCacheRepairResult> {
if (!params.config.enabled) {
return skippedCacheResult(
"disabled",
"Computer Use cache sharing skipped because it is disabled.",
);
}
if (params.config.pluginCacheMode === "independent") {
return skippedCacheResult(
"independent",
"Computer Use cache sharing skipped because pluginCacheMode is independent.",
);
}
if (params.config.marketplaceName || params.config.marketplacePath) {
return skippedCacheResult(
"explicit_marketplace",
"Computer Use cache sharing skipped because an explicit marketplace is configured.",
);
}
const bundledMarketplacePath = resolveComputerUseBundledMarketplacePath(params);
const sourcePluginRoot = path.join(bundledMarketplacePath, "plugins", params.config.pluginName);
const version = await readBundledPluginVersion(sourcePluginRoot);
if (!version) {
return skippedCacheResult(
"source_missing",
`Computer Use bundled plugin source was not found at ${sourcePluginRoot}.`,
);
}
const marketplaceName = params.config.marketplaceName ?? DEFAULT_BUNDLED_MARKETPLACE_NAME;
const cacheRoot = path.join(
params.codexHome,
"plugins",
"cache",
marketplaceName,
params.config.pluginName,
);
const cachePath = path.join(cacheRoot, version);
const changed = await ensureRealDirectoryCopy(cachePath, sourcePluginRoot, version);
return {
status: "shared",
changed,
cachePath,
targetPath: sourcePluginRoot,
version,
removedStaleVersions: [],
warnings: [],
message: `Computer Use plugin cache ${cachePath} contains bundled plugin ${sourcePluginRoot}.`,
};
}
function resolveComputerUseBundledMarketplacePath(params: {
bundledMarketplacePath?: string;
bundledMarketplacePathCandidates?: readonly string[];
}): string {
return (
params.bundledMarketplacePath ??
resolveFirstExistingMacOSDesktopCodexBundledMarketplacePath({
candidates: params.bundledMarketplacePathCandidates,
}) ??
params.bundledMarketplacePathCandidates?.[0] ??
DEFAULT_CODEX_COMPUTER_USE_BUNDLED_MARKETPLACE_PATH
);
}
async function readBundledPluginVersion(sourcePluginRoot: string): Promise<string | undefined> {
const pluginJsonPath = path.join(sourcePluginRoot, ".codex-plugin", "plugin.json");
let raw: string;
try {
raw = await fs.readFile(pluginJsonPath, "utf8");
} catch {
return undefined;
}
try {
const parsed = JSON.parse(raw) as { version?: unknown };
return typeof parsed.version === "string" && parsed.version.trim()
? parsed.version.trim()
: undefined;
} catch {
return undefined;
}
}
async function ensureRealDirectoryCopy(
cachePath: string,
sourcePluginRoot: string,
version: string,
): Promise<boolean> {
await fs.mkdir(path.dirname(cachePath), { recursive: true });
const stat = await fs.lstat(cachePath).catch(() => undefined);
if (stat?.isDirectory() && !stat.isSymbolicLink()) {
const cachedVersion = await readBundledPluginVersion(cachePath);
if (cachedVersion === version) {
return false;
}
}
const cacheRoot = path.dirname(cachePath);
const cacheName = path.basename(cachePath);
const stagingRoot = await fs.mkdtemp(path.join(cacheRoot, `.${cacheName}.staging-`));
const stagedPath = path.join(stagingRoot, cacheName);
const backupPath = path.join(cacheRoot, `.${cacheName}.backup-${process.pid}-${Date.now()}`);
let backupCreated = false;
try {
await fs.cp(sourcePluginRoot, stagedPath, { recursive: true });
if (stat) {
await fs.rename(cachePath, backupPath);
backupCreated = true;
}
try {
await fs.rename(stagedPath, cachePath);
} catch (error) {
if (backupCreated) {
try {
await fs.rename(backupPath, cachePath);
backupCreated = false;
} catch (restoreError) {
throw new Error(
`Failed to install Computer Use cache ${cachePath} and restore its prior copy: ${String(error)}`,
{ cause: restoreError },
);
}
}
throw error;
}
if (backupCreated) {
await fs.rm(backupPath, { recursive: true, force: true });
}
return true;
} finally {
await fs.rm(stagingRoot, { recursive: true, force: true });
}
}
function skippedCacheResult(
status: "disabled" | "explicit_marketplace" | "independent" | "source_missing",
message: string,
): CodexComputerUsePluginCacheRepairResult {
return {
status,
changed: false,
message,
removedStaleVersions: [],
warnings: status === "source_missing" ? [message] : [],
};
}