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
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "ejclaw-agent-runner",
|
"name": "ejclaw-agent-runner",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
"@anthropic-ai/claude-agent-sdk": "0.2.114",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"cron-parser": "^5.0.0",
|
"cron-parser": "^5.0.0",
|
||||||
"ejclaw-runners-shared": "file:../shared",
|
"ejclaw-runners-shared": "file:../shared",
|
||||||
@@ -18,7 +18,23 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"start": "bun dist/index.js"
|
"start": "bun dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
"@anthropic-ai/claude-agent-sdk": "0.2.114",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"cron-parser": "^5.0.0",
|
"cron-parser": "^5.0.0",
|
||||||
"ejclaw-runners-shared": "file:../shared",
|
"ejclaw-runners-shared": "file:../shared",
|
||||||
|
|||||||
146
runners/agent-runner/src/bundled-cli-path.ts
Normal file
146
runners/agent-runner/src/bundled-cli-path.ts
Normal file
@@ -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('<pkg>/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,
|
||||||
|
};
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from 'ejclaw-runners-shared';
|
} from 'ejclaw-runners-shared';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
import { resolveBundledClaudeCodeExecutable } from './bundled-cli-path.js';
|
||||||
import {
|
import {
|
||||||
prependRoomRoleHeader,
|
prependRoomRoleHeader,
|
||||||
type RoomRoleContext,
|
type RoomRoleContext,
|
||||||
@@ -88,6 +89,17 @@ function log(message: string): void {
|
|||||||
console.error(`[agent-runner] ${message}`);
|
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
|
// Graceful shutdown: SIGTERM → abort SDK query, allowing cleanup
|
||||||
const agentAbortController = new AbortController();
|
const agentAbortController = new AbortController();
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
@@ -267,6 +279,8 @@ async function runQuery(
|
|||||||
for await (const message of query({
|
for await (const message of query({
|
||||||
prompt: stream,
|
prompt: stream,
|
||||||
options: {
|
options: {
|
||||||
|
// 번들 CLI binary를 명시해야 SDK가 musl/glibc 잘못 탐색하는 걸 우회함.
|
||||||
|
pathToClaudeCodeExecutable: getClaudeCliPath(),
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
model,
|
model,
|
||||||
thinking,
|
thinking,
|
||||||
@@ -702,6 +716,8 @@ async function main(): Promise<void> {
|
|||||||
for await (const message of query({
|
for await (const message of query({
|
||||||
prompt: trimmedPrompt,
|
prompt: trimmedPrompt,
|
||||||
options: {
|
options: {
|
||||||
|
// 번들 CLI binary를 명시해야 SDK가 musl/glibc 잘못 탐색하는 걸 우회함.
|
||||||
|
pathToClaudeCodeExecutable: getClaudeCliPath(),
|
||||||
cwd: mainEffectiveCwd,
|
cwd: mainEffectiveCwd,
|
||||||
resume: sessionId,
|
resume: sessionId,
|
||||||
systemPrompt: undefined,
|
systemPrompt: undefined,
|
||||||
|
|||||||
136
runners/agent-runner/test/bundled-cli-path.test.ts
Normal file
136
runners/agent-runner/test/bundled-cli-path.test.ts
Normal file
@@ -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/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user