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.

Add Runloop PTY support

+283 -10
+33
apps/api/bun.lock
··· 17 17 "@atproto/xrpc-server": "^0.7.8", 18 18 "@fly/sprites": "^0.0.1", 19 19 "@hono/node-server": "^1.19.9", 20 + "@runloop/api-client": "^1.18.1", 20 21 "@tsndr/cloudflare-worker-jwt": "^3.2.1", 21 22 "@types/node": "^25.2.3", 22 23 "@types/prompts": "^2.4.9", ··· 398 399 399 400 "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w=="], 400 401 402 + "@runloop/api-client": ["@runloop/api-client@1.18.1", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7", "tar": "^7.5.2", "uuidv7": "^1.0.2", "zod": "^3.24.1" } }, "sha512-ku08FDuFPi/2T2JdryNl61FSh9zok4lzkiFyJuAbB6/tDobK9qt2AdlNr4KYaq3rwtNycIoBcy2GOomAlgwP7w=="], 403 + 401 404 "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], 402 405 403 406 "@ts-morph/common": ["@ts-morph/common@0.17.0", "", { "dependencies": { "fast-glob": "^3.2.11", "minimatch": "^5.1.0", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g=="], ··· 438 441 439 442 "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], 440 443 444 + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], 445 + 441 446 "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], 442 447 443 448 "@types/prompts": ["@types/prompts@2.4.9", "", { "dependencies": { "@types/node": "*", "kleur": "^3.0.3" } }, "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA=="], ··· 481 486 "abort-controller-x": ["abort-controller-x@0.4.3", "", {}, "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA=="], 482 487 483 488 "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 489 + 490 + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], 484 491 485 492 "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 486 493 ··· 712 719 713 720 "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], 714 721 722 + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], 723 + 724 + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], 725 + 715 726 "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 716 727 717 728 "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], ··· 755 766 "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], 756 767 757 768 "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], 769 + 770 + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], 758 771 759 772 "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], 760 773 ··· 917 930 "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], 918 931 919 932 "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], 933 + 934 + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 935 + 936 + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 920 937 921 938 "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], 922 939 ··· 1180 1197 1181 1198 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 1182 1199 1200 + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], 1201 + 1183 1202 "ts-error": ["ts-error@1.0.6", "", {}, "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="], 1184 1203 1185 1204 "ts-morph": ["ts-morph@16.0.0", "", { "dependencies": { "@ts-morph/common": "~0.17.0", "code-block-writer": "^11.0.3" } }, "sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw=="], ··· 1224 1243 1225 1244 "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], 1226 1245 1246 + "uuidv7": ["uuidv7@1.2.1", "", { "bin": { "uuidv7": "cli.js" } }, "sha512-4kPkK3/XTQW9Hbm4CaqfICn+kY9LJtDVEOfgsRRra/+n2Ofg4NqzRFceAkxvQ/Ud/6BpHOPzj8cirqM7TzTN5Q=="], 1247 + 1227 1248 "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], 1228 1249 1229 1250 "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], ··· 1238 1259 1239 1260 "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], 1240 1261 1262 + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], 1263 + 1264 + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], 1265 + 1266 + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], 1267 + 1241 1268 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1242 1269 1243 1270 "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], ··· 1335 1362 "@atproto/xrpc-server/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1336 1363 1337 1364 "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], 1365 + 1366 + "@runloop/api-client/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], 1367 + 1368 + "@runloop/api-client/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1338 1369 1339 1370 "@ts-morph/common/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], 1340 1371 ··· 1473 1504 "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], 1474 1505 1475 1506 "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], 1507 + 1508 + "@runloop/api-client/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], 1476 1509 1477 1510 "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1478 1511
+1
apps/api/package.json
··· 30 30 "@atproto/xrpc-server": "^0.7.8", 31 31 "@fly/sprites": "^0.0.1", 32 32 "@hono/node-server": "^1.19.9", 33 + "@runloop/api-client": "^1.18.1", 33 34 "@tsndr/cloudflare-worker-jwt": "^3.2.1", 34 35 "@types/node": "^25.2.3", 35 36 "@types/prompts": "^2.4.9",
+7
apps/api/src/pty/index.ts
··· 9 9 import * as vercel from "./vercel"; 10 10 import * as modal from "./modal"; 11 11 import * as e2b from "./e2b"; 12 + import * as runloop from "./runloop"; 12 13 import { WebSocketServer, type WebSocket } from "ws"; 13 14 import type { IncomingMessage } from "http"; 14 15 ··· 55 56 .select({ 56 57 modalAuth: schema.modalAuth.id, 57 58 e2bAuth: schema.e2bAuth.id, 59 + runloopAuth: schema.runloopAuth.id, 58 60 }) 59 61 .from(schema.sandboxes) 60 62 .leftJoin( ··· 62 64 eq(schema.modalAuth.sandboxId, schema.sandboxes.id), 63 65 ) 64 66 .leftJoin(schema.e2bAuth, eq(schema.e2bAuth.sandboxId, schema.sandboxes.id)) 67 + .leftJoin( 68 + schema.runloopAuth, 69 + eq(schema.runloopAuth.sandboxId, schema.sandboxes.id), 70 + ) 65 71 .where(or(eq(schema.sandboxes.id, id), eq(schema.sandboxes.sandboxId, id))) 66 72 .execute(); 67 73 68 74 if (record?.modalAuth) return modal.createTerminalSession(ctx, id, key); 69 75 if (record?.e2bAuth) return e2b.createTerminalSession(ctx, id, key); 76 + if (record?.runloopAuth) return runloop.createTerminalSession(ctx, id, key); 70 77 return vercel.createTerminalSession(ctx, id, key); 71 78 } 72 79
+204
apps/api/src/pty/runloop/index.ts
··· 1 + import type { Context, Session } from "context"; 2 + import { eq, or } from "drizzle-orm"; 3 + import schema from "schema"; 4 + import { consola } from "consola"; 5 + import { Devbox, RunloopSDK } from "@runloop/api-client"; 6 + import decrypt from "lib/decrypt"; 7 + import { createListener } from "pty/pty-tunnel"; 8 + import chalk from "chalk"; 9 + import { $ } from "zx"; 10 + import crypto from "crypto"; 11 + import fs from "fs/promises"; 12 + import path from "node:path"; 13 + 14 + const TERM = "xterm-256color"; 15 + const PTY_SERVER_DOWNLOAD_URL = 16 + "https://github.com/tsirysndr/pty-tunnel-server/releases/download/v0.0.2/pty-server-linux-x86_64.tar.gz"; 17 + const SERVER_BIN_NAME = "pty-tunnel-server"; 18 + const PTY_PORT = 26661; 19 + 20 + type SandboxEnvironmentOptions = { 21 + id: string; 22 + apiKey: string; 23 + }; 24 + 25 + async function checkIfServerInstalled(sandbox: Devbox) { 26 + const exists = await sandbox.cmd.exec(`command -v ${SERVER_BIN_NAME}`); 27 + return exists.exitCode === 0; 28 + } 29 + 30 + async function setupSandboxEnvironment( 31 + options: SandboxEnvironmentOptions, 32 + stdout: (data: string) => void, 33 + stderr: (data: string) => void, 34 + ): Promise<{ sandbox: Devbox }> { 35 + const sdk = new RunloopSDK({ 36 + bearerToken: options.apiKey, 37 + }); 38 + consola.info("Runloop: fetching sandbox", chalk.greenBright(options.id)); 39 + const sandbox = sdk.devbox.fromId(options.id); 40 + consola.info("Runloop: sandbox fetched", chalk.greenBright(options.id)); 41 + 42 + consola.info( 43 + "Runloop: checking pty-tunnel-server", 44 + chalk.greenBright(options.id), 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 = `pty-server-${crypto.randomUUID()}`; 55 + await sandbox.file.upload({ 56 + path: pathname, 57 + file: new File( 58 + [await fs.readFile(`/tmp/${SERVER_BIN_NAME}`)], 59 + SERVER_BIN_NAME, 60 + ), 61 + }); 62 + 63 + consola.info( 64 + "Setting up pty-tunnel server binary in sandbox", 65 + chalk.greenBright(options.id), 66 + ); 67 + 68 + await sandbox.cmd.exec( 69 + `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}`, 70 + ); 71 + 72 + consola.info( 73 + "Pty-tunnel server binary set up in sandbox", 74 + chalk.greenBright(options.id), 75 + ); 76 + } 77 + 78 + consola.info( 79 + "Starting pty-tunnel server in sandbox", 80 + chalk.greenBright(options.id), 81 + ); 82 + 83 + await sandbox.cmd.execAsync( 84 + `bash -c "TERM==${TERM} ${SERVER_BIN_NAME} --port=${PTY_PORT} --mode=client --cols=${process.stdout.columns ?? 80} --rows=${process.stdout.rows ?? 24} bash"`, 85 + { 86 + stdout: stdout, 87 + stderr: stderr, 88 + }, 89 + ); 90 + 91 + consola.info( 92 + "Runloop: pty-tunnel-server process started", 93 + chalk.greenBright(options.id), 94 + ); 95 + 96 + return { sandbox }; 97 + } 98 + 99 + export async function createTerminalSession( 100 + ctx: Context, 101 + id: string, 102 + key = id, 103 + ) { 104 + const [record] = await ctx.db 105 + .select() 106 + .from(schema.sandboxes) 107 + .leftJoin( 108 + schema.runloopAuth, 109 + eq(schema.runloopAuth.sandboxId, schema.sandboxes.id), 110 + ) 111 + .where(or(eq(schema.sandboxes.id, id), eq(schema.sandboxes.sandboxId, id))) 112 + .execute(); 113 + 114 + if (!record?.runloop_auth) { 115 + consola.error("Runloop auth not found for sandbox", { id }); 116 + throw new Error("Runloop auth not found for sandbox " + id); 117 + } 118 + 119 + if (!record.sandboxes.sandboxId) { 120 + consola.error("Sandbox ID not found for sandbox", { id }); 121 + throw new Error("Sandbox ID not found for sandbox " + id); 122 + } 123 + 124 + const listener = createListener(); 125 + 126 + // setup the sandbox environment for pty-tunnel server 127 + const { sandbox } = await setupSandboxEnvironment( 128 + { 129 + id: record.sandboxes.sandboxId, 130 + apiKey: decrypt(record.runloop_auth.apiKey), 131 + }, 132 + // Pipe the pty-tunnel-server's stdout into the listener so 133 + // readConnectionInfo() can parse the JSON connection handshake. 134 + // We also accept stderr in case the binary writes there instead. 135 + (data) => { 136 + consola.debug(`pty-tunnel-server [stdout]:`, data.trimEnd()); 137 + // jsonlines parser requires newline-terminated data 138 + const chunk = data.endsWith("\n") ? data : data + "\n"; 139 + listener.stdoutStream.write(chunk); 140 + }, 141 + (data) => consola.debug(`pty-tunnel-server [stderr]:`, data.trimEnd()), 142 + ); 143 + 144 + consola.info("Runloop: fetching sandbox tunnels", chalk.greenBright(id)); 145 + const tunnel = (await sandbox.getInfo()).tunnel; 146 + console.log( 147 + `Tunnel URL for port ${PTY_PORT}: https://${PTY_PORT}-${tunnel?.tunnel_key}.tunnel.runloop.ai`, 148 + ); 149 + consola.info( 150 + "Runloop: awaiting pty-tunnel connection info", 151 + chalk.greenBright(id), 152 + ); 153 + const details = await listener.connection; 154 + consola.info( 155 + "Runloop: pty-tunnel connection info received", 156 + chalk.greenBright(id), 157 + ); 158 + 159 + const url = 160 + `wss://${PTY_PORT}-${tunnel?.tunnel_key}.tunnel.runloop.ai` 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 + wsClients: new Set(), 169 + }; 170 + 171 + socket.addEventListener("message", async ({ data }) => { 172 + const text = data.toString("utf-8"); 173 + for (const res of session.clients) { 174 + res.write(`event: output\n`); 175 + res.write(`data: ${JSON.stringify({ data: text })}\n\n`); 176 + } 177 + for (const ws of session.wsClients) { 178 + if (ws.readyState === ws.OPEN) ws.send(text); 179 + } 180 + }); 181 + 182 + socket.addEventListener("close", () => { 183 + ctx.sessions.delete(key); 184 + for (const ws of session.wsClients) { 185 + if (ws.readyState === ws.OPEN) ws.close(1000, "exit"); 186 + } 187 + session.clients.clear(); 188 + session.wsClients.clear(); 189 + }); 190 + 191 + consola.info( 192 + "Runloop: waiting for pty-tunnel socket to open", 193 + chalk.greenBright(id), 194 + ); 195 + await socket.waitForOpen(); 196 + consola.info( 197 + "Runloop: pty-tunnel socket open, sending ready", 198 + chalk.greenBright(id), 199 + ); 200 + socket.sendMessage({ type: "ready" }); 201 + 202 + ctx.sessions.set(key, session); 203 + return session; 204 + }
+11 -2
apps/cli/src/cmd/ssh/index.ts
··· 70 70 process.exit(1); 71 71 } 72 72 73 - // export type Provider = "daytona" | "deno" | "cloudflare" | "vercel" | "sprites" | "modal" | "e2b"; 73 + // export type Provider = "daytona" | "deno" | "cloudflare" | "vercel" | "sprites" | "modal" | "e2b" | "runloop" | "hopx" | "blaxel"; 74 74 switch (sandbox.provider) { 75 75 case "cloudflare": 76 76 await cloudflare(sandbox); ··· 93 93 case "e2b": 94 94 await tty(sandbox, false); // pty 95 95 break; 96 + case "hopx": 97 + await tty(sandbox, false); // pty 98 + break; 99 + case "runloop": 100 + await tty(sandbox, false); // pty 101 + break; 102 + case "blaxel": 103 + await tty(sandbox, false); // pty 104 + break; 96 105 default: 97 106 consola.error( 98 107 `Sandbox ${chalk.yellowBright(sandbox.name)} uses provider ` + 99 108 `${chalk.cyan(sandbox.provider)}, but this command only supports ` + 100 - `${chalk.cyan("cloudflare")}, ${chalk.cyan("daytona")}, ${chalk.cyan("deno")}, ${chalk.cyan("vercel")}, ${chalk.cyan("sprites")}, ${chalk.cyan("modal")} or ${chalk.cyan("e2b")} sandboxes.`, 109 + `${chalk.cyan("cloudflare")}, ${chalk.cyan("daytona")}, ${chalk.cyan("deno")}, ${chalk.cyan("vercel")}, ${chalk.cyan("sprites")}, ${chalk.cyan("modal")}, ${chalk.cyan("hopx")}, ${chalk.cyan("runloop")}, ${chalk.cyan("blaxel")} or ${chalk.cyan("e2b")} sandboxes.`, 101 110 ); 102 111 process.exit(1); 103 112 }
+10 -2
apps/web/src/pages/projects/Project/Project.tsx
··· 121 121 }} 122 122 sandboxId={sandbox.id} 123 123 isCloudflare={sandbox.provider === "cloudflare"} 124 - isTty={["sprites", "vercel", "modal", "e2b"].includes( 124 + isTty={[ 125 + "sprites", 126 + "vercel", 127 + "modal", 128 + "e2b", 129 + "hopx", 130 + "runloop", 131 + "blaxel", 132 + ].includes(sandbox.provider)} 133 + pty={["vercel", "modal", "e2b", "hopx", "runloop", "blaxel"].includes( 125 134 sandbox.provider, 126 135 )} 127 - pty={["vercel", "modal", "e2b"].includes(sandbox.provider)} 128 136 worker={sandbox.baseSandbox} 129 137 /> 130 138 </td>
+17 -6
apps/web/src/pages/sandbox/Sandbox.tsx
··· 327 327 onClose={() => {}} 328 328 worker={data.sandbox.baseSandbox} 329 329 isCloudflare={data.sandbox.provider === "cloudflare"} 330 - isTty={["sprites", "vercel", "modal", "e2b"].includes( 331 - data.sandbox.provider, 332 - )} 333 - pty={["vercel", "modal", "e2b"].includes( 334 - data.sandbox.provider, 335 - )} 330 + isTty={[ 331 + "sprites", 332 + "vercel", 333 + "modal", 334 + "e2b", 335 + "hopx", 336 + "runloop", 337 + "blaxel", 338 + ].includes(data.sandbox.provider)} 339 + pty={[ 340 + "vercel", 341 + "modal", 342 + "e2b", 343 + "hopx", 344 + "runloop", 345 + "blaxel", 346 + ].includes(data.sandbox.provider)} 336 347 /> 337 348 )} 338 349 </div>