feat(providers): server-side credential hiding for Azure and Bedrock (#3884)

* fix: allow Bedrock provider to use AWS SDK default credential chain

Remove hard requirement for explicit AWS credentials in Bedrock provider.
When access key and secret key are not provided, the AWS SDK automatically
falls back to its default credential chain (env vars, instance profile,
ECS task role, EKS IRSA, SSO).

Closes #3694

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: add partial credential guard for Bedrock provider

Reject configurations where only one of bedrockAccessKeyId or
bedrockSecretKey is provided, preventing silent fallback to the
default credential chain with a potentially different identity.

Add tests covering all credential configuration scenarios.

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: clean up bedrock test lint and dead code

Remove unused config parameter and dead _lastConfig assignment
from mock factory. Break long mockReturnValue chain to satisfy
biome line-length rule.

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: address greptile review feedback on PR #3708

Use BedrockRuntimeClientConfig from SDK instead of inline type.
Add default return value for prepareToolsWithUsageControl mock.

Signed-off-by: majiayu000 <1835304752@qq.com>

* feat(providers): server-side credential hiding for Azure and Bedrock

* fix(providers): revert Bedrock credential fields to required with original placeholders

* fix(blocks): add hideWhenEnvSet to getProviderCredentialSubBlocks for Azure and Bedrock

* fix(agent): use getProviderCredentialSubBlocks() instead of duplicating credential subblocks

* fix(blocks): consolidate Vertex credential into shared factory with basic/advanced mode

* fix(types): resolve pre-existing TypeScript errors across auth, secrets, and copilot

* lint

* improvement(blocks): make Vertex AI project ID a password field

* fix(blocks): preserve vertexCredential subblock ID for backwards compatibility

* fix(blocks): follow canonicalParamId pattern correctly for vertex credential subblocks

* fix(blocks): keep vertexCredential subblock ID stable to preserve saved workflow state

* fix(blocks): add canonicalParamId to vertexCredential basic subblock to complete the swap pair

* fix types

* more types

---------

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Waleed
2026-04-01 16:27:54 -07:00
committed by GitHub
parent 076c835ba2
commit 8527ae5d3b
27 changed files with 343 additions and 217 deletions

View File

@@ -192,7 +192,7 @@ In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` t
},
```
The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag.
The visibility is controlled by `isSubBlockHidden()` in `lib/workflows/subblocks/visibility.ts`, which checks both the `isHosted` feature flag (`hideWhenHosted`) and optional env var conditions (`hideWhenEnvSet`).
### Excluding Specific Operations from Hosted Key Support

View File

@@ -29,6 +29,14 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI.
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
# AZURE_OPENAI_API_KEY= # Azure OpenAI API key
# AZURE_OPENAI_API_VERSION= # Azure OpenAI API version
# AZURE_ANTHROPIC_ENDPOINT= # Azure Anthropic endpoint (AI Foundry)
# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key
# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01)
# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI.
# Admin API (Optional - for self-hosted GitOps)
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.

View File

@@ -1,3 +1,4 @@
import type { SecretListEntry, Tag } from '@aws-sdk/client-secrets-manager'
import {
CreateSecretCommand,
DeleteSecretCommand,
@@ -61,7 +62,7 @@ export async function listSecrets(
})
const response = await client.send(command)
const secrets = (response.SecretList ?? []).map((secret) => ({
const secrets = (response.SecretList ?? []).map((secret: SecretListEntry) => ({
name: secret.Name ?? '',
arn: secret.ARN ?? '',
description: secret.Description ?? null,
@@ -69,7 +70,7 @@ export async function listSecrets(
lastChangedDate: secret.LastChangedDate?.toISOString() ?? null,
lastAccessedDate: secret.LastAccessedDate?.toISOString() ?? null,
rotationEnabled: secret.RotationEnabled ?? false,
tags: secret.Tags?.map((t) => ({ key: t.Key ?? '', value: t.Value ?? '' })) ?? [],
tags: secret.Tags?.map((t: Tag) => ({ key: t.Key ?? '', value: t.Value ?? '' })) ?? [],
}))
return {

View File

@@ -3,7 +3,7 @@ import {
buildCanonicalIndex,
evaluateSubBlockCondition,
isSubBlockFeatureEnabled,
isSubBlockHiddenByHostedKey,
isSubBlockHidden,
isSubBlockVisibleForMode,
} from '@/lib/workflows/subblocks/visibility'
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
@@ -109,8 +109,8 @@ export function useEditorSubblockLayout(
// Check required feature if specified - declarative feature gating
if (!isSubBlockFeatureEnabled(block)) return false
// Hide tool API key fields when hosted
if (isSubBlockHiddenByHostedKey(block)) return false
// Hide tool API key fields when hosted or when env var is set
if (isSubBlockHidden(block)) return false
// Special handling for trigger-config type (legacy trigger configuration UI)
if (block.type === ('trigger-config' as SubBlockType)) {

View File

@@ -17,7 +17,7 @@ import {
evaluateSubBlockCondition,
hasAdvancedValues,
isSubBlockFeatureEnabled,
isSubBlockHiddenByHostedKey,
isSubBlockHidden,
isSubBlockVisibleForMode,
resolveDependencyValue,
} from '@/lib/workflows/subblocks/visibility'
@@ -980,7 +980,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
if (block.hidden) return false
if (block.hideFromPreview) return false
if (!isSubBlockFeatureEnabled(block)) return false
if (isSubBlockHiddenByHostedKey(block)) return false
if (isSubBlockHidden(block)) return false
const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers'

View File

@@ -1,9 +1,12 @@
import { createLogger } from '@sim/logger'
import { AgentIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getApiKeyCondition, getModelOptions, RESPONSE_FORMAT_WAND_CONFIG } from '@/blocks/utils'
import {
getModelOptions,
getProviderCredentialSubBlocks,
RESPONSE_FORMAT_WAND_CONFIG,
} from '@/blocks/utils'
import {
getBaseModelProviders,
getMaxTemperature,
@@ -12,7 +15,6 @@ import {
getModelsWithReasoningEffort,
getModelsWithThinking,
getModelsWithVerbosity,
getProviderModels,
getReasoningEffortValuesForModel,
getThinkingLevelsForModel,
getVerbosityValuesForModel,
@@ -23,9 +25,6 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { ToolResponse } from '@/tools/types'
const logger = createLogger('AgentBlock')
const VERTEX_MODELS = getProviderModels('vertex')
const BEDROCK_MODELS = getProviderModels('bedrock')
const AZURE_MODELS = [...getProviderModels('azure-openai'), ...getProviderModels('azure-anthropic')]
const MODELS_WITH_REASONING_EFFORT = getModelsWithReasoningEffort()
const MODELS_WITH_VERBOSITY = getModelsWithVerbosity()
const MODELS_WITH_THINKING = getModelsWithThinking()
@@ -134,34 +133,6 @@ Return ONLY the JSON array.`,
defaultValue: 'claude-sonnet-4-5',
options: getModelOptions,
},
{
id: 'vertexCredential',
title: 'Google Cloud Account',
type: 'oauth-input',
serviceId: 'vertex-ai',
canonicalParamId: 'oauthCredential',
mode: 'basic',
requiredScopes: getScopesForService('vertex-ai'),
placeholder: 'Select Google Cloud account',
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
},
{
id: 'manualCredential',
title: 'Google Cloud Account',
type: 'short-input',
canonicalParamId: 'oauthCredential',
mode: 'advanced',
placeholder: 'Enter credential ID',
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
},
{
id: 'reasoningEffort',
title: 'Reasoning Effort',
@@ -318,100 +289,7 @@ Return ONLY the JSON array.`,
},
},
{
id: 'azureEndpoint',
title: 'Azure Endpoint',
type: 'short-input',
password: true,
placeholder: 'https://your-resource.services.ai.azure.com',
connectionDroppable: false,
condition: {
field: 'model',
value: AZURE_MODELS,
},
},
{
id: 'azureApiVersion',
title: 'Azure API Version',
type: 'short-input',
placeholder: 'Enter API version',
connectionDroppable: false,
condition: {
field: 'model',
value: AZURE_MODELS,
},
},
{
id: 'vertexProject',
title: 'Vertex AI Project',
type: 'short-input',
placeholder: 'your-gcp-project-id',
connectionDroppable: false,
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
},
{
id: 'vertexLocation',
title: 'Vertex AI Location',
type: 'short-input',
placeholder: 'us-central1',
connectionDroppable: false,
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
},
{
id: 'bedrockAccessKeyId',
title: 'AWS Access Key ID',
type: 'short-input',
password: true,
placeholder: 'Enter your AWS Access Key ID',
connectionDroppable: false,
required: true,
condition: {
field: 'model',
value: BEDROCK_MODELS,
},
},
{
id: 'bedrockSecretKey',
title: 'AWS Secret Access Key',
type: 'short-input',
password: true,
placeholder: 'Enter your AWS Secret Access Key',
connectionDroppable: false,
required: true,
condition: {
field: 'model',
value: BEDROCK_MODELS,
},
},
{
id: 'bedrockRegion',
title: 'AWS Region',
type: 'short-input',
placeholder: 'us-east-1',
connectionDroppable: false,
condition: {
field: 'model',
value: BEDROCK_MODELS,
},
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
connectionDroppable: false,
required: true,
condition: getApiKeyCondition(),
},
...getProviderCredentialSubBlocks(),
{
id: 'tools',
title: 'Tools',
@@ -661,7 +539,7 @@ Return ONLY the JSON array.`,
apiKey: { type: 'string', description: 'Provider API key' },
azureEndpoint: { type: 'string', description: 'Azure endpoint URL' },
azureApiVersion: { type: 'string', description: 'Azure API version' },
oauthCredential: { type: 'string', description: 'OAuth credential for Vertex AI' },
vertexCredential: { type: 'string', description: 'OAuth credential for Vertex AI' },
vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' },
vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' },
bedrockAccessKeyId: { type: 'string', description: 'AWS Access Key ID for Bedrock' },

View File

@@ -29,7 +29,7 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
],
placeholder: 'Select language',
value: () => CodeLanguage.JavaScript,
requiresFeature: 'NEXT_PUBLIC_E2B_ENABLED',
showWhenEnvSet: 'NEXT_PUBLIC_E2B_ENABLED',
},
{
id: 'code',

View File

@@ -327,8 +327,9 @@ export interface SubBlockConfig {
connectionDroppable?: boolean
hidden?: boolean
hideFromPreview?: boolean // Hide this subblock from the workflow block preview
requiresFeature?: string // Environment variable name that must be truthy for this subblock to be visible
showWhenEnvSet?: string // Show this subblock only when the named NEXT_PUBLIC_ env var is truthy
hideWhenHosted?: boolean // Hide this subblock when running on hosted sim
hideWhenEnvSet?: string // Hide this subblock when the named NEXT_PUBLIC_ env var is truthy
description?: string
tooltip?: string // Tooltip text displayed via info icon next to the title
value?: (params: Record<string, any>) => string

View File

@@ -1,4 +1,5 @@
import { isHosted } from '@/lib/core/config/feature-flags'
import { isAzureConfigured, isHosted } from '@/lib/core/config/feature-flags'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
import {
getHostedModels,
@@ -8,9 +9,12 @@ import {
} from '@/providers/models'
import { useProvidersStore } from '@/stores/providers/store'
const VERTEX_MODELS = getProviderModels('vertex')
const BEDROCK_MODELS = getProviderModels('bedrock')
const AZURE_MODELS = [...getProviderModels('azure-openai'), ...getProviderModels('azure-anthropic')]
export const VERTEX_MODELS = getProviderModels('vertex')
export const BEDROCK_MODELS = getProviderModels('bedrock')
export const AZURE_MODELS = [
...getProviderModels('azure-openai'),
...getProviderModels('azure-anthropic'),
]
/**
* Returns model options for combobox subblocks, combining all provider sources.
@@ -105,6 +109,16 @@ function shouldRequireApiKeyForModel(model: string): boolean {
return false
}
if (
isAzureConfigured &&
(normalizedModel.startsWith('azure/') ||
normalizedModel.startsWith('azure-openai/') ||
normalizedModel.startsWith('azure-anthropic/') ||
AZURE_MODELS.some((m) => m.toLowerCase() === normalizedModel))
) {
return false
}
if (normalizedModel.startsWith('vllm/')) {
return false
}
@@ -158,7 +172,9 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
title: 'Google Cloud Account',
type: 'oauth-input',
serviceId: 'vertex-ai',
requiredScopes: ['https://www.googleapis.com/auth/cloud-platform'],
canonicalParamId: 'vertexCredential',
mode: 'basic',
requiredScopes: getScopesForService('vertex-ai'),
placeholder: 'Select Google Cloud account',
required: true,
condition: {
@@ -166,6 +182,19 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
value: VERTEX_MODELS,
},
},
{
id: 'vertexManualCredential',
title: 'Google Cloud Account',
type: 'short-input',
canonicalParamId: 'vertexCredential',
mode: 'advanced',
placeholder: 'Enter credential ID',
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
},
{
id: 'apiKey',
title: 'API Key',
@@ -183,6 +212,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
password: true,
placeholder: 'https://your-resource.services.ai.azure.com',
connectionDroppable: false,
hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED',
condition: {
field: 'model',
value: AZURE_MODELS,
@@ -194,6 +224,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
type: 'short-input',
placeholder: 'Enter API version',
connectionDroppable: false,
hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED',
condition: {
field: 'model',
value: AZURE_MODELS,
@@ -203,6 +234,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
id: 'vertexProject',
title: 'Vertex AI Project',
type: 'short-input',
password: true,
placeholder: 'your-gcp-project-id',
connectionDroppable: false,
required: true,
@@ -231,6 +263,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
placeholder: 'Enter your AWS Access Key ID',
connectionDroppable: false,
required: true,
hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS',
condition: {
field: 'model',
value: BEDROCK_MODELS,
@@ -244,6 +277,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
placeholder: 'Enter your AWS Secret Access Key',
connectionDroppable: false,
required: true,
hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS',
condition: {
field: 'model',
value: BEDROCK_MODELS,

View File

@@ -8,6 +8,7 @@ import { isHosted } from '@/lib/core/config/feature-flags'
import { decryptSecret } from '@/lib/core/security/encryption'
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
import { getHostedModels } from '@/providers/models'
import { PROVIDER_PLACEHOLDER_KEY } from '@/providers/utils'
import { useProvidersStore } from '@/stores/providers/store'
import type { BYOKProviderId } from '@/tools/types'
@@ -95,7 +96,15 @@ export async function getApiKeyWithBYOK(
const isBedrockModel = provider === 'bedrock' || model.startsWith('bedrock/')
if (isBedrockModel) {
return { apiKey: 'bedrock-uses-own-credentials', isBYOK: false }
return { apiKey: PROVIDER_PLACEHOLDER_KEY, isBYOK: false }
}
if (provider === 'azure-openai') {
return { apiKey: userProvidedKey || env.AZURE_OPENAI_API_KEY || '', isBYOK: false }
}
if (provider === 'azure-anthropic') {
return { apiKey: userProvidedKey || env.AZURE_ANTHROPIC_API_KEY || '', isBYOK: false }
}
const isOpenAIModel = provider === 'openai'

View File

@@ -559,12 +559,12 @@ export const auth = betterAuth({
github: {
clientId: env.GITHUB_CLIENT_ID as string,
clientSecret: env.GITHUB_CLIENT_SECRET as string,
scopes: ['user:email', 'repo'],
scope: ['user:email', 'repo'],
},
google: {
clientId: env.GOOGLE_CLIENT_ID as string,
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
scopes: [
scope: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
],
@@ -602,7 +602,6 @@ export const auth = betterAuth({
emailAndPassword: {
enabled: true,
requireEmailVerification: isEmailVerificationEnabled,
sendVerificationOnSignUp: isEmailVerificationEnabled, // Auto-send verification OTP on signup when verification is required
throwOnMissingCredentials: true,
throwOnInvalidCredentials: true,
sendResetPassword: async ({ user, url, token }, request) => {

View File

@@ -954,10 +954,10 @@ async function generateOAuthLink(
const { headers: getHeaders } = await import('next/headers')
const reqHeaders = await getHeaders()
const data = (await auth.api.oAuth2LinkAccount({
const data = await auth.api.oAuth2LinkAccount({
body: { providerId, callbackURL },
headers: reqHeaders,
})) as { url?: string; redirect?: boolean }
})
if (!data?.url) {
throw new Error('oAuth2LinkAccount did not return an authorization URL')

View File

@@ -1,6 +1,6 @@
import { getCopilotToolDescription } from '@/lib/copilot/tool-descriptions'
import { isHosted } from '@/lib/core/config/feature-flags'
import { isSubBlockHiddenByHostedKey } from '@/lib/workflows/subblocks/visibility'
import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility'
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
import type { ToolConfig } from '@/tools/types'
@@ -369,7 +369,7 @@ function serializeSubBlock(sb: SubBlockConfig): Record<string, unknown> {
* Serialize a block schema for VFS components/blocks/{type}.json
*/
export function serializeBlockSchema(block: BlockConfig): string {
const hiddenIds = new Set(block.subBlocks.filter(isSubBlockHiddenByHostedKey).map((sb) => sb.id))
const hiddenIds = new Set(block.subBlocks.filter(isSubBlockHidden).map((sb) => sb.id))
const subBlocks = block.subBlocks
.filter((sb) => !hiddenIds.has(sb.id))

View File

@@ -403,6 +403,8 @@ export const env = createEnv({
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
NEXT_PUBLIC_E2B_ENABLED: z.string().optional(),
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: z.string().optional(), // Hide Bedrock credential fields when deployment uses AWS default credential chain (IAM roles, instance profiles, ECS task roles, IRSA)
NEXT_PUBLIC_AZURE_CONFIGURED: z.string().optional(), // Hide Azure credential fields when endpoint/key/version are pre-configured server-side
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: z.string().optional(),
NEXT_PUBLIC_ENABLE_PLAYGROUND: z.string().optional(), // Enable component playground at /playground
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
@@ -461,6 +463,8 @@ export const env = createEnv({
NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: process.env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED,
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: process.env.NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS,
NEXT_PUBLIC_AZURE_CONFIGURED: process.env.NEXT_PUBLIC_AZURE_CONFIGURED,
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED,
NEXT_PUBLIC_ENABLE_PLAYGROUND: process.env.NEXT_PUBLIC_ENABLE_PLAYGROUND,
NEXT_PUBLIC_POSTHOG_ENABLED: process.env.NEXT_PUBLIC_POSTHOG_ENABLED,

View File

@@ -122,6 +122,14 @@ export const isInboxEnabled = isTruthy(env.INBOX_ENABLED)
*/
export const isE2bEnabled = isTruthy(env.E2B_ENABLED)
/**
* Whether Azure OpenAI / Azure Anthropic credentials are pre-configured at the server level
* (via AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_ANTHROPIC_ENDPOINT, etc.).
* When true, the endpoint, API key, and API version fields are hidden in the Agent block UI.
* Set NEXT_PUBLIC_AZURE_CONFIGURED=true in self-hosted deployments on Azure.
*/
export const isAzureConfigured = isTruthy(getEnv('NEXT_PUBLIC_AZURE_CONFIGURED'))
/**
* Are invitations disabled globally
* When true, workspace invitations are disabled for all users

View File

@@ -285,15 +285,19 @@ export function resolveDependencyValue(
* Check if a subblock is gated by a feature flag.
*/
export function isSubBlockFeatureEnabled(subBlock: SubBlockConfig): boolean {
if (!subBlock.requiresFeature) return true
return isTruthy(getEnv(subBlock.requiresFeature))
if (!subBlock.showWhenEnvSet) return true
return isTruthy(getEnv(subBlock.showWhenEnvSet))
}
/**
* Check if a subblock should be hidden because we're running on hosted Sim.
* Used for tool API key fields that should be hidden when Sim provides hosted keys.
* Check if a subblock should be hidden based on environment conditions.
* Covers two cases:
* - `hideWhenHosted`: hidden when running on hosted Sim (tool API key fields)
* - `hideWhenEnvSet`: hidden when a specific NEXT_PUBLIC_ env var is truthy
* (credential fields hidden when the deployment provides them server-side)
*/
export function isSubBlockHiddenByHostedKey(subBlock: SubBlockConfig): boolean {
if (!subBlock.hideWhenHosted) return false
return isHosted
export function isSubBlockHidden(subBlock: SubBlockConfig): boolean {
if (subBlock.hideWhenHosted && isHosted) return true
if (subBlock.hideWhenEnvSet && isTruthy(getEnv(subBlock.hideWhenEnvSet))) return true
return false
}

View File

@@ -1,5 +1,6 @@
import Anthropic from '@anthropic-ai/sdk'
import { createLogger } from '@sim/logger'
import { env } from '@/lib/core/config/env'
import type { StreamingExecution } from '@/executor/types'
import { executeAnthropicProviderRequest } from '@/providers/anthropic/core'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
@@ -18,14 +19,16 @@ export const azureAnthropicProvider: ProviderConfig = {
executeRequest: async (
request: ProviderRequest
): Promise<ProviderResponse | StreamingExecution> => {
if (!request.azureEndpoint) {
const azureEndpoint = request.azureEndpoint || env.AZURE_ANTHROPIC_ENDPOINT
if (!azureEndpoint) {
throw new Error(
'Azure endpoint is required for Azure Anthropic. Please provide it via the azureEndpoint parameter.'
'Azure endpoint is required for Azure Anthropic. Please provide it via the azureEndpoint parameter or AZURE_ANTHROPIC_ENDPOINT environment variable.'
)
}
if (!request.apiKey) {
throw new Error('API key is required for Azure Anthropic')
const apiKey = request.apiKey
if (!apiKey) {
throw new Error('API key is required for Azure Anthropic.')
}
// Strip the azure-anthropic/ prefix from the model name if present
@@ -33,14 +36,16 @@ export const azureAnthropicProvider: ProviderConfig = {
// Azure AI Foundry hosts Anthropic models at {endpoint}/anthropic
// The SDK appends /v1/messages automatically
const baseURL = `${request.azureEndpoint.replace(/\/$/, '')}/anthropic`
const baseURL = `${azureEndpoint.replace(/\/$/, '')}/anthropic`
const anthropicVersion = request.azureApiVersion || '2023-06-01'
const anthropicVersion =
request.azureApiVersion || env.AZURE_ANTHROPIC_API_VERSION || '2023-06-01'
return executeAnthropicProviderRequest(
{
...request,
model: modelName,
apiKey,
},
{
providerId: 'azure-anthropic',

View File

@@ -65,7 +65,7 @@ async function executeChatCompletionsRequest(
})
const azureOpenAI = new AzureOpenAI({
apiKey: request.apiKey,
apiKey: request.apiKey!,
apiVersion: azureApiVersion,
endpoint: azureEndpoint,
})
@@ -623,8 +623,9 @@ export const azureOpenAIProvider: ProviderConfig = {
)
}
if (!request.apiKey) {
throw new Error('API key is required for Azure OpenAI')
const apiKey = request.apiKey
if (!apiKey) {
throw new Error('API key is required for Azure OpenAI.')
}
// Check if the endpoint is a full chat completions URL
@@ -653,7 +654,12 @@ export const azureOpenAIProvider: ProviderConfig = {
apiVersion: azureApiVersion,
})
return executeChatCompletionsRequest(request, baseUrl, azureApiVersion, deploymentName)
return executeChatCompletionsRequest(
{ ...request, apiKey },
baseUrl,
azureApiVersion,
deploymentName
)
}
// Check if the endpoint is already a full responses API URL
@@ -663,18 +669,21 @@ export const azureOpenAIProvider: ProviderConfig = {
const deploymentName = request.model.replace('azure/', '')
// Use the URL as-is since it's already complete
return executeResponsesProviderRequest(request, {
providerId: 'azure-openai',
providerLabel: 'Azure OpenAI',
modelName: deploymentName,
endpoint: azureEndpoint,
headers: {
'Content-Type': 'application/json',
'OpenAI-Beta': 'responses=v1',
'api-key': request.apiKey,
},
logger,
})
return executeResponsesProviderRequest(
{ ...request, apiKey },
{
providerId: 'azure-openai',
providerLabel: 'Azure OpenAI',
modelName: deploymentName,
endpoint: azureEndpoint,
headers: {
'Content-Type': 'application/json',
'OpenAI-Beta': 'responses=v1',
'api-key': apiKey,
},
logger,
}
)
}
// Default: base URL provided, construct the responses API URL
@@ -684,17 +693,20 @@ export const azureOpenAIProvider: ProviderConfig = {
const deploymentName = request.model.replace('azure/', '')
const apiUrl = `${azureEndpoint.replace(/\/$/, '')}/openai/v1/responses?api-version=${azureApiVersion}`
return executeResponsesProviderRequest(request, {
providerId: 'azure-openai',
providerLabel: 'Azure OpenAI',
modelName: deploymentName,
endpoint: apiUrl,
headers: {
'Content-Type': 'application/json',
'OpenAI-Beta': 'responses=v1',
'api-key': request.apiKey,
},
logger,
})
return executeResponsesProviderRequest(
{ ...request, apiKey },
{
providerId: 'azure-openai',
providerLabel: 'Azure OpenAI',
modelName: deploymentName,
endpoint: apiUrl,
headers: {
'Content-Type': 'application/json',
'OpenAI-Beta': 'responses=v1',
'api-key': apiKey,
},
logger,
}
)
},
}

View File

@@ -0,0 +1,115 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockSend = vi.fn()
vi.mock('@aws-sdk/client-bedrock-runtime', () => ({
BedrockRuntimeClient: vi.fn().mockImplementation(() => {
return { send: mockSend }
}),
ConverseCommand: vi.fn(),
ConverseStreamCommand: vi.fn(),
}))
vi.mock('@/providers/bedrock/utils', () => ({
getBedrockInferenceProfileId: vi
.fn()
.mockReturnValue('us.anthropic.claude-3-5-sonnet-20241022-v2:0'),
checkForForcedToolUsage: vi.fn(),
createReadableStreamFromBedrockStream: vi.fn(),
generateToolUseId: vi.fn().mockReturnValue('tool-1'),
}))
vi.mock('@/providers/models', () => ({
getProviderModels: vi.fn().mockReturnValue([]),
getProviderDefaultModel: vi.fn().mockReturnValue('us.anthropic.claude-3-5-sonnet-20241022-v2:0'),
}))
vi.mock('@/providers/utils', () => ({
calculateCost: vi.fn().mockReturnValue({ input: 0, output: 0, total: 0, pricing: null }),
prepareToolExecution: vi.fn(),
prepareToolsWithUsageControl: vi.fn().mockReturnValue({
tools: [],
toolChoice: 'auto',
forcedTools: [],
}),
sumToolCosts: vi.fn().mockReturnValue(0),
}))
vi.mock('@/tools', () => ({
executeTool: vi.fn(),
}))
import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime'
import { bedrockProvider } from '@/providers/bedrock/index'
describe('bedrockProvider credential handling', () => {
beforeEach(() => {
vi.clearAllMocks()
mockSend.mockResolvedValue({
output: { message: { content: [{ text: 'response' }] } },
usage: { inputTokens: 10, outputTokens: 5 },
})
})
const baseRequest = {
model: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
systemPrompt: 'You are helpful.',
messages: [{ role: 'user' as const, content: 'Hello' }],
}
it('throws when only bedrockAccessKeyId is provided', async () => {
await expect(
bedrockProvider.executeRequest({
...baseRequest,
bedrockAccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
})
).rejects.toThrow('Both bedrockAccessKeyId and bedrockSecretKey must be provided together')
})
it('throws when only bedrockSecretKey is provided', async () => {
await expect(
bedrockProvider.executeRequest({
...baseRequest,
bedrockSecretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
})
).rejects.toThrow('Both bedrockAccessKeyId and bedrockSecretKey must be provided together')
})
it('creates client with explicit credentials when both are provided', async () => {
await bedrockProvider.executeRequest({
...baseRequest,
bedrockAccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
bedrockSecretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
})
expect(BedrockRuntimeClient).toHaveBeenCalledWith({
region: 'us-east-1',
credentials: {
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
},
})
})
it('creates client without credentials when neither is provided', async () => {
await bedrockProvider.executeRequest(baseRequest)
expect(BedrockRuntimeClient).toHaveBeenCalledWith({
region: 'us-east-1',
})
})
it('uses custom region when provided', async () => {
await bedrockProvider.executeRequest({
...baseRequest,
bedrockRegion: 'eu-west-1',
})
expect(BedrockRuntimeClient).toHaveBeenCalledWith({
region: 'eu-west-1',
})
})
})

View File

@@ -1,6 +1,7 @@
import {
type Message as BedrockMessage,
BedrockRuntimeClient,
type BedrockRuntimeClientConfig,
type ContentBlock,
type ConversationRole,
ConverseCommand,
@@ -50,14 +51,6 @@ export const bedrockProvider: ProviderConfig = {
executeRequest: async (
request: ProviderRequest
): Promise<ProviderResponse | StreamingExecution> => {
if (!request.bedrockAccessKeyId) {
throw new Error('AWS Access Key ID is required for Bedrock')
}
if (!request.bedrockSecretKey) {
throw new Error('AWS Secret Access Key is required for Bedrock')
}
const region = request.bedrockRegion || 'us-east-1'
const bedrockModelId = getBedrockInferenceProfileId(request.model, region)
@@ -67,13 +60,24 @@ export const bedrockProvider: ProviderConfig = {
region,
})
const client = new BedrockRuntimeClient({
region,
credentials: {
accessKeyId: request.bedrockAccessKeyId || '',
secretAccessKey: request.bedrockSecretKey || '',
},
})
const hasAccessKey = Boolean(request.bedrockAccessKeyId)
const hasSecretKey = Boolean(request.bedrockSecretKey)
if (hasAccessKey !== hasSecretKey) {
throw new Error(
'Both bedrockAccessKeyId and bedrockSecretKey must be provided together. ' +
'Provide both for explicit credentials, or omit both to use the AWS default credential chain.'
)
}
const clientConfig: BedrockRuntimeClientConfig = { region }
if (request.bedrockAccessKeyId && request.bedrockSecretKey) {
clientConfig.credentials = {
accessKeyId: request.bedrockAccessKeyId,
secretAccessKey: request.bedrockSecretKey,
}
}
const client = new BedrockRuntimeClient(clientConfig)
const messages: BedrockMessage[] = []
const systemContent: SystemContentBlock[] = []

View File

@@ -718,6 +718,13 @@ export function shouldBillModelUsage(model: string): boolean {
return hostedModels.some((hostedModel) => model.toLowerCase() === hostedModel.toLowerCase())
}
/**
* Placeholder returned for providers that use their own credential mechanism
* rather than a user-supplied API key (e.g. AWS Bedrock via IAM/instance profiles).
* Must be truthy so upstream key-presence checks don't reject it.
*/
export const PROVIDER_PLACEHOLDER_KEY = 'provider-uses-own-credentials'
/**
* Get an API key for a specific provider, handling rotation and fallbacks
* For use server-side only
@@ -740,7 +747,7 @@ export function getApiKey(provider: string, model: string, userProvidedKey?: str
// Bedrock uses its own credentials (bedrockAccessKeyId/bedrockSecretKey), not apiKey
const isBedrockModel = provider === 'bedrock' || model.startsWith('bedrock/')
if (isBedrockModel) {
return 'bedrock-uses-own-credentials'
return PROVIDER_PLACEHOLDER_KEY
}
const isOpenAIModel = provider === 'openai'

View File

@@ -9,7 +9,7 @@ import {
isCanonicalPair,
isNonEmptyValue,
isSubBlockFeatureEnabled,
isSubBlockHiddenByHostedKey,
isSubBlockHidden,
resolveCanonicalMode,
} from '@/lib/workflows/subblocks/visibility'
import { getBlock } from '@/blocks'
@@ -49,7 +49,7 @@ function shouldSerializeSubBlock(
canonicalModeOverrides?: CanonicalModeOverrides
): boolean {
if (!isSubBlockFeatureEnabled(subBlockConfig)) return false
if (isSubBlockHiddenByHostedKey(subBlockConfig)) return false
if (isSubBlockHidden(subBlockConfig)) return false
if (subBlockConfig.mode === 'trigger') {
if (!isTriggerContext && !isTriggerCategory) return false

View File

@@ -5,7 +5,7 @@ import {
type CanonicalModeOverrides,
evaluateSubBlockCondition,
isCanonicalPair,
isSubBlockHiddenByHostedKey,
isSubBlockHidden,
resolveCanonicalMode,
type SubBlockCondition,
} from '@/lib/workflows/subblocks/visibility'
@@ -320,7 +320,7 @@ export function getToolParametersConfig(
)
if (subBlock) {
if (isSubBlockHiddenByHostedKey(subBlock)) {
if (isSubBlockHidden(subBlock)) {
toolParam.visibility = 'hidden'
}
@@ -946,8 +946,8 @@ export function getSubBlocksForToolInput(
// Skip trigger-mode-only subblocks
if (sb.mode === 'trigger') continue
// Hide tool API key fields when running on hosted Sim
if (isSubBlockHiddenByHostedKey(sb)) continue
// Hide tool API key fields when running on hosted Sim or when env var is set
if (isSubBlockHidden(sb)) continue
// Determine the effective param ID (canonical or subblock id)
const effectiveParamId = sb.canonicalParamId || sb.id

View File

@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",

View File

@@ -45,6 +45,10 @@ app:
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# AWS Bedrock - when using IRSA (see serviceAccount below), the default credential chain
# resolves automatically. Setting this hides the credential fields in the Agent block UI.
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: "true" # Uncomment if using Bedrock with IRSA
# AWS S3 Cloud Storage Configuration (RECOMMENDED for production)
# Create S3 buckets in your AWS account and configure IAM permissions
AWS_REGION: "us-west-2"

View File

@@ -47,6 +47,17 @@ app:
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# Azure OpenAI / Azure Anthropic (Azure AI Foundry) — set to use server-side credentials.
# When NEXT_PUBLIC_AZURE_CONFIGURED is true, the endpoint/key/version fields are hidden in
# the Agent block UI so users just pick a model and run.
AZURE_OPENAI_ENDPOINT: "" # e.g. https://your-resource.openai.azure.com
AZURE_OPENAI_API_KEY: "" # Azure OpenAI API key
AZURE_OPENAI_API_VERSION: "" # e.g. 2024-07-01-preview
AZURE_ANTHROPIC_ENDPOINT: "" # Azure AI Foundry endpoint for Anthropic models
AZURE_ANTHROPIC_API_KEY: "" # Azure Anthropic API key
AZURE_ANTHROPIC_API_VERSION: "" # Azure Anthropic API version (e.g., 2023-06-01)
NEXT_PUBLIC_AZURE_CONFIGURED: "true" # Set to "true" once credentials are configured above
# Azure Blob Storage Configuration (RECOMMENDED for production)
# Create a storage account and containers in your Azure subscription
AZURE_ACCOUNT_NAME: "simstudiostorageacct" # Azure storage account name

View File

@@ -113,13 +113,24 @@ app:
# Google Vertex AI Configuration
VERTEX_PROJECT: "" # Google Cloud project ID for Vertex AI
VERTEX_LOCATION: "us-central1" # Google Cloud region for Vertex AI (e.g., "us-central1")
# Azure OpenAI Configuration (leave empty if not using Azure OpenAI)
AZURE_OPENAI_ENDPOINT: "" # Azure OpenAI service endpoint (e.g., https://your-resource.openai.azure.com)
AZURE_OPENAI_API_KEY: "" # Azure OpenAI API key
AZURE_OPENAI_API_VERSION: "" # Azure OpenAI API version (e.g., 2024-07-01-preview)
# Azure Anthropic Configuration (leave empty if not using Azure Anthropic via AI Foundry)
AZURE_ANTHROPIC_ENDPOINT: "" # Azure AI Foundry endpoint for Anthropic models
AZURE_ANTHROPIC_API_KEY: "" # Azure Anthropic API key
AZURE_ANTHROPIC_API_VERSION: "" # Azure Anthropic API version (e.g., 2023-06-01)
# AI Provider API Keys (leave empty if not using)
OPENAI_API_KEY: "" # Primary OpenAI API key
OPENAI_API_KEY_1: "" # Additional OpenAI API key for load balancing
OPENAI_API_KEY_2: "" # Additional OpenAI API key for load balancing
OPENAI_API_KEY_3: "" # Additional OpenAI API key for load balancing
MISTRAL_API_KEY: "" # Mistral AI API key
FIREWORKS_API_KEY: "" # Fireworks AI API key (for hosted model access)
ANTHROPIC_API_KEY_1: "" # Primary Anthropic Claude API key
ANTHROPIC_API_KEY_2: "" # Additional Anthropic API key for load balancing
ANTHROPIC_API_KEY_3: "" # Additional Anthropic API key for load balancing
@@ -223,6 +234,18 @@ app:
SSO_ENABLED: "" # Enable SSO authentication ("true" to enable)
NEXT_PUBLIC_SSO_ENABLED: "" # Show SSO login button in UI ("true" to enable)
# AWS Bedrock Credential Mode
# Set to "true" when the deployment uses AWS default credential chain (IAM roles, instance
# profiles, ECS task roles, IRSA, etc.) instead of explicit access key/secret per workflow.
# When enabled, the AWS Access Key ID and Secret fields are hidden in the Agent block UI.
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: "" # Set to "true" to hide Bedrock credential fields
# Azure Provider Credential Mode
# Set to "true" when AZURE_OPENAI_ENDPOINT/API_KEY (and/or AZURE_ANTHROPIC_*) are configured
# server-side. When enabled, the Azure endpoint, API key, and API version fields are hidden
# in the Agent block UI — users just pick an Azure model and run.
NEXT_PUBLIC_AZURE_CONFIGURED: "" # Set to "true" to hide Azure credential fields
# AWS S3 Cloud Storage Configuration (optional - for file storage)
# If configured, files will be stored in S3 instead of local storage
AWS_REGION: "" # AWS region (e.g., "us-east-1")