Skip to content

Commit 4a9549c

Browse files
committed
Merge branch 'feat/func-cli-resolver' of github.com:simstudioai/sim into feat/func-cli-resolver
2 parents d2a45d0 + 36fa8e3 commit 4a9549c

7 files changed

Lines changed: 94 additions & 53 deletions

File tree

apps/sim/blocks/blocks/function.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ try {
164164
timeout: { type: 'number', description: 'Execution timeout' },
165165
sandboxId: {
166166
type: 'string',
167-
description: 'Sim sandbox providing dependencies, system packages, and managed CLIs',
167+
description:
168+
'Sim sandbox providing dependencies, system packages, and managed CLIs. Selecting or clearing it requires an active Max or Enterprise plan.',
168169
},
169170
secretScope: { type: 'string', description: 'Secret access mode: all or selected' },
170171
mountedSecrets: {

apps/sim/lib/copilot/sim-sandbox-projection.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import type { CopilotSanitizationOptions } from '@/lib/workflows/sanitization/json-sanitizer'
1+
import { SIM_SANDBOXES_ENTITLEMENT } from '@/lib/copilot/entitlements'
22

3-
export const HIDE_SIM_SANDBOX_INPUTS: CopilotSanitizationOptions = {
4-
hiddenInputIdsByBlockType: new Map([['function', new Set(['sandboxId'])]]),
5-
}
3+
export const RESTRICTED_SIM_SANDBOX_INPUTS = new Map([
4+
[
5+
'sandboxId',
6+
{
7+
requiredEntitlement: SIM_SANDBOXES_ENTITLEMENT,
8+
reason:
9+
'Selecting or clearing a Sim sandbox requires an active Max or Enterprise plan. Preserve any existing selection unless the user upgrades.',
10+
},
11+
],
12+
])
613

714
/** Whether an edit_workflow operation tries to set or clear Function sandboxId. */
815
export function operationsReferenceSimSandbox(

apps/sim/lib/copilot/tools/handlers/workflow/queries.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { toError } from '@sim/utils/errors'
22
import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks'
3-
import { hasWorkspaceSandboxAccess } from '@/lib/billing/core/subscription'
43
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types'
5-
import { HIDE_SIM_SANDBOX_INPUTS } from '@/lib/copilot/sim-sandbox-projection'
64
import { formatNormalizedWorkflowForCopilot } from '@/lib/copilot/tools/shared/workflow-utils'
75
import { mcpService } from '@/lib/mcp/service'
86
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace'
@@ -505,24 +503,16 @@ export async function executeGetDeployedWorkflowState(
505503
return { success: false, error: 'workflowId is required' }
506504
}
507505

508-
const { workflow: workflowRecord, workspaceId } = await ensureWorkflowAccess(
509-
workflowId,
510-
context.userId
511-
)
506+
const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId)
512507

513508
try {
514509
const deployedState = await loadDeployedWorkflowState(workflowId)
515-
const formatted = formatNormalizedWorkflowForCopilot(
516-
{
517-
blocks: deployedState.blocks,
518-
edges: deployedState.edges,
519-
loops: deployedState.loops as Record<string, Loop>,
520-
parallels: deployedState.parallels as Record<string, Parallel>,
521-
},
522-
workspaceId && (await hasWorkspaceSandboxAccess(workspaceId))
523-
? undefined
524-
: HIDE_SIM_SANDBOX_INPUTS
525-
)
510+
const formatted = formatNormalizedWorkflowForCopilot({
511+
blocks: deployedState.blocks,
512+
edges: deployedState.edges,
513+
loops: deployedState.loops as Record<string, Loop>,
514+
parallels: deployedState.parallels as Record<string, Parallel>,
515+
})
526516

527517
return {
528518
success: true,

apps/sim/lib/copilot/vfs/serializers.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ describe('VFS metadata serializers', () => {
177177
})
178178

179179
describe('entitlement-projected block schemas', () => {
180-
it('removes a gated input from both subBlocks and inputs', () => {
180+
it('keeps a gated input readable while marking it unavailable for mutation', () => {
181181
const block = {
182182
type: 'function',
183183
name: 'Function',
@@ -198,12 +198,35 @@ describe('entitlement-projected block schemas', () => {
198198
} as unknown as BlockConfig
199199

200200
const schema = JSON.parse(
201-
serializeBlockSchema(block, { hiddenInputIds: new Set(['sandboxId']) })
201+
serializeBlockSchema(block, {
202+
restrictedInputs: new Map([
203+
[
204+
'sandboxId',
205+
{
206+
requiredEntitlement: 'sim-sandboxes',
207+
reason: 'Requires an active Max or Enterprise plan.',
208+
},
209+
],
210+
]),
211+
})
202212
)
203213

204-
expect(schema.subBlocks.map((subBlock: { id: string }) => subBlock.id)).toEqual(['code'])
214+
expect(schema.subBlocks.map((subBlock: { id: string }) => subBlock.id)).toEqual([
215+
'code',
216+
'sandboxId',
217+
])
218+
expect(schema.subBlocks[1]).toMatchObject({
219+
readOnly: true,
220+
requiredEntitlement: 'sim-sandboxes',
221+
restrictionReason: 'Requires an active Max or Enterprise plan.',
222+
})
205223
expect(schema.inputs).toHaveProperty('code')
206-
expect(schema.inputs).not.toHaveProperty('sandboxId')
224+
expect(schema.inputs.sandboxId).toMatchObject({
225+
type: 'string',
226+
readOnly: true,
227+
requiredEntitlement: 'sim-sandboxes',
228+
restrictionReason: 'Requires an active Max or Enterprise plan.',
229+
})
207230
})
208231
})
209232

apps/sim/lib/copilot/vfs/serializers.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ export interface ComponentSerializationOptions {
9090
ownerBlockType?: string
9191
/** Product-gated inputs removed from both subBlocks and the input schema. */
9292
hiddenInputIds?: ReadonlySet<string>
93+
/** Product-gated inputs that remain discoverable but cannot be mutated by this viewer. */
94+
restrictedInputs?: ReadonlyMap<
95+
string,
96+
{
97+
requiredEntitlement: string
98+
reason: string
99+
}
100+
>
93101
}
94102

95103
/**
@@ -616,6 +624,12 @@ export function serializeBlockSchema(
616624

617625
const subBlocks = visibleSubBlocks.map((sb) => {
618626
const serialized = serializeSubBlock(sb)
627+
const restriction = options?.restrictedInputs?.get(sb.id)
628+
if (restriction) {
629+
serialized.readOnly = true
630+
serialized.requiredEntitlement = restriction.requiredEntitlement
631+
serialized.restrictionReason = restriction.reason
632+
}
619633

620634
if (sb.id === 'model' && sb.type === 'combobox' && typeof sb.options === 'function') {
621635
serialized.options = getStaticModelOptionsForVFS()
@@ -633,10 +647,28 @@ export function serializeBlockSchema(
633647
if (auth) toolAuth[toolId] = auth
634648
}
635649

636-
const inputs =
650+
const visibleInputs =
637651
block.inputs && hiddenIds.size > 0
638652
? Object.fromEntries(Object.entries(block.inputs).filter(([key]) => !hiddenIds.has(key)))
639653
: block.inputs
654+
const inputs = visibleInputs
655+
? Object.fromEntries(
656+
Object.entries(visibleInputs).map(([key, input]) => {
657+
const restriction = options?.restrictedInputs?.get(key)
658+
return restriction
659+
? [
660+
key,
661+
{
662+
...input,
663+
readOnly: true,
664+
requiredEntitlement: restriction.requiredEntitlement,
665+
restrictionReason: restriction.reason,
666+
},
667+
]
668+
: [key, input]
669+
})
670+
)
671+
: visibleInputs
640672

641673
return JSON.stringify(
642674
{

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
filterSecretNamesByMountPolicy,
3939
type SecretMountPolicy,
4040
} from '@/lib/copilot/secret-mount-policy'
41-
import { HIDE_SIM_SANDBOX_INPUTS } from '@/lib/copilot/sim-sandbox-projection'
41+
import { RESTRICTED_SIM_SANDBOX_INPUTS } from '@/lib/copilot/sim-sandbox-projection'
4242
import { compileDoc, getE2BDocFormat } from '@/lib/copilot/tools/server/files/doc-compile'
4343
import { extractDocText, isExtractableDocExt } from '@/lib/copilot/tools/server/files/doc-extract'
4444
import { runE2BCompiledCheck } from '@/lib/copilot/tools/server/files/doc-recalc'
@@ -181,7 +181,7 @@ function bindWorkspaceFileResult<T>(
181181
* (see {@link isStaticFileHidden}).
182182
*/
183183
let staticComponentFiles: Map<string, string> | null = null
184-
let staticFunctionSchemaWithoutSimSandboxes: string | null = null
184+
let staticFunctionSchemaWithRestrictedSimSandboxes: string | null = null
185185

186186
/**
187187
* Owning block for each `components/integrations/**` file, recorded at build
@@ -378,9 +378,9 @@ function getStaticComponentFiles(): Map<string, string> {
378378
const path = `components/blocks/${block.type}.json`
379379
files.set(path, serializeBlockSchema(block, { toolConfigs }))
380380
if (block.type === 'function') {
381-
staticFunctionSchemaWithoutSimSandboxes = serializeBlockSchema(block, {
381+
staticFunctionSchemaWithRestrictedSimSandboxes = serializeBlockSchema(block, {
382382
toolConfigs,
383-
hiddenInputIds: new Set(['sandboxId']),
383+
restrictedInputs: RESTRICTED_SIM_SANDBOX_INPUTS,
384384
})
385385
}
386386
}
@@ -587,9 +587,6 @@ export class WorkspaceVFS {
587587
>()
588588
private deploymentCache = new Map<string, Promise<DeploymentData | null>>()
589589
private _workspaceId = ''
590-
// Defaults to hidden so partial/failed materialization cannot leak a gated
591-
// Function input. Set from the live Sim entitlement before the VFS is used.
592-
private _simSandboxEntitled = false
593590
/**
594591
* Types of the org's CURRENT custom blocks (enabled + disabled — a disabled block
595592
* still resolves/renders). Populated by {@link materializeCustomBlocks}; used to
@@ -832,8 +829,6 @@ export class WorkspaceVFS {
832829
// prompt prefix), so nothing is destructured from this one.
833830
timed('tasks', this.materializeTasks(workspaceId, userId)),
834831
])
835-
this._simSandboxEntitled = sandboxEntitled
836-
837832
const workspaceMdData: WorkspaceMdData = {
838833
workspace: wsRow,
839834
members,
@@ -868,7 +863,7 @@ export class WorkspaceVFS {
868863
if (isStaticFileHidden(path, blockVisibility, allowedIntegrationTypes)) continue
869864
const projectedContent =
870865
path === 'components/blocks/function.json' && !sandboxEntitled
871-
? (staticFunctionSchemaWithoutSimSandboxes ?? content)
866+
? (staticFunctionSchemaWithRestrictedSimSandboxes ?? content)
872867
: content
873868
this.files.set(path, projectedContent)
874869
}
@@ -1592,15 +1587,12 @@ export class WorkspaceVFS {
15921587
// workflow; it still exists and must be readable, so emit an
15931588
// empty-but-valid state.json rather than a 404.
15941589
const sanitized = normalized
1595-
? sanitizeForCopilot(
1596-
{
1597-
blocks: normalized.blocks,
1598-
edges: normalized.edges,
1599-
loops: normalized.loops,
1600-
parallels: normalized.parallels,
1601-
} as any,
1602-
this._simSandboxEntitled ? undefined : HIDE_SIM_SANDBOX_INPUTS
1603-
)
1590+
? sanitizeForCopilot({
1591+
blocks: normalized.blocks,
1592+
edges: normalized.edges,
1593+
loops: normalized.loops,
1594+
parallels: normalized.parallels,
1595+
} as any)
16041596
: sanitizeForCopilot({ blocks: {}, edges: [], loops: {}, parallels: {} } as any)
16051597
return JSON.stringify(sanitized, null, 2)
16061598
})

apps/sim/lib/workflows/sanitization/json-sanitizer.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*/
44
import { resetUrlsMock, urlsMockFns } from '@sim/testing'
55
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
6-
import { HIDE_SIM_SANDBOX_INPUTS } from '@/lib/copilot/sim-sandbox-projection'
76
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
87
import type { WorkflowState } from '@/stores/workflows/workflow/types'
98
import { TRIGGER_WEBHOOK_URL_FIELD } from '@/triggers/constants'
@@ -165,7 +164,7 @@ describe('sanitizeForCopilot server-only block inputs', () => {
165164
})
166165

167166
describe('sanitizeForCopilot product-gated block inputs', () => {
168-
it('can hide a persisted Function sandbox selection without hiding ordinary inputs', () => {
167+
it('retains a persisted Function sandbox selection for model-visible read access', () => {
169168
const state = makeSingleBlockWorkflow('function-1', {
170169
type: 'function',
171170
name: 'Function 1',
@@ -177,13 +176,10 @@ describe('sanitizeForCopilot product-gated block inputs', () => {
177176
},
178177
})
179178

180-
expect(sanitizeForCopilot(state).blocks['function-1'].inputs).toHaveProperty(
181-
'sandboxId',
182-
'sandbox-1'
183-
)
184-
expect(sanitizeForCopilot(state, HIDE_SIM_SANDBOX_INPUTS).blocks['function-1'].inputs).toEqual({
179+
expect(sanitizeForCopilot(state).blocks['function-1'].inputs).toEqual({
185180
code: 'return 1',
186181
language: 'javascript',
182+
sandboxId: 'sandbox-1',
187183
})
188184
})
189185
})

0 commit comments

Comments
 (0)