Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/knowledge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,7 @@ sim knowledge connectors create <knowledgeBaseId> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--connector-type <value>` | Yes | Registered connector type. |
| `--credential-id <value>` | No | OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types), or, when this deployment enables it, a saved credential for an `apiKey` connector type that declares `auth.savedCredential` (such as Amazon S3 with a saved AWS credential); otherwise omit it for `apiKey` connectors. |
| `--credential-id <value>` | No | OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types), or a saved credential for an `apiKey` connector type that declares `auth.savedCredential` (such as Amazon S3 with a saved AWS credential); otherwise omit it for `apiKey` connectors. |
| `--api-key <value>` | No | Write-only API key for connector types whose `auth.mode` is `apiKey` (see connector types), or a personal access token for an OAuth connector that also accepts one, such as GitHub. Send it instead of `credentialId`. Pass a raw key, or a secret reference written as the whole value `{{SECRET_NAME}}`, which the server resolves; `$SECRET_NAME` is not a reference. |
| `--source-config <json\|@file>` | Yes | Connector-specific source selection and filtering configuration. (JSON, or @path / @- to read a file or stdin). |
| `--sync-interval-minutes <value>` | No | Scheduled synchronization interval in minutes; zero disables scheduling. Defaults to 1440. |
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2649,7 +2649,7 @@ sim knowledge connectors create <knowledgeBaseId> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--connector-type <value>` | Yes | Registered connector type. |
| `--credential-id <value>` | No | OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types), or, when this deployment enables it, a saved credential for an `apiKey` connector type that declares `auth.savedCredential` (such as Amazon S3 with a saved AWS credential); otherwise omit it for `apiKey` connectors. |
| `--credential-id <value>` | No | OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types), or a saved credential for an `apiKey` connector type that declares `auth.savedCredential` (such as Amazon S3 with a saved AWS credential); otherwise omit it for `apiKey` connectors. |
| `--api-key <value>` | No | Write-only API key for connector types whose `auth.mode` is `apiKey` (see connector types), or a personal access token for an OAuth connector that also accepts one, such as GitHub. Send it instead of `credentialId`. Pass a raw key, or a secret reference written as the whole value `{{SECRET_NAME}}`, which the server resolves; `$SECRET_NAME` is not a reference. |
| `--source-config <json\|@file>` | Yes | Connector-specific source selection and filtering configuration. (JSON, or @path / @- to read a file or stdin). |
| `--sync-interval-minutes <value>` | No | Scheduled synchronization interval in minutes; zero disables scheduling. Defaults to 1440. |
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/knowledgebase/connectors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ API-key and personal-access-token connections use the credential shown in their
| Azure DevOps | PAT with Project and Team Read, Wiki Read, Work Items Read, and Code Read. |
| GitLab | PAT with `read_api` and access to the selected project; permission-aware methods have additional requirements. |
| YouTube | YouTube Data API key from Google Cloud Console. |
| Amazon S3 | Secret Access Key; enter Access Key ID, region, and bucket as configuration fields. Where enabled, **Authentication → Saved AWS credential** uses a workspace AWS credential instead (access keys, an assumed IAM role, or a keyless role trusted through Sim OIDC); it signs only for Amazon S3 in a commercial or GovCloud AWS region (not China or isolated regions), so it has no Access Key ID or custom endpoint. Either way, the identity needs `s3:ListBucket` on the bucket and `s3:GetObject` on the synced objects. |
| Amazon S3 | Secret Access Key; enter Access Key ID, region, and bucket as configuration fields. **Authentication → Saved AWS credential** uses a workspace AWS credential instead (access keys, an assumed IAM role, or a keyless role trusted through Sim OIDC); it signs only for Amazon S3 in a commercial or GovCloud AWS region (not China or isolated regions), so it has no Access Key ID or custom endpoint. Either way, the identity needs `s3:ListBucket` on the bucket and `s3:GetObject` on the synced objects. |
| Sentry | Auth token with `project:read` and `event:read`. |
| PagerDuty | Create a [REST API key](https://support.pagerduty.com/main/docs/api-access-keys) under **Integrations → Developer Tools → API Access Keys**. |
| SFTP | Password or unencrypted private key, plus a required SHA-256 host-key fingerprint; enter host, port, username, and root path as configuration fields. |
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/openapi-v2-knowledge.json
Original file line number Diff line number Diff line change
Expand Up @@ -7155,7 +7155,7 @@
"description": "Registered connector type."
},
"credentialId": {
"description": "OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types), or, when this deployment enables it, a saved credential for an `apiKey` connector type that declares `auth.savedCredential` (such as Amazon S3 with a saved AWS credential); otherwise omit it for `apiKey` connectors.",
"description": "OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types), or a saved credential for an `apiKey` connector type that declares `auth.savedCredential` (such as Amazon S3 with a saved AWS credential); otherwise omit it for `apiKey` connectors.",
"type": "string",
"minLength": 1,
"maxLength": 255
Expand Down
4 changes: 0 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ import {
kbDocumentSortParams,
} from '@/app/workspace/[workspaceId]/knowledge/[id]/search-params'
import { canDeleteKnowledgeBase } from '@/app/workspace/[workspaceId]/knowledge/permissions'
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { BrandIcon } from '@/blocks/brand-icon'
Expand Down Expand Up @@ -421,7 +420,6 @@ export function KnowledgeBase({
const [showDocumentTagsModal, setShowDocumentTagsModal] = useState(false)
const [documentForTagsId, setDocumentForTagsId] = useState<string | null>(null)
const showAddConnectorModal = addConnectorType != null
const savedCredentialSetupEnabled = useFeatureFlag('knowledge-connector-saved-credentials')
const updateAddConnectorParam = useCallback(
(value: string | null) => {
void setAddConnectorType(value, { history: 'replace', scroll: false })
Expand Down Expand Up @@ -1474,7 +1472,6 @@ export function KnowledgeBase({
knowledgeBaseId={id}
initialConnectorType={addConnectorType || undefined}
isSearchIndex={knowledgeBase?.isSearchIndex}
savedCredentialSetupEnabled={savedCredentialSetupEnabled}
/>
)}

Expand Down Expand Up @@ -1516,7 +1513,6 @@ export function KnowledgeBase({
isLoading={isLoadingConnectors}
canEdit={userPermissions.canEdit}
className='mt-0'
savedCredentialSetupEnabled={savedCredentialSetupEnabled}
/>
</ChipModalBody>
</ChipModal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@ interface AddConnectorModalProps {
initialSyncIntervalMinutes?: number
onCreated?: (connectorType: string, connector: ConnectorData) => void
setupDraftKey?: string
/** Offers a key connector's saved-credential alternative, such as a saved AWS credential for S3. */
savedCredentialSetupEnabled?: boolean
}

type Step = 'select-type' | 'configure'
Expand All @@ -131,7 +129,6 @@ export function AddConnectorModal({
lockedAccessMode,
initialSyncIntervalMinutes = 1440,
onCreated,
savedCredentialSetupEnabled = false,
setupDraftKey,
scope: explicitScope,
}: AddConnectorModalProps) {
Expand Down Expand Up @@ -221,9 +218,7 @@ export function AddConnectorModal({
const isMembersMode = access.accessMode === 'members'
const apiKeyConfig = connectorConfig ? getConnectorApiKeyConfig(connectorConfig.auth) : undefined
const savedCredentialConfig =
savedCredentialSetupEnabled && connectorConfig?.auth.mode === 'apiKey'
? connectorConfig.auth.savedCredential
: undefined
connectorConfig?.auth.mode === 'apiKey' ? connectorConfig.auth.savedCredential : undefined
const isApiKeyMode =
connectorConfig?.auth.mode === 'apiKey'
? !savedCredentialConfig || useApiKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ interface ConnectorsSectionProps {
isLoading: boolean
canEdit: boolean
className?: string
savedCredentialSetupEnabled?: boolean
}

const STATUS_CONFIG = {
Expand All @@ -52,7 +51,6 @@ export function ConnectorsSection({
isLoading,
canEdit,
className,
savedCredentialSetupEnabled = false,
}: ConnectorsSectionProps) {
const scope = explicitScope ?? resourceScopeFromOwner({ workspaceId })
const [editingConnector, setEditingConnector] = useState<ConnectorData | null>(null)
Expand Down Expand Up @@ -86,7 +84,6 @@ export function ConnectorsSection({
knowledgeBaseId={knowledgeBaseId}
isSearchIndex={isSearchIndex}
connector={editingConnector}
savedCredentialSetupEnabled={savedCredentialSetupEnabled}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ interface EditConnectorModalProps {
knowledgeBaseId: string
isSearchIndex?: boolean
connector: ConnectorData
savedCredentialSetupEnabled?: boolean
}

export function EditConnectorModal({
Expand All @@ -36,7 +35,6 @@ export function EditConnectorModal({
isSearchIndex = false,
connector,
scope,
savedCredentialSetupEnabled = false,
}: EditConnectorModalProps) {
const [activeTab, setActiveTab] = useState('settings')
const [documentFilter, setDocumentFilter] = useState<ConnectorDocumentFilter>('active')
Expand All @@ -46,7 +44,6 @@ export function EditConnectorModal({
isSearchIndex,
connector,
onSaved: () => onOpenChange(false),
savedCredentialSetupEnabled,
})

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,6 @@ interface UseConnectorSettingsFormOptions {
connector: ConnectorData
onSaved: (connector: ConnectorData) => void
syncing?: boolean
/** Whether a key connector's saved credential may be replaced; the server refuses it while setup is off. */
savedCredentialSetupEnabled?: boolean
}

/** Editable connector settings shared by the source page and knowledge-base modal. */
Expand All @@ -139,7 +137,6 @@ export function useConnectorSettingsForm({
connector,
onSaved,
syncing = isConnectorSyncingOrPending(connector),
savedCredentialSetupEnabled = false,
}: UseConnectorSettingsFormOptions) {
const liveSearch = isSearchIndex
const connectorConfig = liveSearchSourceMeta(
Expand Down Expand Up @@ -297,7 +294,7 @@ export function useConnectorSettingsForm({
accessDirty && !accessModeChanged && isContentEngineAccessMode(access.accessMode)
/** Exposes credential selection for mode changes and administrator credential recovery. */
const needsWorkspaceCredential =
(usesSavedCredential && savedCredentialSetupEnabled) ||
usesSavedCredential ||
(connectorConfig?.auth.mode === 'oauth' &&
isContentEngineAccessMode(access.accessMode) &&
(persistedAccess.accessMode === 'members' ||
Expand Down
4 changes: 0 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
hasDesktopBackgroundExecutor,
isDesktopBackgroundExecutorAvailable,
} from '@/lib/desktop/executor/availability'
import { isConnectorSavedCredentialSetupEnabled } from '@/lib/knowledge/connectors/saved-credential-rollout'
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
import { resolveOrganizationEntryPath } from '@/lib/navigation/resolve-app-entry'
import { isProjectsEnabled } from '@/lib/projects/rollout.server'
Expand Down Expand Up @@ -136,7 +135,6 @@ async function WorkspaceContent({
desktopExecutorRegistered,
projectsEnabled,
simLinkOrganizationId,
connectorSavedCredentialsEnabled,
] = await Promise.all([
cookies(),
orgSettings,
Expand All @@ -156,7 +154,6 @@ async function WorkspaceContent({
hasDesktopBackgroundExecutor(session.user.id),
isProjectsEnabled(),
resolveSimLinkOrganization(principal),
isConnectorSavedCredentialSetupEnabled(),
prefetchWorkspaceAccess(queryClient, workspaceId, principal),
prefetchWorkspaceForkAvailability(queryClient, workspaceId, principal, hostContext),
])
Expand All @@ -172,7 +169,6 @@ async function WorkspaceContent({
'mothership-model-selector': modelSelectorEnabled,
'mothership-plan-mode': planModeEnabled,
projects: projectsEnabled,
'knowledge-connector-saved-credentials': connectorSavedCredentialsEnabled,
}}
>
<WorkspaceHostProvider workspaceId={workspaceId} initialContext={hostContext}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export interface WorkspaceFeatureFlags {
'mothership-plan-mode': boolean
'mothership-memory-spaces'?: boolean
projects?: boolean
'knowledge-connector-saved-credentials'?: boolean
}

const FeatureFlagsContext = createContext<WorkspaceFeatureFlags | null>(null)
Expand Down
19 changes: 5 additions & 14 deletions apps/sim/connectors/aws-session.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { type AwsSigningKeys, awsSigningKeys } from '@/lib/internal/aws/signing-keys'

/**
* The AWS keys a saved AWS credential resolved to for one connector run.
*
Expand All @@ -8,24 +10,13 @@
* from one place and never from tool or user input. Server-only state: the
* slot is never persisted or logged.
*/
export interface ConnectorAwsSession {
accessKeyId: string
secretAccessKey: string
/** Present for STS sessions (assumed and keyless roles); absent for saved long-lived keys. */
sessionToken?: string
}
export type ConnectorAwsSession = AwsSigningKeys

const AWS_SESSION_CONTEXT_KEY = 'credentialAwsSession'

/** The `syncContext` entries that carry a resolved AWS session to a connector. */
export function awsSessionSyncContext(session: ConnectorAwsSession): Record<string, unknown> {
return {
[AWS_SESSION_CONTEXT_KEY]: {
accessKeyId: session.accessKeyId,
secretAccessKey: session.secretAccessKey,
...(session.sessionToken ? { sessionToken: session.sessionToken } : {}),
},
}
return { [AWS_SESSION_CONTEXT_KEY]: awsSigningKeys(session) }
}

/** The AWS session seeded into this run, or undefined when the connector signs with inline keys. */
Expand All @@ -40,5 +31,5 @@ export function readAwsSession(
if (sessionToken !== undefined && (typeof sessionToken !== 'string' || !sessionToken)) {
return undefined
}
return { accessKeyId, secretAccessKey, ...(sessionToken ? { sessionToken } : {}) }
return awsSigningKeys({ accessKeyId, secretAccessKey, sessionToken })
}
29 changes: 29 additions & 0 deletions apps/sim/executor/handlers/agent/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,35 @@ describe('AgentBlockHandler', () => {
expect(mockResolveBedrockCredentialSession).toHaveBeenCalledTimes(1)
})

it('resolves the AWS credential once for a fallback on the same provider', async () => {
const session = {
credentialId: 'aws-cred',
accessKeyId: 'ASIASESSIONKEY000001',
secretAccessKey: 'session-secret',
sessionToken: 'session-token',
}
mockResolveBedrockCredentialSession.mockResolvedValueOnce(session)
mockExecuteProviderRequest
.mockRejectedValueOnce(new Error('overloaded'))
.mockResolvedValueOnce(providerResponse('bedrock/claude-fallback'))

await handler.execute(mockContext, mockBlock, {
...baseInputs,
model: 'bedrock/claude',
bedrockAuthMode: 'credential',
bedrockCredential: 'aws-cred',
fallbackModels: [{ model: 'bedrock/claude-fallback' }],
})

expect(mockResolveBedrockCredentialSession).toHaveBeenCalledTimes(1)
expect(mockExecuteProviderRequest).toHaveBeenCalledTimes(2)
const [[primaryProvider, primaryRequest], [fallbackProvider, fallbackRequest]] =
mockExecuteProviderRequest.mock.calls
expect([primaryProvider, fallbackProvider]).toEqual(['bedrock', 'bedrock'])
expect(primaryRequest.bedrockSession).toEqual(session)
expect(fallbackRequest.bedrockSession).toEqual(session)
})

it('re-resolves tuning for the fallback: row value wins, caps clamp, undeclared values drop', async () => {
const fallbackCap = getModelCapabilities('gpt-5.4-mini')?.maxOutputTokens
expect(fallbackCap).toEqual(expect.any(Number))
Expand Down
26 changes: 20 additions & 6 deletions apps/sim/executor/handlers/agent/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { collectBlockData } from '@/executor/utils/block-data'
import { stringifyJSON } from '@/executor/utils/json'
import {
BLOCK_MODEL_CREDENTIAL_FIELDS,
type BlockModelCredentials,
resolveBlockModelCredentials,
} from '@/executor/utils/model-credentials'
import {
Expand Down Expand Up @@ -2610,6 +2611,11 @@ export class AgentBlockHandler implements BlockHandler {
* blacklisted, not permitted, or cannot take the attachments is skipped
* rather than counted as a failed try.
*
* The block's saved Vertex or Bedrock credential resolves at most once per
* call and is shared by every candidate on the primary's provider; it is
* awaited inside the attempt, so a failure fails those attempts and other
* providers' fallbacks still run. It never reaches another provider.
*
* A streaming candidate is accepted only once its first chunk has arrived
* (`primeStreamingExecution`), so a startup failure inside the stream still
* falls through; that wait is skipped when no candidate follows, which keeps
Expand All @@ -2635,6 +2641,7 @@ export class AgentBlockHandler implements BlockHandler {
let resultRegistry = config.resultRegistry
const failedModels: string[] = []
let lastError: unknown
let savedCredentials: Promise<BlockModelCredentials> | undefined
let lastErrorRegistries:
| {
error: ResolvedSecretTraceRegistry | undefined
Expand Down Expand Up @@ -2754,6 +2761,16 @@ export class AgentBlockHandler implements BlockHandler {
streaming: config.streaming,
})

const onPrimaryProvider = candidateProviderId === config.primaryProviderId
if (onPrimaryProvider) {
savedCredentials ??= resolveBlockModelCredentials(
ctx,
config.primaryProviderId,
config.modelInputs,
'vertex-agent'
)
}

try {
let result = await this.executeProviderRequest(
ctx,
Expand All @@ -2762,6 +2779,7 @@ export class AgentBlockHandler implements BlockHandler {
config.responseFormat,
resultRegistry,
config.providerErrorRegistry,
onPrimaryProvider ? savedCredentials : undefined,
config.agentConversation
)
if ((hasNext || config.retryPrimaryOnStreamStart) && this.isStreamingExecution(result)) {
Expand Down Expand Up @@ -2983,18 +3001,14 @@ export class AgentBlockHandler implements BlockHandler {
responseFormat: any,
modelRuntimeRegistry: ResolvedSecretTraceRegistry | undefined,
providerErrorRegistry: ResolvedSecretTraceRegistry | undefined,
savedCredentials: Promise<BlockModelCredentials> | undefined,
agentConversation?: AgentTurnSession
): Promise<BlockOutput | StreamingExecution> {
const providerId = providerRequest.provider
const model = providerRequest.model

try {
const credentials = await resolveBlockModelCredentials(
ctx,
providerId,
providerRequest,
'vertex-agent'
)
const credentials: BlockModelCredentials = (await savedCredentials) ?? {}

const { blockData, blockNameMapping } = collectBlockData(ctx)
const agentMemoryRetrieval = agentConversation?.memoryId
Expand Down
Loading
Loading