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