diff --git a/.cursor/skills/add-hosted-key/SKILL.md b/.cursor/skills/add-hosted-key/SKILL.md index a6e0f07052d..2181910f8d9 100644 --- a/.cursor/skills/add-hosted-key/SKILL.md +++ b/.cursor/skills/add-hosted-key/SKILL.md @@ -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 diff --git a/apps/sim/.env.example b/apps/sim/.env.example index 8db5d82c1af..fe9514e358e 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -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. diff --git a/apps/sim/app/api/tools/secrets_manager/utils.ts b/apps/sim/app/api/tools/secrets_manager/utils.ts index 331197fceca..dacbbc30be1 100644 --- a/apps/sim/app/api/tools/secrets_manager/utils.ts +++ b/apps/sim/app/api/tools/secrets_manager/utils.ts @@ -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 { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts index 0cf118e428e..1ebc925262e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts @@ -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)) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 70ddefc95e8..b7b04f38bb2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -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' diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index b44ef4658c3..caad30d9e2d 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -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' }, diff --git a/apps/sim/blocks/blocks/function.ts b/apps/sim/blocks/blocks/function.ts index bdfc15ab11c..341f6482456 100644 --- a/apps/sim/blocks/blocks/function.ts +++ b/apps/sim/blocks/blocks/function.ts @@ -29,7 +29,7 @@ export const FunctionBlock: BlockConfig = { ], placeholder: 'Select language', value: () => CodeLanguage.JavaScript, - requiresFeature: 'NEXT_PUBLIC_E2B_ENABLED', + showWhenEnvSet: 'NEXT_PUBLIC_E2B_ENABLED', }, { id: 'code', diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 614c686ec54..a471af5aada 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -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 diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index e06dbdf5add..b68ac4cbbbe 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -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, diff --git a/apps/sim/lib/api-key/byok.ts b/apps/sim/lib/api-key/byok.ts index 901d86af5fe..a2aa198e859 100644 --- a/apps/sim/lib/api-key/byok.ts +++ b/apps/sim/lib/api-key/byok.ts @@ -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' diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 7bfaa64889d..69a308eccc7 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -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) => { diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index 95252daf8bf..cf6b623717e 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -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') diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index e1d282d0d55..524e8567e01 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -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 { * 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)) diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index d58892e6887..84ffe503d4e 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -404,6 +404,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 @@ -462,6 +464,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, diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 012d1d5e026..b688924afed 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -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 diff --git a/apps/sim/lib/workflows/subblocks/visibility.ts b/apps/sim/lib/workflows/subblocks/visibility.ts index 44cddf1224d..356ab0507bf 100644 --- a/apps/sim/lib/workflows/subblocks/visibility.ts +++ b/apps/sim/lib/workflows/subblocks/visibility.ts @@ -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 } diff --git a/apps/sim/providers/azure-anthropic/index.ts b/apps/sim/providers/azure-anthropic/index.ts index 721e363394a..710d1ba90e8 100644 --- a/apps/sim/providers/azure-anthropic/index.ts +++ b/apps/sim/providers/azure-anthropic/index.ts @@ -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 => { - 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', diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index 930c31035a3..72ee513e80e 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -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, + } + ) }, } diff --git a/apps/sim/providers/bedrock/index.test.ts b/apps/sim/providers/bedrock/index.test.ts new file mode 100644 index 00000000000..8c938e01c87 --- /dev/null +++ b/apps/sim/providers/bedrock/index.test.ts @@ -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', + }) + }) +}) diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index ec0af6ab04b..d3223cfebd0 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -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 => { - 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[] = [] diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 30fe1467eb1..40d4a0ebdf7 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -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' diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 9c21661deb5..485b4e3cc2c 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -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 diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index 925e4fe6f50..9cc8c883655 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -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 diff --git a/bun.lock b/bun.lock index 181248e1ee7..c04ff1f5e07 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/helm/sim/examples/values-aws.yaml b/helm/sim/examples/values-aws.yaml index c8451efc2a9..a5f21eb2294 100644 --- a/helm/sim/examples/values-aws.yaml +++ b/helm/sim/examples/values-aws.yaml @@ -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" diff --git a/helm/sim/examples/values-azure.yaml b/helm/sim/examples/values-azure.yaml index a11b55adc93..30c97798bfa 100644 --- a/helm/sim/examples/values-azure.yaml +++ b/helm/sim/examples/values-azure.yaml @@ -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 diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 92c163b4222..9fbe6195b67 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -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")