fix(env): flag combinations for sandboxes (#6308)

* fix(env): flag combinations for sandboxes

* more changes
This commit is contained in:
Vikhyath Mondreti
2026-08-05 20:22:36 -07:00
committed by GitHub
parent 79bfff728b
commit c530d276b7
15 changed files with 233 additions and 51 deletions
@@ -23,7 +23,10 @@ ENTERPRISE_ENABLED=true
NEXT_PUBLIC_ENTERPRISE_ENABLED=true
```
That turns on organizations, permission groups, SSO, whitelabeling, audit logs, session policies, data retention, data drains, workspace forks, sandboxes, and the inbox.
That turns on organizations, permission groups, SSO, whitelabeling, audit logs,
session policies, data retention, data drains, workspace forks, the Sandbox
entitlement, and the inbox. Sandboxes remain unavailable until their remote
provider and dedicated Function base are configured.
### Turning one feature off
@@ -95,9 +98,16 @@ NEXT_PUBLIC_SANDBOXES_ENABLED=true
```
`SANDBOXES_ENABLED` grants the server-side self-hosted entitlement.
`NEXT_PUBLIC_SANDBOXES_ENABLED` exposes remote Python and Shell plus custom
sandbox management in the browser. Set the public flag only after the selected
provider has credentials and a valid immutable Function base configured.
`NEXT_PUBLIC_SANDBOXES_ENABLED` projects provider readiness to the browser and
exposes Shell plus custom Sandbox management. Set the public flag only after the
selected provider has credentials and a valid immutable Function base configured.
The Function language value itself is never conditioned on these flags, so a
saved Python block cannot be silently serialized or executed as JavaScript.
JavaScript without `import` or `require` does not use this remote provider and
continues to run in the local isolated VM when all Sandbox flags are off. Python,
Shell, JavaScript with external imports, and selected custom Sandboxes fail with
an explicit configuration error until the remote Function base is ready.
Mothership's `function_execute` and `run_code` tools use Mothership's separate
shell image, including for JavaScript without imports. If the deployment uses
@@ -119,7 +119,7 @@ Sim is self-contained for the core editor and execution engine. A few features r
| **Agent blocks** | An API key for at least one model provider | Or a self-hosted OpenAI-compatible endpoint: Ollama, vLLM, or LiteLLM. |
| **Chat module** | `COPILOT_API_KEY` from sim.ai | Set `NEXT_PUBLIC_CHAT_DISABLED=true` to hide the module instead. |
| **Integrations** | Your own OAuth app per service | See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). |
| **Function / Pi blocks at scale** | Optional E2B or Daytona key | Without one, code runs in the in-process isolated-vm sandbox. See [Security](/platform/self-hosting/security). |
| **Remote Function / Pi execution** | Optional E2B or Daytona key | Without one, JavaScript Function code that has no `import` or `require` still runs in the in-process isolated VM. Python, Shell, JavaScript with external imports, custom Function Sandboxes, and Pi require a configured remote provider. See [Security](/platform/self-hosting/security). |
<FAQ items={[
{ question: "What are the minimum requirements to self-host Sim?", answer: "At minimum you need 2 CPU cores, 12 GB RAM, 20 GB SSD storage, and Docker 20.10 or later. Memory is typically the constraining factor due to workflow execution (isolated-vm sandboxing), file processing, and vector operations (pgvector)."},
@@ -127,4 +127,3 @@ Sim is self-contained for the core editor and execution engine. A few features r
{ question: "Can I use Sim with local AI models?", answer: "Yes. Sim supports Ollama for local model inference. Use docker-compose.ollama.yml instead of docker-compose.prod.yml. It offers both GPU (with NVIDIA support) and CPU-only profiles, and automatically pulls gemma3:4b as a starter model." },
]} />
@@ -115,7 +115,9 @@ Workflows can execute user-authored JavaScript and Python. Know which sandbox yo
| **E2B** | `E2B_ENABLED=true`, `E2B_API_KEY` | Remote sandbox per execution. Strongest isolation; requires outbound access to E2B. |
| **Daytona** | `SANDBOX_PROVIDER=daytona`, `DAYTONA_API_KEY` | Remote sandbox per execution. |
Python execution and the tooling-dependent blocks require a remote sandbox provider — the in-process isolate runs JavaScript only.
Python, Shell, JavaScript with external imports, and tooling-dependent blocks
require a remote sandbox provider. JavaScript without `import` or `require`
continues to run in the in-process isolate when no remote provider is configured.
<Callout type="warn">
With the default in-process sandbox, treat everyone who can author a workflow as someone running code in your app container's security context. If your Sim instance is open to a wide or partly-trusted audience, use a remote sandbox provider and enable the NetworkPolicy egress restrictions.
@@ -16,7 +16,11 @@ The **Function block** runs your own JavaScript, Python, or Shell code as one st
### Code
JavaScript is the default. Python and Shell appear when a remote sandbox provider is enabled. Reference an earlier output directly, with no quotes around the tag, and read an environment variable with `{{VAR}}`:
JavaScript is the default. The language field is part of the saved workflow and is
never removed when Sandbox configuration changes. Python remains available as a
language choice; Shell and custom Sandbox controls appear when a remote Function
sandbox provider is enabled. Reference an earlier output directly, with no quotes
around the tag, and read an environment variable with `{{VAR}}`:
<Tabs items={['JavaScript', 'Python', 'Shell']}>
<Tab value="JavaScript">
@@ -98,6 +102,13 @@ on a self-hosted instance, build and configure the provider's dedicated
generate are captured as images automatically.
</Callout>
<Callout type="info">
If no remote provider is configured, JavaScript without `import` or `require`
continues to run in Sim's local isolated VM. Missing E2B or Daytona configuration
does not disable that path. Remote-only code fails with an explicit configuration
error; Sim does not reinterpret Python or Shell as JavaScript.
</Callout>
The dedicated Function base has the same runtime and universal package contract
on E2B and Daytona. It includes this data-science stack; use a workspace sandbox
when another dependency must be present:
@@ -128,7 +139,9 @@ Create and edit sandboxes in **Settings → Sandboxes**. Only workspace admins c
create or edit them. On sim.ai they need an active Max or Enterprise plan;
self-hosted deployments turn them on with `SANDBOXES_ENABLED` (see
[self-hosted enterprise](/platform/enterprise/self-hosted)). The section is
hidden when a deployment has no sandbox provider configured.
usable only when the deployment also has a remote provider and immutable Function
base configured. The Function block hides its custom Sandbox selector when that
runtime is unavailable.
1. **Name** the sandbox — `bigquery-etl`, `scraping`, whatever the job is.
2. Pick the **language**. This selects pip or npm for the dependency list. Python
@@ -363,8 +376,9 @@ The lazy `sim.files` and `sim.values` helpers are available only in JavaScript f
- **Use stdout to debug.** `console.log()`, `print()`, and ordinary shell output land in `<function.stdout>` and the run logs.
<FAQ items={[
{ question: "What languages does the Function block support?", answer: "JavaScript, Python, and Shell. JavaScript is the default. Python and Shell appear when a remote sandbox provider is enabled." },
{ question: "What languages does the Function block support?", answer: "JavaScript, Python, and Shell. JavaScript is the default. Python remains a stable saved language choice; Shell and custom Sandbox controls appear when a remote sandbox provider is enabled. Python and Shell execution require that provider." },
{ question: "When does code run locally vs. in a sandbox?", answer: "JavaScript without external imports runs in a local isolated sandbox for speed. JavaScript that uses import or require, Python, and Shell run in the configured remote sandbox." },
{ question: "Does JavaScript still work without E2B or Daytona?", answer: "Yes. JavaScript without import or require runs in Sim's local isolated VM and does not require a remote provider. JavaScript with external imports, Python, Shell, and custom Sandboxes require E2B or Daytona and fail explicitly when it is unavailable." },
{ question: "How do I reference outputs from other blocks inside my code?", answer: "Use angle-bracket syntax directly, like <agent.content> or <api.data>, with no quotes around the tag — Sim replaces it with the real value before execution. For environment variables, use double curly braces: {{API_KEY}}." },
{ question: "What does the Function block return?", answer: "Two outputs: result and stdout. Use return in JavaScript, assign __sim_result__ in Python, or print an __SIM_RESULT__= marker in Shell to set result. Ordinary console, print, and command output goes to stdout." },
{ question: "Can I make HTTP requests from a Function block?", answer: "Yes. fetch() is available in JavaScript with async/await. In Python, use requests or httpx. In Shell, use curl or a CLI available on the selected sandbox." },
@@ -218,7 +218,7 @@ describe('Function Execute API Route', () => {
expect(data).toHaveProperty('error', 'Unauthorized')
})
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
it('runs import-free JavaScript in isolated-vm without a remote provider', async () => {
const req = createMockRequest('POST', {
code: 'return "test"',
})
@@ -229,6 +229,9 @@ describe('Function Execute API Route', () => {
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.result).toBe('test')
expect(mockExecuteInIsolatedVM).toHaveBeenCalledTimes(1)
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
expect(mockExecuteShellInSandbox).not.toHaveBeenCalled()
})
it('does not accept a Mothership sandbox profile from the request body', async () => {
+1 -1
View File
@@ -340,7 +340,7 @@ describe.concurrent('Blocks Module', () => {
const languageSubBlock = block?.subBlocks.find((sb) => sb.id === 'language')
const codeSubBlock = block?.subBlocks.find((sb) => sb.id === 'code')
const sandboxSubBlock = block?.subBlocks.find((sb) => sb.id === 'sandboxId')
expect(languageSubBlock?.showWhenEnvSet).toBe('NEXT_PUBLIC_SANDBOXES_ENABLED')
expect(languageSubBlock?.showWhenEnvSet).toBeUndefined()
expect(sandboxSubBlock?.showWhenEnvSet).toBe('NEXT_PUBLIC_SANDBOXES_ENABLED')
expect(codeSubBlock).toBeDefined()
expect(codeSubBlock?.type).toBe('code')
+2 -3
View File
@@ -1,5 +1,5 @@
import { CodeIcon } from '@/components/icons'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { isSandboxesEnabled } from '@/lib/core/config/env-flags'
import { CodeLanguage, getLanguageDisplayName } from '@/lib/execution/languages'
import {
fetchWorkspaceSandboxOption,
@@ -34,13 +34,12 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
options: () => [
{ label: getLanguageDisplayName(CodeLanguage.JavaScript), id: CodeLanguage.JavaScript },
{ label: getLanguageDisplayName(CodeLanguage.Python), id: CodeLanguage.Python },
...(isTruthy(getEnv('NEXT_PUBLIC_SANDBOXES_ENABLED'))
...(isSandboxesEnabled
? [{ label: getLanguageDisplayName(CodeLanguage.Shell), id: CodeLanguage.Shell }]
: []),
],
placeholder: 'Select language',
value: () => CodeLanguage.JavaScript,
showWhenEnvSet: 'NEXT_PUBLIC_SANDBOXES_ENABLED',
},
{
id: 'code',
+35 -12
View File
@@ -336,16 +336,15 @@ describe('hasWorkspaceLiveSyncAccess', () => {
})
})
/**
* Sandboxes are an enterprise feature, so `SANDBOXES_ENABLED` must win over the
* plan gate the way `INBOX_ENABLED` does. Both cases run with billing enabled —
* the `!isBillingEnabled` bail would otherwise answer every one of them, hiding
* whether the override is wired at all.
*/
describe('hasWorkspaceSandboxAccess', () => {
beforeEach(() => {
vi.clearAllMocks()
setEnvFlags({ isBillingEnabled: true, isHosted: true, isSandboxesEnabled: false })
setEnvFlags({
isBillingEnabled: true,
isHosted: true,
isSandboxDeploymentEntitled: false,
isSandboxesEnabled: true,
})
mockGetWorkspaceWithOwner.mockResolvedValue({
id: 'workspace-host',
billedAccountUserId: 'workspace-owner',
@@ -361,17 +360,23 @@ describe('hasWorkspaceSandboxAccess', () => {
})
})
afterAll(() => setEnvFlags({ isSandboxesEnabled: true }))
it('fails closed before resolving a payer when the remote feature is unavailable', async () => {
setEnvFlags({ isSandboxesEnabled: false })
it('grants access from the self-hosted override without resolving a payer', async () => {
setEnvFlags({ isSandboxesEnabled: true })
await expect(hasWorkspaceSandboxAccess('workspace-host')).resolves.toBe(false)
expect(mockGetWorkspaceWithOwner).not.toHaveBeenCalled()
expect(mockGetHighestPriorityPersonalSubscription).not.toHaveBeenCalled()
})
it('grants an explicit deployment override without resolving a payer', async () => {
setEnvFlags({ isSandboxDeploymentEntitled: true })
await expect(hasWorkspaceSandboxAccess('workspace-host')).resolves.toBe(true)
expect(mockGetWorkspaceWithOwner).not.toHaveBeenCalled()
expect(mockGetHighestPriorityPersonalSubscription).not.toHaveBeenCalled()
})
it('falls back to the Max plan gate when the override is unset', async () => {
it('uses the Max plan gate on a billing-enabled deployment', async () => {
mockGetHighestPriorityPersonalSubscription.mockResolvedValue({
referenceId: 'workspace-owner',
plan: 'pro_25000',
@@ -383,7 +388,7 @@ describe('hasWorkspaceSandboxAccess', () => {
expect(mockGetHighestPriorityPersonalSubscription).toHaveBeenCalledWith('workspace-owner')
})
it('denies a sub-Max payer when the override is unset', async () => {
it('denies a sub-Max payer on a billing-enabled deployment', async () => {
mockGetHighestPriorityPersonalSubscription.mockResolvedValue({
referenceId: 'workspace-owner',
plan: 'pro_6000',
@@ -393,4 +398,22 @@ describe('hasWorkspaceSandboxAccess', () => {
await expect(hasWorkspaceSandboxAccess('workspace-host')).resolves.toBe(false)
})
it('requires an Enterprise or Sandbox deployment entitlement when billing is disabled', async () => {
setEnvFlags({
isBillingEnabled: false,
isSandboxDeploymentEntitled: false,
isSandboxesEnabled: false,
})
await expect(hasWorkspaceSandboxAccess('workspace-host')).resolves.toBe(false)
setEnvFlags({
isSandboxDeploymentEntitled: true,
isSandboxesEnabled: true,
})
await expect(hasWorkspaceSandboxAccess('workspace-host')).resolves.toBe(true)
expect(mockGetWorkspaceWithOwner).not.toHaveBeenCalled()
})
})
+10 -7
View File
@@ -30,6 +30,7 @@ import {
isBillingEnabled,
isHosted,
isInboxEnabled,
isSandboxDeploymentEntitled,
isSandboxesEnabled,
isSsoEnabled,
} from '@/lib/core/config/env-flags'
@@ -691,11 +692,12 @@ export async function hasWorkspaceLiveSyncAccess(workspaceId: string): Promise<b
* Checks whether the exact workspace payer can discover, author, or directly
* select custom Sim sandboxes through Copilot.
*
* Same entitlement as the inbox (Sim Mailer), and the same shape: the
* `SANDBOXES_ENABLED` self-hosted override wins first, then a deployment
* without billing is unrestricted, and otherwise the workspace payer must hold
* a usable Max or Enterprise subscription. Builds cost provider compute and
* storage, so this deliberately sits above the plain paid tier.
* A configured remote Function provider is mandatory. On billing-free
* deployments, the Enterprise pair or Sandbox-specific pair grants access. With
* billing enabled, an explicit Sandbox deployment override wins; otherwise the
* workspace payer must hold a usable Max or Enterprise subscription. Builds cost
* provider compute and storage, so this deliberately sits above the plain paid
* tier.
*
* Existing Function execution deliberately does not consult it (see
* `resolveWorkspaceSandbox`), so a workspace that downgrades keeps running the
@@ -704,8 +706,9 @@ export async function hasWorkspaceLiveSyncAccess(workspaceId: string): Promise<b
*/
export async function hasWorkspaceSandboxAccess(workspaceId: string): Promise<boolean> {
try {
if (isSandboxesEnabled) return true
if (!isBillingEnabled) return true
if (!isSandboxesEnabled) return false
if (isSandboxDeploymentEntitled) return true
if (!isBillingEnabled) return false
return await hasMaxTierWorkspaceAccess(workspaceId)
} catch (error) {
logger.error('Error checking workspace sandbox access', { error, workspaceId })
@@ -6,6 +6,7 @@ import {
ENTERPRISE_FEATURE_LEGACY_DEFAULTS,
type EnterpriseFeature,
resolveEnterpriseEntitlement,
resolveSandboxFeatureAvailability,
} from '@/lib/core/config/enterprise-entitlements'
describe('resolveEnterpriseEntitlement', () => {
@@ -110,6 +111,49 @@ describe('resolveEnterpriseEntitlement', () => {
expect(ENTERPRISE_FEATURE_LEGACY_DEFAULTS.accessControl).toBe(false)
expect(ENTERPRISE_FEATURE_LEGACY_DEFAULTS.organizations).toBe(false)
expect(ENTERPRISE_FEATURE_LEGACY_DEFAULTS.sso).toBe(false)
expect(ENTERPRISE_FEATURE_LEGACY_DEFAULTS.sandboxes).toBe(false)
})
})
})
describe('resolveSandboxFeatureAvailability', () => {
it.each([
{
name: 'hosted billing with a provider',
billingEnabled: true,
deploymentEntitled: false,
remoteProviderEnabled: true,
expected: true,
},
{
name: 'hosted billing without a provider',
billingEnabled: true,
deploymentEntitled: false,
remoteProviderEnabled: false,
expected: false,
},
{
name: 'billing-free Enterprise or feature entitlement with a provider',
billingEnabled: false,
deploymentEntitled: true,
remoteProviderEnabled: true,
expected: true,
},
{
name: 'billing-free provider credentials without an entitlement',
billingEnabled: false,
deploymentEntitled: false,
remoteProviderEnabled: true,
expected: false,
},
{
name: 'billing-free entitlement without a provider',
billingEnabled: false,
deploymentEntitled: true,
remoteProviderEnabled: false,
expected: false,
},
])('$name resolves to $expected', ({ expected, ...input }) => {
expect(resolveSandboxFeatureAvailability(input)).toBe(expected)
})
})
@@ -59,11 +59,10 @@ export type EnterpriseFeature =
* delete pass is gated here. Defaulting it on would start expiring logs on
* upgrade against plan defaults the operator never chose.
*
* `sandboxes` is `true` for the mirror-image reason: its gate already returns
* true whenever billing is off, exactly like `inbox`. A `false` here would make
* the settings-nav override disagree with the gate that actually answers the
* request. Self-hosted builds run on the operator's own E2B/Daytona
* credentials, so there is no Sim-side cost to withhold.
* `sandboxes` is deliberately `false`. A remote Function provider and immutable
* base are operational prerequisites, so a billing-free deployment must opt in
* through either the Enterprise pair or the Sandbox-specific pair. This keeps a
* settings surface from appearing when the deployment cannot execute it.
*
* Do not "tidy" these to a uniform value. Each records observed prior behavior,
* and changing one silently alters a live deployment on upgrade.
@@ -76,7 +75,7 @@ export const ENTERPRISE_FEATURE_LEGACY_DEFAULTS: Readonly<Record<EnterpriseFeatu
forking: false,
inbox: true,
organizations: false,
sandboxes: true,
sandboxes: false,
sessionPolicies: true,
sso: false,
whitelabeling: true,
@@ -102,3 +101,25 @@ export function resolveEnterpriseEntitlement({
}: ResolveEnterpriseEntitlementParams): boolean {
return explicit ?? (masterEnabled || legacyDefault)
}
interface ResolveSandboxFeatureAvailabilityParams {
/** Whether hosted subscription enforcement supplies the deployment entitlement. */
billingEnabled: boolean
/** Enterprise-master or Sandbox-specific deployment entitlement. */
deploymentEntitled: boolean
/** Server-verified provider readiness or its public browser projection. */
remoteProviderEnabled: boolean
}
/**
* Combines Sandbox entitlement with runtime capability. Neither dimension may
* substitute for the other: a plan cannot create a provider, and provider
* credentials cannot grant a workspace feature by themselves.
*/
export function resolveSandboxFeatureAvailability({
billingEnabled,
deploymentEntitled,
remoteProviderEnabled,
}: ResolveSandboxFeatureAvailabilityParams): boolean {
return remoteProviderEnabled && (billingEnabled || deploymentEntitled)
}
+29 -8
View File
@@ -12,6 +12,7 @@ import {
ENTERPRISE_FEATURE_LEGACY_DEFAULTS,
type EnterpriseFeature,
resolveEnterpriseEntitlement,
resolveSandboxFeatureAvailability,
} from './enterprise-entitlements'
import { env, envBoolean, getEnv, isFalsy, isTruthy } from './env'
import { hasEnvCapabilityValue, inspectCapability, SANDBOX_CAPABILITY } from './env-capabilities'
@@ -319,13 +320,14 @@ export const isInboxEnabled = enterpriseFeatureEnabled(
)
/**
* Are custom sandboxes (workspace dependency sets for Function blocks) enabled.
* Whether deployment configuration entitles custom Function sandboxes.
*
* The server flag is the self-hosted entitlement override. In the browser,
* custom sandbox management follows the same public flag as the remote Function
* runtime because custom images cannot work without that provider/base.
* With billing enabled this is an explicit plan-gate override. Without billing,
* either the Enterprise master switch or the Sandbox-specific server/client pair
* enables the feature. Provider readiness is applied separately by
* {@link isSandboxesEnabled} so entitlement can never advertise a missing runtime.
*/
export const isSandboxesEnabled = enterpriseFeatureEnabled(
export const isSandboxDeploymentEntitled = enterpriseFeatureEnabled(
'sandboxes',
env.SANDBOXES_ENABLED,
'NEXT_PUBLIC_SANDBOXES_ENABLED'
@@ -407,9 +409,10 @@ const sandboxProvider = inspectCapability(SANDBOX_CAPABILITY, env).providerId
* deployment must build and configure the Function-owned image before exposing
* the runtime.
*
* The browser twin is `NEXT_PUBLIC_SANDBOXES_ENABLED`, read by the Function
* block's `showWhenEnvSet` gates. Set it only after this server-side provider
* check succeeds; `bun run setup --doctor` reports mismatches in either direction.
* The browser cannot inspect provider credentials, so
* `NEXT_PUBLIC_SANDBOXES_ENABLED` is its readiness projection. Set the public
* value only after this server-side check succeeds; `bun run setup --doctor`
* reports mismatches in either direction.
*/
export const isRemoteSandboxEnabled =
sandboxProvider === 'daytona'
@@ -430,6 +433,24 @@ export const isRemoteSandboxEnabled =
)
: false
/**
* Whether the complete custom-Sandbox feature is available on this deployment.
*
* Billing supplies hosted entitlement, while billing-free deployments require
* the Enterprise pair or the Sandbox-specific pair. Both modes additionally
* require a configured remote Function provider. The public flag projects that
* provider readiness into the browser; the server always verifies credentials
* and the immutable Function base directly.
*/
export const isSandboxesEnabled = resolveSandboxFeatureAvailability({
billingEnabled: isBillingEnabled,
deploymentEntitled: isSandboxDeploymentEntitled,
remoteProviderEnabled:
typeof window === 'undefined'
? isRemoteSandboxEnabled
: isTruthy(getEnv('NEXT_PUBLIC_SANDBOXES_ENABLED')),
})
/**
* Whether the selected provider can serve Mothership's own code image.
* This is intentionally independent of {@link isRemoteSandboxEnabled}: the
-2
View File
@@ -10,7 +10,6 @@ import {
getCanonicalValues,
isCanonicalPair,
isNonEmptyValue,
isSubBlockFeatureEnabled,
isSubBlockHidden,
isToolInputOnlySubBlock,
resolveCanonicalMode,
@@ -52,7 +51,6 @@ function shouldSerializeSubBlock(
canonicalIndex: ReturnType<typeof buildCanonicalIndex>,
canonicalModeOverrides?: CanonicalModeOverrides
): boolean {
if (!isSubBlockFeatureEnabled(subBlockConfig)) return false
// Only meaningful when the block is invoked as an agent tool, where the
// value lives on the tool entry rather than the block. Serializing it here
// would let a non-UI writer (copilot, YAML import) set an invisible secret
@@ -96,6 +96,26 @@ const { mockBlockConfigs, createMockGetBlock, slackWithCanonicalParam } = vi.hoi
],
inputs: { input: { type: 'any' } },
},
envGatedFunction: {
name: 'Environment-gated Function',
description: 'Execute custom code',
category: 'code',
bgColor: '#9C27B0',
tools: {
access: ['function'],
config: { tool: () => 'function' },
},
subBlocks: [
{ id: 'code', type: 'code', label: 'Code' },
{
id: 'language',
type: 'dropdown',
label: 'Language',
showWhenEnvSet: 'NEXT_PUBLIC_TEST_REMOTE_RUNTIME',
},
],
inputs: { input: { type: 'any' } },
},
condition: {
name: 'Condition',
description: 'Branch based on condition',
@@ -1447,6 +1467,29 @@ describe('Serializer Extended Tests', () => {
})
describe('edge cases with empty and null values', () => {
it('preserves execution parameters hidden by a presentation-only environment gate', () => {
const serializer = new Serializer()
const block: BlockState = {
id: 'func-python',
type: 'envGatedFunction',
name: 'Python Function',
position: { x: 0, y: 0 },
subBlocks: {
code: { id: 'code', type: 'code', value: 'from datetime import datetime' },
language: { id: 'language', type: 'dropdown', value: 'python' },
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'func-python': block }, [], {})
expect(serialized.blocks[0].config.params).toMatchObject({
code: 'from datetime import datetime',
language: 'python',
})
})
it('should handle blocks with all null subBlock values', () => {
const serializer = new Serializer()
const block: BlockState = {
+3 -1
View File
@@ -31,6 +31,7 @@ export interface EnvFlagsMockState {
isAccessControlEnabled: boolean
isOrganizationsEnabled: boolean
isInboxEnabled: boolean
isSandboxDeploymentEntitled: boolean
isSandboxesEnabled: boolean
isWhitelabelingEnabled: boolean
isAuditLogsEnabled: boolean
@@ -81,7 +82,8 @@ const defaultEnvFlagsState: EnvFlagsMockState = {
// `true` so upgrades do not remove a feature. See
// ENTERPRISE_FEATURE_LEGACY_DEFAULTS.
isInboxEnabled: true,
isSandboxesEnabled: true,
isSandboxDeploymentEntitled: false,
isSandboxesEnabled: false,
isWhitelabelingEnabled: true,
isSessionPoliciesEnabled: true,
isAuditLogsEnabled: false,