Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
158d832
feat(sandboxes): workspace dependency sets for Function blocks
icecrasher321 Jul 29, 2026
d5d30b9
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 29, 2026
3f6b6bc
chore(db): regenerate the sandboxes migration as 0273
icecrasher321 Jul 29, 2026
8849268
chore(db): drop the sandboxes migration ahead of the staging merge
icecrasher321 Jul 29, 2026
41bc220
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 29, 2026
de81dfe
chore(db): regenerate the sandboxes migration as 0275
icecrasher321 Jul 29, 2026
6766c36
fix(billing): consolidate the Max-tier entitlement onto one predicate
icecrasher321 Jul 29, 2026
4509743
chore(db): drop the sandboxes migration ahead of the staging merge
icecrasher321 Jul 29, 2026
ce615d3
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 29, 2026
49272f3
chore(db): regenerate the sandboxes migration as 0277
icecrasher321 Jul 29, 2026
71f24cc
feat(sandboxes): gate on the enterprise feature flags, drop the rollo…
icecrasher321 Jul 29, 2026
8e636dc
fix(sandboxes): let the language menu match its trigger width
icecrasher321 Jul 29, 2026
3f6f715
fix(sandboxes): re-queue a build when resolution finds the image unus…
icecrasher321 Jul 29, 2026
20162f6
improvement(sandboxes): let the picker show just the sandbox name
icecrasher321 Jul 29, 2026
c45c39d
fix(sandboxes): show the sandbox name on the block card, not its uuid
icecrasher321 Jul 29, 2026
7bcd8fa
fix(sandboxes): hide the Sandboxes section with no provider configured
icecrasher321 Jul 29, 2026
5893e27
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 29, 2026
3d789d7
docs(sandboxes): correct three claims the code no longer makes
icecrasher321 Jul 29, 2026
d97ae29
feat(sandboxes): release the provider image when nothing references it
icecrasher321 Jul 29, 2026
b18d312
fix(sandboxes): rate-limit the automatic rebuild, drop the one-off st…
icecrasher321 Jul 29, 2026
9b1b022
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 30, 2026
0d68a8c
fix(sandboxes): claim the image row and its reference check in one st…
icecrasher321 Jul 30, 2026
2d84fe2
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 30, 2026
d9bcc57
fix(sandboxes): route the retention sweep through the same image claim
icecrasher321 Jul 30, 2026
70ea74b
fix(sandboxes): rebuild a hash adopted while its image was being deleted
icecrasher321 Jul 30, 2026
d63cfd4
fix(sandboxes): reclaim a ready row whose image was deleted underneat…
icecrasher321 Jul 30, 2026
e446f2b
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 30, 2026
081485c
fix(sandboxes): let a same-spec save retry a failed build
icecrasher321 Jul 30, 2026
c505851
docs(sandboxes): correct the image cache's staleness invariant
icecrasher321 Jul 30, 2026
964b129
docs(sandboxes): note that a JavaScript sandbox needs an import to apply
icecrasher321 Jul 30, 2026
4170319
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 30, 2026
6fca142
fix(sandboxes): stop create mode surviving a return to an open sandbox
icecrasher321 Jul 30, 2026
ae753cb
fix(ci): pin the sandbox flag in the second nav catalog test, bump th…
icecrasher321 Jul 30, 2026
edcfb40
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 30, 2026
a549393
fix(sandboxes): keep the row restore to a refused delete only
icecrasher321 Jul 30, 2026
29b7442
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 30, 2026
f09dc99
fix(sandboxes): drop the dead row when a re-adopt rebuild cannot be s…
icecrasher321 Jul 30, 2026
2994141
feat(sandboxes): repair a missing image at create, where the truth is…
icecrasher321 Jul 30, 2026
af63079
Merge remote-tracking branch 'origin/staging' into staging-v11
icecrasher321 Jul 30, 2026
4e21e64
fix(sandboxes): key the build trigger by attempt, not by spec
icecrasher321 Jul 30, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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, and the inbox.
That turns on organizations, permission groups, SSO, whitelabeling, audit logs, session policies, data retention, data drains, workspace forks, sandboxes, and the inbox.

### Turning one feature off

Expand Down Expand Up @@ -51,6 +51,9 @@ The individual flags also work on their own if you would rather opt in one at a
| Data drains | `DATA_DRAINS_ENABLED` | `NEXT_PUBLIC_DATA_DRAINS_ENABLED` |
| Workspace forks | `FORKING_ENABLED` | — |
| Sim Mailer inbox | `INBOX_ENABLED` | `NEXT_PUBLIC_INBOX_ENABLED` |
| Sandboxes | `SANDBOXES_ENABLED` | `NEXT_PUBLIC_SANDBOXES_ENABLED` |

Sandboxes also need a remote execution provider, since the deployment builds the images itself: set `E2B_API_KEY`, or `SANDBOX_PROVIDER=daytona` with a `DAYTONA_API_KEY` that has `write:snapshots` and `write:sandboxes`. Without one the settings section appears but builds fail.

<Callout type="warning">
Data retention is the one feature that deletes data. Its flag controls the cleanup pass, not the settings screen — retention windows are always configurable. Nothing is ever deleted until you enable it, and even then only against windows you configured explicitly. Sim never applies the hosted plan defaults to a self-hosted deployment.
Expand Down
78 changes: 77 additions & 1 deletion apps/docs/content/docs/en/workflows/blocks/function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,82 @@ Beyond the Python standard library, the sandbox ships E2B's data-science stack p
- **Math and testing:** `sympy`, `pytest`
- **Sim additions:** `awscli`, `yq`, `csvkit`

## Sandboxes

A sandbox is a named dependency set your workspace maintains — a language plus a
list of pip or npm packages. Select one on a Function block and its code can
import everything on that list. Leave it empty and the block runs on the default
image, exactly as before.

Create and edit sandboxes in **Settings → Sandboxes**. Only workspace admins can
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.

1. **Name** the sandbox — `bigquery-etl`, `scraping`, whatever the job is.
2. Pick the **language**. A sandbox is language-scoped, so a Python block only
ever lists Python sandboxes.
3. Paste your **dependencies**, one per line. Version pins are optional.

```
google-cloud-bigquery==3.25.0
pyairtable>=3.0
pandas
```

Then open the block's advanced options and choose the sandbox under **Sandbox**.

In JavaScript the sandbox applies to code that uses `import` or `require` — that is
what sends the block to a remote sandbox in the first place, so a block without them
keeps running locally and ignores the selection. Python always runs remotely, so a
selected sandbox always applies.

<Callout type="info">
Two sandboxes with the same language and the same package list share one build,
so duplicating a set costs nothing. Editing a package list starts a new build;
runs already in flight keep using the old one. Deleting a sandbox frees its build
once nothing else uses it.
</Callout>

### Build status

On sim.ai, each dependency set is prebuilt into a reusable image, so runs pay no
install cost. The status row in Settings shows **Queued**, **Building**,
**Ready**, or **Failed**. A failed build reports what went wrong — a package that
does not exist, a version that has no match, a resolver conflict — with the
installer log behind a disclosure.

Running a block before its sandbox is **Ready** stops the run and shows you the
status. A failed build is retried periodically on its own; to retry immediately,
save the sandbox again in Settings.

On a self-hosted deployment using Daytona, dependencies install inside the
sandbox at the start of every run instead, adding roughly 10–30 seconds per
execution. Prebuilt images require E2B.

### What is allowed

Package names and version specifiers only. URLs, `git+` references, `-e`, local
paths, `--index-url`, and npm aliases are rejected, with the offending line
number reported. A sandbox may declare up to 50 packages.

## Scoping secrets for agent tools

When a Function block is used as an Agent tool, its code can read every workspace
secret by default — both `{{MY_SECRET}}` and `environmentVariables['MY_SECRET']`.

To narrow that, set **Secret access** to *Selected secrets* in the block's
tool configuration and pick the names the code may read. Two things change:

- Only those secrets are injected. `{{OTHER_SECRET}}` no longer resolves either.
- The selected **names** are added to the tool's description, so the model knows
what it can reference. Values are never sent to the model — they are injected
server-side at execution.

Leaving the default (*All secrets*) resolves the list at run time, so a secret
added next month is included automatically.

## Examples

### Reshape an API response
Expand Down Expand Up @@ -170,6 +246,6 @@ The lazy `sim.files` and `sim.values` helpers are available only in JavaScript f
{ 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 runs in E2B. Python always runs in the E2B sandbox, with or without imports." },
{ 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 (the return value of your code, read as <function.result>) and stdout (anything logged with console.log or print, read as <function.stdout>). Include a return statement in JavaScript, or print JSON in Python, to pass data downstream." },
{ question: "Can I make HTTP requests from a Function block?", answer: "Yes. fetch() is available in JavaScript with async/await; libraries like axios are not, only the built-in fetch. In Python, use requests or httpx in the E2B sandbox." },
{ question: "Can I make HTTP requests from a Function block?", answer: "Yes. fetch() is available in JavaScript with async/await; libraries like axios are only available when the block has a sandbox selected. In Python, use requests or httpx in the E2B sandbox." },
{ question: "Is there a timeout for Function block execution?", answer: "Yes, a configurable execution timeout. If your code exceeds it, the run is terminated and the block reports an error. Keep this in mind for external calls or heavy processing." },
]} />
31 changes: 31 additions & 0 deletions apps/sim/app/api/cron/cleanup-sandbox-images/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { runCleanupSandboxImages } from '@/background/cleanup-sandbox-images'

export const dynamic = 'force-dynamic'

const logger = createLogger('CleanupSandboxImagesAPI')

/**
* Retention sweep for prebuilt sandbox images. A runtime-strategy deployment has
* nothing to collect, so the sweep is a no-op there rather than a special case
* here.
*/
export const GET = withRouteHandler(async (request: NextRequest) => {
const authError = verifyCronAuth(request, 'sandbox image cleanup')
if (authError) return authError

try {
const result = await runCleanupSandboxImages()
return NextResponse.json({ success: true, ...result })
} catch (error) {
logger.error('Failed to sweep sandbox images', { error })
return NextResponse.json(
{ error: getErrorMessage(error, 'Failed to sweep sandbox images') },
{ status: 500 }
)
}
})
51 changes: 50 additions & 1 deletion apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,42 @@ function resolveWorkflowVariables(
return resolvedCode
}

/**
* Narrows the secrets an execution can see, per the block's stored scope.
*
* | Stored value | Behavior |
* |-----------------------------|-------------------------------------------------|
* | unset (every block today) | all secrets — the regression-safe default |
* | `'all'` | all secrets, resolved now so later additions land |
* | `'selected'` + names | only those |
* | `'selected'` + empty list | none — an explicit deny |
*
* Unset and `'all'` must both inject everything: agent-authored code already
* reads `{{MY_SECRET}}` and `environmentVariables['MY_SECRET']` today, so a
* default-deny would silently break prompts that work right now.
*/
function scopeEnvironmentVariables(
envVars: Record<string, string>,
scope: 'all' | 'selected' | undefined,
mountedSecrets: string[] | undefined
): Record<string, string> {
if (scope !== 'selected') return envVars

const allowed = new Set(mountedSecrets ?? [])
const scoped: Record<string, string> = {}
const missing: string[] = []
for (const name of allowed) {
if (name in envVars) scoped[name] = envVars[name]
else missing.push(name)
}
if (missing.length > 0) {
// A secret that was renamed or deleted since the block was configured. Drop
// it rather than failing: the code's own error is clearer than ours.
logger.warn('Mounted secrets no longer exist in this workspace', { missing })
}
return scoped
}

function resolveEnvironmentVariables(
code: string,
params: Record<string, any>,
Expand Down Expand Up @@ -1401,7 +1437,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
outputSandboxPath,
overwriteFileId,
outputs,
envVars = {},
envVars: rawEnvVars = {},
secretScope,
mountedSecrets,
sandboxId: selectedSandboxId,
blockData = {},
blockNameMapping = {},
blockOutputSchemas = {},
Expand All @@ -1417,6 +1456,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
isCustomTool = false,
_sandboxFiles,
} = body
// Scoped before {{VAR}} resolution so the `{{NAME}}` path and the
// `environmentVariables[...]` dict narrow together — filtering only the dict
// would leave `{{OTHER_SECRET}}` resolving, which is a hole, not a scope.
const envVars = scopeEnvironmentVariables(rawEnvVars, secretScope, mountedSecrets)
sourceCodeForErrors = sourceCode
const outputFiles = getOutputFileDeclarations({
outputs,
Expand Down Expand Up @@ -1547,6 +1590,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
sandboxFiles: _sandboxFiles,
outputSandboxPath,
outputSandboxPaths,
workspaceId,
sandboxId: selectedSandboxId,
Comment thread
icecrasher321 marked this conversation as resolved.
})
const executionTime = Date.now() - execStart

Expand Down Expand Up @@ -1706,6 +1751,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
sandboxFiles: _sandboxFiles,
outputSandboxPath,
outputSandboxPaths,
workspaceId,
sandboxId: selectedSandboxId,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
Expand Down Expand Up @@ -1794,6 +1841,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
sandboxFiles: _sandboxFiles,
outputSandboxPath,
outputSandboxPaths,
workspaceId,
sandboxId: selectedSandboxId,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/organizations/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ vi.mock('@/lib/billing/organizations/create-organization', () => ({
OrganizationSlugTakenError: class OrganizationSlugTakenError extends Error {},
}))

/** Mirrors the real predicate, which also admits the `team_*` credit tiers. */
vi.mock('@/lib/billing/plan-helpers', () => ({
isOrgPlan: (plan: string) => plan === 'team' || plan === 'enterprise',
isOrgPlan: (plan: string) => plan === 'team' || plan.startsWith('team_') || plan === 'enterprise',
}))

vi.mock('@/lib/billing/subscriptions/utils', () => ({
ENTITLED_SUBSCRIPTION_STATUSES: ['active', 'trialing'],
ENTITLED_SUBSCRIPTION_STATUSES: ['active', 'past_due'],
}))

vi.mock('@/lib/workspaces/organization-workspaces', () => ({
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/wand/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { env } from '@/lib/core/config/env'
import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/env-flags'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { enrichSandboxPackages } from '@/lib/execution/remote-sandbox/wand-enricher'
import { enrichTableSchema } from '@/lib/table/llm/wand'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
import { extractResponseText, parseResponsesUsage } from '@/providers/openai/utils'
Expand Down Expand Up @@ -90,6 +91,9 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
},

'table-schema': enrichTableSchema,
// Both the JavaScript and Python code prompts generate under this type — the
// Python swap in `code.tsx` replaces the prompt text but keeps the type.
'javascript-function-body': enrichSandboxPackages,
}

async function updateUserStatsForWand(
Expand Down
Loading
Loading