Skip to content

Commit edf6486

Browse files
Authentication adjustments for JB integration (#350)
Updated authentication methods for IDEA compatibility and SDK compliance #### New authentication type: gateway (url + custom headers) - Required to support native IDEA authentication flows #### Process argument to hide Claude Code authentication - Ensures compliance with Anthropic’s third-party authentication policies. https://platform.claude.com/docs/en/agent-sdk/overview > Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Please use the API key authentication methods described in this document instead. --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
1 parent 232a8cd commit edf6486

2 files changed

Lines changed: 246 additions & 2 deletions

File tree

src/acp-agent.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
Agent,
33
AgentSideConnection,
44
AuthenticateRequest,
5+
AuthMethod,
56
AvailableCommand,
67
CancelNotification,
78
ClientCapabilities,
@@ -152,6 +153,34 @@ export type NewSessionMeta = {
152153
};
153154
};
154155

156+
/**
157+
* Extended ClientCapabilities with `auth` field.
158+
* TODO: Remove once `auth` is added to the ACP SDK schema.
159+
*/
160+
type ClientCapabilitiesWithAuth = ClientCapabilities & {
161+
auth?: {
162+
_meta?: {
163+
gateway?: boolean;
164+
};
165+
};
166+
};
167+
168+
/**
169+
* Extra metadata for 'gateway' authentication requests.
170+
*/
171+
type GatewayAuthMeta = {
172+
/**
173+
* These parameters are mapped to environment variables to:
174+
* - Redirect API calls via baseUrl
175+
* - Inject custom headers
176+
* - Bypass the default Claude login requirement
177+
*/
178+
gateway: {
179+
baseUrl: string;
180+
headers: Record<string, string>;
181+
};
182+
};
183+
155184
/**
156185
* Extra metadata that the agent provides for each tool_call / tool_update update.
157186
*/
@@ -190,6 +219,10 @@ function isStaticBinary(): boolean {
190219
return process.env.CLAUDE_AGENT_ACP_IS_SINGLE_FILE_BUN !== undefined;
191220
}
192221

222+
function shouldHideClaudeAuth(): boolean {
223+
return process.argv.includes("--hide-claude-auth");
224+
}
225+
193226
// Bypass Permissions doesn't work if we are a root/sudo user
194227
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
195228
const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
@@ -241,6 +274,7 @@ export class ClaudeAcpAgent implements Agent {
241274
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
242275
clientCapabilities?: ClientCapabilities;
243276
logger: Logger;
277+
gatewayAuthMeta?: GatewayAuthMeta;
244278

245279
constructor(client: AgentSideConnection, logger?: Logger) {
246280
this.sessions = {};
@@ -259,8 +293,25 @@ export class ClaudeAcpAgent implements Agent {
259293
id: "claude-login",
260294
};
261295

296+
// Bypasses standard auth by routing requests through a custom Anthropic-protocol gateway.
297+
// Only offered when the client advertises `auth._meta.gateway` capability.
298+
const clientCaps = request.clientCapabilities as ClientCapabilitiesWithAuth | undefined;
299+
const supportsGatewayAuth = clientCaps?.auth?._meta?.gateway === true;
300+
301+
const gatewayAuthMethod: AuthMethod = {
302+
id: "gateway",
303+
name: "Custom model gateway",
304+
description: "Use a custom gateway to authenticate and access models",
305+
_meta: {
306+
gateway: {
307+
protocol: "anthropic",
308+
},
309+
},
310+
};
311+
262312
// If client supports terminal-auth capability, use that instead.
263-
if (request.clientCapabilities?._meta?.["terminal-auth"] === true) {
313+
const supportsTerminalAuth = request.clientCapabilities?._meta?.["terminal-auth"] === true;
314+
if (supportsTerminalAuth) {
264315
let command: string;
265316
let args: string[];
266317

@@ -310,12 +361,17 @@ export class ClaudeAcpAgent implements Agent {
310361
title: "Claude Agent",
311362
version: packageJson.version,
312363
},
313-
authMethods: [authMethod],
364+
authMethods: [
365+
// Terminal auth can also be used for API keys, so don't gate it on --hide-claude-auth.
366+
...(shouldHideClaudeAuth() && !supportsTerminalAuth ? [] : [authMethod]),
367+
...(supportsGatewayAuth ? [gatewayAuthMethod] : []),
368+
],
314369
};
315370
}
316371

317372
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
318373
if (
374+
!this.gatewayAuthMeta &&
319375
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
320376
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
321377
) {
@@ -415,6 +471,10 @@ export class ClaudeAcpAgent implements Agent {
415471
}
416472

417473
async authenticate(_params: AuthenticateRequest): Promise<void> {
474+
if (_params.methodId === "gateway") {
475+
this.gatewayAuthMeta = _params._meta as GatewayAuthMeta | undefined;
476+
return;
477+
}
418478
throw new Error("Method not implemented.");
419479
}
420480

@@ -1179,6 +1239,10 @@ export class ClaudeAcpAgent implements Agent {
11791239
settingSources: ["user", "project", "local"],
11801240
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
11811241
...userProvidedOptions,
1242+
env: {
1243+
...userProvidedOptions?.env,
1244+
...createEnvForGateway(this.gatewayAuthMeta),
1245+
},
11821246
// Override certain fields that must be controlled by ACP
11831247
cwd: params.cwd,
11841248
includePartialMessages: true,
@@ -1262,6 +1326,13 @@ export class ClaudeAcpAgent implements Agent {
12621326
throw error;
12631327
}
12641328

1329+
if (shouldHideClaudeAuth() && initializationResult.account.subscriptionType) {
1330+
throw RequestError.authRequired(
1331+
undefined,
1332+
"This integration does not support using claude.ai subscriptions.",
1333+
);
1334+
}
1335+
12651336
const models = await getAvailableModels(q, initializationResult.models, settingsManager);
12661337

12671338
const availableModes = [
@@ -1329,6 +1400,19 @@ export class ClaudeAcpAgent implements Agent {
13291400
}
13301401
}
13311402

1403+
function createEnvForGateway(gatewayMeta?: GatewayAuthMeta) {
1404+
if (!gatewayMeta) {
1405+
return {};
1406+
}
1407+
return {
1408+
ANTHROPIC_BASE_URL: gatewayMeta.gateway.baseUrl,
1409+
ANTHROPIC_CUSTOM_HEADERS: Object.entries(gatewayMeta.gateway.headers)
1410+
.map(([key, value]) => `${key}: ${value}`)
1411+
.join("\n"),
1412+
ANTHROPIC_AUTH_TOKEN: "", // Must be specified to bypass claude login requirement
1413+
};
1414+
}
1415+
13321416
function buildConfigOptions(
13331417
modes: SessionModeState,
13341418
models: SessionModelState,

src/tests/authorization.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
2+
import { ClaudeAcpAgent } from "../acp-agent.js";
3+
import { AgentSideConnection } from "@agentclientprotocol/sdk";
4+
5+
describe("authorization", () => {
6+
beforeEach(() => {
7+
vi.useFakeTimers();
8+
});
9+
10+
afterEach(() => {
11+
//await all pending events like
12+
vi.runAllTimers();
13+
vi.useRealTimers();
14+
15+
vi.unstubAllGlobals();
16+
vi.resetAllMocks();
17+
});
18+
19+
async function createAgentMock(): Promise<[ClaudeAcpAgent, Mock]> {
20+
const mockQuery = vi.hoisted(() =>
21+
vi.fn(() => ({
22+
initializationResult: vi.fn().mockResolvedValue({
23+
models: [{ value: "id", displayName: "name", description: "description" }],
24+
}),
25+
setModel: vi.fn(),
26+
supportedCommands: vi.fn().mockResolvedValue([]),
27+
})),
28+
);
29+
30+
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
31+
query: mockQuery,
32+
}));
33+
34+
const connectionMock = {
35+
sessionUpdate: async (_: any) => {},
36+
} as AgentSideConnection;
37+
38+
const agent = new ClaudeAcpAgent(connectionMock);
39+
40+
return [agent, mockQuery];
41+
}
42+
43+
it("gateway auth not offered without capability", async () => {
44+
const [agent] = await createAgentMock();
45+
46+
const initializeResponse = await agent.initialize({
47+
protocolVersion: 1,
48+
clientCapabilities: {},
49+
});
50+
expect(initializeResponse.authMethods).not.toContainEqual(
51+
expect.objectContaining({ id: "gateway" }),
52+
);
53+
});
54+
55+
it("gateway auth offered when client advertises auth._meta.gateway capability", async () => {
56+
const [agent] = await createAgentMock();
57+
58+
const initializeResponse = await agent.initialize({
59+
protocolVersion: 1,
60+
clientCapabilities: {
61+
auth: { _meta: { gateway: true } },
62+
} as any,
63+
});
64+
expect(initializeResponse.authMethods).toContainEqual(
65+
expect.objectContaining({ id: "gateway" }),
66+
);
67+
});
68+
69+
it("uses gateway env after gateway auth", async () => {
70+
const [agent, mockQuery] = await createAgentMock();
71+
72+
const initializeResponse = await agent.initialize({
73+
protocolVersion: 1,
74+
clientCapabilities: {
75+
auth: { _meta: { gateway: true } },
76+
} as any,
77+
});
78+
expect(initializeResponse.authMethods).toContainEqual(
79+
expect.objectContaining({ id: "gateway" }),
80+
);
81+
82+
await agent.authenticate({
83+
methodId: "gateway",
84+
_meta: { gateway: { baseUrl: "https://gateway.example", headers: { "x-api-key": "test" } } },
85+
});
86+
87+
await agent.newSession({
88+
cwd: "testRoot",
89+
mcpServers: [],
90+
_meta: {
91+
claudeCode: {
92+
options: {
93+
env: {
94+
userEnv: "userEnv",
95+
},
96+
},
97+
},
98+
},
99+
});
100+
101+
expect(mockQuery).toHaveBeenCalledWith(
102+
expect.objectContaining({
103+
options: expect.objectContaining({
104+
env: {
105+
ANTHROPIC_AUTH_TOKEN: "",
106+
ANTHROPIC_BASE_URL: "https://gateway.example",
107+
ANTHROPIC_CUSTOM_HEADERS: "x-api-key: test",
108+
userEnv: "userEnv",
109+
},
110+
}),
111+
}),
112+
);
113+
});
114+
115+
it("hide claude authentication without terminal-auth", async () => {
116+
const [agent] = await createAgentMock();
117+
vi.stubGlobal("process", { ...process, argv: ["--hide-claude-auth"] });
118+
119+
const initializeResponse = await agent.initialize({
120+
protocolVersion: 1,
121+
clientCapabilities: {
122+
auth: { _meta: { gateway: true } },
123+
} as any,
124+
});
125+
expect(initializeResponse.authMethods).not.toContainEqual(
126+
expect.objectContaining({ id: "claude-login" }),
127+
);
128+
expect(initializeResponse.authMethods).toContainEqual(
129+
expect.objectContaining({ id: "gateway" }),
130+
);
131+
});
132+
133+
it("terminal auth still offered when --hide-claude-auth is set", async () => {
134+
const [agent] = await createAgentMock();
135+
vi.stubGlobal("process", { ...process, argv: ["--hide-claude-auth"] });
136+
137+
const initializeResponse = await agent.initialize({
138+
protocolVersion: 1,
139+
clientCapabilities: {
140+
_meta: { "terminal-auth": true },
141+
},
142+
});
143+
expect(initializeResponse.authMethods).toContainEqual(
144+
expect.objectContaining({ id: "claude-login" }),
145+
);
146+
});
147+
148+
it("show claude authentication", async () => {
149+
const [agent] = await createAgentMock();
150+
151+
const initializeResponse = await agent.initialize({
152+
protocolVersion: 1,
153+
clientCapabilities: {},
154+
});
155+
156+
expect(initializeResponse.authMethods).toContainEqual(
157+
expect.objectContaining({ id: "claude-login" }),
158+
);
159+
});
160+
});

0 commit comments

Comments
 (0)