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 203 lines 6.1 kB view raw
1import type { Context, Session } from "context"; 2import { eq, or } from "drizzle-orm"; 3import schema from "schema"; 4import { consola } from "consola"; 5import { Devbox, RunloopSDK } from "@runloop/api-client"; 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"; 12 13const TERM = "xterm-256color"; 14const PTY_SERVER_DOWNLOAD_URL = 15 "https://github.com/tsirysndr/pty-tunnel-server/releases/download/v0.0.2/pty-server-linux-x86_64.tar.gz"; 16const SERVER_BIN_NAME = "pty-tunnel-server"; 17const PTY_PORT = 26661; 18 19type SandboxEnvironmentOptions = { 20 id: string; 21 apiKey: string; 22}; 23 24async function checkIfServerInstalled(sandbox: Devbox) { 25 const exists = await sandbox.cmd.exec(`command -v ${SERVER_BIN_NAME}`); 26 return exists.exitCode === 0; 27} 28 29async function setupSandboxEnvironment( 30 options: SandboxEnvironmentOptions, 31 stdout: (data: string) => void, 32 stderr: (data: string) => void, 33): Promise<{ sandbox: Devbox }> { 34 const sdk = new RunloopSDK({ 35 bearerToken: options.apiKey, 36 }); 37 consola.info("Runloop: fetching sandbox", chalk.greenBright(options.id)); 38 const sandbox = sdk.devbox.fromId(options.id); 39 consola.info("Runloop: sandbox fetched", chalk.greenBright(options.id)); 40 41 consola.info( 42 "Runloop: checking pty-tunnel-server", 43 chalk.greenBright(options.id), 44 ); 45 if (!(await checkIfServerInstalled(sandbox))) { 46 await $`bash -c "type /tmp/${SERVER_BIN_NAME} || curl -L ${PTY_SERVER_DOWNLOAD_URL} | tar xz -C /tmp"`; 47 48 consola.info( 49 "Uploading pty-tunnel server binary to sandbox", 50 chalk.greenBright(options.id), 51 ); 52 53 const pathname = `pty-server-${crypto.randomUUID()}`; 54 await sandbox.file.upload({ 55 path: pathname, 56 file: new File( 57 [await fs.readFile(`/tmp/${SERVER_BIN_NAME}`)], 58 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.cmd.exec( 68 `bash -c mv "${pathname}" /usr/bin/${SERVER_BIN_NAME} || sudo mv "${pathname}" /usr/bin/${SERVER_BIN_NAME}; chmod a+x /usr/bin/${SERVER_BIN_NAME} || sudo chmod a+x /usr/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 await sandbox.cmd.execAsync( 83 `bash -c "TERM==${TERM} ${SERVER_BIN_NAME} --port=${PTY_PORT} --mode=client --cols=${process.stdout.columns ?? 80} --rows=${process.stdout.rows ?? 24} bash"`, 84 { 85 stdout: stdout, 86 stderr: stderr, 87 }, 88 ); 89 90 consola.info( 91 "Runloop: pty-tunnel-server process started", 92 chalk.greenBright(options.id), 93 ); 94 95 return { sandbox }; 96} 97 98export async function createTerminalSession( 99 ctx: Context, 100 id: string, 101 key = id, 102) { 103 const [record] = await ctx.db 104 .select() 105 .from(schema.sandboxes) 106 .leftJoin( 107 schema.runloopAuth, 108 eq(schema.runloopAuth.sandboxId, schema.sandboxes.id), 109 ) 110 .where(or(eq(schema.sandboxes.id, id), eq(schema.sandboxes.sandboxId, id))) 111 .execute(); 112 113 if (!record?.runloop_auth) { 114 consola.error("Runloop auth not found for sandbox", { id }); 115 throw new Error("Runloop auth not found for sandbox " + id); 116 } 117 118 if (!record.sandboxes.sandboxId) { 119 consola.error("Sandbox ID not found for sandbox", { id }); 120 throw new Error("Sandbox ID not found for sandbox " + id); 121 } 122 123 const listener = createListener(); 124 125 // setup the sandbox environment for pty-tunnel server 126 const { sandbox } = await setupSandboxEnvironment( 127 { 128 id: record.sandboxes.sandboxId, 129 apiKey: decrypt(record.runloop_auth.apiKey), 130 }, 131 // Pipe the pty-tunnel-server's stdout into the listener so 132 // readConnectionInfo() can parse the JSON connection handshake. 133 // We also accept stderr in case the binary writes there instead. 134 (data) => { 135 consola.debug(`pty-tunnel-server [stdout]:`, data.trimEnd()); 136 // jsonlines parser requires newline-terminated data 137 const chunk = data.endsWith("\n") ? data : data + "\n"; 138 listener.stdoutStream.write(chunk); 139 }, 140 (data) => consola.debug(`pty-tunnel-server [stderr]:`, data.trimEnd()), 141 ); 142 143 consola.info("Runloop: fetching sandbox tunnels", chalk.greenBright(id)); 144 const tunnel = (await sandbox.getInfo()).tunnel; 145 console.log( 146 `Tunnel URL for port ${PTY_PORT}: https://${PTY_PORT}-${tunnel?.tunnel_key}.tunnel.runloop.ai`, 147 ); 148 consola.info( 149 "Runloop: awaiting pty-tunnel connection info", 150 chalk.greenBright(id), 151 ); 152 const details = await listener.connection; 153 consola.info( 154 "Runloop: pty-tunnel connection info received", 155 chalk.greenBright(id), 156 ); 157 158 const url = 159 `wss://${PTY_PORT}-${tunnel?.tunnel_key}.tunnel.runloop.ai` as const; 160 consola.info("Connecting to WebSocket URL:", url); 161 162 const socket = details.createClient(url); 163 164 const session: Session = { 165 socket, 166 clients: new Set(), 167 wsClients: new Set(), 168 }; 169 170 socket.addEventListener("message", async ({ data }) => { 171 const text = data.toString("utf-8"); 172 for (const res of session.clients) { 173 res.write(`event: output\n`); 174 res.write(`data: ${JSON.stringify({ data: text })}\n\n`); 175 } 176 for (const ws of session.wsClients) { 177 if (ws.readyState === ws.OPEN) ws.send(text); 178 } 179 }); 180 181 socket.addEventListener("close", () => { 182 ctx.sessions.delete(key); 183 for (const ws of session.wsClients) { 184 if (ws.readyState === ws.OPEN) ws.close(1000, "exit"); 185 } 186 session.clients.clear(); 187 session.wsClients.clear(); 188 }); 189 190 consola.info( 191 "Runloop: waiting for pty-tunnel socket to open", 192 chalk.greenBright(id), 193 ); 194 await socket.waitForOpen(); 195 consola.info( 196 "Runloop: pty-tunnel socket open, sending ready", 197 chalk.greenBright(id), 198 ); 199 socket.sendMessage({ type: "ready" }); 200 201 ctx.sessions.set(key, session); 202 return session; 203}