Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
11531bd
fix: add Bedrock IAM credentials connect flow and environment variabl…
Feb 25, 2026
048f578
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 25, 2026
c9d99b7
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 25, 2026
4179396
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 25, 2026
82cd54f
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 25, 2026
8efede1
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 26, 2026
f67b24b
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 26, 2026
8c0981f
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 26, 2026
fd528f6
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
8772036
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
c34770f
fix: address review feedback — update docs, CLI auth flow, tests, and…
Feb 27, 2026
30fcf90
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
f902ff8
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
9e10aed
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
be950a9
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
e612979
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
02c1875
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 27, 2026
82b643e
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 28, 2026
07e4462
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Feb 28, 2026
7655f98
Merge remote-tracking branch 'upstream/dev' into feat/bedrock-connect…
Mar 2, 2026
a711323
Merge branch 'feat/bedrock-connect-improvements' of https://github.co…
Mar 2, 2026
1d741bf
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 2, 2026
3366d81
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 3, 2026
cba084f
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 3, 2026
ffbcb12
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 3, 2026
32e0294
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 4, 2026
b4497fc
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 4, 2026
5378ad1
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 6, 2026
47a2be9
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 6, 2026
4e08ac2
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 6, 2026
d0febd7
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 7, 2026
2172966
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 7, 2026
09172aa
Merge branch 'anomalyco:dev' into feat/bedrock-connect-improvements
tristan-stahnke-GPS Mar 7, 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
110 changes: 110 additions & 0 deletions packages/app/src/components/dialog-connect-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export function DialogConnectProvider(props: { provider: string }) {
const methodLabel = (value?: { type?: string; label?: string }) => {
if (!value) return ""
if (value.type === "api") return language.t("provider.connect.method.apiKey")
if (value.type === "env") return language.t("provider.connect.method.env")
if (value.type === "aws") return language.t("provider.connect.method.aws")
return value.label ?? ""
}

Expand Down Expand Up @@ -308,6 +310,108 @@ export function DialogConnectProvider(props: { provider: string }) {
)
}

function AwsAuthView() {
const [formStore, setFormStore] = createStore({
accessKeyId: "",
secretAccessKey: "",
region: "us-east-1",
error: undefined as string | undefined,
})

async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
if (!formStore.accessKeyId.trim()) {
setFormStore("error", language.t("provider.connect.aws.accessKeyId.required"))
return
}
if (!formStore.secretAccessKey.trim()) {
setFormStore("error", language.t("provider.connect.aws.secretAccessKey.required"))
return
}
setFormStore("error", undefined)
await globalSDK.client.auth.set({
providerID: props.provider,
auth: {
type: "aws",
accessKeyId: formStore.accessKeyId.trim(),
secretAccessKey: formStore.secretAccessKey.trim(),
region: formStore.region.trim() || undefined,
},
})
await globalSDK.client.global.config.update({
config: {
provider: {
"amazon-bedrock": {
options: {
region: formStore.region.trim() || "us-east-1",
},
},
},
},
})
await complete()
}

return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">{language.t("provider.connect.aws.description")}</div>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label={language.t("provider.connect.aws.accessKeyId.label")}
placeholder="AKIA..."
name="accessKeyId"
value={formStore.accessKeyId}
onChange={(v) => setFormStore("accessKeyId", v)}
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<TextField
type="password"
label={language.t("provider.connect.aws.secretAccessKey.label")}
placeholder={language.t("provider.connect.aws.secretAccessKey.placeholder")}
name="secretAccessKey"
value={formStore.secretAccessKey}
onChange={(v) => setFormStore("secretAccessKey", v)}
/>
<TextField
type="text"
label={language.t("provider.connect.aws.region.label")}
placeholder="us-east-1"
name="region"
value={formStore.region}
onChange={(v) => setFormStore("region", v)}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
{language.t("common.connect")}
</Button>
</form>
</div>
)
}

function EnvAuthView() {
const envVars = createMemo(() => (method() as { env?: string[] })?.env ?? [])

return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
{language.t("provider.connect.env.description", { provider: provider().name })}
</div>
<div class="flex flex-col gap-2">
{envVars().map((v: string) => (
<code class="text-13-regular text-text-strong bg-surface-inset px-2 py-1 rounded font-mono">{v}</code>
))}
</div>
<div class="text-14-regular text-text-weak">{language.t("provider.connect.env.desktopNote")}</div>
<Button class="w-auto" size="large" variant="ghost" onClick={() => dialog.close()}>
{language.t("common.close")}
</Button>
</div>
)
}

function OAuthCodeView() {
const [formStore, setFormStore] = createStore({
value: "",
Expand Down Expand Up @@ -481,6 +585,12 @@ export function DialogConnectProvider(props: { provider: string }) {
<Match when={method()?.type === "api"}>
<ApiAuthView />
</Match>
<Match when={method()?.type === "aws"}>
<AwsAuthView />
</Match>
<Match when={method()?.type === "env"}>
<EnvAuthView />
</Match>
<Match when={method()?.type === "oauth"}>
<Switch>
<Match when={store.authorization?.method === "code"}>
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ export const dict = {
"provider.connect.apiKey.label": "{{provider}} API key",
"provider.connect.apiKey.placeholder": "API key",
"provider.connect.apiKey.required": "API key is required",
"provider.connect.env.description":
"{{provider}} uses environment variables for authentication. Set one of the following in your shell profile (e.g. ~/.zshrc) or opencode.json config:",
"provider.connect.env.configHint": "Or configure in opencode.json:",
"provider.connect.env.desktopNote":
"If using the desktop app, you may need to restart it after changing your shell profile so the new environment is picked up.",
"provider.connect.method.env": "Environment variables",
"provider.connect.method.aws": "IAM credentials",
"provider.connect.aws.description":
"Enter your AWS IAM credentials to connect to Amazon Bedrock. You should use a properly scoped IAM user with Bedrock access.",
"provider.connect.aws.accessKeyId.label": "AWS access key ID",
"provider.connect.aws.accessKeyId.required": "Access key ID is required",
"provider.connect.aws.secretAccessKey.label": "AWS secret access key",
"provider.connect.aws.secretAccessKey.placeholder": "Secret access key",
"provider.connect.aws.secretAccessKey.required": "Secret access key is required",
"provider.connect.aws.region.label": "AWS region",
"provider.connect.opencodeZen.line1":
"OpenCode Zen gives you access to a curated set of reliable optimized models for coding agents.",
"provider.connect.opencodeZen.line2":
Expand Down
11 changes: 10 additions & 1 deletion packages/opencode/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ export namespace Auth {
})
.meta({ ref: "WellKnownAuth" })

export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
export const Aws = z
.object({
type: z.literal("aws"),
accessKeyId: z.string(),
secretAccessKey: z.string(),
region: z.string().optional(),
})
.meta({ ref: "AwsAuth" })

export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown, Aws]).meta({ ref: "Auth" })
export type Info = z.infer<typeof Info>

const filepath = path.join(Global.Path.data, "auth.json")
Expand Down
71 changes: 64 additions & 7 deletions packages/opencode/src/cli/cmd/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,70 @@ export const AuthLoginCommand = cmd({
}

if (provider === "amazon-bedrock") {
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, 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_WEB_IDENTITY_TOKEN_FILE).",
)
const method = await prompts.select({
message: "Select authentication method",
options: [
{ value: "aws", label: "IAM credentials (Access Key ID + Secret Access Key)" },
{ value: "bearer", label: "Bearer token" },
{ value: "env", label: "Environment variables (view guidance)" },
],
})
if (prompts.isCancel(method)) throw new UI.CancelledError()

if (method === "env") {
prompts.log.info(
"Set one of the following environment variables in your shell profile:\n" +
" • AWS_PROFILE\n" +
" • AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY\n" +
" • AWS_BEARER_TOKEN_BEDROCK\n\n" +
"If using the desktop app, restart it after changing your shell profile.",
)
prompts.outro("Done")
return
}

if (method === "aws") {
const accessKeyId = await prompts.text({
message: "AWS Access Key ID",
placeholder: "AKIA...",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(accessKeyId)) throw new UI.CancelledError()

const secretAccessKey = await prompts.password({
message: "AWS Secret Access Key",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(secretAccessKey)) throw new UI.CancelledError()

const region = await prompts.text({
message: "AWS Region",
placeholder: "us-east-1",
defaultValue: "us-east-1",
})
if (prompts.isCancel(region)) throw new UI.CancelledError()

await Auth.set(provider, {
type: "aws",
accessKeyId,
secretAccessKey,
region: region || "us-east-1",
})
prompts.outro("Done")
return
}

const key = await prompts.password({
message: "Enter your bearer token",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(key)) throw new UI.CancelledError()
await Auth.set(provider, {
type: "api",
key,
})
prompts.outro("Done")
return
}

if (provider === "opencode") {
Expand Down
24 changes: 22 additions & 2 deletions packages/opencode/src/provider/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,44 @@ export namespace ProviderAuth {

export const Method = z
.object({
type: z.union([z.literal("oauth"), z.literal("api")]),
type: z.union([z.literal("oauth"), z.literal("api"), z.literal("env"), z.literal("aws")]),
label: z.string(),
env: z.array(z.string()).optional(),
})
.meta({
ref: "ProviderAuthMethod",
})
export type Method = z.infer<typeof Method>

const ENV_AUTH_PROVIDERS: Record<string, Method[]> = {
"amazon-bedrock": [
{
type: "aws",
label: "IAM credentials",
},
{
type: "env",
label: "Environment variables",
env: ["AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_BEARER_TOKEN_BEDROCK"],
},
],
}

export async function methods() {
const s = await state().then((x) => x.methods)
return mapValues(s, (x) =>
const result = mapValues(s, (x) =>
x.methods.map(
(y): Method => ({
type: y.type,
label: y.label,
}),
),
)
for (const [providerID, methods] of Object.entries(ENV_AUTH_PROVIDERS)) {
if (!result[providerID]) result[providerID] = []
result[providerID].push(...methods)
}
return result
}

export const Authorization = z
Expand Down
13 changes: 10 additions & 3 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,18 @@ export namespace Provider {

const auth = await Auth.get("amazon-bedrock")

// Region precedence: 1) config file, 2) env var, 3) default
// Region precedence: 1) config file, 2) auth.json aws creds, 3) env var, 4) default
const configRegion = providerConfig?.options?.region
const authRegion = auth?.type === "aws" ? auth.region : undefined
const envRegion = Env.get("AWS_REGION")
const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
const defaultRegion = configRegion ?? authRegion ?? envRegion ?? "us-east-1"

// Profile: config file takes precedence over env var
const configProfile = providerConfig?.options?.profile
const envProfile = Env.get("AWS_PROFILE")
const profile = configProfile ?? envProfile

const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID")
const awsAccessKeyId = auth?.type === "aws" ? auth.accessKeyId : Env.get("AWS_ACCESS_KEY_ID")

// TODO: Using process.env directly because Env.set only updates a process.env shallow copy,
// until the scope of the Env API is clarified (test only or runtime?)
Expand Down Expand Up @@ -255,6 +256,12 @@ export namespace Provider {
// 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) {
if (auth?.type === "aws") {
process.env.AWS_ACCESS_KEY_ID = auth.accessKeyId
process.env.AWS_SECRET_ACCESS_KEY = auth.secretAccessKey
if (auth.region) process.env.AWS_REGION = auth.region
}

// Build credential provider options (only pass profile if specified)
const credentialProviderOptions = profile ? { profile } : {}

Expand Down
Loading
Loading