the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 200 lines 5.6 kB view raw
1import { consola } from "consola"; 2import type { Context, Session } from "context"; 3import { eq, or } from "drizzle-orm"; 4import decrypt from "lib/decrypt"; 5import { createListener } from "pty/pty-tunnel"; 6import schema from "schema"; 7import { Sandbox } from "@vercel/sandbox"; 8import type { Command } from "@vercel/sandbox"; 9import { $ } from "zx"; 10import chalk from "chalk"; 11import crypto from "crypto"; 12import fs from "fs/promises"; 13import path from "node:path"; 14 15const TERM = "xterm-256color"; 16const PTY_SERVER_DOWNLOAD_URL = 17 "https://github.com/tsirysndr/pty-tunnel-server/releases/download/v0.0.2/pty-server-linux-x86_64.tar.gz"; 18const SERVER_BIN_NAME = "pty-tunnel-server"; 19const PTY_PORT = 26661; 20 21type SandboxEnvironmentOptions = { 22 id: string; 23 vercelApiToken: string; 24 vercelProjectId: string; 25 vercelTeamId: string; 26}; 27 28async function checkIfServerInstalled(sandbox: Sandbox) { 29 const exists = await sandbox.runCommand({ 30 cmd: "command", 31 args: ["-v", SERVER_BIN_NAME], 32 }); 33 return exists.exitCode === 0; 34} 35 36async function setupSandboxEnvironment( 37 options: SandboxEnvironmentOptions, 38): Promise<{ sandbox: Sandbox; cmd: Command }> { 39 const sandbox = await Sandbox.get({ 40 sandboxId: options.id, 41 token: options.vercelApiToken, 42 projectId: options.vercelProjectId, 43 teamId: options.vercelTeamId, 44 }); 45 46 if (!(await checkIfServerInstalled(sandbox))) { 47 await $`bash -c "type /tmp/${SERVER_BIN_NAME} || curl -L ${PTY_SERVER_DOWNLOAD_URL} | tar xz -C /tmp"`; 48 49 consola.info( 50 "Uploading pty-tunnel server binary to sandbox", 51 chalk.greenBright(options.id), 52 ); 53 54 const pathname = path.join("/tmp", `pty-server-${crypto.randomUUID()}`); 55 await sandbox.writeFiles([ 56 { 57 path: pathname, 58 content: await fs.readFile(`/tmp/${SERVER_BIN_NAME}`), 59 }, 60 ]); 61 62 consola.info( 63 "Setting up pty-tunnel server binary in sandbox", 64 chalk.greenBright(options.id), 65 ); 66 67 await sandbox.runCommand({ 68 cmd: "bash", 69 args: [ 70 "-c", 71 `mv "${pathname}" /usr/local/bin/${SERVER_BIN_NAME}; chmod +x /usr/local/bin/${SERVER_BIN_NAME}`, 72 ], 73 sudo: true, 74 }); 75 76 consola.info( 77 "Pty-tunnel server binary set up in sandbox", 78 chalk.greenBright(options.id), 79 ); 80 } 81 82 consola.info( 83 "Starting pty-tunnel server in sandbox", 84 chalk.greenBright(options.id), 85 ); 86 87 const cmd = await sandbox.runCommand({ 88 cmd: SERVER_BIN_NAME, 89 args: [ 90 `--port=${PTY_PORT}`, 91 `--mode=client`, 92 `--cols=${process.stdout.columns ?? 80}`, 93 `--rows=${process.stdout.rows ?? 24}`, 94 "bash", 95 ], 96 env: { 97 TERM, 98 PS1: `\\[\\e[2m\\]\\w/\\[\\e[0m\\] `, 99 }, 100 detached: true, 101 }); 102 103 consola.info( 104 "Sandbox environment set up for sandbox", 105 chalk.greenBright(options.id), 106 ); 107 108 return { sandbox, cmd }; 109} 110 111export async function createTerminalSession( 112 ctx: Context, 113 id: string, 114 key = id, 115) { 116 const [record] = await ctx.db 117 .select() 118 .from(schema.sandboxes) 119 .leftJoin( 120 schema.vercelAuth, 121 eq(schema.vercelAuth.sandboxId, schema.sandboxes.id), 122 ) 123 .where(or(eq(schema.sandboxes.id, id), eq(schema.sandboxes.sandboxId, id))) 124 .execute(); 125 126 if (!record?.vercel_auth) { 127 consola.error("Vercel auth not found for sandbox", { id }); 128 throw new Error("Vercel auth not found for sandbox " + id); 129 } 130 131 if (!record.sandboxes.sandboxId) { 132 consola.error("Sandbox ID not found for sandbox", { id }); 133 throw new Error("Sandbox ID not found for sandbox " + id); 134 } 135 136 const { sandbox, cmd } = await setupSandboxEnvironment({ 137 id: record.sandboxes.sandboxId, 138 vercelApiToken: decrypt(record.vercel_auth.vercelToken), 139 vercelProjectId: record.vercel_auth.projectId, 140 vercelTeamId: record.vercel_auth.teamId, 141 }); 142 143 const listener = createListener(); 144 145 // Pipe the pty-tunnel-server's stdout into the listener so 146 // readConnectionInfo() can parse the JSON connection handshake. 147 // We also accept stderr in case the binary writes there instead. 148 (async () => { 149 for await (const log of cmd.logs()) { 150 consola.debug(`pty-tunnel-server [${log.stream}]:`, log.data.trimEnd()); 151 if (log.stream === "stdout") { 152 // jsonlines parser requires newline-terminated data 153 const data = log.data.endsWith("\n") ? log.data : log.data + "\n"; 154 listener.stdoutStream.write(data); 155 } 156 } 157 listener.stdoutStream.end(); 158 })().catch((err) => 159 consola.error("pty-tunnel-server log stream error:", err), 160 ); 161 162 const details = await listener.connection; 163 const url = 164 `wss://${sandbox.domain(PTY_PORT).replace(/^https?:\/\//, "")}` as const; 165 consola.info("Connecting to WebSocket URL:", url); 166 167 const socket = details.createClient(url); 168 169 const session: Session = { 170 socket, 171 clients: new Set(), 172 wsClients: new Set(), 173 }; 174 175 socket.addEventListener("message", async ({ data }) => { 176 const text = data.toString("utf-8"); 177 for (const res of session.clients) { 178 res.write(`event: output\n`); 179 res.write(`data: ${JSON.stringify({ data: text })}\n\n`); 180 } 181 for (const ws of session.wsClients) { 182 if (ws.readyState === ws.OPEN) ws.send(text); 183 } 184 }); 185 186 socket.addEventListener("close", () => { 187 ctx.sessions.delete(key); 188 for (const ws of session.wsClients) { 189 if (ws.readyState === ws.OPEN) ws.close(1000, "exit"); 190 } 191 session.clients.clear(); 192 session.wsClients.clear(); 193 }); 194 195 await socket.waitForOpen(); 196 socket.sendMessage({ type: "ready" }); 197 198 ctx.sessions.set(key, session); 199 return session; 200}