Skip to content

Commit 5cbafee

Browse files
committed
separate out mship template and func template
1 parent dcd162f commit 5cbafee

25 files changed

Lines changed: 529 additions & 57 deletions

File tree

apps/docs/content/docs/en/platform/enterprise/self-hosted.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ NEXT_PUBLIC_SANDBOXES_ENABLED=true
9999
sandbox management in the browser. Set the public flag only after the selected
100100
provider has credentials and a valid immutable Function base configured.
101101

102+
Mothership's `function_execute` and `run_code` tools use Mothership's separate
103+
shell image, including for JavaScript without imports. If the deployment uses
104+
Mothership code tools, also configure the image produced by the Mothership
105+
release process for the selected provider:
106+
107+
```bash
108+
# E2B
109+
MOTHERSHIP_E2B_TEMPLATE_ID=<mothership-shell-template-ref>
110+
111+
# Daytona
112+
DAYTONA_SHELL_SNAPSHOT_ID=<mothership-shell-snapshot-ref>
113+
```
114+
115+
These values are selected only for workflow Copilot and workspace Mothership
116+
code-tool calls. They never replace or act as a fallback for
117+
`E2B_FUNCTION_TEMPLATE_ID` or
118+
`DAYTONA_FUNCTION_SNAPSHOT_ID`; Function blocks and custom workspace sandboxes
119+
continue to use the dedicated Function base.
120+
102121
Use E2B as the release baseline before building or promoting Daytona:
103122

104123
```bash

apps/sim/.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
2727

2828
# Remote Function sandboxes (Optional)
2929
# Build a dedicated Function base before enabling JavaScript imports, Python, or Shell.
30-
# Mothership shell images are separate and are intentionally never used as a fallback.
30+
# Copilot/Mothership code tools use a separate shell image. It is selected only
31+
# for those code-tool calls and is never a fallback for Function blocks.
3132
# SANDBOXES_ENABLED=true # Enables custom sandboxes for self-hosted workspaces
3233
# NEXT_PUBLIC_SANDBOXES_ENABLED=true # Shows remote Function languages and custom sandbox settings; set only after the provider/base is ready
3334
#
@@ -38,12 +39,14 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
3839
# E2B_API_KEY=
3940
# E2B_FUNCTION_TEMPLATE_ID=<sim-function-template>:<sim-function-build-id> # Copy the exact ref printed by the builder
4041
# E2B_FUNCTION_TEMPLATE_GENERATION=<release-epoch-ms> # Copy the monotonic generation printed by the builder
42+
# MOTHERSHIP_E2B_TEMPLATE_ID= # Mothership shell template ref, required when Mothership runs code on E2B
4143
#
4244
# Daytona
4345
# Build from an accepted E2B parity manifest with: bun run apps/sim/scripts/build-function-daytona-snapshot.ts --name <name> --parity-manifest <path>
4446
# SANDBOX_PROVIDER=daytona
4547
# DAYTONA_API_KEY=
4648
# DAYTONA_FUNCTION_SNAPSHOT_ID=<snapshot-uuid> # Copy the immutable ID printed by the Daytona builder
49+
# DAYTONA_SHELL_SNAPSHOT_ID= # Mothership shell snapshot ref, required when Mothership runs code on Daytona
4750

4851
# Security (Required)
4952
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ describe('Function Execute API Route', () => {
125125
beforeEach(() => {
126126
vi.clearAllMocks()
127127
envFlagsMock.isRemoteSandboxEnabled = false
128+
envFlagsMock.isMothershipSandboxEnabled = false
128129

129130
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
130131
success: true,
@@ -210,6 +211,86 @@ describe('Function Execute API Route', () => {
210211
expect(data.output.result).toBe('test')
211212
})
212213

214+
it('does not accept a Mothership sandbox profile from the request body', async () => {
215+
const req = createMockRequest('POST', {
216+
code: 'return "test"',
217+
sandboxProfile: 'mothership',
218+
})
219+
220+
const response = await POST(req)
221+
222+
expect(response.status).toBe(200)
223+
expect(mockExecuteInIsolatedVM).toHaveBeenCalledTimes(1)
224+
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
225+
})
226+
227+
it('fails closed when a trusted Mothership call has no configured image', async () => {
228+
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({
229+
success: true,
230+
userId: 'user-123',
231+
authType: 'internal_jwt',
232+
sandboxProfile: 'mothership',
233+
})
234+
235+
const response = await POST(
236+
createMockRequest('POST', { code: 'return "test"', language: 'javascript' })
237+
)
238+
239+
expect(response.status).toBe(503)
240+
await expect(response.json()).resolves.toMatchObject({
241+
success: false,
242+
error: 'Mothership code sandbox is not configured',
243+
})
244+
expect(mockExecuteInIsolatedVM).not.toHaveBeenCalled()
245+
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
246+
})
247+
248+
it.each([
249+
{ language: 'javascript', code: 'return 42' },
250+
{ language: 'python', code: '__sim_result__ = 42' },
251+
])(
252+
'runs trusted Mothership $language in the Mothership sandbox image',
253+
async ({ language, code }) => {
254+
envFlagsMock.isMothershipSandboxEnabled = true
255+
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({
256+
success: true,
257+
userId: 'user-123',
258+
authType: 'internal_jwt',
259+
sandboxProfile: 'mothership',
260+
})
261+
262+
const response = await POST(createMockRequest('POST', { code, language }))
263+
264+
expect(response.status).toBe(200)
265+
expect(mockExecuteInSandbox).toHaveBeenCalledWith(
266+
expect.objectContaining({
267+
language,
268+
sandboxKind: 'mothership',
269+
})
270+
)
271+
expect(mockExecuteInIsolatedVM).not.toHaveBeenCalled()
272+
}
273+
)
274+
275+
it('runs trusted Mothership Shell in the Mothership sandbox image', async () => {
276+
envFlagsMock.isMothershipSandboxEnabled = true
277+
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({
278+
success: true,
279+
userId: 'user-123',
280+
authType: 'internal_jwt',
281+
sandboxProfile: 'mothership',
282+
})
283+
284+
const response = await POST(
285+
createMockRequest('POST', { code: 'echo ready', language: 'shell' })
286+
)
287+
288+
expect(response.status).toBe(200)
289+
expect(mockExecuteShellInSandbox).toHaveBeenCalledWith(
290+
expect.objectContaining({ sandboxKind: 'mothership' })
291+
)
292+
})
293+
213294
it('should prevent VM escape via constructor chain', async () => {
214295
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: undefined, stdout: '' })
215296

apps/sim/app/api/function/execute/route.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
validateWorkspaceFileWriteTarget,
1717
writeWorkspaceFileByPath,
1818
} from '@/lib/copilot/vfs/resource-writer'
19-
import { isRemoteSandboxEnabled } from '@/lib/core/config/env-flags'
19+
import { isMothershipSandboxEnabled, isRemoteSandboxEnabled } from '@/lib/core/config/env-flags'
2020
import {
2121
createTimeoutAbortController,
2222
isTimeoutAbortReason,
@@ -1687,6 +1687,16 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
16871687
logger.warn(`[${requestId}] Unauthorized function execution attempt`)
16881688
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
16891689
}
1690+
const usesMothershipSandbox = auth.sandboxProfile === 'mothership'
1691+
if (usesMothershipSandbox && !isMothershipSandboxEnabled) {
1692+
return NextResponse.json(
1693+
{ success: false, error: 'Mothership code sandbox is not configured' },
1694+
{ status: 503 }
1695+
)
1696+
}
1697+
const remoteSandboxEnabled = usesMothershipSandbox
1698+
? isMothershipSandboxEnabled
1699+
: isRemoteSandboxEnabled
16901700

16911701
executionDeadlineAt = parseExecutionDeadlineHeader(req.headers)
16921702
includePrivateResolvedSecretNames = requestsPrivateToolMetadata(
@@ -1886,7 +1896,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
18861896
}
18871897

18881898
if (lang === CodeLanguage.Shell) {
1889-
if (!isRemoteSandboxEnabled) {
1899+
if (!remoteSandboxEnabled) {
18901900
throw new Error(
18911901
'Shell execution requires a remote code sandbox to be enabled. Please contact your administrator to enable it.'
18921902
)
@@ -1901,7 +1911,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
19011911
}
19021912

19031913
logger.info(`[${requestId}] E2B shell execution`, {
1904-
enabled: isRemoteSandboxEnabled,
1914+
enabled: remoteSandboxEnabled,
19051915
hasApiKey: Boolean(process.env.E2B_API_KEY),
19061916
envVarCount: Object.keys(shellEnvs).length,
19071917
})
@@ -1924,6 +1934,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
19241934
outputSandboxPaths,
19251935
workspaceId,
19261936
sandboxId: selectedSandboxId,
1937+
...(usesMothershipSandbox ? { sandboxKind: 'mothership' as const } : {}),
19271938
signal: executionSignal,
19281939
})
19291940
const executionTime = Date.now() - execStart
@@ -1971,22 +1982,23 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
19711982
)
19721983
}
19731984

1974-
if (lang === CodeLanguage.Python && !isRemoteSandboxEnabled) {
1985+
if (lang === CodeLanguage.Python && !remoteSandboxEnabled) {
19751986
throw new Error(
19761987
'Python execution requires a remote code sandbox to be enabled. Please contact your administrator to enable it, or use JavaScript instead.'
19771988
)
19781989
}
19791990

1980-
if (lang === CodeLanguage.JavaScript && hasImports && !isRemoteSandboxEnabled) {
1991+
if (lang === CodeLanguage.JavaScript && hasImports && !remoteSandboxEnabled) {
19811992
throw new Error(
19821993
'JavaScript code with import statements requires a remote code sandbox to be enabled. Please remove the import statements, or contact your administrator to enable it.'
19831994
)
19841995
}
19851996

19861997
const useRemoteSandbox =
1987-
isRemoteSandboxEnabled &&
1988-
!isCustomTool &&
1989-
(lang === CodeLanguage.Python || (lang === CodeLanguage.JavaScript && hasImports))
1998+
usesMothershipSandbox ||
1999+
(remoteSandboxEnabled &&
2000+
!isCustomTool &&
2001+
(lang === CodeLanguage.Python || (lang === CodeLanguage.JavaScript && hasImports)))
19902002

19912003
if (useRemoteSandbox && containsLargeValueRef(contextVariables)) {
19922004
throw new Error(
@@ -2004,7 +2016,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
20042016
!useRemoteSandbox &&
20052017
(outputSandboxPaths.length > 0 || outputSandboxPath || _sandboxFiles?.length)
20062018
) {
2007-
const remediation = !isRemoteSandboxEnabled
2019+
const remediation = !remoteSandboxEnabled
20082020
? "No remote code sandbox is enabled on this deployment, so there is no sandbox filesystem for any language. Pass input data via params and return output as the code's return value with outputs.files[].path (no sandboxPath)."
20092021
: isCustomTool
20102022
? "custom tools always run in the isolated JavaScript VM, which has no sandbox filesystem. Pass input data via params and return output as the code's return value."
@@ -2022,7 +2034,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
20222034

20232035
if (useRemoteSandbox) {
20242036
logger.info(`[${requestId}] E2B status`, {
2025-
enabled: isRemoteSandboxEnabled,
2037+
enabled: remoteSandboxEnabled,
20262038
hasApiKey: Boolean(process.env.E2B_API_KEY),
20272039
language: lang,
20282040
})
@@ -2086,6 +2098,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
20862098
outputSandboxPaths,
20872099
workspaceId,
20882100
sandboxId: selectedSandboxId,
2101+
...(usesMothershipSandbox ? { sandboxKind: 'mothership' as const } : {}),
20892102
signal: executionSignal,
20902103
})
20912104
const executionTime = Date.now() - execStart
@@ -2172,6 +2185,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
21722185
outputSandboxPaths,
21732186
workspaceId,
21742187
sandboxId: selectedSandboxId,
2188+
...(usesMothershipSandbox ? { sandboxKind: 'mothership' as const } : {}),
21752189
signal: executionSignal,
21762190
})
21772191
const executionTime = Date.now() - execStart

apps/sim/lib/auth/hybrid.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ vi.mock('@/lib/auth/internal', () => ({
3030
verifyInternalToken: mockVerifyInternalToken,
3131
}))
3232

33-
import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
33+
import { AuthType, checkHybridAuth, checkInternalAuth } from '@/lib/auth/hybrid'
3434

3535
function createRequest(headers: Record<string, string>): NextRequest {
3636
return new NextRequest('http://localhost/api/test', { headers })
@@ -111,4 +111,23 @@ describe('checkHybridAuth credential precedence', () => {
111111
expect(mockAuthenticateApiKeyFromHeader).not.toHaveBeenCalled()
112112
expect(mockGetSession).not.toHaveBeenCalled()
113113
})
114+
115+
it('propagates a signed Mothership sandbox profile through internal auth', async () => {
116+
mockVerifyInternalToken.mockResolvedValue({
117+
valid: true,
118+
userId: 'internal-user',
119+
sandboxProfile: 'mothership',
120+
})
121+
122+
const result = await checkInternalAuth(
123+
createRequest({ authorization: 'Bearer internal-token' })
124+
)
125+
126+
expect(result).toEqual({
127+
success: true,
128+
userId: 'internal-user',
129+
authType: AuthType.INTERNAL_JWT,
130+
sandboxProfile: 'mothership',
131+
})
132+
})
114133
})

apps/sim/lib/auth/hybrid.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
22
import type { NextRequest } from 'next/server'
33
import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service'
44
import { getSession } from '@/lib/auth'
5-
import { verifyInternalToken } from '@/lib/auth/internal'
5+
import { type InternalSandboxProfile, verifyInternalToken } from '@/lib/auth/internal'
66

77
const logger = createLogger('HybridAuth')
88

@@ -36,6 +36,7 @@ export interface AuthResult {
3636
userEmail?: string | null
3737
authType?: AuthTypeValue
3838
apiKeyType?: 'personal' | 'workspace'
39+
sandboxProfile?: InternalSandboxProfile
3940
error?: string
4041
}
4142

@@ -44,18 +45,27 @@ export interface AuthResult {
4445
* Only trusts the userId embedded in the JWT payload — never from user-controlled sources.
4546
*/
4647
function resolveUserFromJwt(
47-
verificationUserId: string | null,
48+
verification: { userId?: string; sandboxProfile?: InternalSandboxProfile },
4849
options: { requireWorkflowId?: boolean }
4950
): AuthResult {
50-
if (verificationUserId) {
51-
return { success: true, userId: verificationUserId, authType: AuthType.INTERNAL_JWT }
51+
if (verification.userId) {
52+
return {
53+
success: true,
54+
userId: verification.userId,
55+
authType: AuthType.INTERNAL_JWT,
56+
...(verification.sandboxProfile ? { sandboxProfile: verification.sandboxProfile } : {}),
57+
}
5258
}
5359

5460
if (options.requireWorkflowId !== false) {
5561
return { success: false, error: 'userId required but not present in JWT' }
5662
}
5763

58-
return { success: true, authType: AuthType.INTERNAL_JWT }
64+
return {
65+
success: true,
66+
authType: AuthType.INTERNAL_JWT,
67+
...(verification.sandboxProfile ? { sandboxProfile: verification.sandboxProfile } : {}),
68+
}
5969
}
6070

6171
/**
@@ -96,7 +106,7 @@ export async function checkInternalAuth(
96106
return { success: false, error: 'Invalid internal token' }
97107
}
98108

99-
return resolveUserFromJwt(verification.userId || null, options)
109+
return resolveUserFromJwt(verification, options)
100110
} catch (error) {
101111
logger.error('Error in internal authentication:', error)
102112
return {
@@ -136,7 +146,7 @@ export async function checkSessionOrInternalAuth(
136146
const verification = await verifyInternalToken(token)
137147

138148
if (verification.valid) {
139-
return resolveUserFromJwt(verification.userId || null, options)
149+
return resolveUserFromJwt(verification, options)
140150
}
141151
}
142152

@@ -184,7 +194,7 @@ export async function checkHybridAuth(
184194
const verification = await verifyInternalToken(token)
185195

186196
if (verification.valid) {
187-
return resolveUserFromJwt(verification.userId || null, options)
197+
return resolveUserFromJwt(verification, options)
188198
}
189199
}
190200

0 commit comments

Comments
 (0)