Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ it.layer(NodeServices.layer)("Claude capability probe SDK boundary", (it) => {
email: "dev@example.com",
subscriptionType: "pro",
tokenSource: "oauth",
apiKeySource: undefined,
apiProvider: undefined,
slashCommands: [
{
Expand Down
39 changes: 38 additions & 1 deletion apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
type ServerProviderDraft,
} from "../providerSnapshot.ts";
import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts";
import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";
import { claudeSignedOutMessage, makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";
import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts";
import { makeUnavailableUsageLimits } from "../providerUsageLimits.ts";
import {
Expand Down Expand Up @@ -227,6 +227,11 @@ type ClaudeCapabilitiesProbe = {
readonly email: string | undefined;
readonly subscriptionType: string | undefined;
readonly tokenSource: string | undefined;
/**
* Where an API key came from (e.g. "ANTHROPIC_API_KEY"), reported by the
* CLI alongside `tokenSource: "none"` for key-based first-party auth.
*/
readonly apiKeySource: string | undefined;
/**
* Active API backend reported by the SDK's `AccountInfo`. Anthropic OAuth
* login only applies when `"firstParty"`; for Amazon Bedrock (`"bedrock"`)
Expand Down Expand Up @@ -376,13 +381,15 @@ const probeClaudeCapabilities = (
readonly email?: string;
readonly subscriptionType?: string;
readonly tokenSource?: string;
readonly apiKeySource?: string;
readonly apiProvider?: string;
}
| undefined;
return {
email: account?.email,
subscriptionType: account?.subscriptionType,
tokenSource: account?.tokenSource,
apiKeySource: account?.apiKeySource,
apiProvider: account?.apiProvider,
slashCommands: parseClaudeInitializationCommands(init.commands),
...(usage ? { usage } : {}),
Expand Down Expand Up @@ -554,6 +561,36 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
});
}

// A logged-out first-party CLI still initializes, reporting tokenSource
// "none" and no apiKeySource. API-key setups share the tokenSource but the
// CLI names their key source, so they stay authenticated.
if (
capabilities.apiProvider === "firstParty" &&
capabilities.tokenSource === "none" &&
!capabilities.apiKeySource
Comment thread
none23 marked this conversation as resolved.
) {
const path = yield* Path.Path;
const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, resolvedEnvironment);
return buildServerProvider({
presentation: CLAUDE_PRESENTATION,
enabled: claudeSettings.enabled,
checkedAt,
models,
slashCommands: dedupedSlashCommands,
skills,
probe: {
installed: true,
version: parsedVersion,
status: "error",
auth: { status: "unauthenticated" },
message: claudeSignedOutMessage({
Comment thread
none23 marked this conversation as resolved.
configDir: claudeEnvironment.CLAUDE_CONFIG_DIR,
cwd: path.resolve(cwd ?? "."),
}),
},
});
}

const authMetadata =
claudeAuthMetadata({
subscriptionType: capabilities.subscriptionType,
Expand Down
46 changes: 46 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ type TestClaudeCapabilities = {
readonly email: string | undefined;
readonly subscriptionType: string | undefined;
readonly tokenSource: string | undefined;
readonly apiKeySource: string | undefined;
readonly apiProvider: string | undefined;
readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;
};
Expand All @@ -151,6 +152,7 @@ function claudeCapabilities(overrides: Partial<TestClaudeCapabilities> = {}) {
email: undefined,
subscriptionType: undefined,
tokenSource: undefined,
apiKeySource: undefined,
apiProvider: undefined,
slashCommands: [],
...overrides,
Expand Down Expand Up @@ -2766,6 +2768,50 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
),
);

it.effect("reports unauthenticated when the first-party CLI is logged out", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
defaultClaudeSettings,
claudeCapabilities({ tokenSource: "none", apiProvider: "firstParty" }),
);
assert.strictEqual(status.status, "error");
assert.strictEqual(status.installed, true);
assert.strictEqual(status.auth.status, "unauthenticated");
assert.match(status.message ?? "", /claude auth login/);
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect("stays authenticated when an API key backs a tokenSource of none", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
defaultClaudeSettings,
claudeCapabilities({
tokenSource: "none",
apiKeySource: "ANTHROPIC_API_KEY",
apiProvider: "firstParty",
}),
);
assert.strictEqual(status.status, "ready");
assert.strictEqual(status.auth.status, "authenticated");
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect("returns claude auth email from initialization result", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
Expand Down
7 changes: 7 additions & 0 deletions docs/user/providers-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ Claude Code's verbose mode can stay enabled when you use Claude for text generat
thread titles, branch names, commit messages, and pull request descriptions. On a remote connection,
T3 Code uses the Claude configuration on the connected server.

## Signed out

If **Settings > Providers** shows a Claude instance as not authenticated, its
config directory has no login. Run `claude auth login` on the environment's
machine with the same `CLAUDE_CONFIG_DIR` the instance uses, then start a new
thread. Existing threads keep their signed-out Claude process until they end.

## Compact long conversations

Set **Auto-compact after** in the Claude provider settings to an integer between
Expand Down
Loading