feat: add bedrock gateway authentication (#665)

This commit is contained in:
Aleksandr Suhinin
2026-05-15 21:20:49 +03:00
committed by GitHub
parent e0ea9d898a
commit 002c63a78b
2 changed files with 88 additions and 14 deletions

View File

@@ -232,6 +232,8 @@ type GatewayAuthMeta = {
};
};
type GatewayAuthRequest = AuthenticateRequest & { _meta?: GatewayAuthMeta };
/**
* Extra metadata that the agent provides for each tool_call / tool_update update.
*/
@@ -476,7 +478,7 @@ export class ClaudeAcpAgent implements Agent {
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
clientCapabilities?: ClientCapabilities;
logger: Logger;
gatewayAuthMeta?: GatewayAuthMeta;
gatewayAuthRequest?: GatewayAuthRequest;
constructor(client: AgentSideConnection, logger?: Logger) {
this.sessions = {};
@@ -503,6 +505,17 @@ export class ClaudeAcpAgent implements Agent {
},
};
const gatewayBedrockAuthMethod: AuthMethod = {
id: "gateway-bedrock",
name: "Custom model gateway",
description: "Use a custom gateway to authenticate and access models",
_meta: {
gateway: {
protocol: "bedrock",
},
},
};
const supportsTerminalAuth = request.clientCapabilities?.auth?.terminal === true;
const supportsMetaTerminalAuth = request.clientCapabilities?._meta?.["terminal-auth"] === true;
@@ -613,7 +626,10 @@ export class ClaudeAcpAgent implements Agent {
title: "Claude Agent",
version: packageJson.version,
},
authMethods: [...terminalAuthMethods, ...(supportsGatewayAuth ? [gatewayAuthMethod] : [])],
authMethods: [
...terminalAuthMethods,
...(supportsGatewayAuth ? [gatewayAuthMethod, gatewayBedrockAuthMethod] : []),
],
};
}
@@ -690,8 +706,8 @@ export class ClaudeAcpAgent implements Agent {
}
async authenticate(_params: AuthenticateRequest): Promise<void> {
if (_params.methodId === "gateway") {
this.gatewayAuthMeta = _params._meta as GatewayAuthMeta | undefined;
if (_params.methodId === "gateway" || _params.methodId === "gateway-bedrock") {
this.gatewayAuthRequest = _params as GatewayAuthRequest;
return;
}
throw new Error("Method not implemented.");
@@ -1864,7 +1880,7 @@ export class ClaudeAcpAgent implements Agent {
env: {
...process.env,
...userProvidedOptions?.env,
...createEnvForGateway(this.gatewayAuthMeta),
...createEnvForGateway(this.gatewayAuthRequest),
// Opt-in to session state events like when the agent is idle
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
},
@@ -1948,7 +1964,7 @@ export class ClaudeAcpAgent implements Agent {
if (
shouldHideClaudeAuth() &&
initializationResult.account.subscriptionType &&
!this.gatewayAuthMeta
!this.gatewayAuthRequest
) {
throw RequestError.authRequired(
undefined,
@@ -2137,15 +2153,25 @@ function snapshotFromUsage(usage: {
};
}
function createEnvForGateway(gatewayMeta?: GatewayAuthMeta) {
if (!gatewayMeta) {
function createEnvForGateway(request?: GatewayAuthRequest) {
if (!request?._meta) {
return {};
}
const customHeaders = Object.entries(request._meta.gateway.headers)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
if (request.methodId === "gateway-bedrock") {
return {
CLAUDE_CODE_USE_BEDROCK: "1",
AWS_BEARER_TOKEN_BEDROCK: "",
ANTHROPIC_BEDROCK_BASE_URL: request._meta.gateway.baseUrl,
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
};
}
return {
ANTHROPIC_BASE_URL: gatewayMeta.gateway.baseUrl,
ANTHROPIC_CUSTOM_HEADERS: Object.entries(gatewayMeta.gateway.headers)
.map(([key, value]) => `${key}: ${value}`)
.join("\n"),
ANTHROPIC_BASE_URL: request._meta.gateway.baseUrl,
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
ANTHROPIC_AUTH_TOKEN: "", // Must be specified to bypass claude login requirement
};
}

View File

@@ -53,6 +53,9 @@ describe("authorization", () => {
expect(initializeResponse.authMethods).not.toContainEqual(
expect.objectContaining({ id: "gateway" }),
);
expect(initializeResponse.authMethods).not.toContainEqual(
expect.objectContaining({ id: "gateway-bedrock" }),
);
});
it("gateway auth offered when client advertises auth._meta.gateway capability", async () => {
@@ -65,11 +68,20 @@ describe("authorization", () => {
} as any,
});
expect(initializeResponse.authMethods).toContainEqual(
expect.objectContaining({ id: "gateway" }),
expect.objectContaining({
id: "gateway",
_meta: { gateway: { protocol: "anthropic" } },
}),
);
expect(initializeResponse.authMethods).toContainEqual(
expect.objectContaining({
id: "gateway-bedrock",
_meta: { gateway: { protocol: "bedrock" } },
}),
);
});
it("uses gateway env after gateway auth", async () => {
it("uses gateway env after anthropic gateway auth", async () => {
const [agent, mockQuery] = await createAgentMock();
const initializeResponse = await agent.initialize({
@@ -115,6 +127,42 @@ describe("authorization", () => {
);
});
it("uses gateway env after gateway auth", async () => {
const [agent, mockQuery] = await createAgentMock();
await agent.initialize({
protocolVersion: 1,
clientCapabilities: {
auth: { terminal: true, _meta: { gateway: true } },
} as any,
});
await agent.authenticate({
methodId: "gateway-bedrock",
_meta: {
gateway: { baseUrl: "https://gateway.example", headers: { "custom-header": "test" } },
},
});
await agent.newSession({
cwd: "testRoot",
mcpServers: [],
});
expect(mockQuery).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
env: expect.objectContaining({
CLAUDE_CODE_USE_BEDROCK: "1",
AWS_BEARER_TOKEN_BEDROCK: "",
ANTHROPIC_BEDROCK_BASE_URL: "https://gateway.example",
ANTHROPIC_CUSTOM_HEADERS: "custom-header: test",
}),
}),
}),
);
});
it("hide claude authentication without terminal-auth", async () => {
const [agent] = await createAgentMock();
vi.stubGlobal("process", { ...process, argv: ["--hide-claude-auth"] });