Skip to content

Commit ccdb679

Browse files
committed
improvement(managed-agents): select a Claude Platform credential instead of BYOK
- register Claude Platform as a token-paste service-account credential (descriptor + validator) - add no-OAuth OAUTH_PROVIDERS entry; generalize the shared credential picker with a 'service-account' kind - block: oauth-input credential picker + dependsOn dropdowns; list route resolves the key server-side (audit-logged) - run via directExecution with the executor-injected key; drop the internal run route - remove the interim claude-platform BYOK provider
1 parent f88b08d commit ccdb679

18 files changed

Lines changed: 420 additions & 302 deletions

File tree

apps/sim/app/api/tools/managed-agent/list/route.ts

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1+
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
12
import { createLogger } from '@sim/logger'
23
import { getErrorMessage } from '@sim/utils/errors'
34
import { type NextRequest, NextResponse } from 'next/server'
45
import {
56
listManagedAgentOptionsContract,
6-
MANAGED_AGENT_BYOK_PROVIDER,
77
type ManagedAgentOption,
88
type ManagedAgentResource,
99
} from '@/lib/api/contracts/managed-agents'
1010
import { parseRequest } from '@/lib/api/server'
11-
import { getBYOKKey } from '@/lib/api-key/byok'
12-
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
11+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
1312
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
13+
import { CLAUDE_PLATFORM_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/credentials/token-service-accounts/descriptors'
1414
import { AGENT_MEMORY_BETA, managedAgentsList } from '@/lib/managed-agents/session-client'
15-
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
15+
import { captureServerEvent } from '@/lib/posthog/server'
16+
import { resolveOAuthAccountId, resolveServiceAccountToken } from '@/app/api/auth/oauth/utils'
1617

1718
export const dynamic = 'force-dynamic'
1819

@@ -55,36 +56,79 @@ function toOption(
5556

5657
/**
5758
* Resolves Managed Agent dropdown options (agents / environments / vaults /
58-
* memory stores) for the block editor. The workspace's Claude Platform BYOK
59-
* key is decrypted server-side and never crosses the client boundary — the
60-
* browser only ever receives `{ id, label }` options.
59+
* memory stores) for the block editor against a selected Claude Platform
60+
* credential. The credential's API key is decrypted server-side and never
61+
* crosses the client boundary — the browser only ever receives `{ id, label }`
62+
* options.
6163
*/
6264
export const GET = withRouteHandler(async (request: NextRequest) => {
63-
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
64-
if (!auth.success || !auth.userId) {
65-
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
66-
}
67-
6865
const parsed = await parseRequest(listManagedAgentOptionsContract, request, {})
6966
if (!parsed.success) return parsed.response
70-
const { workspaceId, resource } = parsed.data.query
67+
const { credentialId, resource } = parsed.data.query
68+
69+
// Authenticates the caller AND verifies they may use this credential.
70+
const authz = await authorizeCredentialUse(request, {
71+
credentialId,
72+
requireWorkflowIdForInternal: false,
73+
})
74+
if (!authz.ok) {
75+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
76+
}
7177

72-
const permission = await getUserEntityPermissions(auth.userId, 'workspace', workspaceId)
73-
if (!permission) {
74-
return NextResponse.json({ error: 'Workspace access denied' }, { status: 403 })
78+
const resolved = await resolveOAuthAccountId(credentialId)
79+
if (
80+
resolved?.credentialType !== 'service_account' ||
81+
resolved.providerId !== CLAUDE_PLATFORM_SERVICE_ACCOUNT_PROVIDER_ID
82+
) {
83+
return NextResponse.json({ error: 'Not a Claude Platform credential' }, { status: 400 })
7584
}
7685

77-
const byok = await getBYOKKey(workspaceId, MANAGED_AGENT_BYOK_PROVIDER)
78-
if (!byok) {
79-
// No Claude Platform key linked yet — return an empty list so the
80-
// dropdown renders cleanly rather than erroring.
86+
let apiKey: string
87+
try {
88+
const token = await resolveServiceAccountToken(
89+
credentialId,
90+
CLAUDE_PLATFORM_SERVICE_ACCOUNT_PROVIDER_ID
91+
)
92+
apiKey = token.accessToken
93+
} catch (error) {
94+
logger.warn('Failed to resolve Claude Platform credential', { error: getErrorMessage(error) })
8195
return NextResponse.json({ options: [] })
8296
}
8397

98+
// Decrypting and using the credential's key is a credential access — record
99+
// it, mirroring the OAuth token route's service-account audit trail.
100+
const actorId = authz.requesterUserId
101+
const workspaceId = resolved.workspaceId ?? authz.workspaceId ?? null
102+
if (actorId) {
103+
recordAudit({
104+
workspaceId,
105+
actorId,
106+
action: AuditAction.CREDENTIAL_ACCESSED,
107+
resourceType: AuditResourceType.CREDENTIAL,
108+
resourceId: credentialId,
109+
description: 'Accessed Claude Platform credential to list Managed Agent resources',
110+
metadata: {
111+
provider: CLAUDE_PLATFORM_SERVICE_ACCOUNT_PROVIDER_ID,
112+
credentialType: 'service_account',
113+
},
114+
request,
115+
})
116+
captureServerEvent(
117+
actorId,
118+
'credential_used',
119+
{
120+
credential_type: 'service_account',
121+
provider_id: CLAUDE_PLATFORM_SERVICE_ACCOUNT_PROVIDER_ID,
122+
...(workspaceId ? { workspace_id: workspaceId } : {}),
123+
},
124+
workspaceId ? { groups: { workspace: workspaceId } } : undefined
125+
)
126+
}
127+
84128
try {
85129
const endpoint = RESOURCE_ENDPOINTS[resource]
86130
const rows = await managedAgentsList<AnthropicListRow>({
87-
apiKey: byok.apiKey,
131+
apiKey,
88132
path: endpoint.path,
89133
beta: endpoint.beta,
90134
signal: request.signal,
@@ -96,11 +140,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
96140
} catch (error) {
97141
// Some beta workspaces may not expose every resource (e.g. vaults). Log
98142
// and degrade to an empty list rather than breaking the editor.
99-
logger.warn('Managed agent list proxy failed', {
100-
workspaceId,
101-
resource,
102-
error: getErrorMessage(error),
103-
})
143+
logger.warn('Managed agent list proxy failed', { resource, error: getErrorMessage(error) })
104144
return NextResponse.json({ options: [] })
105145
}
106146
})

apps/sim/app/api/tools/managed-agent/run/route.ts

Lines changed: 0 additions & 125 deletions
This file was deleted.

apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
AnthropicIcon,
77
BasetenIcon,
88
BrandfetchIcon,
9-
ClaudeIcon,
109
ContextDevIcon,
1110
DatagmaIcon,
1211
DropcontactIcon,
@@ -67,13 +66,6 @@ const PROVIDERS: (BYOKManagerProvider & { id: BYOKProviderId })[] = [
6766
description: 'LLM calls',
6867
placeholder: 'sk-ant-...',
6968
},
70-
{
71-
id: 'claude-platform',
72-
name: 'Claude Platform',
73-
icon: ClaudeIcon,
74-
description: 'Managed Agents block',
75-
placeholder: 'sk-ant-...',
76-
},
7769
{
7870
id: 'google',
7971
name: 'Google',
@@ -311,7 +303,6 @@ const PROVIDER_SECTIONS: BYOKProviderSection[] = [
311303
ids: [
312304
'openai',
313305
'anthropic',
314-
'claude-platform',
315306
'google',
316307
'mistral',
317308
'xai',

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import {
1212
type OAuthProvider,
1313
parseProvider,
1414
} from '@/lib/oauth'
15-
import { getMissingRequiredScopes } from '@/lib/oauth/utils'
15+
import { getMissingRequiredScopes, getServiceByProviderAndId } from '@/lib/oauth/utils'
1616
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
17+
import {
18+
ConnectServiceAccountModal,
19+
type ServiceAccountProviderId,
20+
} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal'
1721
import { ConnectSlackBotModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal'
1822
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
1923
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
@@ -50,6 +54,7 @@ export function CredentialSelector({
5054
const [showConnectModal, setShowConnectModal] = useState(false)
5155
const [showOAuthModal, setShowOAuthModal] = useState(false)
5256
const [showSlackBotModal, setShowSlackBotModal] = useState(false)
57+
const [showServiceAccountModal, setShowServiceAccountModal] = useState(false)
5358
const [editingValue, setEditingValue] = useState('')
5459
const [isEditing, setIsEditing] = useState(false)
5560
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
@@ -101,16 +106,23 @@ export function CredentialSelector({
101106
const credentialKind = subBlock.credentialKind
102107

103108
const credentials = useMemo(() => {
104-
// A custom-bot picker lists only the reusable Slack bot credentials
105-
// (service-account type), including in trigger mode.
106-
if (credentialKind === 'custom-bot') {
109+
// A custom-bot or service-account picker lists only the reusable
110+
// service-account credentials, including in trigger mode.
111+
if (credentialKind === 'custom-bot' || credentialKind === 'service-account') {
107112
return rawCredentials.filter((cred) => cred.type === 'service_account')
108113
}
109114
return isTriggerMode && !subBlock.allowServiceAccounts
110115
? rawCredentials.filter((cred) => cred.type !== 'service_account')
111116
: rawCredentials
112117
}, [rawCredentials, isTriggerMode, credentialKind, subBlock.allowServiceAccounts])
113118

119+
// Resolved service-account provider metadata for the token-paste connect
120+
// modal (only used when `credentialKind === 'service-account'`).
121+
const serviceAccountService = useMemo(
122+
() => (serviceId ? getServiceByProviderAndId(provider, serviceId) : undefined),
123+
[provider, serviceId]
124+
)
125+
114126
const selectedCredential = useMemo(
115127
() => credentials.find((cred) => cred.id === selectedId),
116128
[credentials, selectedId]
@@ -189,6 +201,10 @@ export function CredentialSelector({
189201
setShowSlackBotModal(true)
190202
return
191203
}
204+
if (credentialKind === 'service-account') {
205+
setShowServiceAccountModal(true)
206+
return
207+
}
192208
setShowConnectModal(true)
193209
}, [credentialKind])
194210

@@ -235,9 +251,13 @@ export function CredentialSelector({
235251
? credentials.length > 0
236252
? 'Connect another custom bot'
237253
: 'Set up a custom bot'
238-
: credentials.length > 0
239-
? `Connect another ${getProviderName(provider)} account`
240-
: `Connect ${getProviderName(provider)} account`,
254+
: credentialKind === 'service-account'
255+
? credentials.length > 0
256+
? `Add another ${getProviderName(provider)} key`
257+
: `Add ${getProviderName(provider)} key`
258+
: credentials.length > 0
259+
? `Connect another ${getProviderName(provider)} account`
260+
: `Connect ${getProviderName(provider)} account`,
241261
value: '__connect_account__',
242262
iconElement: <ExternalLink className='size-3' />,
243263
})
@@ -407,6 +427,22 @@ export function CredentialSelector({
407427
}}
408428
/>
409429
)}
430+
431+
{showServiceAccountModal && serviceAccountService?.serviceAccountProviderId && (
432+
<ConnectServiceAccountModal
433+
open={showServiceAccountModal}
434+
onOpenChange={(open) => {
435+
setShowServiceAccountModal(open)
436+
if (!open) refetchCredentials()
437+
}}
438+
workspaceId={workspaceId}
439+
serviceAccountProviderId={
440+
serviceAccountService.serviceAccountProviderId as ServiceAccountProviderId
441+
}
442+
serviceName={serviceAccountService.name}
443+
serviceIcon={serviceAccountService.icon}
444+
/>
445+
)}
410446
</div>
411447
)
412448
}

0 commit comments

Comments
 (0)