Skip to content

Commit 7288b2d

Browse files
authored
feat(bedrock): run Bedrock models with a saved AWS credential (#9932)
* feat(bedrock): sign with a saved AWS credential's session when one is resolved A resolved saved-credential session on the provider request is used alone, passes its session token, and never falls back to inline keys or the deployment's own AWS credentials. Its client cache key names the credential and session expiry plus a digest of the key material, never a raw secret. * feat(bedrock): authorize and resolve a saved AWS credential for Bedrock model calls resolveBedrockCredentialSession runs the shared credential-use authorization, then loads the credential only within its workspace and only as a live AWS credential, resolving it to the stored key pair or a fresh STS session and recording the credential access. * fix(bedrock): bind native conversation continuation to the saved AWS credential A saved credential resolves to fresh session keys every run, so the binding names the credential instead; requests without one hash exactly as before. * feat(bedrock): sign agent, router, and evaluator Bedrock calls with a saved AWS credential A block's bedrockCredential is authorized and resolved for the run's acting user within the workflow's workspace before the provider call; a failure fails the call instead of falling back. The session stays off fallbacks on another provider. * feat(bedrock): accept a saved AWS credential in the LLM chat and guardrails operations Translate (llm_chat) and the guardrails hallucination check resolve a selected Bedrock credential for the acting user before the scoring or chat call, the same way the executor handlers do; a credential that cannot be used fails the call before any provider work. * feat(bedrock): offer a saved AWS credential in the shared LLM model credential fields Agent, router, evaluator, and translate get an AWS Authentication choice for Bedrock models: access keys (the default, and what every existing block keeps) or a saved AWS credential picker, following the Vertex credential pattern. * test(bedrock): keep the LLM operation credential test free of type errors * fix(bedrock): refuse a block set to a saved AWS credential whose ID resolves blank A block whose AWS Authentication is a saved credential, but whose credential field resolves empty at run time (an empty reference), fell through to inline keys or the deployment's own AWS credentials. The agent, router, evaluator, and translate blocks now share one selector that refuses that case, and honors an explicit access-keys mode over a stale credential ID. * docs(bedrock): document signing Bedrock model calls with a saved AWS credential * fix(bedrock): build role-session Bedrock clients per request instead of filling the shared client cache An assumed or keyless role resolves to a fresh STS session on every call, so a session-keyed cache entry was never read again and pushed other providers' warm clients out of the shared LRU. Long-lived saved keys stay cached. The session type now derives from AwsSessionCredentials, and authorization refusals carry a reason. * fix(bedrock): decide the AWS auth mode with one rule for fields and signing A block signs with a saved AWS credential only when bedrockAuthMode is 'credential', the same rule that shows and serializes the picker. The agent test no longer leaks a persistent mock rejection. * fix(bedrock): refuse an unauthorized saved AWS credential with 401 like Vertex * revert(bedrock): drop the unreachable guardrails saved-credential plumbing The Guardrails block evaluates its provider credential conditions without values, so its Bedrock fields never render or serialize; the param could only arrive outside the shared auth-mode rule. * docs(bedrock): cover inference-profile IAM scope and role session lifetime * fix(bedrock): offer the saved AWS credential only on blocks that forward it Guardrails and Pi spread the shared provider credential fields and inputs but never pass a saved credential to their call, so the AWS Authentication choice and credential picker are now opt-in. Agent, Router, Evaluator, and Translate opt in; Guardrails and Pi keep the fields and catalog schema they had before. * fix(bedrock): address review on saved AWS credential signing - Translate resolves the saved credential only for a Bedrock model, so a leftover credential choice no longer blocks a call to another provider. - A transient STS failure resolving the credential reports 503 instead of 400, using the classification the AWS tool operations already share. - llm_chat forwards the run's execution ID to the resolver, naming the role session and the credential-access audit after the run. - Native conversation binding keys a saved-credential request on the credential alone, ignoring inline keys left on the block. - Handler tests share one central mock for the Bedrock credential module; the block declaration test is replaced by Translate behavior tests. - The integration suite writes its credential-access audit inside a rollup scope and asserts it before teardown.
1 parent d5e98fa commit 7288b2d

34 files changed

Lines changed: 1400 additions & 72 deletions

‎apps/docs/content/docs/integrations/aws-credentials.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: Save an AWS identity once and use it across every AWS block, with a
66
import { Callout } from '@/components/ui/callout'
77
import { Step, Steps } from '@/components/ui/steps'
88

9-
An AWS credential holds one AWS identity for the whole workspace. Every AWS block (S3, DynamoDB, Lambda, SQS, Athena, CloudWatch, and the rest) signs in with a saved credential, so keys live in one place instead of in each block.
9+
An AWS credential holds one AWS identity for the whole workspace. Every AWS block (S3, DynamoDB, Lambda, SQS, Athena, CloudWatch, and the rest) signs in with a saved credential, so keys live in one place instead of in each block. The Agent, Router, and Evaluator blocks can also run AWS Bedrock models with a saved credential: choose **Saved AWS credential** under **AWS Authentication**.
1010

1111
<Callout type="info">
1212
Blocks added before saved credentials, which take access keys typed into the block, are marked legacy and keep working exactly as they are. Upgrade one by replacing it with the current block and selecting a credential.

‎apps/docs/content/docs/workflows/blocks/agent.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ The model that runs the step. Defaults to `claude-sonnet-5-5`. Type or pick any
3636

3737
For a custom cloud deployment, enter its provider prefix and model ID: `azure/my-deployment`, `azure-anthropic/my-deployment`, `bedrock/my-inference-profile`, or `vertex/my-gemini-model`. The prefix selects the provider and shows its credential fields even when the ID is absent from the catalog. Bedrock accepts full inference profile ARNs after `bedrock/`; Vertex uses the Gemini API and accepts Google model resource names. The deployment must support the selected provider's API. Custom IDs have no catalog pricing or token limits.
3838

39+
For an AWS Bedrock model, set **AWS Authentication** to **Access keys** to type a key pair into the block, or to **Saved AWS credential** to sign with a workspace [AWS credential](/integrations/aws-credentials) (access keys, an assumed IAM role, or a keyless role). A saved credential is used on its own: if it cannot be used, the run fails rather than signing with other keys. Its identity needs `bedrock:InvokeModel`, plus `bedrock:InvokeModelWithResponseStream` for streamed runs. Sim calls catalog Claude models through a geographic inference profile (`us.`, `eu.`, or `au.`, picked from the block's **AWS Region**), so a policy scoped to specific resources must allow the inference profile and the foundation model in every Region that profile routes to, as AWS's [inference profile prerequisites](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) describe. An assumed or keyless role's session starts when the block runs and is not renewed during the run, so one run must finish within the credential's session duration. The Router and Evaluator blocks offer the same choice.
40+
3941
Ollama Cloud, OpenRouter, Fireworks, Together AI, Baseten, Ollama, vLLM, and LiteLLM load their available models from the configured provider. New models appear through that discovery without a Sim catalog release. You can also enter a namespaced ID directly, such as `ollama-cloud/deepseek-v4.1-flash`, `openrouter/provider/model`, or `ollama/my-local-model`. Provider prefixes are case-insensitive; the model ID after the prefix keeps its original casing.
4042

4143
#### Nebius Token Factory

‎apps/sim/blocks/blocks/agent.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getModelFallbackSubBlock, MODEL_FALLBACK_INPUTS } from '@/blocks/model-
44
import type { BlockConfig } from '@/blocks/types'
55
import { AuthMode, IntegrationType } from '@/blocks/types'
66
import {
7+
BEDROCK_SAVED_CREDENTIAL_INPUTS,
78
getAgentModelOptions,
89
getModelCapabilityCondition,
910
getProviderCredentialSubBlocks,
@@ -136,7 +137,7 @@ Return ONLY the JSON array.`,
136137
options: getAgentModelOptions,
137138
commandSearchable: true,
138139
},
139-
...getProviderCredentialSubBlocks(),
140+
...getProviderCredentialSubBlocks({ bedrockSavedCredential: true }),
140141
{
141142
id: 'evaluationState',
142143
title: 'State',
@@ -559,6 +560,7 @@ Return ONLY the JSON array.`,
559560
bedrockAccessKeyId: { type: 'string', description: 'AWS Access Key ID for Bedrock' },
560561
bedrockSecretKey: { type: 'string', description: 'AWS Secret Access Key for Bedrock' },
561562
bedrockRegion: { type: 'string', description: 'AWS region for Bedrock' },
563+
...BEDROCK_SAVED_CREDENTIAL_INPUTS,
562564
responseFormat: {
563565
type: 'json',
564566
description: 'JSON response format schema',

‎apps/sim/blocks/blocks/evaluator.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChartBarIcon } from '@/components/icons'
33
import { getModelFallbackSubBlock, MODEL_FALLBACK_INPUTS } from '@/blocks/model-fallbacks'
44
import type { BlockConfig, ParamType } from '@/blocks/types'
55
import {
6+
BEDROCK_SAVED_CREDENTIAL_INPUTS,
67
getModelOptions,
78
getProviderCredentialSubBlocks,
89
getSerializedModelProviderId,
@@ -170,7 +171,7 @@ export const EvaluatorBlock: BlockConfig = {
170171
defaultValue: 'claude-sonnet-5-5',
171172
options: getModelOptions,
172173
},
173-
...getProviderCredentialSubBlocks(),
174+
...getProviderCredentialSubBlocks({ bedrockSavedCredential: true }),
174175
getModelFallbackSubBlock(),
175176
{
176177
id: 'temperature',
@@ -277,6 +278,7 @@ export const EvaluatorBlock: BlockConfig = {
277278
},
278279
model: { type: 'string' as ParamType, description: 'AI model to use' },
279280
...PROVIDER_CREDENTIAL_INPUTS,
281+
...BEDROCK_SAVED_CREDENTIAL_INPUTS,
280282
...MODEL_FALLBACK_INPUTS,
281283
temperature: {
282284
type: 'number' as ParamType,

‎apps/sim/blocks/blocks/router.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ConnectIcon } from '@/components/icons'
22
import { getModelFallbackSubBlock, MODEL_FALLBACK_INPUTS } from '@/blocks/model-fallbacks'
33
import { AuthMode, type BlockConfig } from '@/blocks/types'
44
import {
5+
BEDROCK_SAVED_CREDENTIAL_INPUTS,
56
getModelOptions,
67
getProviderCredentialSubBlocks,
78
getSerializedModelProviderId,
@@ -163,7 +164,7 @@ export const RouterBlock: BlockConfig = {
163164
defaultValue: 'claude-sonnet-5-5',
164165
options: getModelOptions,
165166
},
166-
...getProviderCredentialSubBlocks(),
167+
...getProviderCredentialSubBlocks({ bedrockSavedCredential: true }),
167168
getModelFallbackSubBlock(),
168169
{
169170
id: 'temperature',
@@ -200,6 +201,7 @@ export const RouterBlock: BlockConfig = {
200201
prompt: { type: 'string', description: 'Routing prompt content' },
201202
model: { type: 'string', description: 'AI model to use' },
202203
...PROVIDER_CREDENTIAL_INPUTS,
204+
...BEDROCK_SAVED_CREDENTIAL_INPUTS,
203205
...MODEL_FALLBACK_INPUTS,
204206
temperature: {
205207
type: 'number',
@@ -254,7 +256,7 @@ export const RouterV2Block: BlockConfig = {
254256
defaultValue: 'claude-sonnet-5-5',
255257
options: getModelOptions,
256258
},
257-
...getProviderCredentialSubBlocks(),
259+
...getProviderCredentialSubBlocks({ bedrockSavedCredential: true }),
258260
getModelFallbackSubBlock(),
259261
],
260262
tools: {
@@ -275,6 +277,7 @@ export const RouterV2Block: BlockConfig = {
275277
routes: { type: 'json', description: 'Route definitions with descriptions' },
276278
model: { type: 'string', description: 'AI model to use' },
277279
...PROVIDER_CREDENTIAL_INPUTS,
280+
...BEDROCK_SAVED_CREDENTIAL_INPUTS,
278281
...MODEL_FALLBACK_INPUTS,
279282
},
280283
outputs: {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { TranslateBlock } from '@/blocks/blocks/translate'
3+
4+
const toolParams = (params: Record<string, unknown>) => {
5+
const build = TranslateBlock.tools.config?.params
6+
if (!build) throw new Error('TranslateBlock.tools.config.params is missing')
7+
return build({ context: 'Hello', targetLanguage: 'French', ...params })
8+
}
9+
10+
describe('Translate Bedrock credential', () => {
11+
it('sends the saved AWS credential with a Bedrock model call', () => {
12+
expect(
13+
toolParams({
14+
model: 'bedrock/my-inference-profile',
15+
bedrockAuthMode: 'credential',
16+
bedrockCredential: 'aws-cred',
17+
})
18+
).toMatchObject({ model: 'bedrock/my-inference-profile', bedrockCredential: 'aws-cred' })
19+
})
20+
21+
it('refuses a Bedrock model call set to a saved AWS credential with none selected', () => {
22+
expect(() =>
23+
toolParams({
24+
model: 'bedrock/my-inference-profile',
25+
bedrockAuthMode: 'credential',
26+
bedrockCredential: '',
27+
})
28+
).toThrow('Select an AWS credential for Bedrock')
29+
})
30+
31+
it('ignores a leftover Bedrock credential choice once the model is not on Bedrock', () => {
32+
const params = toolParams({ model: 'gpt-4o', bedrockAuthMode: 'credential' })
33+
34+
expect(params).toMatchObject({ model: 'gpt-4o' })
35+
expect(params.bedrockCredential).toBeUndefined()
36+
})
37+
})

‎apps/sim/blocks/blocks/translate.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { TranslateIcon } from '@/components/icons'
22
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
33
import {
4+
BEDROCK_SAVED_CREDENTIAL_INPUTS,
45
getModelOptions,
56
getProviderCredentialSubBlocks,
7+
getSelectedBedrockCredential,
68
PROVIDER_CREDENTIAL_INPUTS,
79
} from '@/blocks/utils'
10+
import { findProviderFromModel } from '@/providers/models'
811

912
const getTranslationPrompt = (targetLanguage: string) =>
1013
`Translate the following text into ${targetLanguage || 'English'}. Output ONLY the translated text with no additional commentary, explanations, or notes.`
@@ -52,7 +55,7 @@ export const TranslateBlock: BlockConfig = {
5255
required: true,
5356
options: getModelOptions,
5457
},
55-
...getProviderCredentialSubBlocks(),
58+
...getProviderCredentialSubBlocks({ bedrockSavedCredential: true }),
5659
{
5760
id: 'systemPrompt',
5861
title: 'System Prompt',
@@ -80,13 +83,19 @@ export const TranslateBlock: BlockConfig = {
8083
bedrockAccessKeyId: params.bedrockAccessKeyId,
8184
bedrockSecretKey: params.bedrockSecretKey,
8285
bedrockRegion: params.bedrockRegion,
86+
bedrockCredential:
87+
typeof params.model === 'string' &&
88+
findProviderFromModel(params.model.trim()) === 'bedrock'
89+
? getSelectedBedrockCredential(params)
90+
: undefined,
8391
}),
8492
},
8593
},
8694
inputs: {
8795
context: { type: 'string', description: 'Text to translate' },
8896
targetLanguage: { type: 'string', description: 'Target language' },
8997
...PROVIDER_CREDENTIAL_INPUTS,
98+
...BEDROCK_SAVED_CREDENTIAL_INPUTS,
9099
systemPrompt: { type: 'string', description: 'Translation instructions' },
91100
},
92101
outputs: {

‎apps/sim/blocks/utils.test.ts‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@ vi.mock('@/stores/providers/store', () => ({
5959
vi.mock('@/lib/oauth/utils', () => oauthUtilsMock)
6060

6161
import { S3Icon } from '@/components/icons'
62+
import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility'
6263
import type { BlockConfig } from '@/blocks/types'
6364
import {
6465
getApiKeyCondition,
66+
getProviderCredentialSubBlocks,
67+
getSelectedBedrockCredential,
6568
getSerializedModelProviderId,
6669
parseOptionalBooleanInput,
6770
parseOptionalJsonInput,
@@ -102,6 +105,96 @@ describe('requiresProviderFamilyCredentials', () => {
102105
})
103106
})
104107

108+
describe('Bedrock credential fields', () => {
109+
const BEDROCK_FIELDS = [
110+
'bedrockAuthMode',
111+
'bedrockCredential',
112+
'bedrockManualCredential',
113+
'bedrockAccessKeyId',
114+
'bedrockSecretKey',
115+
'bedrockRegion',
116+
]
117+
118+
const visibleBedrockFields = (
119+
values: Record<string, unknown>,
120+
subBlocks = getProviderCredentialSubBlocks({ bedrockSavedCredential: true })
121+
) =>
122+
subBlocks
123+
.filter((subBlock) => BEDROCK_FIELDS.includes(subBlock.id))
124+
.filter((subBlock) => evaluateSubBlockCondition(subBlock.condition, values))
125+
.map((subBlock) => subBlock.id)
126+
127+
it('keeps showing access keys, and no credential picker, on a block saved before auth modes', () => {
128+
expect(visibleBedrockFields({ model: 'bedrock/my-inference-profile' })).toEqual([
129+
'bedrockAuthMode',
130+
'bedrockAccessKeyId',
131+
'bedrockSecretKey',
132+
'bedrockRegion',
133+
])
134+
})
135+
136+
it('swaps the access keys for the AWS credential picker when a saved credential is chosen', () => {
137+
expect(
138+
visibleBedrockFields({ model: 'bedrock/my-inference-profile', bedrockAuthMode: 'credential' })
139+
).toEqual(['bedrockAuthMode', 'bedrockCredential', 'bedrockManualCredential', 'bedrockRegion'])
140+
})
141+
142+
it('shows no Bedrock field for a model of another provider', () => {
143+
expect(visibleBedrockFields({ model: 'gpt-4o', bedrockAuthMode: 'credential' })).toEqual([])
144+
})
145+
146+
it('offers no saved credential, and keeps the access keys, for a block that cannot forward one', () => {
147+
expect(
148+
visibleBedrockFields(
149+
{ model: 'bedrock/my-inference-profile', bedrockAuthMode: 'credential' },
150+
getProviderCredentialSubBlocks()
151+
)
152+
).toEqual(['bedrockAccessKeyId', 'bedrockSecretKey', 'bedrockRegion'])
153+
})
154+
})
155+
156+
describe('getSelectedBedrockCredential', () => {
157+
it('returns the trimmed saved credential a block signs Bedrock calls with', () => {
158+
expect(
159+
getSelectedBedrockCredential({ bedrockAuthMode: 'credential', bedrockCredential: ' cred-1 ' })
160+
).toBe('cred-1')
161+
})
162+
163+
it('refuses a block set to a saved credential whose ID resolves blank', () => {
164+
expect(() =>
165+
getSelectedBedrockCredential({ bedrockAuthMode: 'credential', bedrockCredential: '' })
166+
).toThrow('Select an AWS credential for Bedrock')
167+
expect(() => getSelectedBedrockCredential({ bedrockAuthMode: 'credential' })).toThrow(
168+
'Select an AWS credential for Bedrock'
169+
)
170+
})
171+
172+
it('returns nothing for a block on access keys or saved before auth modes', () => {
173+
expect(
174+
getSelectedBedrockCredential({ bedrockAuthMode: 'access_keys', bedrockCredential: 'cred-1' })
175+
).toBeUndefined()
176+
expect(getSelectedBedrockCredential({})).toBeUndefined()
177+
})
178+
179+
it('signs with a credential exactly when the block shows and serializes the credential picker', () => {
180+
const picker = getProviderCredentialSubBlocks({ bedrockSavedCredential: true }).find(
181+
(subBlock) => subBlock.id === 'bedrockCredential'
182+
)
183+
const cases: Record<string, unknown>[] = [
184+
{ bedrockCredential: 'cred-1' },
185+
{ bedrockAuthMode: '', bedrockCredential: 'cred-1' },
186+
{ bedrockAuthMode: 'access_keys', bedrockCredential: 'cred-1' },
187+
{ bedrockAuthMode: 'credential', bedrockCredential: 'cred-1' },
188+
]
189+
for (const values of cases) {
190+
const withModel = { model: 'bedrock/my-inference-profile', ...values }
191+
expect(getSelectedBedrockCredential(values) !== undefined, JSON.stringify(values)).toBe(
192+
evaluateSubBlockCondition(picker?.condition, withModel)
193+
)
194+
}
195+
})
196+
})
197+
105198
describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => {
106199
const evaluateCondition = (model: string): boolean => {
107200
const conditionFn = getApiKeyCondition()

0 commit comments

Comments
 (0)