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.

Pipe pty-tunnel-server stdout into listener

Return sandbox and cmd from setup so cmd logs can be streamed into the
listener for parsing the JSON connection handshake. Also default
cols/rows
to 80x24, send a ready message after socket opens, collapse resize into
a
single message, and log/close the server stdout stream on error/end.

+24 -13
+24 -13
apps/api/src/pty/index.ts
··· 11 11 import crypto from "node:crypto"; 12 12 import fs from "fs/promises"; 13 13 import { createListener } from "./pty-tunnel"; 14 - import { Sandbox, type Command } from "@vercel/sandbox"; 14 + import { Sandbox } from "@vercel/sandbox"; 15 + import type { Command } from "@vercel/sandbox"; 15 16 import type { ListenerSocket } from "./pty-tunnel/websocket"; 16 17 import { $ } from "zx"; 17 18 ··· 70 71 71 72 async function setupSandboxEnvironment( 72 73 options: SandboxEnvironmentOptions, 73 - ): Promise<Sandbox> { 74 + ): Promise<{ sandbox: Sandbox; cmd: Command }> { 74 75 const sandbox = await Sandbox.get({ 75 76 sandboxId: options.id, 76 77 token: options.vercelApiToken, ··· 107 108 108 109 consola.info("Starting pty-tunnel server in sandbox", options.id); 109 110 110 - await sandbox.runCommand({ 111 + const cmd = await sandbox.runCommand({ 111 112 cmd: SERVER_BIN_NAME, 112 113 args: [ 113 114 `--port=${sandbox.interactivePort}`, 114 115 `--mode=client`, 115 - `--cols=${process.stdout.columns}`, 116 - `--rows=${process.stdout.rows}`, 116 + `--cols=${process.stdout.columns ?? 80}`, 117 + `--rows=${process.stdout.rows ?? 24}`, 117 118 ], 118 119 env: { 119 120 TERM, ··· 124 125 125 126 consola.info("Sandbox environment set up for sandbox", options.id); 126 127 127 - return sandbox; 128 + return { sandbox, cmd }; 128 129 } 129 130 130 131 async function createTerminalSession(ctx: Context, id: string) { ··· 148 149 throw new Error("Sandbox ID not found for sandbox " + id); 149 150 } 150 151 151 - const sandbox = await setupSandboxEnvironment({ 152 + const { sandbox, cmd } = await setupSandboxEnvironment({ 152 153 id: record.sandboxes.sandboxId, 153 154 vercelApiToken: decrypt(record.vercel_auth.vercelToken), 154 155 vercelProjectId: record.vercel_auth.projectId, ··· 156 157 }); 157 158 158 159 const listener = createListener(); 160 + 161 + // Pipe the pty-tunnel-server's stdout (running inside the sandbox) into the 162 + // listener so readConnectionInfo() can parse the JSON connection handshake. 163 + (async () => { 164 + for await (const log of cmd.logs()) { 165 + if (log.stream === "stdout") { 166 + listener.stdoutStream.write(log.data); 167 + } 168 + } 169 + listener.stdoutStream.end(); 170 + })().catch((err) => 171 + consola.error("pty-tunnel-server log stream error:", err), 172 + ); 173 + 159 174 const details = await listener.connection; 160 175 const url = 161 176 `wss://${sandbox.domain(sandbox.interactivePort!).replace(/^https?:\/\//, "")}` as const; ··· 178 193 }); 179 194 180 195 await socket.waitForOpen(); 196 + socket.sendMessage({ type: "ready" }); 181 197 182 198 sessions.set(id, session); 183 199 return session; ··· 233 249 return; 234 250 } 235 251 236 - session.socket.sendMessage({ type: "ready" }); 237 - session.socket.sendMessage({ 238 - type: "resize", 239 - cols, 240 - rows, 241 - }); 252 + session.socket.sendMessage({ type: "resize", cols, rows }); 242 253 res.status(204).end(); 243 254 }); 244 255