Skip to content

Commit c8b9bd7

Browse files
committed
feat(managed-agents): environment-type selector; hide cloud-only fields on self-hosted
Collapse what #5769 split into two blocks into one, natively: - Add an Environment type selector (Cloud / Self-hosted) that filters the environment list to the matching type and gates cloud-only fields - Memory store, memory access/instructions, and files are cloud-only (self- hosted rejects the resources[] attach — verified live, 400) and are now hidden on self-hosted instead of silently dropped. A self-hosted worker that uses a memory store reads its id from a Metadata key the author sets explicitly - Expose each environment's config.type on the list options so the picker can filter by mode; pass the selected type as a routing hint (server still re- resolves the authoritative type via getEnvironmentType)
1 parent 67c8d26 commit c8b9bd7

9 files changed

Lines changed: 99 additions & 42 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function toOption(
4646
if (resource === 'environments') {
4747
const type = row.config?.type
4848
const suffix = type ? ` (${type})` : ''
49-
return { id: row.id, label: `${name || row.id}${suffix}` }
49+
return { id: row.id, label: `${name || row.id}${suffix}`, ...(type ? { type } : {}) }
5050
}
5151
if (resource === 'vaults') {
5252
return { id: row.id, label: name || row.id }

apps/sim/blocks/blocks/managed_agent.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import { AuthMode, IntegrationType } from '@/blocks/types'
1111
/**
1212
* Claude Managed Agents block.
1313
*
14-
* Invokes a Claude Platform Managed Agent (cloud or self-hosted) as a
15-
* workflow node and returns the assistant's final text. Memory and metadata
16-
* route automatically, so one block covers both cloud and self-hosted
17-
* environments.
14+
* Invokes a Claude Platform Managed Agent (cloud or self-hosted) as a workflow
15+
* node and returns the assistant's final text. One block covers both models:
16+
* the Environment type selector filters the environment list and shows only the
17+
* fields that apply — memory stores and files are cloud-only (self-hosted
18+
* rejects the `resources` attach), while session metadata works for both.
1819
*
1920
* Authentication is a selectable Claude Platform credential (an Anthropic
2021
* workspace API key). The credential's key is resolved server-side at run
@@ -43,6 +44,19 @@ export const ManagedAgentBlock: BlockConfig = {
4344
required: true,
4445
placeholder: 'Select a Claude Platform credential',
4546
},
47+
{
48+
id: 'environmentType',
49+
title: 'Environment type',
50+
type: 'dropdown',
51+
required: true,
52+
options: [
53+
{ label: 'Cloud', id: 'cloud' },
54+
{ label: 'Self-hosted', id: 'self_hosted' },
55+
],
56+
value: () => 'cloud',
57+
description:
58+
'Self-hosted environments run on your own infrastructure and route memory via session metadata; file attachments are cloud-only.',
59+
},
4660
{
4761
id: 'agent',
4862
title: 'Agent',
@@ -62,7 +76,7 @@ export const ManagedAgentBlock: BlockConfig = {
6276
placeholder: 'Select an environment…',
6377
commandSearchable: true,
6478
options: [],
65-
dependsOn: ['credential'],
79+
dependsOn: ['credential', 'environmentType'],
6680
fetchOptions: fetchManagedAgentEnvironmentOptions,
6781
},
6882
{
@@ -104,6 +118,10 @@ export const ManagedAgentBlock: BlockConfig = {
104118
commandSearchable: true,
105119
options: [],
106120
dependsOn: ['credential'],
121+
// Cloud only: memory stores attach as `resources[]`, which self-hosted
122+
// rejects. A self-hosted worker that uses a store reads its id from a
123+
// Metadata key the author sets explicitly.
124+
condition: { field: 'environmentType', value: 'cloud' },
107125
fetchOptions: fetchManagedAgentMemoryStoreOptions,
108126
},
109127
{
@@ -117,7 +135,12 @@ export const ManagedAgentBlock: BlockConfig = {
117135
{ label: 'Read only', id: 'read_only' },
118136
],
119137
value: () => 'read_write',
120-
condition: { field: 'memoryStoreId', value: '', not: true },
138+
condition: {
139+
field: 'memoryStoreId',
140+
value: '',
141+
not: true,
142+
and: { field: 'environmentType', value: 'cloud' },
143+
},
121144
description: 'read_write pushes changes back on session exit; read_only never writes.',
122145
},
123146
{
@@ -127,7 +150,14 @@ export const ManagedAgentBlock: BlockConfig = {
127150
required: false,
128151
mode: 'advanced',
129152
placeholder: 'Optional — how the agent should use this memory store',
130-
condition: { field: 'memoryStoreId', value: '', not: true },
153+
// Cloud only: instructions are a `resources[]` memory-attach concept the
154+
// API renders into the system prompt; self-hosted has no resource attach.
155+
condition: {
156+
field: 'memoryStoreId',
157+
value: '',
158+
not: true,
159+
and: { field: 'environmentType', value: 'cloud' },
160+
},
131161
description: 'Per-attachment guidance rendered into the memory section of the system prompt.',
132162
},
133163
{
@@ -136,9 +166,11 @@ export const ManagedAgentBlock: BlockConfig = {
136166
type: 'table',
137167
required: false,
138168
mode: 'advanced',
169+
// Cloud only: files attach as `resources[]`, which self-hosted rejects.
170+
condition: { field: 'environmentType', value: 'cloud' },
139171
columns: ['File ID', 'Mount path'],
140172
description:
141-
'Files-API file ids (file_...) to attach as file resources (cloud environments). Mount path is optional.',
173+
'Files-API file ids (file_...) to attach as file resources. Mount path is optional.',
142174
},
143175
{
144176
id: 'sessionParameters',
@@ -156,6 +188,11 @@ export const ManagedAgentBlock: BlockConfig = {
156188
},
157189
inputs: {
158190
credential: { type: 'string', description: 'Claude Platform credential id.' },
191+
environmentType: {
192+
type: 'string',
193+
description:
194+
"Environment execution model — 'cloud' or 'self_hosted'. Filters the environment picker and gates cloud-only fields; the actual type is re-resolved server-side for routing.",
195+
},
159196
agent: { type: 'string', description: 'Managed-agent id inside the linked Claude workspace.' },
160197
environment: {
161198
type: 'string',

apps/sim/lib/api/contracts/managed-agents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contra
1212
export const managedAgentOptionSchema = z.object({
1313
id: z.string(),
1414
label: z.string(),
15+
/** Environment execution model — only set for the `environments` resource, used to filter by mode. */
16+
type: z.enum(['cloud', 'self_hosted']).optional(),
1517
})
1618
export type ManagedAgentOption = z.output<typeof managedAgentOptionSchema>
1719

apps/sim/lib/managed-agents/run-session.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type AnthropicSessionEvent,
77
type CreateSessionInput,
88
createSession,
9+
type EnvironmentType,
910
getEnvironmentType,
1011
getSession,
1112
interruptSession,
@@ -40,6 +41,8 @@ export interface RunManagedAgentInput {
4041
apiKey: string
4142
agentId: string
4243
environmentId: string
44+
/** Env-type hint from the block; used only if server-side resolution fails. */
45+
environmentType?: EnvironmentType
4346
userMessage: string
4447
title?: string
4548
vaultIds?: string[]
@@ -106,12 +109,11 @@ export async function runManagedAgentSession(
106109

107110
// Resolve the environment type up front so the payload routes correctly:
108111
// self-hosted environments reject `resources`, so memory must go via metadata
109-
// there. Best-effort — an undefined result falls back to cloud behavior.
110-
const environmentType = await getEnvironmentType({
111-
apiKey,
112-
environmentId: input.environmentId,
113-
signal,
114-
})
112+
// there. The authoritative source is the API; the block's hint is only a
113+
// fallback if that lookup fails.
114+
const environmentType =
115+
(await getEnvironmentType({ apiKey, environmentId: input.environmentId, signal })) ??
116+
input.environmentType
115117

116118
const createInput: CreateSessionInput = {
117119
apiKey,

apps/sim/lib/managed-agents/session-client.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,26 +124,26 @@ describe('buildSessionCreatePayload — metadata', () => {
124124
})
125125

126126
describe('buildSessionCreatePayload — self-hosted routing', () => {
127-
it('routes memory to metadata and never sends resources (self-hosted rejects resources)', () => {
127+
it('never sends resources on self-hosted and does not auto-route memory (no native support)', () => {
128128
const payload = buildSessionCreatePayload({
129129
...BASE,
130130
environmentType: 'self_hosted',
131131
memoryStoreId: 'memstore_01',
132132
memoryAccess: 'read_only',
133+
memoryInstructions: 'use it',
134+
files: [{ fileId: 'file_1' }],
133135
sessionParameters: { SOURCE_TYPE: 'git' },
134136
})
135137
expect(payload.resources).toBeUndefined()
136-
expect(payload.metadata).toEqual({
137-
SOURCE_TYPE: 'git',
138-
memory_store_ids: 'memstore_01',
139-
memory_access: 'read_only',
140-
})
138+
// Only the author's explicit metadata is forwarded — memory is NOT injected.
139+
expect(payload.metadata).toEqual({ SOURCE_TYPE: 'git' })
141140
})
142141

143-
it('drops file attachments on self-hosted (not supported as resources)', () => {
142+
it('sends no metadata on self-hosted when the author set none', () => {
144143
const payload = buildSessionCreatePayload({
145144
...BASE,
146145
environmentType: 'self_hosted',
146+
memoryStoreId: 'memstore_01',
147147
files: [{ fileId: 'file_1' }],
148148
})
149149
expect(payload.resources).toBeUndefined()

apps/sim/lib/managed-agents/session-client.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,11 @@ function managedAgentsHeaders(
118118
*
119119
* Cloud environments attach memory stores and files via the `resources[]`
120120
* array. Self-hosted environments REJECT `resources` (a documented 400 —
121-
* "resources are not supported with self-hosted environments"), so there the
122-
* memory store is surfaced to the worker via `metadata.memory_store_ids` /
123-
* `metadata.memory_access` and files are dropped. Session parameters always go
124-
* on `metadata` for both.
121+
* "resources are not supported with self-hosted environments") and have no
122+
* native memory/file attach, so those are omitted there; the block hides the
123+
* fields accordingly. Session parameters always go on `metadata` for both — a
124+
* self-hosted worker that consumes a memory store reads it from a metadata key
125+
* the author sets explicitly.
125126
*/
126127
export function buildSessionCreatePayload(input: CreateSessionInput): Record<string, unknown> {
127128
const payload: Record<string, unknown> = {
@@ -131,16 +132,8 @@ export function buildSessionCreatePayload(input: CreateSessionInput): Record<str
131132
if (input.title) payload.title = input.title
132133
if (input.vaultIds && input.vaultIds.length > 0) payload.vault_ids = input.vaultIds
133134

134-
const metadata: Record<string, string> = { ...(input.sessionParameters ?? {}) }
135-
136-
if (input.environmentType === 'self_hosted') {
137-
// No `resources` on self-hosted — route memory through metadata (the
138-
// worker consumes these keys) and drop file attachments.
139-
if (input.memoryStoreId) {
140-
metadata.memory_store_ids = input.memoryStoreId
141-
metadata.memory_access = input.memoryAccess ?? 'read_write'
142-
}
143-
} else {
135+
// `resources` (memory stores + files) are cloud-only. Self-hosted rejects them.
136+
if (input.environmentType !== 'self_hosted') {
144137
const resources: Array<Record<string, unknown>> = []
145138
if (input.memoryStoreId) {
146139
const memory: Record<string, unknown> = {
@@ -162,7 +155,9 @@ export function buildSessionCreatePayload(input: CreateSessionInput): Record<str
162155
if (resources.length > 0) payload.resources = resources
163156
}
164157

165-
if (Object.keys(metadata).length > 0) payload.metadata = metadata
158+
if (input.sessionParameters && Object.keys(input.sessionParameters).length > 0) {
159+
payload.metadata = { ...input.sessionParameters }
160+
}
166161
return payload
167162
}
168163

apps/sim/lib/managed-agents/subblock-options.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1414
* the browser.
1515
*/
1616

17-
function credentialIdForBlock(blockId: string): string | null {
17+
function readSubBlockValue(blockId: string, key: string): string | null {
1818
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
1919
if (!activeWorkflowId) return null
20-
const value = useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[blockId]?.credential
20+
const value = useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[blockId]?.[key]
2121
return typeof value === 'string' && value.length > 0 ? value : null
2222
}
2323

2424
async function fetchOptions(
2525
blockId: string,
2626
resource: ManagedAgentResource
2727
): Promise<ManagedAgentOption[]> {
28-
const credentialId = credentialIdForBlock(blockId)
28+
const credentialId = readSubBlockValue(blockId, 'credential')
2929
if (!credentialId) return []
3030
try {
3131
const { options } = await requestJson(listManagedAgentOptionsContract, {
@@ -41,10 +41,16 @@ export function fetchManagedAgentAgentOptions(blockId: string): Promise<ManagedA
4141
return fetchOptions(blockId, 'agents')
4242
}
4343

44-
export function fetchManagedAgentEnvironmentOptions(
44+
export async function fetchManagedAgentEnvironmentOptions(
4545
blockId: string
4646
): Promise<ManagedAgentOption[]> {
47-
return fetchOptions(blockId, 'environments')
47+
const options = await fetchOptions(blockId, 'environments')
48+
// Filter to the selected environment type so cloud/self-hosted stay separate
49+
// (self-hosted rejects `resources`, so the two modes expose different fields).
50+
// Options with an unknown type are kept as a safety net.
51+
const mode = readSubBlockValue(blockId, 'environmentType')
52+
if (mode !== 'cloud' && mode !== 'self_hosted') return options
53+
return options.filter((option) => option.type === undefined || option.type === mode)
4854
}
4955

5056
export function fetchManagedAgentVaultOptions(blockId: string): Promise<ManagedAgentOption[]> {

apps/sim/tools/managed_agent/run_session.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ export const managedAgentRunSessionTool: ToolConfig<
5858
visibility: 'user-only',
5959
description: 'Environment id inside the linked Claude workspace.',
6060
},
61+
environmentType: {
62+
type: 'string',
63+
required: false,
64+
visibility: 'user-only',
65+
description:
66+
"Environment execution model hint ('cloud' | 'self_hosted'); the actual type is re-resolved server-side for routing.",
67+
},
6168
userMessage: {
6269
type: 'string',
6370
required: true,
@@ -158,11 +165,17 @@ export const managedAgentRunSessionTool: ToolConfig<
158165
const workflowId = params._context?.workflowId?.trim()
159166
const title = workflowId ? `Sim workflow ${workflowId}` : undefined
160167

168+
const environmentType =
169+
params.environmentType === 'self_hosted' || params.environmentType === 'cloud'
170+
? params.environmentType
171+
: undefined
172+
161173
const result = await runManagedAgentSession({
162174
apiKey,
163175
agentId,
164176
environmentId,
165177
userMessage: (params.userMessage ?? '').toString(),
178+
...(environmentType ? { environmentType } : {}),
166179
...(title ? { title } : {}),
167180
...(vaultIds.length > 0 ? { vaultIds } : {}),
168181
...(memoryStoreId ? { memoryStoreId } : {}),

apps/sim/tools/managed_agent/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface ManagedAgentRunSessionParams {
1515
agent: string
1616
/** Environment id from the linked Claude workspace. */
1717
environment: string
18+
/** Env-type hint ('cloud' | 'self_hosted') from the block; re-resolved server-side. */
19+
environmentType?: string
1820
/** The user's turn as plain text. Resolved by the executor. */
1921
userMessage: string
2022
/** Zero or more vault ids for MCP auth (array, json string, or comma-list). */

0 commit comments

Comments
 (0)