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
138 changes: 0 additions & 138 deletions src/core/server/actions/terminal-actions.ts

This file was deleted.

104 changes: 102 additions & 2 deletions src/core/server/api/routers/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server'
import { millisecondsInDay } from 'date-fns/constants'
import { Sandbox } from 'e2b'
import { Sandbox, TimeoutError } from 'e2b'
import { z } from 'zod'
import { authHeaders } from '@/configs/api'
import {
Expand All @@ -18,7 +19,11 @@ import { createTRPCRouter } from '@/core/server/trpc/init'
import { protectedTeamProcedure } from '@/core/server/trpc/procedures'
import { SandboxIdSchema } from '@/core/shared/schemas/api'
import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants'
import { TERMINAL_SANDBOX_TIMEOUT_MS } from '@/features/dashboard/terminal/constants'
import {
TERMINAL_SANDBOX_TIMEOUT_ERROR,
TERMINAL_SANDBOX_TIMEOUT_MS,
} from '@/features/dashboard/terminal/constants'
import { normalizeTerminalTemplate } from '@/features/dashboard/terminal/template'

const sandboxRepositoryProcedure = protectedTeamProcedure.use(
withTeamAuthedRequestRepository(
Expand Down Expand Up @@ -209,6 +214,101 @@ export const sandboxRouter = createTRPCRouter({

// MUTATIONS

// Runs the control-plane create/connect server-side so the user's
// account-level access token never reaches the browser. Returns only the
// sandbox-scoped envd credentials the client needs for PTY access.
openTerminal: protectedTeamProcedure
.input(
z.object({
template: z.string().min(1, 'Template is required'),
sandboxId: SandboxIdSchema.optional(),
requestTimeoutMs: z.number().int().positive().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { sandboxId, template, requestTimeoutMs } = input
const { session, teamId } = ctx

const normalizedTemplate = normalizeTerminalTemplate(template)
if (!normalizedTemplate) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Invalid terminal template',
})
}

const connectionOpts = {
apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
headers: authHeaders(session.access_token, teamId),
}

let resolvedSandboxId: string
try {
if (sandboxId) {
const sandbox = await Sandbox.connect(sandboxId, {
...connectionOpts,
timeoutMs: TERMINAL_SANDBOX_TIMEOUT_MS,
requestTimeoutMs,
})
resolvedSandboxId = sandbox.sandboxId
} else {
const sandbox = await Sandbox.create(normalizedTemplate, {
...connectionOpts,
timeoutMs: TERMINAL_SANDBOX_TIMEOUT_MS,
lifecycle: {
onTimeout: 'pause',
autoResume: true,
},
metadata: {
source: 'dashboard-terminal',
template: normalizedTemplate,
userId: session.user.id,
},
})
resolvedSandboxId = sandbox.sandboxId
}
} catch (error) {
// Surface timeouts with a stable sentinel so the client can rethrow a
// TimeoutError and let the attach-retry logic recognize them.
if (error instanceof TimeoutError) {
throw new TRPCError({
code: 'TIMEOUT',
message: TERMINAL_SANDBOX_TIMEOUT_ERROR,
cause: error,
})
}

throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: sandboxId
? 'Failed to connect to terminal sandbox'
: 'Failed to create terminal sandbox',
cause: error,
})
}

// `Sandbox.create`/`connect` build a full SDK instance but only expose
// the sandbox id/domain publicly; fetch the envd credentials via the
// public info endpoint rather than reading the SDK's internal fields.
const info = await Sandbox.getFullInfo(resolvedSandboxId, connectionOpts)

if (!info.envdAccessToken) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Sandbox is not ready for terminal access',
})
}

return {
sandboxId: resolvedSandboxId,
sandboxDomain: info.sandboxDomain,
envdVersion: info.envdVersion,
envdAccessToken: info.envdAccessToken,
}
}),

killTerminalPty: protectedTeamProcedure
.input(
z.object({
Expand Down
6 changes: 3 additions & 3 deletions src/features/dashboard/terminal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const TERMINAL_ATTACH_ATTEMPT_TIMEOUT_MS = 15_000
export const TERMINAL_ATTACH_MAX_RETRIES = 3
export const TERMINAL_ATTACH_RETRY_BASE_DELAY_MS = 1500
export const TERMINAL_ATTACH_RETRY_MAX_DELAY_MS = 5000
// Sentinel server-error returned by openTerminalSandboxAction when the
// server-side control-plane connect times out, so the client can rethrow a
// TimeoutError and let the attach-retry logic recognize it.
// Sentinel error message returned by the sandbox.openTerminal tRPC mutation
// when the server-side control-plane connect times out, so the client can
// rethrow a TimeoutError and let the attach-retry logic recognize it.
export const TERMINAL_SANDBOX_TIMEOUT_ERROR = 'terminal-sandbox-timeout'
6 changes: 6 additions & 0 deletions src/features/dashboard/terminal/dashboard-terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { type CommandHandle, type Sandbox, TimeoutError } from 'e2b'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTRPCClient } from '@/trpc/client'
import { attachTerminalWithRetry } from './attach-terminal'
import {
DEFAULT_CWD,
Expand Down Expand Up @@ -52,6 +53,8 @@ export default function DashboardTerminal({
teamSlug,
userId,
}: DashboardTerminalProps) {
const trpcClient = useTRPCClient()

const [status, setStatus] = useState<TerminalStatus>('idle')
const [activeSandboxId, setActiveSandboxId] = useState<string>()
const [template, setTemplate] = useState(
Expand Down Expand Up @@ -332,6 +335,8 @@ export default function DashboardTerminal({
const terminalSandbox = await openTerminalSandbox({
forceNewSandbox: options.forceNewSandbox,
onStatus: appendOutput,
openTerminal: (mutationInput) =>
trpcClient.sandbox.openTerminal.mutate(mutationInput),
requestTimeoutMs: requestedSandboxId
? TERMINAL_ATTACH_ATTEMPT_TIMEOUT_MS
: undefined,
Expand Down Expand Up @@ -436,6 +441,7 @@ export default function DashboardTerminal({
getSandbox,
runCommand,
teamSlug,
trpcClient,
sandboxScoped,
template,
onSandboxAttached,
Expand Down
Loading
Loading