Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5d3e4f0
test(server): API-key Claude processes keep the Agent SDK entrypoint
kyle944 Nov 2, 2026
9613eff
fix(server): API-key Claude instances can spend subscriber API credits
kyle944 Nov 2, 2026
fbe6dda
test(server): an empty API key in Claude settings keeps the t3-code l…
kyle944 Nov 2, 2026
8a89037
fix(server): an empty API key in Claude settings is not an API key
kyle944 Nov 2, 2026
069304e
test(server): Claude sessions take their label from effective settings
kyle944 Nov 2, 2026
cddc8c7
fix(server): Claude sessions take their label from effective settings
kyle944 Nov 2, 2026
66118e3
test(server): long-lived Claude env keeps only configured labels
kyle944 Nov 2, 2026
bb5068f
fix(server): long-lived Claude env keeps only configured labels
kyle944 Nov 2, 2026
6934847
test(server): a settings file Claude Code rejects does not change the…
kyle944 Nov 2, 2026
307bcb5
fix(server): read Claude settings env with strict JSON
kyle944 Nov 2, 2026
d8d4f9a
test(server): Windows Claude env names match in any case
kyle944 Nov 2, 2026
339b177
fix(server): match Windows Claude env names in any case
kyle944 Nov 2, 2026
eda0e6c
test(server): all inherited Windows Claude env names match in any case
kyle944 Nov 2, 2026
f69ac89
fix(server): upper-case all inherited Windows names for Claude
kyle944 Nov 2, 2026
9d37d87
merge: sync main into claude api-key entrypoint fix
maria-rcks Nov 4, 2026
e9f63e5
test(server): provide crypto for claude entrypoint session test
maria-rcks Nov 4, 2026
a1dedaa
test(server): Claude managed drop-ins count toward the session label
kyle944 Nov 4, 2026
5d02ac7
fix(server): read Claude managed drop-ins for the session label
kyle944 Nov 4, 2026
ed320ef
test(server): the Claude label follows only settings the process loads
kyle944 Nov 4, 2026
2e5ae7b
fix(server): the Claude label follows only settings the process loads
kyle944 Nov 4, 2026
e330887
test(server): the Claude label reads settings from --project-config-root
kyle944 Nov 4, 2026
562c2c7
fix(server): the Claude label reads settings from --project-config-root
kyle944 Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4361,6 +4361,58 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

it.effect("labels a turn from the Claude settings of its working directory", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-home-" });
const project = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-project-" });
yield* fileSystem.makeDirectory(path.join(project, ".claude"));
const openedEntrypoint = (launchArgs = "") =>
Effect.scoped(
Effect.gen(function* () {
const harness = yield* makeWakeHarnessWithOptions({
settings: { ...DEFAULT_CLAUDE_SETTINGS, homePath: home, launchArgs },
environment: {},
});
yield* harness.runtime.startTurn(
makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("attempt-entrypoint"),
text: "Run git status.",
attachments: [],
runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
...CLAUDE_TEST_RUNTIME_POLICY,
cwd: project,
}),
}),
);
return harness.getOpenedOptions()?.env?.CLAUDE_CODE_ENTRYPOINT;
}),
);
assert.equal(yield* openedEntrypoint(), "t3-code");
assert.isUndefined(
yield* openedEntrypoint(`--settings '{"env":{"ANTHROPIC_API_KEY":"key"}}'`),
);
yield* fileSystem.writeFileString(
path.join(project, ".claude", "settings.local.json"),
'{"env":{"ANTHROPIC_API_KEY":"key"}}',
);
assert.isUndefined(yield* openedEntrypoint());
}).pipe(
Effect.provide(
Layer.mergeAll(
IdAllocator.layer,
McpProviderSessions.layer,
NodeServices.layer,
NodeCrypto.layer,
),
),
),
);

it.effect("pins tool search on when the user's Claude settings choose auto", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
63 changes: 20 additions & 43 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ import {
import {
claudeLaunchArgDirectories,
discoverClaudeSkills,
resolveClaudeLocalSettingsDirectory,
} from "../../provider/Drivers/ClaudeSkills.ts";
import {
compileClaudeModelSelection,
Expand All @@ -164,8 +165,9 @@ import { signalProcessGroup } from "@t3tools/provider-core/server/processGroup";
import {
claudeSignedOutMessage,
makeClaudeEnvironment,
mergeClaudeProcessEnvironment,
mergeClaudeBaseEnvironment,
resolveClaudeHomePath,
resolveClaudeSessionEnvironment,
} from "../../provider/Drivers/ClaudeHome.ts";
import {
BUNDLED_CLAUDE_MODEL_CATALOG,
Expand Down Expand Up @@ -4705,49 +4707,20 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
}),
Effect.orElseSucceed(() => undefined),
);
// A worktree's .git file points at <main>/.git/worktrees/<name>, whose commondir leads back to
// <main>/.git. A bare repository has no main checkout, so its worktree keeps its own file.
const mainCheckoutRoot = (checkout: string) =>
fileSystem.readFileString(path.join(checkout, ".git")).pipe(
Effect.flatMap((pointer) => {
const gitDir = /^gitdir:\s*(.+)$/m.exec(pointer)?.[1]?.trim();
if (gitDir === undefined) return Effect.succeed(checkout);
const worktreeGitDir = path.resolve(checkout, gitDir);
return fileSystem.readFileString(path.join(worktreeGitDir, "commondir")).pipe(
Effect.map((commonDir) => {
const common = path.resolve(worktreeGitDir, commonDir.trim());
return path.basename(common) === ".git" ? path.dirname(common) : checkout;
}),
);
}),
Effect.orElseSucceed(() => checkout),
);
// Claude Code keeps .claude/settings.local.json at the repository root, or in cwd outside a
// repository, on Windows, or when the root is home. Its owner check is skipped: missing it only
// leaves tool search on, Claude Code's default.
const localSettingsDirectory = Effect.fn("ClaudeAdapterV2.localSettingsDirectory")(function* (
cwd: string,
) {
if ((yield* HostProcess.Platform) === "win32") return cwd;
for (let directory = cwd; ; directory = path.dirname(directory)) {
const isCheckout = yield* fileSystem
.exists(path.join(directory, ".git"))
.pipe(Effect.orElseSucceed(() => false));
if (isCheckout) {
const root = yield* mainCheckoutRoot(directory);
return root === (yield* HostProcess.HomeDirectory) ? cwd : root;
}
if (path.dirname(directory) === directory) return cwd;
}
});
// Settings env beats the process env, --settings over local over project over user, and a local
// file an older Claude Code left in cwd applies under the root one. resolveSettings reads local
// from cwd only.
const resolveToolSearchOverride = Effect.fn("ClaudeAdapterV2.resolveToolSearchOverride")(
function* (cwd: string | null, launchArgSettings: ClaudeSdkSettings | undefined) {
const launchValue = launchArgSettings?.env?.ENABLE_TOOL_SEARCH;
if (typeof launchValue === "string") return claudeToolSearchOverride(launchValue);
const localDirectory = cwd === null ? null : yield* localSettingsDirectory(cwd);
const localDirectory =
cwd === null
? null
: yield* resolveClaudeLocalSettingsDirectory(cwd).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
);
const settingsFiles = [
...(cwd === null || localDirectory === null
? []
Expand Down Expand Up @@ -10510,7 +10483,15 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
? {}
: { sdkSettings: claudeSdkLaunchArgSettings(launchArgSettings) }),
environment: claudeQueryEnvironment(
adapterOptions.environment,
yield* resolveClaudeSessionEnvironment(
adapterOptions.settings,
adapterOptions.environment,
turnInput.runtimePolicy.cwd ?? undefined,
launchArgSettings?.env,
).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
),
turnInput.appThread,
turnInput.appThread.lineage.relationshipToParent === "subagent"
? yield* resolveSubagentPromptCacheTtl(turnInput.runtimePolicy.cwd)
Expand Down Expand Up @@ -11852,11 +11833,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(
const queryRunner = yield* ClaudeAgentSdkQueryRunner;
const serverConfig = yield* ServerConfig.ServerConfig;
const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
const baseEnvironment = yield* mergeClaudeProcessEnvironment(
config,
environment,
hostEnvironment,
);
const baseEnvironment = yield* mergeClaudeBaseEnvironment(environment, hostEnvironment);
const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment);
const path = yield* Path.Path;
const random = yield* SecureRandom.SecureRandom;
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import {
makeClaudeCapabilitiesCacheKey,
makeClaudeContinuationGroupKey,
mergeClaudeBaseEnvironment,
mergeClaudeProcessEnvironment,
resolveClaudeHomePath,
} from "./ClaudeHome.ts";
Expand Down Expand Up @@ -169,7 +170,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
);
const textGeneration = yield* makeClaudeTextGeneration(
effectiveConfig,
processEnv,
yield* mergeClaudeBaseEnvironment(environment, process.env),
modelCatalog,
);

Expand Down
Loading
Loading