Skip to content

feat(provider): add Amazon Bedrock IAM Identity Center SSO - #3370

Open
keyuchen21 wants to merge 4 commits into
apache:mainfrom
keyuchen21:feat/amazon-bedrock-sso
Open

feat(provider): add Amazon Bedrock IAM Identity Center SSO#3370
keyuchen21 wants to merge 4 commits into
apache:mainfrom
keyuchen21:feat/amazon-bedrock-sso

Conversation

@keyuchen21

Copy link
Copy Markdown

Summary

  • register amazon-bedrock as a first-class provider with typed IAM Identity Center configuration and a dedicated aws_sso credential kind
  • add a Host-owned bedrock.sso.* device-authorization lifecycle with account/role selection, bounded model discovery, manual model validation, and atomic onboarding recovery
  • resolve and CAS-refresh SSO sessions in Runtime Host, while keeping temporary AWS role credentials memory-only and singleflight refreshed
  • add Bedrock Converse/ConverseStream execution through @ai-sdk/amazon-bedrock, including auxiliary model calls, connection tests, inference-profile capability inheritance, and source-model pricing
  • add the Desktop account setup flow and make the resulting connection available to Desktop, CLI, TUI, bots, and other Runtime Host clients
  • route AWS SDK control-plane traffic through Maka's scoped fetch/proxy boundary and add permission/configuration error classification

Security properties

  • OIDC client secrets, access/refresh tokens, device codes, and temporary role credentials never cross the Runtime Host protocol into renderer state
  • temporary AWS role credentials are never persisted
  • uncommitted login attempts are cancelled and cleared after a bounded TTL
  • credential refresh uses canonical generation binding and CAS reconciliation, so logout/reauth cannot revive an old credential generation
  • Bedrock connections reject custom request headers and body overlays to avoid post-signature mutation
  • Desktop configuration exports omit IAM Identity Center sessions and require reauthorization after import

Validation

  • npm run build
  • npm run lint
  • Core, Storage, Runtime, Runtime Host, and Desktop test suites
  • Bedrock contract, protocol-secret-boundary, SSO session, provider conformance, pricing, and error-classification tests
  • Desktop and CLI third-party notice checks
  • live IAM Identity Center device authorization against a test tenant
  • live Bedrock foundation-model and inference-profile discovery through the new Smithy-to-scoped-fetch handler
  • live ConverseStream request with tool call, tool result replay, and final text

npm test completed every JavaScript/workspace suite, but the local @maka/eval Python continuation stopped because the machine has Python 3.9 and the existing test uses Python 3.10 union syntax (int | None). Its 75 Node tests passed before that environment-only failure.

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • apps/desktop/src/main/runtime-host-client.ts
  • apps/desktop/src/preload/bridge-contract.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/settings/provider-connection-detail.tsx
  • …(more)

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review at exact head c87e304c41c233e75cdca9086a4cee0064d0f7e5. Thanks for taking on Bedrock SSO — it's a genuinely wanted provider path, and the Host-side credential handling here is careful work. Three [P1]s stand between it and landing, and the first one is the important one.


[P1] The SSO sign-in flow cannot complete — apps/desktop/src/renderer/settings/bedrock-sso-setup.tsx:31-35

const mounted = useRef(true);

useEffect(() => () => {
  mounted.current = false;
  if (attemptId) void window.maka.amazonBedrockSso.cancel(attemptId, host).catch(() => undefined);
}, [attemptId, host]);

A cleanup-only effect with dependencies runs its cleanup on every dependency change, not only on unmount. login() calls setAttemptId(started.attemptId) at :42, which changes a dependency, so the cleanup fires mid-sign-in and sets mounted.current = false. The if (attemptId) guard covers only the cancel call — the sentinel is cleared unconditionally. Nothing ever restores it, because the effect body is just () => cleanup.

The polling loop at :45 is while (mounted.current && ...), and :50 is if (!mounted.current) return. So the first sign-in — necessarily the render where attemptId goes from undefined to a value — always returns early and never reaches accounts or roles. This isn't a race; it's unconditional.

Suggested shape: separate the mount-lifetime effect (empty deps, owns the sentinel) from the attempt-cancellation effect (deps on attemptId/host, owns only cancel), so changing the attempt id can't retire the component's liveness flag.

Worth noting alongside it: there is no renderer behaviour test over BedrockSsoSetup, so nothing in 69 files and +21,862 lines could have caught this. A test that drives login() through a fake bridge and asserts roles appear would pin the whole flow.

[P1] Ambient AWS_BEARER_TOKEN_BEDROCK silently overrides the configured SSO identity

@ai-sdk/amazon-bedrock, pinned to 5.0.58 by this PR's lockfile:

const rawApiKey = loadOptionalSetting({
  settingValue: options.apiKey,
  environmentVariableName: "AWS_BEARER_TOKEN_BEDROCK"
});
const apiKey = rawApiKey && rawApiKey.trim().length > 0 ? rawApiKey.trim() : void 0;
const fetchFunction = apiKey
  ? createApiKeyFetchFunction(apiKey, options.fetch)
  : createSigV4FetchFunction(async () => { /* ... options.credentialProvider ... */ });

credentialProvider lives inside the SigV4 branch. model-factory.ts:99-103 passes it correctly, but with a non-empty ambient bearer token that branch is never taken and the provider is never constructed, let alone called. A fake-only probe confirmed Bearer auth on the wire with zero calls into the explicit SSO provider.

The harm is wrong identity, not failed auth: a user who configured SSO gets inference, connection tests and manual probes running as whoever the stale environment token belongs to, and Settings still reports the connection as authenticated because that display is derived from the vault entry alone. Nothing on the running path discloses which credential source is actually in use.

One trap when fixing: passing apiKey: undefined does not suppress the lookup — loadOptionalSetting falls through to the environment variable exactly when the setting value is absent. Suppressing it requires an explicit empty string, or an upstream option that genuinely disables the env bearer.

[P1] Cancel only aborts device authorization, not what follows it

The cancel path aborts the device-authorization polling loop, but the post-auth calls — listAccounts, listRoles, model discovery, the manual Converse probe — take no abort signal and continue after a cancel returns. The Cancel button also remains clickable while busy, so a user who cancels can still have credentials in use afterwards, including model probes that cost money. Host close doesn't wait on these either.


Credential lifecycle — no findings. Reporting this explicitly because it is the part most likely to be assumed rather than checked: the SSO session lands in the Host workspace's runtime-policy-onboarding.json crash journal and finally in credential-vault.json, written atomically at 0600; role credentials stay in Host memory with a 5-minute skew refresh and never touch disk or the protocol. No token or role-key exposure was found in logs or protocol frames within the reviewed surface. With no ambient bearer present, the explicit provider is invoked and refresh failures classify correctly as Auth — so there is no access-key or default-chain fallback defect.

Deletion axis — no closed candidate. +21,862 lines invites the question of how much duplicates the existing OAuth credential path. Merging the two generation/CAS authorities is a probe, not a recommendation: AWS's two-level session→role expiry and OAuth's uncertain-commit semantics are different enough that a shared owner might add branching rather than remove it. Not claiming a net deletion without proving that.

Validation limits. Core→Storage→MCP→Runtime→Runtime Host clean build; 17/17 targeted tests; two fake-only probes. astryx:surface-inventory reproduces a required test failure on this head (the new setup file is not in the inventory). Full repo suite, renderer/Electron E2E and Windows packaging were not run, and no real AWS credentials or live AWS calls were used at any point — every probe used fakes and placeholder values.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at c87e304c. One [P1] and one [P2], both inline on bedrock-sso-coordinator.ts. Holding.

Both are lifecycle rather than cryptography: the login flow's happy path reads fine, but the commit and start paths each lose an invariant the rest of the codebase already keeps.

This review was performed statically with fakes and placeholders only — no AWS calls were made and no real credentials were used, so anything that would require live IAM Identity Center access is explicitly unverified here rather than passed.

Gate status: audit passed; test fails on Astryx stale surface inventory (the new bedrock-sso-setup.tsx is not listed) and package fails on an unrelated Windows packaged-app CDP timeout. The PR is also currently dirty.

中文

c87e304c 上审,一条 [P1] 加一条 [P2],均行内提在 bedrock-sso-coordinator.ts,暂缓。

两条都属生命周期问题而非加密问题:登录流程的正常路径没问题,但提交路径和启动路径各自丢掉了一个仓库其他地方已经维持的不变量。

本次审查仅为静态审查,全程使用 fake 与占位符——没有发起任何 AWS 调用,也没有使用真实凭证;因此凡是需要真实 IAM Identity Center 访问才能确认的部分,这里明确标为未验证,而不是视为通过。

门禁状态:audit 通过;test 因 Astryx surface inventory 过期而失败(新增的 bedrock-sso-setup.tsx 未登记),package 因与本 PR 无关的 Windows 打包应用 CDP 超时而失败。此外该 PR 当前处于冲突状态。

);
if (committed.kind !== 'committed') {
return failure('persistence_failed', 'Amazon Bedrock connection could not be committed');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] A durable commit can be reported to the client as a persistence failure, and activation is left unpoisoned.

commitConnectionOnboarding returns committed — the connection is durably persisted. The very next statement is await this.invalidateBackends(), and the enclosing catch maps any throw to failure('persistence_failed', …). If invalidation throws:

  • the client is told persistence failed while the connection is in fact persisted;
  • this.#committed.set(attemptId, result) is never reached, so there is no replay record for the retry to find;
  • #attempts still holds the attempt;
  • activation is not poisoned, so a later Turn can proceed against backends whose invalidation never completed.

This file's own sibling already encodes the correct rule. runtime-policy-coordinator.ts:282-299 wraps the same shape and comments it explicitly:

The durable outcome is authoritative, but no later Turn may activate against a backend whose invalidation did not complete.

…followed by this.activation.poison(). That is the invariant this path drops.

Suggest preserving the durable outcome (record the replay result before invalidation, or report success-with-degraded-activation) and poisoning or fataling activation when invalidation fails, matching the policy coordinator.

中文

commitConnectionOnboarding 返回 committed 时连接已经持久化,紧接着的 await this.invalidateBackends() 若抛出,外层 catch 会把它一律映射成 persistence_failed。后果有四点:客户端被告知持久化失败,而数据其实已经落盘;#committed.set(attemptId, result) 根本没执行到,重试找不到 replay 记录;#attempts 仍留着该 attempt;并且 activation 没有被 poison,后续 Turn 可以在"失效流程未完成"的 backend 上继续跑。

同一仓库里的 runtime-policy-coordinator.ts:282-299 已经把正确规则写下来了——注释明确说"持久结果是权威的,但任何后续 Turn 都不得在失效流程未完成的 backend 上激活",随后调用 this.activation.poison()。这条路径丢掉的正是这个不变量。

建议:保住持久结果(在失效之前先记录 replay 结果,或返回"成功但激活降级"),并在失效失败时按 policy coordinator 的做法 poison 或 fatal 掉 activation。

}
for (const previous of this.#attempts.values()) {
this.#cancelAttempt(previous);
await previous.settlement.catch(() => undefined);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] #start has no admission gate, so a concurrent start can orphan an in-flight AWS device flow.

The only early return is this.#attempts.get(input.attemptId), which two starts carrying different attempt ids both pass. Each then cancels the other's attempts and awaits previous.settlement.

The problem is when settlement becomes real: it is initialised to Promise.resolve() at :137 and only replaced with the actual #poll promise at :168. The awaited AWS device-authorization start sits between those two points, so a second start arriving in that window awaits an already-resolved promise, clears #attempts, and proceeds. The first flow then completes, opens a browser URL and begins polling with no attempt record owning it.

Suggest serialising admissions through a lease and awaiting the genuinely in-flight start before superseding or clearing.

中文

#start 唯一的提前返回是 this.#attempts.get(input.attemptId),两个携带不同 attemptId 的并发 start 都能通过,随后各自取消对方的 attempt 并 await previous.settlement

关键在于 settlement 何时变成真的:它在 :137 被初始化为 Promise.resolve(),直到 :168 才被替换成真正的 #poll promise,而那段被 await 的 AWS device authorization 启动过程正好位于两者之间。因此落在这个窗口里的第二个 start 会 await 一个已经 resolve 的 promise,清空 #attempts 然后继续;第一个流程随后完成、打开浏览器 URL 并开始轮询,却已经没有任何 attempt 记录持有它,成为孤儿。

建议用 lease 把 admission 串行化,并在顶替或清空之前 await 真正在飞的那个 start。

@github-actions github-actions Bot added the effort/XL Over 1000 readable lines label Aug 27, 2026
@keyuchen21
keyuchen21 force-pushed the feat/amazon-bedrock-sso branch 2 times, most recently from d70794a to 3ab6af6 Compare August 29, 2026 16:43
@keyuchen21

keyuchen21 commented Aug 29, 2026

Copy link
Copy Markdown
Author

@Astro-Han Thanks for the detailed reviews, Astro-Han — especially for spelling out the lifecycle and identity invariants and the limits of fake-only validation.

All reported blockers are addressed on the current rebased head ab20534e0:

Coordinator lifecycle findings

  • Durable commit / invalidation (P1): the durable onboarding result is recorded and replayable before backend invalidation. Invalidation stays inside the activation mutation fence; if it fails, durable success is preserved and activation is poisoned instead of returning persistence_failed.
  • Concurrent starts (P2): starts pass through a serialized admission lease. A superseding start waits for the genuinely in-flight start/settlement, and close() waits for admission, so a device flow cannot continue without an owning attempt.

Independent-review P1s

  • Renderer cleanup / first login: mount lifetime and attempt cancellation now use separate effects, so changing attemptId no longer marks the component unmounted. The login sequence was moved into a testable flow with coverage through account and role selection and cancellation before post-auth calls.
  • Ambient AWS_BEARER_TOKEN_BEDROCK: the Bedrock provider is constructed with an explicit empty apiKey, suppressing the SDK environment fallback and preserving the Host-owned SSO identity. A regression test sets the environment variable and proves the explicit credential provider is called and the request is SigV4-signed rather than Bearer-authenticated.
  • Post-auth cancellation / Host close: account listing, role listing, role credential resolution, model discovery, and manual probes share the attempt abort signal. Admitted post-auth work is tracked; Cancel aborts it, and Host close waits for settlement even after attempt removal.
  • Astryx inventory: regenerated; bedrock-sso-setup.tsx is listed and the inventory check passes.

The branch is rebased onto current main, adapted to the current multi-account onboarding target/ticket API, and GitHub reports it mergeable.

Validation on the current head:

  • Bedrock Runtime tests: 4/4 passed
  • Bedrock Runtime Host lifecycle tests: 4/4 passed
  • Bedrock renderer login-flow tests: 2/2 passed
  • Runtime Host suite: 1382 passed, 9 skipped, 0 failed
  • Runtime + Runtime Host typechecks, Desktop main/renderer typechecks, Biome, diff check, notice inventory, and Astryx inventory checks passed

The cancellation and identity tests use fakes/placeholders only; no live AWS credentials or calls were used. Ready for re-review when convenient.

Add Host-owned IAM Identity Center device authorization, account and role selection, atomic credential onboarding, Bedrock model discovery, and Converse execution through dynamic temporary role credentials.

Expose the flow in Desktop settings, keep temporary AWS credentials inside Runtime Host memory, and make configured Bedrock connections available to every execution client. Include inference-profile metadata and pricing projection, error classification, protocol coverage, and dependency notices.

Generated-by: Maka
@keyuchen21
keyuchen21 force-pushed the feat/amazon-bedrock-sso branch from 62cbddf to ab20534 Compare August 29, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Over 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants