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 Modal provider to sandbox app

Implement ModalProvider and ModalSandbox, add modal as a dependency,
and update types and provider dispatch to support the "modal" provider.
Update lockfile with new dependency entries and native bindings.

+298 -4
+103 -1
apps/sandbox/deno.lock
··· 22 22 "npm:envalid@^8.1.1": "8.1.1", 23 23 "npm:hono@^4.11.9": "4.11.9", 24 24 "npm:libsodium-wrappers@~0.8.2": "0.8.2", 25 + "npm:modal@~0.7.4": "0.7.4", 25 26 "npm:pg@^8.18.0": "8.18.0", 26 27 "npm:ramda@0.32": "0.32.0", 27 28 "npm:unique-username-generator@^1.5.1": "1.5.1", ··· 585 586 }, 586 587 "@aws/lambda-invoke-store@0.2.3": { 587 588 "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==" 589 + }, 590 + "@cbor-extract/cbor-extract-darwin-arm64@2.2.0": { 591 + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", 592 + "os": ["darwin"], 593 + "cpu": ["arm64"] 594 + }, 595 + "@cbor-extract/cbor-extract-darwin-x64@2.2.0": { 596 + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", 597 + "os": ["darwin"], 598 + "cpu": ["x64"] 599 + }, 600 + "@cbor-extract/cbor-extract-linux-arm64@2.2.0": { 601 + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", 602 + "os": ["linux"], 603 + "cpu": ["arm64"] 604 + }, 605 + "@cbor-extract/cbor-extract-linux-arm@2.2.0": { 606 + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", 607 + "os": ["linux"], 608 + "cpu": ["arm"] 609 + }, 610 + "@cbor-extract/cbor-extract-linux-x64@2.2.0": { 611 + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", 612 + "os": ["linux"], 613 + "cpu": ["x64"] 614 + }, 615 + "@cbor-extract/cbor-extract-win32-x64@2.2.0": { 616 + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", 617 + "os": ["win32"], 618 + "cpu": ["x64"] 588 619 }, 589 620 "@daytonaio/api-client@0.141.0": { 590 621 "integrity": "sha512-DSPCurIEjfFyXCd07jkDgfsoFppVhTLyIJdvfb0LgG1EgV75BPqqzk2WM4ragBFJUuK2URF5CK7qkaHW0AXKMA==", ··· 1743 1774 "zod@3.24.4" 1744 1775 ] 1745 1776 }, 1777 + "abort-controller-x@0.4.3": { 1778 + "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==" 1779 + }, 1746 1780 "acorn-import-attributes@1.9.5_acorn@8.15.0": { 1747 1781 "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", 1748 1782 "dependencies": [ ··· 1820 1854 "function-bind" 1821 1855 ] 1822 1856 }, 1857 + "cbor-extract@2.2.0": { 1858 + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", 1859 + "dependencies": [ 1860 + "node-gyp-build-optional-packages" 1861 + ], 1862 + "optionalDependencies": [ 1863 + "@cbor-extract/cbor-extract-darwin-arm64", 1864 + "@cbor-extract/cbor-extract-darwin-x64", 1865 + "@cbor-extract/cbor-extract-linux-arm", 1866 + "@cbor-extract/cbor-extract-linux-arm64", 1867 + "@cbor-extract/cbor-extract-linux-x64", 1868 + "@cbor-extract/cbor-extract-win32-x64" 1869 + ], 1870 + "scripts": true, 1871 + "bin": true 1872 + }, 1873 + "cbor-x@1.6.0": { 1874 + "integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==", 1875 + "optionalDependencies": [ 1876 + "cbor-extract" 1877 + ] 1878 + }, 1823 1879 "chalk@5.6.2": { 1824 1880 "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" 1825 1881 }, ··· 1863 1919 }, 1864 1920 "delayed-stream@1.0.0": { 1865 1921 "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 1922 + }, 1923 + "detect-libc@2.1.2": { 1924 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" 1866 1925 }, 1867 1926 "dotenv@17.2.4": { 1868 1927 "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==" ··· 2234 2293 "minipass" 2235 2294 ] 2236 2295 }, 2296 + "modal@0.7.4": { 2297 + "integrity": "sha512-md/+L67tM1RazAt2xvLO+gUqRz6zllyYoNNiM8h+Eb1wLy7JzliH7vnx9f9Sq4zE3qQHENpX0Tjy/LSkIyrANA==", 2298 + "dependencies": [ 2299 + "cbor-x", 2300 + "long", 2301 + "nice-grpc", 2302 + "protobufjs", 2303 + "smol-toml", 2304 + "uuid" 2305 + ] 2306 + }, 2237 2307 "module-details-from-path@1.0.4": { 2238 2308 "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==" 2239 2309 }, 2240 2310 "ms@2.1.3": { 2241 2311 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 2242 2312 }, 2313 + "nice-grpc-common@2.0.2": { 2314 + "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==", 2315 + "dependencies": [ 2316 + "ts-error" 2317 + ] 2318 + }, 2319 + "nice-grpc@2.1.14": { 2320 + "integrity": "sha512-GK9pKNxlvnU5FAdaw7i2FFuR9CqBspcE+if2tqnKXBcE0R8525wj4BZvfcwj7FjvqbssqKxRHt2nwedalbJlww==", 2321 + "dependencies": [ 2322 + "@grpc/grpc-js", 2323 + "abort-controller-x", 2324 + "nice-grpc-common" 2325 + ] 2326 + }, 2327 + "node-gyp-build-optional-packages@5.1.1": { 2328 + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", 2329 + "dependencies": [ 2330 + "detect-libc" 2331 + ], 2332 + "bin": true 2333 + }, 2243 2334 "os-paths@4.4.0": { 2244 2335 "integrity": "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==" 2245 2336 }, ··· 2389 2480 "shell-quote@1.8.3": { 2390 2481 "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==" 2391 2482 }, 2483 + "smol-toml@1.6.1": { 2484 + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==" 2485 + }, 2392 2486 "source-map-support@0.5.21": { 2393 2487 "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 2394 2488 "dependencies": [ ··· 2474 2568 "is-number" 2475 2569 ] 2476 2570 }, 2571 + "ts-error@1.0.6": { 2572 + "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==" 2573 + }, 2477 2574 "tslib@2.8.1": { 2478 2575 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 2479 2576 }, ··· 2490 2587 "util-deprecate@1.0.2": { 2491 2588 "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 2492 2589 }, 2590 + "uuid@11.1.0": { 2591 + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 2592 + "bin": true 2593 + }, 2493 2594 "wrap-ansi@7.0.0": { 2494 2595 "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 2495 2596 "dependencies": [ ··· 2568 2669 "packageJson": { 2569 2670 "dependencies": [ 2570 2671 "npm:@aws-sdk/client-s3@^3.1024.0", 2571 - "npm:@fly/sprites@^0.0.1" 2672 + "npm:@fly/sprites@^0.0.1", 2673 + "npm:modal@~0.7.4" 2572 2674 ] 2573 2675 } 2574 2676 }
+2 -1
apps/sandbox/package.json
··· 1 1 { 2 2 "dependencies": { 3 3 "@aws-sdk/client-s3": "^3.1024.0", 4 - "@fly/sprites": "^0.0.1" 4 + "@fly/sprites": "^0.0.1", 5 + "modal": "^0.7.4" 5 6 } 6 7 }
+13 -1
apps/sandbox/src/providers/mod.ts
··· 30 30 abstract create(options: SandboxOptions): Promise<BaseSandbox>; 31 31 } 32 32 33 - export type Provider = "daytona" | "deno" | "vercel" | "sprites"; 33 + export type Provider = "daytona" | "deno" | "vercel" | "sprites" | "modal"; 34 34 35 35 export interface SandboxOptions { 36 36 id?: string; ··· 51 51 vercelApiToken?: string; 52 52 vercelProjectId?: string; 53 53 vercelTeamId?: string; 54 + modalTokenId?: string; 55 + modalTokenSecret?: string; 56 + modalAppName?: string; 57 + image?: string; 54 58 [key: string]: any; 55 59 } 56 60 ··· 75 79 return import("./sprites/mod.ts").then((module) => 76 80 new module.default().create(options), 77 81 ); 82 + case "modal": 83 + return import("./modal/mod.ts").then((module) => 84 + new module.default().create(options), 85 + ); 78 86 default: 79 87 console.log(`Provider ${provider} is not supported yet.`); 80 88 throw new Error(`Unsupported provider: ${provider}`); ··· 124 132 case "sprites": 125 133 return import("./sprites/mod.ts").then((module) => 126 134 new module.default().get(id, options?.spriteToken), 135 + ); 136 + case "modal": 137 + return import("./modal/mod.ts").then((module) => 138 + new module.default().get(id, options), 127 139 ); 128 140 default: 129 141 consola.error(`Provider ${provider} is not supported yet.`);
+179
apps/sandbox/src/providers/modal/mod.ts
··· 1 + import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 + import { ModalClient, Sandbox } from "modal"; 3 + import consola from "consola"; 4 + import path from "node:path"; 5 + import { env } from "node:process"; 6 + import { Buffer } from "node:buffer"; 7 + import { 8 + adjectives, 9 + generateUniqueAsync, 10 + nouns, 11 + } from "unique-username-generator"; 12 + 13 + export class ModalSandbox implements BaseSandbox { 14 + constructor(private sandbox: Sandbox) {} 15 + 16 + async start(): Promise<void> { 17 + // Modal's sandbox starts immediately upon creation, so we can just return here. 18 + } 19 + 20 + async stop(): Promise<void> { 21 + try { 22 + consola.info("Stopping Modal sandbox with ID:", await this.id()); 23 + await this.sandbox.terminate(); 24 + } catch (error) { 25 + consola.error("Error stopping Modal sandbox:", error); 26 + } 27 + } 28 + 29 + async delete(): Promise<void> { 30 + // Modal's sandbox does not have a separate delete method, so we just stop it. 31 + try { 32 + consola.info("Deleting Modal sandbox with ID:", await this.id()); 33 + await this.stop(); 34 + } catch (error) { 35 + consola.error("Error deleting Modal sandbox:", error); 36 + } 37 + } 38 + 39 + async sh( 40 + strings: TemplateStringsArray, 41 + ...values: any[] 42 + ): Promise<{ 43 + stdout?: string | Buffer<ArrayBufferLike>; 44 + stderr?: string | Buffer<ArrayBufferLike>; 45 + exitCode: number; 46 + }> { 47 + const command = strings.reduce((acc, str, i) => { 48 + return acc + str + (values[i] || ""); 49 + }, ""); 50 + const result = await this.sandbox.exec(["bash", "-c", command]); 51 + 52 + return { 53 + ...result, 54 + stdout: await result.stdout.readText(), 55 + stderr: await result.stderr.readText(), 56 + exitCode: await result.wait(), 57 + }; 58 + } 59 + 60 + async id(): Promise<string | null> { 61 + return this.sandbox.sandboxId; 62 + } 63 + 64 + async ssh(): Promise<any> {} 65 + 66 + async mkdir(dir: string): Promise<void> { 67 + await this.sh`mkdir -p ${dir}`; 68 + } 69 + 70 + async writeFile(absolutePath: string, content: string): Promise<void> { 71 + const basePath = path.dirname(absolutePath); 72 + if (basePath !== "/" && basePath != ".") { 73 + await this.mkdir(basePath); 74 + } 75 + await this.sh`echo '${content}' > ${absolutePath}`; 76 + } 77 + 78 + async setupSshKeys(privateKey: string, publicKey: string): Promise<void> { 79 + await this.writeFile("~/.ssh/id_ed25519", privateKey); 80 + await this.writeFile("~/.ssh/id_ed25519.pub", publicKey); 81 + await this.sh`chmod 600 ~/.ssh/id_ed25519`; 82 + await this.sh`chmod 644 ~/.ssh/id_ed25519.pub`; 83 + await this.sh`ssh-keyscan -t rsa tangled.org >> $HOME/.ssh/known_hosts`; 84 + await this.sh`ssh-keyscan -t rsa github.com >> $HOME/.ssh/known_hosts`; 85 + } 86 + 87 + async setupDefaultSshKeys(): Promise<void> { 88 + await this 89 + .sh`[ -f ~/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -q -N "" || true`; 90 + } 91 + 92 + async setupTailscale(authKey: string): Promise<void> { 93 + await this 94 + .sh`type tailscaled || curl -fsSL https://tailscale.com/install.sh | sh || true`; 95 + await this.sh`pm2 start tailscaled || true`; 96 + await this.sh`tailscale up --auth-key=${authKey} || true`; 97 + } 98 + clone(repoUrl: string): Promise<any> { 99 + return this.sh`git clone ${repoUrl}`; 100 + } 101 + async mount(path: string, prefix?: string): Promise<void> { 102 + const VERSION = "v1.2.1"; 103 + const ARCH = "amd64"; 104 + await this.sh`mkdir -p $HOME/.local/bin`; 105 + await this 106 + .sh`command -v tigrisfs || ARCH=amd64 && curl -L "https://github.com/tigrisdata/tigrisfs/releases/download/${VERSION}/tigrisfs_${VERSION.replace("v", "")}_linux_${ARCH}.tar.gz" -o /tmp/tigrisfs.tar.gz`; 107 + await this 108 + .sh`command -v tigrisfs || tar -xzf /tmp/tigrisfs.tar.gz -C ~/.local/bin`; 109 + await this.sh`command -v tigrisfs || rm -rf /tmp/tigrisfs.tar.gz`; 110 + await this.sh`command -v tigrisfs || chmod +x ~/.local/bin/tigrisfs`; 111 + await this 112 + .sh`cp ~/.local/bin/tigrisfs /usr/bin || sudo cp ~/.local/bin/tigrisfs /usr/bin || true`; 113 + await this.sh`mkdir -p ${path} || sudo mkdir -p ${path}`; 114 + // install fuse ? 115 + 116 + await this.mkdir(path); 117 + 118 + const bucketPath = prefix 119 + ? `${env.VOLUME_BUCKET}:${prefix}` 120 + : env.VOLUME_BUCKET; 121 + 122 + await this.sandbox.exec( 123 + [ 124 + "sh", 125 + "-c", 126 + `tigrisfs --endpoint "https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com" -o allow_other,default_permissions ${bucketPath} ${path} || sudo tigrisfs --endpoint "https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com" -o allow_other,default_permissions ${bucketPath} ${path}`, 127 + ], 128 + { 129 + env: { 130 + AWS_ACCESS_KEY_ID: env.R2_ACCESS_KEY_ID!, 131 + AWS_SECRET_ACCESS_KEY: env.R2_SECRET_ACCESS_KEY!, 132 + }, 133 + }, 134 + ); 135 + } 136 + 137 + async unmount(path: string): Promise<void> { 138 + await this 139 + .sh`fusermount -u ${path} || sudo fusermount -u ${path} || umount ${path}`; 140 + } 141 + } 142 + 143 + class ModalProvider implements BaseProvider { 144 + async create(options: SandboxOptions): Promise<BaseSandbox> { 145 + const suffix = Math.random().toString(36).substring(2, 6); 146 + let modalAppName = await generateUniqueAsync( 147 + { dictionaries: [adjectives, nouns], separator: "-" }, 148 + () => false, 149 + ); 150 + modalAppName = `${modalAppName}-${suffix}`; 151 + const modal = new ModalClient({ 152 + tokenId: options.modalTokenId || env.MODAL_TOKEN_ID!, 153 + tokenSecret: options.modalTokenSecret || env.MODAL_TOKEN_SECRET!, 154 + }); 155 + const app = await modal.apps.fromName( 156 + options.modalAppName || modalAppName, 157 + { 158 + createIfMissing: true, 159 + }, 160 + ); 161 + const image = modal.images.fromRegistry( 162 + options.image || "ghcr.io/pocketenv-io/daytona-openclaw:0.6.0", 163 + ); 164 + const sandbox = await modal.sandboxes.create(app, image); 165 + 166 + return new ModalSandbox(sandbox); 167 + } 168 + 169 + async get(id: string, options?: SandboxOptions): Promise<BaseSandbox> { 170 + const modal = new ModalClient({ 171 + tokenId: options?.modalTokenId || env.MODAL_TOKEN_ID!, 172 + tokenSecret: options?.modalTokenSecret || env.MODAL_TOKEN_SECRET!, 173 + }); 174 + const sandbox = await modal.sandboxes.fromId(id); 175 + return new ModalSandbox(sandbox); 176 + } 177 + } 178 + 179 + export default ModalProvider;
+1 -1
apps/sandbox/src/types/sandbox.ts
··· 32 32 name: z.string().optional(), 33 33 description: z.string().optional(), 34 34 provider: z 35 - .enum(["daytona", "vercel", "deno", "sprites"]) 35 + .enum(["daytona", "vercel", "deno", "sprites", "modal"]) 36 36 .optional() 37 37 .default("deno"), 38 38 base: z.enum(["openclaw"]).optional().default("openclaw"),