diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 48b64dbd5d66..124b044387cc 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -1,3 +1,5 @@ +import * as NodeOS from "node:os"; + import type { Query as ClaudeQuery, SDKMessage, @@ -46,6 +48,7 @@ import { Tool } from "effect/unstable/ai"; import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction"; import { attachmentRelativePath } from "../../attachmentStore.ts"; +import { ServerConfig } from "../../config.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import { PreviewControlsToolkit } from "../../mcp/toolkits/previewControls/tools.ts"; import { EnvironmentToolkit } from "../../mcp/toolkits/environment/tools.ts"; @@ -68,6 +71,7 @@ import { CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS, CLAUDE_T3_MCP_TOOL_WILDCARD, ClaudeProviderCapabilitiesV2, + ClaudeAgentSdkQueryRunner, ClaudeAgentSdkQueryRunnerError, claudeEffectiveQueryPolicyKey, claudeProviderTurnTokenUsage, @@ -79,6 +83,7 @@ import { claudeTodoSteps, claudeProposedPlan, awaitClaudeApprovalDecision, + createClaudeAdapterV2, loggedClaudeQueryOptions, makeClaudeAdapterV2, makeClaudeAgentSdkProtocolLogger, @@ -961,6 +966,71 @@ describe("ClaudeAdapterV2 approval cancellation", () => { ); }); +describe("ClaudeAdapterV2 executable path", () => { + it.effect("expands ~ in the configured binary path for the SDK", () => + Effect.scoped( + Effect.gen(function* () { + const path = yield* Path.Path; + const executablePaths: Array = []; + const adapter = yield* createClaudeAdapterV2( + { + instanceId: CLAUDE_DEFAULT_INSTANCE_ID, + displayName: undefined, + environment: [], + enabled: true, + config: { ...DEFAULT_CLAUDE_SETTINGS, binaryPath: "~/bin/claude" }, + }, + {}, + ).pipe( + Effect.provide( + ServerConfig.layerTest(process.cwd(), { prefix: "t3-claude-binary-home-" }), + ), + Effect.provideService(ClaudeAgentSdkQueryRunner, { + allocateSessionId: Effect.succeed("native-thread-claude-binary-home"), + open: (input) => + Effect.sync(() => { + executablePaths.push(input.options.pathToClaudeCodeExecutable); + return { + messages: Stream.never, + offer: () => Effect.void, + setModel: () => Effect.void, + interrupt: Effect.void, + close: Effect.void, + }; + }), + forkSession: () => Effect.die("unused"), + assertComplete: Effect.void, + }), + ); + const threadId = ThreadId.make("thread-claude-binary-home"); + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("provider-session-claude-binary-home"), + modelSelection: CLAUDE_TEST_MODEL_SELECTION, + runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection: CLAUDE_TEST_MODEL_SELECTION, + runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, + }); + yield* runtime.startTurn( + makeClaudeTestTurnInput({ + threadId, + providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("attempt-claude-binary-home"), + text: "hello", + attachments: [], + }), + ); + + assert.deepEqual(executablePaths, [path.join(NodeOS.homedir(), "bin", "claude")]); + }), + ).pipe(Effect.provide(Layer.merge(idAllocatorLayer, NodeServices.layer))), + ); +}); + describe("ClaudeAdapterV2 resume compaction", () => { it.effect("resolves and cancels the SDK resume dialog through structured runtime input", () => Effect.scoped( diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 7ba2b376ac83..1bcb7716dd4e 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -82,6 +82,7 @@ import { planClaudeSkillDispatch } from "../../provider/Drivers/ClaudeSkillDispa import { discoverClaudeSkills } from "../../provider/Drivers/ClaudeSkills.ts"; import { compileClaudeModelSelection } from "../../claudeModelOptions.ts"; import { ServerConfig } from "../../config.ts"; +import { expandHomePath } from "../../pathExpansion.ts"; import { claudeSignedOutMessage, makeClaudeEnvironment, @@ -6558,7 +6559,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( const path = yield* Path.Path; return makeClaudeAdapterV2({ instanceId, - settings: { ...config, enabled }, + settings: { ...config, enabled, binaryPath: expandHomePath(config.binaryPath) }, environment: claudeEnvironment, attachmentsDir: serverConfig.attachmentsDir, fileSystem,