feat(server): bundle Codex and OpenCode binaries instead of requiring global installs

This commit is contained in:
Mohamed Boudra
2026-03-12 10:25:53 +07:00
parent 586b48e150
commit 91dde29146
6 changed files with 435 additions and 52 deletions
@@ -1,4 +1,4 @@
import { execSync, spawn } from "node:child_process";
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import { promises } from "node:fs";
@@ -69,8 +69,6 @@ import type {
} from "../agent-sdk-types.js";
import {
applyProviderEnv,
findExecutable,
isProviderCommandAvailable,
type ProviderRuntimeSettings,
} from "../provider-launch-config.js";
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
@@ -358,22 +356,11 @@ type ClaudeAgentClientOptions = {
type ClaudeAgentSessionOptions = {
defaults?: { agents?: Record<string, AgentDefinition> };
claudePath: string | null;
runtimeSettings?: ProviderRuntimeSettings;
handle?: AgentPersistenceHandle;
logger: Logger;
};
function resolveClaudeBinary(): string {
const claudePath = findExecutable("claude");
if (claudePath) {
return claudePath;
}
throw new Error(
"Claude CLI not found. Install claude or configure agents.providers.claude.command.mode='replace'."
);
}
function resolveClaudeSpawnCommand(
spawnOptions: SpawnOptions,
runtimeSettings?: ProviderRuntimeSettings
@@ -1400,31 +1387,18 @@ export class ClaudeAgentClient implements AgentClient {
private readonly defaults?: { agents?: Record<string, AgentDefinition> };
private readonly logger: Logger;
private readonly claudePath: string | null;
private readonly runtimeSettings?: ProviderRuntimeSettings;
constructor(options: ClaudeAgentClientOptions) {
this.defaults = options.defaults;
this.logger = options.logger.child({ module: "agent", provider: "claude" });
this.runtimeSettings = options.runtimeSettings;
this.claudePath = findExecutable("claude");
if (this.claudePath) {
try {
const version = execSync(`${this.claudePath} --version`, { encoding: "utf8" }).trim();
this.logger.trace({ claudePath: this.claudePath, version }, "Resolved Claude binary");
} catch {
this.logger.trace({ claudePath: this.claudePath }, "Resolved Claude binary (version unknown)");
}
} else {
this.logger.trace("Claude binary not found in PATH; SDK will use bundled binary");
}
}
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
const claudeConfig = this.assertConfig(config);
return new ClaudeAgentSession(claudeConfig, {
defaults: this.defaults,
claudePath: this.claudePath,
runtimeSettings: this.runtimeSettings,
logger: this.logger,
});
@@ -1443,7 +1417,6 @@ export class ClaudeAgentClient implements AgentClient {
const claudeConfig = this.assertConfig(mergedConfig);
return new ClaudeAgentSession(claudeConfig, {
defaults: this.defaults,
claudePath: this.claudePath,
runtimeSettings: this.runtimeSettings,
handle,
logger: this.logger,
@@ -1478,11 +1451,11 @@ export class ClaudeAgentClient implements AgentClient {
}
async isAvailable(): Promise<boolean> {
const commandConfig = this.runtimeSettings?.command;
if (commandConfig?.mode === "replace") {
return isProviderCommandAvailable(commandConfig, resolveClaudeBinary);
const command = this.runtimeSettings?.command;
if (command?.mode === "replace") {
return fs.existsSync(command.argv[0]);
}
return this.claudePath !== null;
return true;
}
private assertConfig(config: AgentSessionConfig): ClaudeAgentConfig {
@@ -1499,7 +1472,6 @@ class ClaudeAgentSession implements AgentSession {
private readonly config: ClaudeAgentConfig;
private readonly defaults?: { agents?: Record<string, AgentDefinition> };
private readonly claudePath: string | null;
private readonly runtimeSettings?: ProviderRuntimeSettings;
private readonly logger: Logger;
private query: Query | null = null;
@@ -1546,7 +1518,6 @@ class ClaudeAgentSession implements AgentSession {
) {
this.config = config;
this.defaults = options.defaults;
this.claudePath = options.claudePath;
this.runtimeSettings = options.runtimeSettings;
this.logger = options.logger;
const handle = options.handle;
@@ -2319,7 +2290,6 @@ class ClaudeAgentSession implements AgentSession {
permissionMode: this.currentMode,
agents: this.defaults?.agents,
canUseTool: this.handlePermissionRequest,
...(this.claudePath ? { pathToClaudeCodeExecutable: this.claudePath } : {}),
// Use Claude Code preset system prompt and load CLAUDE.md files
// Append provider-agnostic system prompt and orchestrator instructions for agents.
systemPrompt: {
@@ -27,8 +27,10 @@ import type { Logger } from "pino";
import { execSync, spawn } from "node:child_process";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { Dirent } from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import readline from "node:readline";
@@ -40,8 +42,6 @@ import {
} from "./codex/tool-call-mapper.js";
import {
applyProviderEnv,
findExecutable,
isProviderCommandAvailable,
resolveProviderCommandPrefix,
type ProviderRuntimeSettings,
} from "../provider-launch-config.js";
@@ -150,13 +150,52 @@ function mergeCodexConfiguredDefaults(
};
}
const CODEX_TARGET_TRIPLE: Record<string, Record<string, string>> = {
linux: { x64: "x86_64-unknown-linux-musl", arm64: "aarch64-unknown-linux-musl" },
darwin: { x64: "x86_64-apple-darwin", arm64: "aarch64-apple-darwin" },
win32: { x64: "x86_64-pc-windows-msvc", arm64: "aarch64-pc-windows-msvc" },
};
const CODEX_PLATFORM_PACKAGE: Record<string, string> = {
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
"aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
"x86_64-apple-darwin": "@openai/codex-darwin-x64",
"aarch64-apple-darwin": "@openai/codex-darwin-arm64",
"x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
"aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
};
function resolveBundledCodexBinary(): string | null {
const targetTriple = CODEX_TARGET_TRIPLE[process.platform]?.[process.arch];
if (!targetTriple) {
return null;
}
const platformPackage = CODEX_PLATFORM_PACKAGE[targetTriple];
if (!platformPackage) {
return null;
}
const binaryName = process.platform === "win32" ? "codex.exe" : "codex";
try {
const require = createRequire(import.meta.url);
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
const vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
const binaryPath = path.join(vendorRoot, targetTriple, "codex", binaryName);
if (existsSync(binaryPath)) {
return binaryPath;
}
} catch {
// Platform package not installed
}
return null;
}
function resolveCodexBinary(): string {
const codexPath = findExecutable("codex");
if (codexPath) {
return codexPath;
const bundled = resolveBundledCodexBinary();
if (bundled) {
return bundled;
}
throw new Error(
"Codex CLI not found. Please install codex globally: npm install -g @openai/codex"
`Bundled Codex binary not found for platform ${process.platform}-${process.arch}. Ensure the @openai/codex package and its platform-specific optional dependency are installed.`
);
}
@@ -3235,6 +3274,10 @@ export class CodexAppServerAgentClient implements AgentClient {
}
async isAvailable(): Promise<boolean> {
return isProviderCommandAvailable(this.runtimeSettings?.command, resolveCodexBinary);
const command = this.runtimeSettings?.command;
if (command?.mode === "replace") {
return existsSync(command.argv[0]);
}
return true;
}
}
@@ -1,4 +1,8 @@
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import os from "node:os";
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
import net from "node:net";
import type { Logger } from "pino";
@@ -29,8 +33,6 @@ import type {
} from "../agent-sdk-types.js";
import {
applyProviderEnv,
findExecutable,
isProviderCommandAvailable,
resolveProviderCommandPrefix,
type ProviderRuntimeSettings,
} from "../provider-launch-config.js";
@@ -150,13 +152,40 @@ const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSch
})
);
const OPENCODE_PLATFORM_MAP: Record<string, string> = {
darwin: "darwin",
linux: "linux",
win32: "windows",
};
function resolveBundledOpenCodeBinary(): string | null {
const platform = OPENCODE_PLATFORM_MAP[os.platform()];
if (!platform) {
return null;
}
const arch = os.arch();
const packageName = `opencode-${platform}-${arch}`;
const binaryName = platform === "windows" ? "opencode.exe" : "opencode";
try {
const require = createRequire(import.meta.url);
const packageJsonPath = require.resolve(`${packageName}/package.json`);
const binaryPath = path.join(path.dirname(packageJsonPath), "bin", binaryName);
if (existsSync(binaryPath)) {
return binaryPath;
}
} catch {
// Platform package not installed
}
return null;
}
function resolveOpenCodeBinary(): string {
const opencodePath = findExecutable("opencode");
if (opencodePath) {
return opencodePath;
const bundled = resolveBundledOpenCodeBinary();
if (bundled) {
return bundled;
}
throw new Error(
"OpenCode CLI not found. Please install opencode globally so Paseo can launch the provider."
`Bundled OpenCode binary not found for platform ${os.platform()}-${os.arch()}. Ensure the opencode-ai package and its platform-specific optional dependency are installed.`
);
}
@@ -522,10 +551,11 @@ export class OpenCodeAgentClient implements AgentClient {
}
async isAvailable(): Promise<boolean> {
return isProviderCommandAvailable(
this.runtimeSettings?.command,
resolveOpenCodeBinary
);
const command = this.runtimeSettings?.command;
if (command?.mode === "replace") {
return existsSync(command.argv[0]);
}
return true;
}
private assertConfig(config: AgentSessionConfig): OpenCodeAgentConfig {