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
18 changes: 14 additions & 4 deletions apps/docs/content/docs/en/platform/enterprise/self-hosted.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions apps/docs/content/docs/en/platform/self-hosting/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,11 @@ 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)."},
{ question: "What are the required environment variables for production?", answer: "Three secrets are required: BETTER_AUTH_SECRET (authentication), ENCRYPTION_KEY (data encryption), and INTERNAL_API_SECRET (service-to-service auth). Generate each with openssl rand -hex 32. You also need to set NEXT_PUBLIC_APP_URL and BETTER_AUTH_URL to your domain."},
{ 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." },
]} />


4 changes: 3 additions & 1 deletion apps/docs/content/docs/en/platform/self-hosting/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 17 additions & 3 deletions apps/docs/content/docs/en/workflows/blocks/function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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." },
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/function/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
})
Expand All @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/blocks/blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/blocks/blocks/function.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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',
Expand Down
47 changes: 35 additions & 12 deletions apps/sim/lib/billing/core/subscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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()
})
})
17 changes: 10 additions & 7 deletions apps/sim/lib/billing/core/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
isBillingEnabled,
isHosted,
isInboxEnabled,
isSandboxDeploymentEntitled,
isSandboxesEnabled,
isSsoEnabled,
} from '@/lib/core/config/env-flags'
Expand Down Expand Up @@ -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
Expand All @@ -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 })
Expand Down
44 changes: 44 additions & 0 deletions apps/sim/lib/core/config/enterprise-entitlements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ENTERPRISE_FEATURE_LEGACY_DEFAULTS,
type EnterpriseFeature,
resolveEnterpriseEntitlement,
resolveSandboxFeatureAvailability,
} from '@/lib/core/config/enterprise-entitlements'

describe('resolveEnterpriseEntitlement', () => {
Expand Down Expand Up @@ -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)
})
})
Loading
Loading