From 5de25ca0aa4dc42b5e3b392f8c831b4829d2899c Mon Sep 17 00:00:00 2001 From: ejclaw Date: Mon, 20 Apr 2026 11:09:50 +0900 Subject: [PATCH] fix(runners): explicitly resolve bundled Claude Code binary path SDK 0.2.114's optional-dep resolution tries linux-x64-musl first even on glibc systems, failing if the musl package is installed as empty shell (directory exists but binary missing). This caused 'Claude Code native binary not found' errors on Ubuntu hosts after upgrading SDK. Add a platform-aware helper (resolveBundledClaudeCodeExecutable) that: - Respects EJCLAW_CLAUDE_CLI_PATH env override for custom deployments - Probes platform-specific bundled binary paths in order (glibc before musl) - Throws with tried-paths list if none exist (vs SDK's silent fallback) Wire it into agent-runner query() calls (main run + session command). Also bumps @anthropic-ai/claude-agent-sdk ^0.2.101 -> 0.2.114 so the bundled CLI supports the 'xhigh' effort level introduced in CLI 2.1.111. Keeps EJClaw self-contained: no system /home/*/.local/bin/claude dependency. Verified with CLAUDE_EFFORT=xhigh: - 37/37 agent-runner tests pass (+ 8 new bundled-cli-path tests) - reviewer agent-run Exit Code 0 (Duration 183s) after restart - paired_turn_attempts failed count: 97 -> 0 after fix - Discord reviewer delivery confirmed --- runners/agent-runner/bun.lock | 52 +++---- runners/agent-runner/package.json | 2 +- runners/agent-runner/src/bundled-cli-path.ts | 146 ++++++++++++++++++ runners/agent-runner/src/index.ts | 16 ++ .../test/bundled-cli-path.test.ts | 136 ++++++++++++++++ 5 files changed, 317 insertions(+), 35 deletions(-) create mode 100644 runners/agent-runner/src/bundled-cli-path.ts create mode 100644 runners/agent-runner/test/bundled-cli-path.test.ts diff --git a/runners/agent-runner/bun.lock b/runners/agent-runner/bun.lock index 56a71b6..b550b8c 100644 --- a/runners/agent-runner/bun.lock +++ b/runners/agent-runner/bun.lock @@ -5,7 +5,7 @@ "": { "name": "ejclaw-agent-runner", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.101", + "@anthropic-ai/claude-agent-sdk": "0.2.114", "@modelcontextprotocol/sdk": "^1.12.1", "cron-parser": "^5.0.0", "ejclaw-runners-shared": "file:../shared", @@ -18,7 +18,23 @@ }, }, "packages": { - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.101", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-LwDQ6ueXnd1EVJbP0XICUO5en4FYJ8Cv/98/+RofoRr4l1cdAdYn1/n758DrTeGkMvTqb4lbdyMNs0RnT7mtoA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.114", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.114", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.114" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-plJ+j17jew9tDMHir/90hXrwoB8cZ9GrIyG19zIJcFyQ8pVhRXjZRJCtF2ElfPoiwkxMmNu1Klqyui4xP4shPg=="], + + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.114", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0/6LWrNilWpmiX6Xrj5plsBmCrCdKGERgAlKUZQEJZplnfuweFAJu7WXZB4KBaUpGlPO91zB/yqDh6kp5aZFbA=="], + + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.114", "", { "os": "darwin", "cpu": "x64" }, "sha512-sOHxq1rEO/KZg2iEZILTPn62lMRRMPqtxKx41uGLi3xjVDrAej6Ury9dDZjYBKkK9n4kBylXV0Oom2CZ14dDYw=="], + + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.114", "", { "os": "linux", "cpu": "arm64" }, "sha512-j/SfEoN6+fyEsp8EuPe+xKcGfsZtaBmdUUH+YSRk5H/lYgy38yNsDhdt+AJMQcdMKfHsiwZ3Y9Ajoe9G9wNwHQ=="], + + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.114", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mhd7bumTwWvkgjSJnYvCgyt8DfmLiUoK92mfvAKxHX7i5YSw+h5Kprqh2Cap+2SBbpwZvnwIoEYGCxhGwE5ddg=="], + + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.114", "", { "os": "linux", "cpu": "x64" }, "sha512-wbaExKDleLlm2zHEhb74GKMLVhtO0IUmFhdimQcdL6CdTkmDE8ZJi53tYWE9+jq+XWNRXoM2yEmKPzXoUmsJng=="], + + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.114", "", { "os": "linux", "cpu": "x64" }, "sha512-c1URsameGHAcghen+mY6jvr2oypiAPHXJIdP4huxR25zPdXWv2x+BCy+vcRVeajsq4VmFzAyQJwaM+BXkmXjAw=="], + + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.114", "", { "os": "win32", "cpu": "arm64" }, "sha512-qeWdUpQymcKCA92osPmffG4QogrOSvuffPvm6c2OlMDjCPYs8vKG7bSe1Vq5tP9tfBszKPVJWBDh+2ANkNissQ=="], + + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.114", "", { "os": "win32", "cpu": "x64" }, "sha512-nVr43WwsKvWA6rojw15qBS/f31srukdLxy1KwKzpftlpmkzQ9Lh8uhIafOmoIPzz67f8VJ8JqHE0caA5YrhX9A=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="], @@ -26,38 +42,6 @@ "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], "@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], diff --git a/runners/agent-runner/package.json b/runners/agent-runner/package.json index d3c49a7..1727ebd 100644 --- a/runners/agent-runner/package.json +++ b/runners/agent-runner/package.json @@ -10,7 +10,7 @@ "start": "bun dist/index.js" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.101", + "@anthropic-ai/claude-agent-sdk": "0.2.114", "@modelcontextprotocol/sdk": "^1.12.1", "cron-parser": "^5.0.0", "ejclaw-runners-shared": "file:../shared", diff --git a/runners/agent-runner/src/bundled-cli-path.ts b/runners/agent-runner/src/bundled-cli-path.ts new file mode 100644 index 0000000..ac9d58d --- /dev/null +++ b/runners/agent-runner/src/bundled-cli-path.ts @@ -0,0 +1,146 @@ +/** + * Resolves the path to the Claude Code native CLI binary bundled with + * `@anthropic-ai/claude-agent-sdk`. + * + * SDK 0.2.113+ ships a per-platform native binary via optional dependencies. + * Its internal auto-detection (W7() in sdk.mjs) tries the linux-x64-musl + * package BEFORE linux-x64, so on glibc hosts where bun has installed the musl + * package as an empty shell (no executable binary) the SDK throws: + * + * "Claude Code native binary not found at .../claude-agent-sdk-linux-x64-musl/claude" + * + * By resolving the bundled binary path explicitly we: + * - Skip the SDK's unreliable musl/glibc auto-detection. + * - Keep EJClaw self-contained (no dependency on a host `claude` install). + * + * Override: set `EJCLAW_CLAUDE_CLI_PATH` to a custom absolute binary path. + */ + +import fs from 'fs'; +import { createRequire } from 'module'; +import path from 'path'; + +const ENV_OVERRIDE = 'EJCLAW_CLAUDE_CLI_PATH'; + +interface PlatformCandidate { + pkg: string; + file: string; +} + +function platformCandidates( + platform: NodeJS.Platform = process.platform, + arch: string = process.arch, +): PlatformCandidate[] { + if (platform === 'linux') { + // glibc-first: the SDK gets this wrong (musl-first) — we correct it here. + return [ + { pkg: `@anthropic-ai/claude-agent-sdk-linux-${arch}`, file: 'claude' }, + { + pkg: `@anthropic-ai/claude-agent-sdk-linux-${arch}-musl`, + file: 'claude', + }, + ]; + } + if (platform === 'darwin') { + return [ + { + pkg: `@anthropic-ai/claude-agent-sdk-darwin-${arch}`, + file: arch === 'arm64' ? 'cli' : 'cli', + }, + ]; + } + if (platform === 'win32') { + return [ + { + pkg: `@anthropic-ai/claude-agent-sdk-win32-${arch}`, + file: 'cli.exe', + }, + ]; + } + return []; +} + +/** + * Returns an absolute path to the bundled Claude Code CLI binary. + * + * Resolution order: + * 1. `EJCLAW_CLAUDE_CLI_PATH` env var (if set and file exists). + * 2. Platform-appropriate bundled binary under + * `runners/agent-runner/node_modules/@anthropic-ai/claude-agent-sdk-*`. + * + * Throws if no binary can be found — callers must not fall back silently. + */ +export function resolveBundledClaudeCodeExecutable(options?: { + /** Override platform (tests only). */ + platform?: NodeJS.Platform; + /** Override arch (tests only). */ + arch?: string; + /** Override env source (tests only). */ + env?: NodeJS.ProcessEnv; + /** Override fs.existsSync (tests only). */ + existsSync?: (p: string) => boolean; + /** Override how candidate dirs are resolved (tests only). */ + resolvePackageDir?: (pkg: string) => string | null; +}): string { + const env = options?.env ?? process.env; + const existsSync = options?.existsSync ?? fs.existsSync; + const platform = options?.platform ?? process.platform; + const arch = options?.arch ?? process.arch; + + const override = env[ENV_OVERRIDE]; + if (override && override.trim().length > 0) { + const resolved = path.resolve(override); + if (!existsSync(resolved)) { + throw new Error( + `${ENV_OVERRIDE} is set to "${override}" but no file exists at that path.`, + ); + } + return resolved; + } + + const resolvePackageDir = + options?.resolvePackageDir ?? defaultResolvePackageDir; + + const candidates = platformCandidates(platform, arch); + const tried: string[] = []; + for (const { pkg, file } of candidates) { + const dir = resolvePackageDir(pkg); + if (!dir) { + tried.push(`${pkg} (package not resolvable)`); + continue; + } + const candidatePath = path.join(dir, file); + if (existsSync(candidatePath)) { + return candidatePath; + } + tried.push(candidatePath); + } + + throw new Error( + `Unable to locate a bundled Claude Code CLI binary for platform=${platform} arch=${arch}. ` + + `Tried: ${tried.join(', ') || '(no candidates for this platform)'}. ` + + `Set ${ENV_OVERRIDE} to an absolute path to the CLI binary if you have a custom install.`, + ); +} + +/** + * Default package directory resolver. Uses `require.resolve` against this + * module's location so it works regardless of whether agent-runner is invoked + * from its own node_modules layout or via a parent workspace. + */ +function defaultResolvePackageDir(pkg: string): string | null { + try { + // `require.resolve('/package.json')` returns the absolute path to the + // package's package.json — its dirname is the package root. + const req = createRequire(import.meta.url); + const pkgJson = req.resolve(`${pkg}/package.json`); + return path.dirname(pkgJson); + } catch { + return null; + } +} + +export const __test__ = { + platformCandidates, + ENV_OVERRIDE, +}; diff --git a/runners/agent-runner/src/index.ts b/runners/agent-runner/src/index.ts index 5a7ad62..9665bba 100644 --- a/runners/agent-runner/src/index.ts +++ b/runners/agent-runner/src/index.ts @@ -24,6 +24,7 @@ import { } from 'ejclaw-runners-shared'; import { fileURLToPath } from 'url'; +import { resolveBundledClaudeCodeExecutable } from './bundled-cli-path.js'; import { prependRoomRoleHeader, type RoomRoleContext, @@ -88,6 +89,17 @@ function log(message: string): void { console.error(`[agent-runner] ${message}`); } +// 번들 CLI binary를 명시해야 SDK가 musl/glibc 잘못 탐색하는 걸 우회함. +// (SDK 0.2.114의 W7() 헬퍼는 linux-x64-musl를 linux-x64보다 먼저 시도하므로 +// glibc 호스트에서 musl 패키지가 빈 껍데기로 설치돼 있으면 실패한다.) +let cachedClaudeCliPath: string | null = null; +function getClaudeCliPath(): string { + if (cachedClaudeCliPath) return cachedClaudeCliPath; + cachedClaudeCliPath = resolveBundledClaudeCodeExecutable(); + log(`Resolved bundled Claude Code CLI: ${cachedClaudeCliPath}`); + return cachedClaudeCliPath; +} + // Graceful shutdown: SIGTERM → abort SDK query, allowing cleanup const agentAbortController = new AbortController(); process.on('SIGTERM', () => { @@ -267,6 +279,8 @@ async function runQuery( for await (const message of query({ prompt: stream, options: { + // 번들 CLI binary를 명시해야 SDK가 musl/glibc 잘못 탐색하는 걸 우회함. + pathToClaudeCodeExecutable: getClaudeCliPath(), cwd: effectiveCwd, model, thinking, @@ -702,6 +716,8 @@ async function main(): Promise { for await (const message of query({ prompt: trimmedPrompt, options: { + // 번들 CLI binary를 명시해야 SDK가 musl/glibc 잘못 탐색하는 걸 우회함. + pathToClaudeCodeExecutable: getClaudeCliPath(), cwd: mainEffectiveCwd, resume: sessionId, systemPrompt: undefined, diff --git a/runners/agent-runner/test/bundled-cli-path.test.ts b/runners/agent-runner/test/bundled-cli-path.test.ts new file mode 100644 index 0000000..2c5dd54 --- /dev/null +++ b/runners/agent-runner/test/bundled-cli-path.test.ts @@ -0,0 +1,136 @@ +import fs from 'fs'; +import path from 'path'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { resolveBundledClaudeCodeExecutable } from '../src/bundled-cli-path.js'; + +describe('resolveBundledClaudeCodeExecutable', () => { + const origEnv = process.env.EJCLAW_CLAUDE_CLI_PATH; + + afterEach(() => { + if (origEnv === undefined) { + delete process.env.EJCLAW_CLAUDE_CLI_PATH; + } else { + process.env.EJCLAW_CLAUDE_CLI_PATH = origEnv; + } + }); + + it('honors EJCLAW_CLAUDE_CLI_PATH when the file exists', () => { + const fake = '/tmp/fake-claude-binary-path'; + const existsSync = (p: string): boolean => p === fake; + const result = resolveBundledClaudeCodeExecutable({ + env: { EJCLAW_CLAUDE_CLI_PATH: fake }, + existsSync, + platform: 'linux', + arch: 'x64', + resolvePackageDir: () => null, + }); + expect(result).toBe(path.resolve(fake)); + }); + + it('throws when env override points at a non-existent file', () => { + const existsSync = () => false; + expect(() => + resolveBundledClaudeCodeExecutable({ + env: { EJCLAW_CLAUDE_CLI_PATH: '/does/not/exist' }, + existsSync, + platform: 'linux', + arch: 'x64', + resolvePackageDir: () => null, + }), + ).toThrow(/EJCLAW_CLAUDE_CLI_PATH/); + }); + + it('prefers Linux glibc binary over musl binary', () => { + const glibcDir = '/fake/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64'; + const muslDir = + '/fake/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl'; + const existsSync = (p: string): boolean => { + // Simulate: both dirs resolve, but only glibc has an actual binary. + return p === path.join(glibcDir, 'claude'); + }; + const result = resolveBundledClaudeCodeExecutable({ + env: {}, + existsSync, + platform: 'linux', + arch: 'x64', + resolvePackageDir: (pkg) => + pkg === '@anthropic-ai/claude-agent-sdk-linux-x64' + ? glibcDir + : pkg === '@anthropic-ai/claude-agent-sdk-linux-x64-musl' + ? muslDir + : null, + }); + expect(result).toBe(path.join(glibcDir, 'claude')); + }); + + it('falls back to musl binary if glibc package is not present', () => { + const muslDir = + '/fake/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl'; + const existsSync = (p: string): boolean => + p === path.join(muslDir, 'claude'); + const result = resolveBundledClaudeCodeExecutable({ + env: {}, + existsSync, + platform: 'linux', + arch: 'x64', + resolvePackageDir: (pkg) => + pkg === '@anthropic-ai/claude-agent-sdk-linux-x64-musl' ? muslDir : null, + }); + expect(result).toBe(path.join(muslDir, 'claude')); + }); + + it('resolves Darwin arm64 binary under `cli`', () => { + const dir = '/fake/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64'; + const existsSync = (p: string): boolean => p === path.join(dir, 'cli'); + const result = resolveBundledClaudeCodeExecutable({ + env: {}, + existsSync, + platform: 'darwin', + arch: 'arm64', + resolvePackageDir: (pkg) => + pkg === '@anthropic-ai/claude-agent-sdk-darwin-arm64' ? dir : null, + }); + expect(result).toBe(path.join(dir, 'cli')); + }); + + it('resolves Windows binary under `cli.exe`', () => { + const dir = 'C:\\\\fake\\\\node_modules\\\\@anthropic-ai\\\\claude-agent-sdk-win32-x64'; + const binary = path.join(dir, 'cli.exe'); + const existsSync = (p: string): boolean => p === binary; + const result = resolveBundledClaudeCodeExecutable({ + env: {}, + existsSync, + platform: 'win32', + arch: 'x64', + resolvePackageDir: (pkg) => + pkg === '@anthropic-ai/claude-agent-sdk-win32-x64' ? dir : null, + }); + expect(result).toBe(binary); + }); + + it('throws a descriptive error when no binary is found', () => { + const existsSync = () => false; + expect(() => + resolveBundledClaudeCodeExecutable({ + env: {}, + existsSync, + platform: 'linux', + arch: 'x64', + resolvePackageDir: () => null, + }), + ).toThrow(/Unable to locate a bundled Claude Code CLI binary/); + }); + + it('end-to-end: resolves the actual bundled binary on this host', () => { + // Real-world smoke test — the file must exist in the installed workspace. + const result = resolveBundledClaudeCodeExecutable({ + env: {}, + }); + expect(fs.existsSync(result)).toBe(true); + // On Linux glibc we expect the non-musl binary to win. + if (process.platform === 'linux') { + expect(result).not.toMatch(/linux-x64-musl/); + } + }); +});