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 support for volume to sprites and other providers

+252 -44
+4
apps/api/src/lib/env.ts
··· 30 30 CF_LOCAL: bool({ default: false }), 31 31 SPRITE_TOKEN: str({}), 32 32 SPRITE_NAME: str({}), 33 + ACCOUNT_ID: str({}), 34 + VOLUME_BUCKET: str({}), 35 + R2_ACCESS_KEY_ID: str({}), 36 + R2_SECRET_ACCESS_KEY: str({}), 33 37 });
+101 -38
apps/api/src/tty/index.tsx
··· 55 55 throw new Error(`Sandbox not found: ${id}`); 56 56 } 57 57 58 - const [variables, secrets, files, sshKeys, [tailscale]] = await Promise.all([ 59 - ctx.db 60 - .select() 61 - .from(schema.sandboxVariables) 62 - .leftJoin( 63 - schema.variables, 64 - eq(schema.variables.id, schema.sandboxVariables.variableId), 65 - ) 66 - .where(eq(schema.sandboxVariables.sandboxId, id)) 67 - .execute(), 68 - ctx.db 69 - .select() 70 - .from(schema.sandboxSecrets) 71 - .leftJoin( 72 - schema.secrets, 73 - eq(schema.secrets.id, schema.sandboxSecrets.secretId), 74 - ) 75 - .where(eq(schema.sandboxSecrets.sandboxId, id)) 76 - .execute(), 77 - ctx.db 78 - .select() 79 - .from(schema.sandboxFiles) 80 - .leftJoin(schema.files, eq(schema.files.id, schema.sandboxFiles.fileId)) 81 - .where(eq(schema.sandboxFiles.sandboxId, id)) 82 - .execute(), 83 - ctx.db 84 - .select() 85 - .from(schema.sshKeys) 86 - .where(eq(schema.sshKeys.sandboxId, id)) 87 - .execute(), 88 - ctx.db 89 - .select() 90 - .from(schema.tailscaleAuthKeys) 91 - .where(eq(schema.tailscaleAuthKeys.sandboxId, id)) 92 - .execute(), 93 - ]); 58 + const [variables, secrets, files, sshKeys, [tailscale], volumes] = 59 + await Promise.all([ 60 + ctx.db 61 + .select() 62 + .from(schema.sandboxVariables) 63 + .leftJoin( 64 + schema.variables, 65 + eq(schema.variables.id, schema.sandboxVariables.variableId), 66 + ) 67 + .where(eq(schema.sandboxVariables.sandboxId, id)) 68 + .execute(), 69 + ctx.db 70 + .select() 71 + .from(schema.sandboxSecrets) 72 + .leftJoin( 73 + schema.secrets, 74 + eq(schema.secrets.id, schema.sandboxSecrets.secretId), 75 + ) 76 + .where(eq(schema.sandboxSecrets.sandboxId, id)) 77 + .execute(), 78 + ctx.db 79 + .select() 80 + .from(schema.sandboxFiles) 81 + .leftJoin(schema.files, eq(schema.files.id, schema.sandboxFiles.fileId)) 82 + .where(eq(schema.sandboxFiles.sandboxId, id)) 83 + .execute(), 84 + ctx.db 85 + .select() 86 + .from(schema.sshKeys) 87 + .where(eq(schema.sshKeys.sandboxId, id)) 88 + .execute(), 89 + ctx.db 90 + .select() 91 + .from(schema.tailscaleAuthKeys) 92 + .where(eq(schema.tailscaleAuthKeys.sandboxId, id)) 93 + .execute(), 94 + ctx.db 95 + .select() 96 + .from(schema.sandboxVolumes) 97 + .leftJoin( 98 + schema.sandboxes, 99 + eq(schema.sandboxes.id, schema.sandboxVolumes.sandboxId), 100 + ) 101 + .leftJoin(schema.users, eq(schema.users.id, schema.sandboxes.userId)) 102 + .where(eq(schema.sandboxVolumes.sandboxId, id)) 103 + .execute(), 104 + ]); 94 105 95 106 const client = new SpritesClient(env.SPRITE_TOKEN); 96 107 const sprite = client.sprite(sandbox.sandboxId!); ··· 185 196 } 186 197 }; 187 198 199 + const mount = async (path: string, prefix?: string): Promise<void> => { 200 + try { 201 + await sprite.execFile("bash", [ 202 + "-c", 203 + `type s3fs || apt-get update && apt-get install -y s3fs || sudo apt-get update && sudo apt-get install -y s3fs`, 204 + ]); 205 + await sprite.execFile("bash", [ 206 + "-c", 207 + `mkdir -p ${path} || sudo mkdir -p ${path}`, 208 + ]); 209 + 210 + const passwdFile = `/tmp/.passwd-s3fs-${crypto.randomUUID()}`; 211 + 212 + await writeFile( 213 + passwdFile, 214 + `${env.R2_ACCESS_KEY_ID}:${env.R2_SECRET_ACCESS_KEY}`, 215 + ); 216 + 217 + await sprite.execFile("bash", ["-c", `chmod 0600 '${passwdFile}'`]); 218 + 219 + const bucketPath = prefix 220 + ? `${env.VOLUME_BUCKET}:${prefix}` 221 + : env.VOLUME_BUCKET; 222 + 223 + await sprite.execFile("bash", [ 224 + "-c", 225 + `s3fs '${bucketPath}' '${path}' -o 'passwd_file=${passwdFile},nomixupload,compat_dir,url=https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com'`, 226 + ]); 227 + } catch (error) { 228 + consola.error("Error mounting S3 bucket:", error); 229 + } 230 + }; 231 + 232 + const unmount = async (path: string): Promise<void> => { 233 + try { 234 + await sprite.execFile("bash", [ 235 + "-c", 236 + `fusermount -u ${path} || sudo fusermount -u ${path} || umount ${path}`, 237 + ]); 238 + } catch (error) { 239 + consola.error("Error unmounting S3 bucket:", error); 240 + } 241 + }; 242 + 188 243 await setupDefaultSshKeys(); 189 244 190 - await Promise.all([ 245 + Promise.all([ 191 246 ...files 192 247 .filter((x) => x.files !== null) 193 248 .map(async (record) => ··· 197 252 setupSshKeys(decrypt(record.privateKey), record.publicKey), 198 253 ), 199 254 tailscale && setupTailscale(decrypt(tailscale.authKey)), 200 - ]); 255 + ...volumes.map((volume) => 256 + mount( 257 + volume.sandbox_volumes.path, 258 + `/${volume.users?.did || ""}${volume.users?.did ? "/" : ""}${volume.sandbox_volumes.id}/`, 259 + ), 260 + ), 261 + ]) 262 + .then(() => consola.success(`Sandbox ${id} is ready`)) 263 + .catch((err) => consola.error(`Error setting up sandbox ${id}:`, err)); 201 264 202 265 const session: Session = { 203 266 cmd,
-2
apps/cli/src/cmd/start.ts
··· 16 16 17 17 try { 18 18 const authToken = env.POCKETENV_TOKEN || token; 19 - console.log(repo); 20 - 21 19 await client.post( 22 20 "/xrpc/io.pocketenv.sandbox.startSandbox", 23 21 {
+14
apps/sandbox/src/index.ts
··· 8 8 sandboxFiles, 9 9 sandboxSecrets, 10 10 sandboxVariables, 11 + sandboxVolumes, 11 12 secrets, 12 13 sshKeys, 13 14 tailscaleAuthKeys, ··· 243 244 .from(tailscaleAuthKeys) 244 245 .where(eq(tailscaleAuthKeys.sandboxId, c.req.param("sandboxId"))) 245 246 .execute(), 247 + c.var.db 248 + .select() 249 + .from(sandboxVolumes) 250 + .leftJoin(sandboxes, eq(sandboxes.id, sandboxVolumes.sandboxId)) 251 + .leftJoin(users, eq(users.id, sandboxes.userId)) 252 + .where(eq(sandboxVolumes.sandboxId, c.req.param("sandboxId"))) 253 + .execute(), 246 254 ]); 247 255 248 256 await sandbox.setupDefaultSshKeys(); ··· 261 269 ), 262 270 params[2].length > 0 && 263 271 sandbox?.setupTailscale(decrypt(params[2][0].authKey)), 272 + ...params[3].map((volume) => 273 + sandbox?.mount( 274 + volume.sandbox_volumes.path, 275 + `/${volume.users?.did || ""}${volume.users?.did ? "/" : ""}${volume.sandbox_volumes.id}/`, 276 + ), 277 + ), 264 278 ]); 265 279 266 280 if (record.repo) {
+31 -1
apps/sandbox/src/providers/daytona/mod.ts
··· 1 1 import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 2 import { Daytona, Sandbox } from "@daytonaio/sdk"; 3 - import process from "node:process"; 3 + import process, { env } from "node:process"; 4 4 import consola from "consola"; 5 5 import path from "node:path"; 6 6 import { Buffer } from "node:buffer"; 7 + import crypto from "node:crypto"; 7 8 8 9 export class DaytonaSandbox implements BaseSandbox { 9 10 constructor(private sandbox: Sandbox) {} ··· 95 96 clone(repoUrl: string): Promise<any> { 96 97 const dir = repoUrl.split("/").pop()?.replace(".git", ""); 97 98 return this.sh`git clone ${repoUrl} || git -C ${dir} pull`; 99 + } 100 + 101 + async mount(path: string, prefix?: string): Promise<void> { 102 + await this 103 + .sh`type s3fs || apt-get update && apt-get install -y s3fs || sudo apt-get update && sudo apt-get install -y s3fs || true`; 104 + await this.sh`mkdir -p ${path} || sudo mkdir -p ${path}`; 105 + 106 + await this.mkdir(path); 107 + 108 + const passwdFile = `/tmp/.passwd-s3fs-${crypto.randomUUID()}`; 109 + 110 + await this.writeFile( 111 + passwdFile, 112 + `${env.R2_ACCESS_KEY_ID}:${env.R2_SECRET_ACCESS_KEY}`, 113 + ); 114 + 115 + await this.sh`chmod 0600 '${passwdFile}'`; 116 + 117 + const bucketPath = prefix 118 + ? `${env.VOLUME_BUCKET}:${prefix}` 119 + : env.VOLUME_BUCKET; 120 + 121 + await this 122 + .sh`s3fs '${bucketPath}' '${path}' -o 'passwd_file=${passwdFile},nomixupload,compat_dir,url=https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com'`; 123 + } 124 + 125 + async unmount(path: string): Promise<void> { 126 + await this 127 + .sh`fusermount -u ${path} || sudo fusermount -u ${path} || umount ${path}`; 98 128 } 99 129 } 100 130
+31 -1
apps/sandbox/src/providers/deno/mod.ts
··· 1 1 import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 2 import { Sandbox } from "@deno/sandbox"; 3 - import process from "node:process"; 3 + import process, { env } from "node:process"; 4 4 import consola from "consola"; 5 5 import path from "node:path"; 6 6 import { Buffer } from "node:buffer"; 7 + import crypto from "node:crypto"; 7 8 8 9 export class DenoSandbox implements BaseSandbox { 9 10 constructor(private sandbox: Sandbox) {} ··· 106 107 clone(repoUrl: string): Promise<any> { 107 108 const dir = repoUrl.split("/").pop()?.replace(".git", ""); 108 109 return this.sh`git clone ${repoUrl} || git -C ${dir} pull`; 110 + } 111 + 112 + async mount(path: string, prefix?: string): Promise<void> { 113 + await this 114 + .sh`type s3fs || apt-get update && apt-get install -y s3fs || sudo apt-get update && sudo apt-get install -y s3fs || true`; 115 + await this.sh`mkdir -p ${path} || sudo mkdir -p ${path}`; 116 + 117 + await this.mkdir(path); 118 + 119 + const passwdFile = `/tmp/.passwd-s3fs-${crypto.randomUUID()}`; 120 + 121 + await this.writeFile( 122 + passwdFile, 123 + `${env.R2_ACCESS_KEY_ID}:${env.R2_SECRET_ACCESS_KEY}`, 124 + ); 125 + 126 + await this.sh`chmod 0600 '${passwdFile}'`; 127 + 128 + const bucketPath = prefix 129 + ? `${env.VOLUME_BUCKET}:${prefix}` 130 + : env.VOLUME_BUCKET; 131 + 132 + await this 133 + .sh`s3fs '${bucketPath}' '${path}' -o 'passwd_file=${passwdFile},nomixupload,compat_dir,url=https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com'`; 134 + } 135 + 136 + async unmount(path: string): Promise<void> { 137 + await this 138 + .sh`fusermount -u ${path} || sudo fusermount -u ${path} || umount ${path}`; 109 139 } 110 140 } 111 141
+2
apps/sandbox/src/providers/mod.ts
··· 22 22 abstract setupTailscale(authKey: string): Promise<void>; 23 23 abstract clone(repoUrl: string): Promise<any>; 24 24 abstract setupDefaultSshKeys(): Promise<void>; 25 + abstract mount(path: string, prefix?: string): Promise<void>; 26 + abstract unmount(path: string): Promise<void>; 25 27 } 26 28 27 29 abstract class BaseProvider {
+39 -1
apps/sandbox/src/providers/sprites/mod.ts
··· 1 1 import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 - import process from "node:process"; 2 + import process, { env } from "node:process"; 3 3 import consola from "consola"; 4 4 import { Sprite, SpritesClient } from "@fly/sprites"; 5 5 import path from "node:path"; 6 6 import { Buffer } from "node:buffer"; 7 + import crypto from "node:crypto"; 7 8 8 9 export class SpriteSandbox implements BaseSandbox { 9 10 constructor(private sprite: Sprite) {} ··· 117 118 "-c", 118 119 `git clone ${repoUrl} || git -C ${dir} pull`, 119 120 ]); 121 + } 122 + 123 + async mount(path: string, prefix?: string): Promise<void> { 124 + try { 125 + await this 126 + .sh`type s3fs || apt-get update && apt-get install -y s3fs || sudo apt-get update && sudo apt-get install -y s3fs || true`; 127 + await this.sh`mkdir -p ${path} || sudo mkdir -p ${path}`; 128 + 129 + await this.mkdir(path); 130 + 131 + const passwdFile = `/tmp/.passwd-s3fs-${crypto.randomUUID()}`; 132 + 133 + await this.writeFile( 134 + passwdFile, 135 + `${env.R2_ACCESS_KEY_ID}:${env.R2_SECRET_ACCESS_KEY}`, 136 + ); 137 + 138 + await this.sh`chmod 0600 '${passwdFile}'`; 139 + 140 + const bucketPath = prefix 141 + ? `${env.VOLUME_BUCKET}:${prefix}` 142 + : env.VOLUME_BUCKET; 143 + 144 + await this 145 + .sh`s3fs '${bucketPath}' '${path}' -o 'passwd_file=${passwdFile},nomixupload,compat_dir,url=https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com'`; 146 + } catch (error) { 147 + consola.error("Error mounting S3 bucket:", error); 148 + } 149 + } 150 + 151 + async unmount(path: string): Promise<void> { 152 + try { 153 + await this 154 + .sh`fusermount -u ${path} || sudo fusermount -u ${path} || umount ${path}`; 155 + } catch (error) { 156 + consola.error("Error unmounting S3 bucket:", error); 157 + } 120 158 } 121 159 } 122 160
+30 -1
apps/sandbox/src/providers/vercel/mod.ts
··· 1 1 import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 2 import { Sandbox } from "@vercel/sandbox"; 3 - import process from "node:process"; 3 + import process, { env } from "node:process"; 4 4 import consola from "consola"; 5 5 import path from "node:path"; 6 6 import { Buffer } from "node:buffer"; 7 + import crypto from "node:crypto"; 7 8 8 9 export class VercelSandbox implements BaseSandbox { 9 10 constructor(private sandbox: Sandbox) {} ··· 86 87 } 87 88 clone(repoUrl: string): Promise<any> { 88 89 return this.sh`git clone ${repoUrl}`; 90 + } 91 + async mount(path: string, prefix?: string): Promise<void> { 92 + await this 93 + .sh`type s3fs || apt-get update && apt-get install -y s3fs || sudo apt-get update && sudo apt-get install -y s3fs || true`; 94 + await this.sh`mkdir -p ${path} || sudo mkdir -p ${path}`; 95 + 96 + await this.mkdir(path); 97 + 98 + const passwdFile = `/tmp/.passwd-s3fs-${crypto.randomUUID()}`; 99 + 100 + await this.writeFile( 101 + passwdFile, 102 + `${env.R2_ACCESS_KEY_ID}:${env.R2_SECRET_ACCESS_KEY}`, 103 + ); 104 + 105 + await this.sh`chmod 0600 '${passwdFile}'`; 106 + 107 + const bucketPath = prefix 108 + ? `${env.VOLUME_BUCKET}:${prefix}` 109 + : env.VOLUME_BUCKET; 110 + 111 + await this 112 + .sh`s3fs '${bucketPath}' '${path}' -o 'passwd_file=${passwdFile},nomixupload,compat_dir,url=https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com'`; 113 + } 114 + 115 + async unmount(path: string): Promise<void> { 116 + await this 117 + .sh`fusermount -u ${path} || sudo fusermount -u ${path} || umount ${path}`; 89 118 } 90 119 } 91 120