From 3a9e6b558c883cde60398b05e7aab5242478a7b6 Mon Sep 17 00:00:00 2001 From: Jacopo Binosi Date: Wed, 14 Jan 2026 17:20:47 +0100 Subject: [PATCH] feat(opencode): add AWS Web Identity Token File support for Bedrock (#8461) --- packages/opencode/src/cli/cmd/auth.ts | 4 +- packages/opencode/src/provider/provider.ts | 19 ++-- .../test/provider/amazon-bedrock.test.ts | 102 ++++++++++++++---- packages/web/src/content/docs/providers.mdx | 3 +- 4 files changed, 97 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 3dd7bcc35..bbaecfd8c 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -338,9 +338,9 @@ export const AuthLoginCommand = cmd({ prompts.log.info( "Amazon Bedrock authentication priority:\n" + " 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" + - " 2. AWS credential chain (profile, access keys, IAM roles)\n\n" + + " 2. AWS credential chain (profile, access keys, IAM roles, EKS IRSA)\n\n" + "Configure via opencode.json options (profile, region, endpoint) or\n" + - "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID).", + "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE).", ) } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 5374d76ee..c7559003e 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -197,16 +197,23 @@ export namespace Provider { return undefined }) - if (!profile && !awsAccessKeyId && !awsBearerToken) return { autoload: false } + const awsWebIdentityTokenFile = Env.get("AWS_WEB_IDENTITY_TOKEN_FILE") - const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) - - // Build credential provider options (only pass profile if specified) - const credentialProviderOptions = profile ? { profile } : {} + if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile) return { autoload: false } const providerOptions: AmazonBedrockProviderSettings = { region: defaultRegion, - credentialProvider: fromNodeProviderChain(credentialProviderOptions), + } + + // Only use credential chain if no bearer token exists + // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens) + if (!awsBearerToken) { + const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) + + // Build credential provider options (only pass profile if specified) + const credentialProviderOptions = profile ? { profile } : {} + + providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions) } // Add custom endpoint if specified (endpoint takes precedence over baseURL) diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 05f5bd01f..6b5cf681c 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -1,5 +1,6 @@ import { test, expect, mock } from "bun:test" import path from "path" +import { unlink } from "fs/promises" // === Mocks === // These mocks are required because Provider.list() triggers: @@ -118,29 +119,52 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { }) const authPath = path.join(Global.Path.data, "auth.json") - await Bun.write( - authPath, - JSON.stringify({ - "amazon-bedrock": { - type: "api", - key: "test-bearer-token", - }, - }), - ) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("AWS_PROFILE", "") - Env.set("AWS_ACCESS_KEY_ID", "") - Env.set("AWS_BEARER_TOKEN_BEDROCK", "") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") - }, - }) + // Save original auth.json if it exists + let originalAuth: string | undefined + try { + originalAuth = await Bun.file(authPath).text() + } catch { + // File doesn't exist, that's fine + } + + try { + // Write test auth.json + await Bun.write( + authPath, + JSON.stringify({ + "amazon-bedrock": { + type: "api", + key: "test-bearer-token", + }, + }), + ) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "") + Env.set("AWS_ACCESS_KEY_ID", "") + Env.set("AWS_BEARER_TOKEN_BEDROCK", "") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") + }, + }) + } finally { + // Restore original or delete + if (originalAuth !== undefined) { + await Bun.write(authPath, originalAuth) + } else { + try { + await unlink(authPath) + } catch { + // Ignore errors if file doesn't exist + } + } + } }) test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async () => { @@ -208,3 +232,37 @@ test("Bedrock: includes custom endpoint in options when specified", async () => }, }) }) + +test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "us-east-1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token") + Env.set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role") + Env.set("AWS_PROFILE", "") + Env.set("AWS_ACCESS_KEY_ID", "") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1") + }, + }) +}) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index effdd8d3c..e1d684de0 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -211,12 +211,13 @@ To use Amazon Bedrock with OpenCode: - **`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`**: Create an IAM user and generate access keys in the AWS Console - **`AWS_PROFILE`**: Use named profiles from `~/.aws/credentials`. First configure with `aws configure --profile my-profile` or `aws sso login` - **`AWS_BEARER_TOKEN_BEDROCK`**: Generate long-term API keys from the Amazon Bedrock console + - **`AWS_WEB_IDENTITY_TOKEN_FILE` / `AWS_ROLE_ARN`**: For EKS IRSA (IAM Roles for Service Accounts) or other Kubernetes environments with OIDC federation. These environment variables are automatically injected by Kubernetes when using service account annotations. #### Authentication Precedence Amazon Bedrock uses the following authentication priority: 1. **Bearer Token** - `AWS_BEARER_TOKEN_BEDROCK` environment variable or token from `/connect` command - 2. **AWS Credential Chain** - Profile, access keys, shared credentials, IAM roles, instance metadata + 2. **AWS Credential Chain** - Profile, access keys, shared credentials, IAM roles, Web Identity Tokens (EKS IRSA), instance metadata :::note When a bearer token is set (via `/connect` or `AWS_BEARER_TOKEN_BEDROCK`), it takes precedence over all AWS credential methods including configured profiles.