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 sprite token support for sandboxes

+230 -109
+8
apps/api/lexicons/sandbox/createSandbox.json
··· 82 82 "keepAlive": { 83 83 "type": "boolean", 84 84 "description": "Prevent the sandbox from being automatically stop after a period of inactivity. Use with caution, as this may lead to increased costs." 85 + }, 86 + "spriteToken": { 87 + "type": "string", 88 + "description": "A token (encrypted) for accessing sprite resources" 89 + }, 90 + "redactedSpriteToken": { 91 + "type": "string", 92 + "description": "A redacted token for accessing sprite resources" 85 93 } 86 94 } 87 95 }
+8
apps/api/pkl/defs/sandbox/createSandbox.pkl
··· 80 80 description = 81 81 "Prevent the sandbox from being automatically stop after a period of inactivity. Use with caution, as this may lead to increased costs." 82 82 } 83 + ["spriteToken"] = new StringType { 84 + type = "string" 85 + description = "A token (encrypted) for accessing sprite resources" 86 + } 87 + ["redactedSpriteToken"] = new StringType { 88 + type = "string" 89 + description = "A redacted token for accessing sprite resources" 90 + } 83 91 } 84 92 } 85 93 }
+9
apps/api/src/lexicon/lexicons.ts
··· 586 586 description: 587 587 "Prevent the sandbox from being automatically stop after a period of inactivity. Use with caution, as this may lead to increased costs.", 588 588 }, 589 + spriteToken: { 590 + type: "string", 591 + description: 592 + "A token (encrypted) for accessing sprite resources", 593 + }, 594 + redactedSpriteToken: { 595 + type: "string", 596 + description: "A redacted token for accessing sprite resources", 597 + }, 589 598 }, 590 599 }, 591 600 },
+4
apps/api/src/lexicon/types/io/pocketenv/sandbox/createSandbox.ts
··· 36 36 envs?: IoPocketenvSandboxDefs.Envs; 37 37 /** Prevent the sandbox from being automatically stop after a period of inactivity. Use with caution, as this may lead to increased costs. */ 38 38 keepAlive?: boolean; 39 + /** A token (encrypted) for accessing sprite resources */ 40 + spriteToken?: string; 41 + /** A redacted token for accessing sprite resources */ 42 + redactedSpriteToken?: string; 39 43 [k: string]: unknown; 40 44 } 41 45
-2
apps/api/src/lib/env.ts
··· 28 28 CF_SANDBOX_API_URL: str({ default: "http://localhost:8787" }), 29 29 CF_SECRET_KEY: str({}), 30 30 CF_LOCAL: bool({ default: false }), 31 - SPRITE_TOKEN: str({}), 32 - SPRITE_NAME: str({}), 33 31 ACCOUNT_ID: str({}), 34 32 VOLUME_BUCKET: str({}), 35 33 R2_ACCESS_KEY_ID: str({}),
+1 -3
apps/api/src/schema/sprite-auth.ts
··· 6 6 const spriteAuth = pgTable( 7 7 "sprite_auth", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("id").primaryKey().default(sql`xata_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+61 -48
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], 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 - ]); 58 + const [ 59 + variables, 60 + secrets, 61 + files, 62 + sshKeys, 63 + [tailscale], 64 + volumes, 65 + [spriteAuth], 66 + ] = await Promise.all([ 67 + ctx.db 68 + .select() 69 + .from(schema.sandboxVariables) 70 + .leftJoin( 71 + schema.variables, 72 + eq(schema.variables.id, schema.sandboxVariables.variableId), 73 + ) 74 + .where(eq(schema.sandboxVariables.sandboxId, id)) 75 + .execute(), 76 + ctx.db 77 + .select() 78 + .from(schema.sandboxSecrets) 79 + .leftJoin( 80 + schema.secrets, 81 + eq(schema.secrets.id, schema.sandboxSecrets.secretId), 82 + ) 83 + .where(eq(schema.sandboxSecrets.sandboxId, id)) 84 + .execute(), 85 + ctx.db 86 + .select() 87 + .from(schema.sandboxFiles) 88 + .leftJoin(schema.files, eq(schema.files.id, schema.sandboxFiles.fileId)) 89 + .where(eq(schema.sandboxFiles.sandboxId, id)) 90 + .execute(), 91 + ctx.db 92 + .select() 93 + .from(schema.sshKeys) 94 + .where(eq(schema.sshKeys.sandboxId, id)) 95 + .execute(), 96 + ctx.db 97 + .select() 98 + .from(schema.tailscaleAuthKeys) 99 + .where(eq(schema.tailscaleAuthKeys.sandboxId, id)) 100 + .execute(), 101 + ctx.db 102 + .select() 103 + .from(schema.sandboxVolumes) 104 + .leftJoin( 105 + schema.sandboxes, 106 + eq(schema.sandboxes.id, schema.sandboxVolumes.sandboxId), 107 + ) 108 + .leftJoin(schema.users, eq(schema.users.id, schema.sandboxes.userId)) 109 + .where(eq(schema.sandboxVolumes.sandboxId, id)) 110 + .execute(), 111 + ctx.db 112 + .select() 113 + .from(schema.spriteAuth) 114 + .where(eq(schema.spriteAuth.sandboxId, sandbox.id)) 115 + .execute(), 116 + ]); 105 117 106 - const client = new SpritesClient(env.SPRITE_TOKEN); 118 + const spriteToken = decrypt(spriteAuth!.spriteToken); 119 + const client = new SpritesClient(spriteToken); 107 120 const sprite = client.sprite(sandbox.sandboxId!); 108 121 const cmd = sprite.spawn("bash", [], { 109 122 tty: true,
+2
apps/api/src/xrpc/io/pocketenv/sandbox/createSandbox.ts
··· 109 109 base: input.base.split("/").pop()!, 110 110 repo: input.repo, 111 111 keepAlive: input.keepAlive, 112 + spriteToken: input.spriteToken, 113 + redactedSpriteToken: input.redactedSpriteToken, 112 114 }, 113 115 { 114 116 headers: {
+2 -1
apps/api/src/xrpc/io/pocketenv/sandbox/putPreferences.ts
··· 197 197 }) 198 198 .execute(); 199 199 break; 200 - case "cloudflare": 200 + case "cloudflare": { 201 201 const [record] = await tx 202 202 .select() 203 203 .from(sandboxes) ··· 251 251 .execute(), 252 252 ]); 253 253 break; 254 + } 254 255 default: 255 256 break; 256 257 }
+53 -3
apps/sandbox/src/index.ts
··· 14 14 tailscaleAuthKeys, 15 15 users, 16 16 variables, 17 + spriteAuth, 17 18 } from "./schema/mod.ts"; 18 19 import { 19 20 adjectives, ··· 41 42 import process from "node:process"; 42 43 import jwt from "@tsndr/cloudflare-worker-jwt"; 43 44 import decrypt from "./lib/decrypt.ts"; 45 + import { InsertSpriteAuth } from "./schema/sprite-auth.ts"; 44 46 45 47 const app = new Hono<{ Variables: Context }>(); 46 48 ··· 149 151 150 152 if (params.variables.length > 0) { 151 153 await saveVariables(tx, record, { variables: params.variables }); 154 + } 155 + 156 + if (params.spriteToken && user?.id) { 157 + await tx 158 + .insert(spriteAuth) 159 + .values({ 160 + sandboxId: record.id, 161 + spriteToken: params.spriteToken, 162 + redactedSpriteToken: params.redacredSpriteToken ?? "", 163 + userId: user.id, 164 + } satisfies InsertSpriteAuth) 165 + .execute(); 152 166 } 153 167 154 168 const sandbox = await createSandbox(params.provider, { ··· 157 171 sleepAfter: params.sleepAfter, 158 172 organizationId: process.env.DAYTONA_ORGANIZATION_ID, 159 173 snapshotRoot: process.env.DENO_SNAPSHOT_ROOT, 174 + spriteToken: decrypt(params.spriteToken), 160 175 spriteName, 161 176 }); 162 177 const sandboxId = await sandbox.id(); ··· 218 233 const body = await c.req.json<StartSandboxInput>(); 219 234 const { repo } = StartSandboxInputSchema.parse(body); 220 235 236 + const [spriteAuthParams] = await c.var.db 237 + .select() 238 + .from(spriteAuth) 239 + .where(eq(spriteAuth.sandboxId, record.id)) 240 + .execute(); 241 + 221 242 sandbox = await getSandboxById( 222 243 record.provider as Provider, 223 244 record.sandboxId!, 245 + decrypt(spriteAuthParams?.spriteToken), 224 246 ); 225 247 226 248 if (!sandbox) { ··· 261 283 .map((record) => 262 284 sandbox?.writeFile( 263 285 record.sandbox_files.path, 264 - decrypt(record.files!.content), 286 + decrypt(record.files!.content)!, 265 287 ), 266 288 ), 267 289 ...params[1].map((record) => 268 - sandbox?.setupSshKeys(decrypt(record.privateKey), record.publicKey), 290 + sandbox?.setupSshKeys(decrypt(record.privateKey)!, record.publicKey), 269 291 ), 270 292 params[2].length > 0 && 271 - sandbox?.setupTailscale(decrypt(params[2][0].authKey)), 293 + sandbox?.setupTailscale(decrypt(params[2][0].authKey)!), 272 294 ...params[3].map((volume) => 273 295 sandbox?.mount( 274 296 volume.sandbox_volumes.path, ··· 328 350 return c.json({ error: "Sandbox provider not supported" }, 400); 329 351 } 330 352 353 + const [spriteAuthParams] = await c.var.db 354 + .select() 355 + .from(spriteAuth) 356 + .where(eq(spriteAuth.sandboxId, record.id)) 357 + .execute(); 358 + 331 359 sandbox = await getSandboxById( 332 360 record.provider as Provider, 333 361 record.sandboxId!, 362 + decrypt(spriteAuthParams?.spriteToken), 334 363 ); 335 364 336 365 if (!sandbox) { ··· 359 388 return c.json({ error: "Sandbox provider not supported" }, 400); 360 389 } 361 390 391 + const [spriteAuthParams] = await c.var.db 392 + .select() 393 + .from(spriteAuth) 394 + .where(eq(spriteAuth.sandboxId, record.id)) 395 + .execute(); 396 + 362 397 sandbox = await getSandboxById( 363 398 record.provider as Provider, 364 399 record.sandboxId!, 400 + decrypt(spriteAuthParams?.spriteToken), 365 401 ); 366 402 367 403 if (!sandbox) { ··· 386 422 return c.json({ error: "Sandbox provider not supported" }, 400); 387 423 } 388 424 425 + const [spriteAuthParams] = await c.var.db 426 + .select() 427 + .from(spriteAuth) 428 + .where(eq(spriteAuth.sandboxId, record.id)) 429 + .execute(); 430 + 389 431 sandbox = await getSandboxById( 390 432 record.provider as Provider, 391 433 record.sandboxId!, 434 + decrypt(spriteAuthParams?.spriteToken), 392 435 ); 393 436 394 437 if (!sandbox) { ··· 418 461 return c.json({ error: "Sandbox provider not supported" }, 400); 419 462 } 420 463 464 + const [spriteAuthParams] = await c.var.db 465 + .select() 466 + .from(spriteAuth) 467 + .where(eq(spriteAuth.sandboxId, record.id)) 468 + .execute(); 469 + 421 470 sandbox = await getSandboxById( 422 471 record.provider as Provider, 423 472 record.sandboxId!, 473 + decrypt(spriteAuthParams?.spriteToken), 424 474 ); 425 475 426 476 if (!sandbox) {
+6 -2
apps/sandbox/src/lib/decrypt.ts
··· 3 3 4 4 await sodium.ready; 5 5 6 - export default function decrypt(value: string): string { 6 + export default function decrypt(value?: string): string | undefined { 7 + if (!value) { 8 + return undefined; 9 + } 10 + 7 11 const sealed = sodium.from_base64( 8 12 value, 9 13 sodium.base64_variants.URLSAFE_NO_PADDING, 10 14 ); 11 - let decryptedBytes = sodium.crypto_box_seal_open( 15 + const decryptedBytes = sodium.crypto_box_seal_open( 12 16 sealed, 13 17 sodium.from_hex(process.env.PUBLIC_KEY!), 14 18 sodium.from_hex(process.env.PRIVATE_KEY!),
+3 -1
apps/sandbox/src/providers/mod.ts
··· 43 43 snapshotRoot?: string; 44 44 port?: number; 45 45 memory?: Memory; 46 + spriteToken?: string; 46 47 spriteName?: string; 47 48 [key: string]: any; 48 49 } ··· 77 78 export async function getSandboxById( 78 79 provider: Provider, 79 80 id: string, 81 + token?: string, 80 82 ): Promise<BaseSandbox> { 81 83 switch (provider) { 82 84 case "daytona": ··· 101 103 ); 102 104 case "sprites": 103 105 return import("./sprites/mod.ts").then((module) => 104 - new module.default().get(id), 106 + new module.default().get(id, token), 105 107 ); 106 108 default: 107 109 console.log(`Provider ${provider} is not supported yet.`);
+10 -7
apps/sandbox/src/providers/sprites/mod.ts
··· 7 7 import crypto from "node:crypto"; 8 8 9 9 export class SpriteSandbox implements BaseSandbox { 10 - constructor(private sprite: Sprite) {} 10 + constructor( 11 + private sprite: Sprite, 12 + private spriteToken: string, 13 + ) {} 11 14 12 15 async start(): Promise<void> { 13 - const client = new SpritesClient(process.env.SPRITE_TOKEN!); 16 + const client = new SpritesClient(this.spriteToken); 14 17 const name = await this.id(); 15 18 if (!name) { 16 19 consola.error("Sprite name is not available"); ··· 160 163 161 164 class SpritesProvider implements BaseProvider { 162 165 async create(options: SandboxOptions): Promise<BaseSandbox> { 163 - const client = new SpritesClient(process.env.SPRITE_TOKEN!); 166 + const client = new SpritesClient(options.spriteToken!); 164 167 165 168 if (!options.spriteName) { 166 169 throw new Error("spriteName is required"); 167 170 } 168 171 169 172 const sprite = await client.createSprite(options.spriteName); 170 - return new SpriteSandbox(sprite); 173 + return new SpriteSandbox(sprite, options.spriteToken!); 171 174 } 172 175 173 - async get(id: string): Promise<BaseSandbox> { 174 - const client = new SpritesClient(process.env.SPRITE_TOKEN!); 176 + async get(id: string, token?: string): Promise<BaseSandbox> { 177 + const client = new SpritesClient(token!); 175 178 const sprite = client.sprite(id); 176 - return new SpriteSandbox(sprite); 179 + return new SpriteSandbox(sprite, token!); 177 180 } 178 181 } 179 182
+63 -42
apps/sandbox/src/types/sandbox.ts
··· 27 27 value: z.string(), 28 28 }); 29 29 30 - export const SandboxConfigSchema = z.object({ 31 - name: z.string().optional(), 32 - description: z.string().optional(), 33 - provider: z 34 - .enum(["daytona", "vercel", "deno", "sprites"]) 35 - .optional() 36 - .default("deno"), 37 - base: z.enum(["openclaw"]).optional().default("openclaw"), 38 - keepAlive: z.boolean().optional().default(false), 39 - vcpus: z.number().optional().default(2), 40 - memory: z.number().optional().default(4), 41 - disk: z.number().optional().default(3), 42 - sleepAfter: z 43 - .string() 44 - .regex( 45 - /^\d+(h|m|s)$/, 46 - "Invalid format. Use a number followed by 'h', 'm', or 's' (e.g., '1h', '30m', '15s').", 47 - ) 48 - .optional(), 49 - variables: z 50 - .array(VariableSchema) 51 - .optional() 52 - .default([]) 53 - .refine( 54 - (secrets) => { 55 - enforceUniqueNames(secrets); 56 - return true; 57 - }, 58 - { message: "Duplicate secret names are not allowed." }, 59 - ), 60 - secrets: z 61 - .array(SecretSchema) 62 - .optional() 63 - .default([]) 64 - .refine( 65 - (secrets) => { 66 - enforceUniqueNames(secrets); 67 - return true; 68 - }, 69 - { message: "Duplicate secret names are not allowed." }, 70 - ), 71 - }); 30 + export const SandboxConfigSchema = z 31 + .object({ 32 + name: z.string().optional(), 33 + description: z.string().optional(), 34 + provider: z 35 + .enum(["daytona", "vercel", "deno", "sprites"]) 36 + .optional() 37 + .default("deno"), 38 + base: z.enum(["openclaw"]).optional().default("openclaw"), 39 + keepAlive: z.boolean().optional().default(false), 40 + spriteToken: z.string().optional(), 41 + redacredSpriteToken: z.string().optional(), 42 + vcpus: z.number().optional().default(2), 43 + memory: z.number().optional().default(4), 44 + disk: z.number().optional().default(3), 45 + sleepAfter: z 46 + .string() 47 + .regex( 48 + /^\d+(h|m|s)$/, 49 + "Invalid format. Use a number followed by 'h', 'm', or 's' (e.g., '1h', '30m', '15s').", 50 + ) 51 + .optional(), 52 + variables: z 53 + .array(VariableSchema) 54 + .optional() 55 + .default([]) 56 + .refine( 57 + (secrets) => { 58 + enforceUniqueNames(secrets); 59 + return true; 60 + }, 61 + { message: "Duplicate secret names are not allowed." }, 62 + ), 63 + secrets: z 64 + .array(SecretSchema) 65 + .optional() 66 + .default([]) 67 + .refine( 68 + (secrets) => { 69 + enforceUniqueNames(secrets); 70 + return true; 71 + }, 72 + { message: "Duplicate secret names are not allowed." }, 73 + ), 74 + }) 75 + .superRefine((data, ctx) => { 76 + if (data.provider === "sprites") { 77 + if (!data.spriteToken) { 78 + ctx.addIssue({ 79 + code: z.ZodIssueCode.custom, 80 + message: "spriteToken is required when provider is 'sprites'", 81 + path: ["spriteToken"], 82 + }); 83 + } 84 + if (!data.redacredSpriteToken) { 85 + ctx.addIssue({ 86 + code: z.ZodIssueCode.custom, 87 + message: "redacredSpriteToken is required when provider is 'sprites'", 88 + path: ["redacredSpriteToken"], 89 + }); 90 + } 91 + } 92 + }); 72 93 73 94 export const StartSandboxInputSchema = z.object({ 74 95 repo: z.string().optional(),