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.

Refactor PTY session handling into Vercel module

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