diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index 8bc961834f270ee3027d7cc7e41a1d1734d8cb73..6d53c7c79c994c72e7b1a6ae25e3dcf530eac152 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -455,6 +455,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* ( claudeSettings: ClaudeSettings, args: ReadonlyArray, environment?: NodeJS.ProcessEnv, + cwd?: string, ) { const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment); const spawnCommand = yield* resolveSpawnCommand(claudeSettings.binaryPath, args, { @@ -463,6 +464,7 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* ( const command = ChildProcess.make(spawnCommand.command, spawnCommand.args, { env: claudeEnvironment, shell: spawnCommand.shell, + ...(cwd ? { cwd } : {}), }); return yield* spawnAndCollect(claudeSettings.binaryPath, command); }); @@ -533,6 +535,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( claudeSettings, ["--version"], resolvedEnvironment, + cwd, ).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result); if (Result.isFailure(versionProbe)) { diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index 0169eb87ba7618d38842334795f2700041d512b9..a379280969ae6756d6b31eeb9bd7e04bfe801061 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -123,7 +123,11 @@ export const ClaudeDriver: ProviderDriver Effect.all([ - probeClaudeCapabilities(effectiveConfig, processEnv, cwd), + probeClaudeCapabilities(effectiveConfig, processEnv, stateDir), ClaudeResetCredits.readClaudeOrganizationId(accountConfigPath), ]).pipe( Effect.map(([capabilities, workspaceId]) => @@ -216,7 +220,7 @@ export const ClaudeDriver: ProviderDriver Cache.get(capabilitiesProbeCache, capabilitiesCacheKey), processEnv, - cwd, + stateDir, catalog, scopedLimitNames, (version) => diff --git a/apps/server/src/provider/ProviderInstanceRegistry.test.ts b/apps/server/src/provider/ProviderInstanceRegistry.test.ts index cc400c170f4adbaf86115b2d7b63a8a929f41b35..eb3dc4109b851de3298e1f3c258b03203d87b280 100644 --- a/apps/server/src/provider/ProviderInstanceRegistry.test.ts +++ b/apps/server/src/provider/ProviderInstanceRegistry.test.ts @@ -437,6 +437,40 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => { }).pipe(Effect.provide(layerTest)), ); + it.live("runs Claude instance probes in the server state dir, not the server cwd", () => + Effect.gen(function* () { + if (yield* HostProcess.isWindows) return; + + const fixtures = yield* makeTildeProviderFixtures(); + const { stateDir } = yield* ServerConfig.ServerConfig; + const claudeId = ProviderInstanceId.make("claude_state_dir"); + const { registry } = yield* makeProviderInstanceRegistry({ + drivers: [ClaudeDriver], + configMap: { + [claudeId]: { + driver: ProviderDriverKind.make("claudeAgent"), + enabled: true, + // Packaged desktop builds start the server in $HOME, where some + // `claude` wrappers refuse to run. + environment: [{ name: "T3_CLAUDE_REQUIRED_CWD", value: stateDir, sensitive: false }], + config: makeClaudeConfig({ + enabled: true, + binaryPath: fixtures.claudeBinaryPath, + homePath: fixtures.claudeHomePath, + }), + }, + }, + }); + const claude = yield* registry.getInstance(claudeId); + + expect(yield* claude!.snapshot.refresh).toMatchObject({ + status: "ready", + version: "2.1.219", + auth: { status: "authenticated" }, + }); + }).pipe(Effect.provide(layerTest)), + ); + const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/testing/ProviderInstanceRegistryLive.fixture.mjs b/apps/server/src/provider/testing/ProviderInstanceRegistryLive.fixture.mjs index 054ca04703e5149b7219d45708e0c3ed824557bf..1726bceeab6236e7e1fac6b5857f841847625f26 100755 --- a/apps/server/src/provider/testing/ProviderInstanceRegistryLive.fixture.mjs +++ b/apps/server/src/provider/testing/ProviderInstanceRegistryLive.fixture.mjs @@ -1,6 +1,12 @@ #!/usr/bin/env node import * as NodeFS from "node:fs"; import * as NodeReadline from "node:readline"; +// Opt-in: act like a `claude` wrapper that only runs inside one directory. +const requiredCwd = process.env.T3_CLAUDE_REQUIRED_CWD; +if (requiredCwd && process.cwd() !== NodeFS.realpathSync(requiredCwd)) { + process.stderr.write(`claude: refusing to run outside ${requiredCwd}\n`); + process.exit(1); +} if (process.argv.includes("--version")) { process.stdout.write("claude 2.1.219\n"); process.exit(0);