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.

Allow specifying repo when starting sandbox

Thread new start input through lexicon, pkl, types, and handlers
Perform git clone in sandbox/cf-sandbox when starting

+208 -49
+12
apps/api/lexicons/sandbox/startSandbox.json
··· 17 17 } 18 18 } 19 19 }, 20 + "input": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "properties": { 25 + "repo": { 26 + "type": "string", 27 + "description": "The git repository URL to clone into the sandbox before starting it. Optional." 28 + } 29 + } 30 + } 31 + }, 20 32 "output": { 21 33 "encoding": "application/json", 22 34 "schema": {
+13
apps/api/pkl/defs/sandbox/startSandbox.pkl
··· 16 16 } 17 17 } 18 18 } 19 + input { 20 + encoding = "application/json" 21 + schema { 22 + type = "object" 23 + properties { 24 + ["repo"] = new StringType { 25 + type = "string" 26 + description = 27 + "The git repository URL to clone into the sandbox before starting it. Optional." 28 + } 29 + } 30 + } 31 + } 19 32 output { 20 33 encoding = "application/json" 21 34 schema = new Ref {
+13
apps/api/src/lexicon/lexicons.ts
··· 1704 1704 }, 1705 1705 }, 1706 1706 }, 1707 + input: { 1708 + encoding: "application/json", 1709 + schema: { 1710 + type: "object", 1711 + properties: { 1712 + repo: { 1713 + type: "string", 1714 + description: 1715 + "The git repository URL to clone into the sandbox before starting it. Optional.", 1716 + }, 1717 + }, 1718 + }, 1719 + }, 1707 1720 output: { 1708 1721 encoding: "application/json", 1709 1722 schema: {
+11 -2
apps/api/src/lexicon/types/io/pocketenv/sandbox/startSandbox.ts
··· 14 14 id: string; 15 15 } 16 16 17 - export type InputSchema = undefined; 17 + export interface InputSchema { 18 + /** The git repository URL to clone into the sandbox before starting it. Optional. */ 19 + repo?: string; 20 + [k: string]: unknown; 21 + } 22 + 18 23 export type OutputSchema = IoPocketenvSandboxDefs.SandboxViewBasic; 19 - export type HandlerInput = undefined; 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 20 29 21 30 export interface HandlerSuccess { 22 31 encoding: "application/json";
+22 -20
apps/api/src/xrpc/io/pocketenv/sandbox/createSandbox.ts
··· 3 3 import { consola } from "consola"; 4 4 import type { Context } from "context"; 5 5 import type { Server } from "lexicon"; 6 - import type { HandlerInput } from "lexicon/types/io/pocketenv/sandbox/createSandbox"; 6 + import type { InputSchema } from "lexicon/types/io/pocketenv/sandbox/createSandbox"; 7 7 import generateJwt from "lib/generateJwt"; 8 8 import type * as Sandbox from "lexicon/types/io/pocketenv/sandbox"; 9 9 import chalk from "chalk"; ··· 19 19 import sandboxes from "schema/sandboxes"; 20 20 21 21 export default function (server: Server, ctx: Context) { 22 - const createSandbox = async (input: HandlerInput, auth: HandlerAuth) => { 22 + const createSandbox = async (input: InputSchema, auth: HandlerAuth) => { 23 23 let res; 24 24 try { 25 25 const { credentials, artifacts } = auth; ··· 31 31 ); 32 32 } 33 33 34 - if (!input.body.base.startsWith("at://")) { 34 + if (!input.base.startsWith("at://")) { 35 35 const [sandbox] = await ctx.db 36 36 .select() 37 37 .from(sandboxes) 38 38 .where( 39 39 or( 40 - eq(sandboxes.name, input.body.base), 41 - eq(sandboxes.id, input.body.base), 42 - eq(sandboxes.sandboxId, input.body.base), 40 + eq(sandboxes.name, input.base), 41 + eq(sandboxes.id, input.base), 42 + eq(sandboxes.sandboxId, input.base), 43 43 ), 44 44 ) 45 45 .execute(); 46 46 if (!sandbox?.uri) { 47 47 throw new XRPCError(404, "Sandbox not found", "SandboxNotFoundError"); 48 48 } 49 - input.body.base = sandbox.uri; 49 + input.base = sandbox.uri; 50 50 } 51 51 52 - const provider = input.body.provider || Providers.CLOUDFLARE; 52 + const provider = input.provider || Providers.CLOUDFLARE; 53 53 const sandbox = 54 54 provider === Providers.CLOUDFLARE 55 - ? ctx.cfsandbox(input.body.base.split("/").pop()!) 55 + ? ctx.cfsandbox(input.base.split("/").pop()!) 56 56 : ctx.sandbox(); 57 57 res = await sandbox.post( 58 58 "/v1/sandboxes", 59 59 { 60 60 provider, 61 - base: input.body.base.split("/").pop()!, 61 + base: input.base.split("/").pop()!, 62 + repo: input.repo, 62 63 }, 63 64 { 64 65 headers: { ··· 102 103 const base = await ctx.db 103 104 .select() 104 105 .from(schema.sandboxes) 105 - .where(eq(schema.sandboxes.name, input.body.base.split("/").pop()!)) 106 + .where(eq(schema.sandboxes.name, input.base.split("/").pop()!)) 106 107 .execute() 107 108 .then(([row]) => row); 108 109 ··· 139 140 vcpus: res.data.vcpus, 140 141 memory: res.data.memory, 141 142 disk: res.data.disk, 143 + repo: res.data.repo, 142 144 ...(baseRef && { base: baseRef.value }), 143 145 createdAt: new Date().toISOString(), 144 146 }; ··· 178 180 return { 179 181 id: res.data.id, 180 182 name: res.data.name, 181 - provider: input.body.provider || Providers.CLOUDFLARE, 182 - description: input.body.description, 183 - topics: input.body.topics, 184 - repo: input.body.repo, 185 - vcpus: input.body.vcpus, 186 - memory: input.body.memory, 187 - disk: input.body.disk, 188 - readme: input.body.readme, 183 + provider: input.provider || Providers.CLOUDFLARE, 184 + description: input.description, 185 + topics: input.topics, 186 + repo: input.repo, 187 + vcpus: input.vcpus, 188 + memory: input.memory, 189 + disk: input.disk, 190 + readme: input.readme, 189 191 createdAt: new Date().toISOString(), 190 192 uri, 191 193 }; ··· 194 196 server.io.pocketenv.sandbox.createSandbox({ 195 197 auth: ctx.authVerifier, 196 198 handler: async ({ input, auth }) => { 197 - const result = await createSandbox(input, auth); 199 + const result = await createSandbox(input.body, auth); 198 200 return { 199 201 encoding: "application/json", 200 202 body: result,
+21 -8
apps/api/src/xrpc/io/pocketenv/sandbox/startSandbox.ts
··· 3 3 import type { Context } from "context"; 4 4 import { and, eq, isNull, or } from "drizzle-orm"; 5 5 import type { Server } from "lexicon"; 6 - import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/startSandbox"; 6 + import type { 7 + QueryParams, 8 + InputSchema, 9 + } from "lexicon/types/io/pocketenv/sandbox/startSandbox"; 7 10 import generateJwt from "lib/generateJwt"; 8 11 import schema from "schema"; 9 12 10 13 export default function (server: Server, ctx: Context) { 11 - const startSandbox = async (params: QueryParams, auth: HandlerAuth) => { 14 + const startSandbox = async ( 15 + params: QueryParams, 16 + input: InputSchema, 17 + auth: HandlerAuth, 18 + ) => { 12 19 let userId: string | undefined; 13 20 if (auth.credentials) { 14 21 const [user] = await ctx.db ··· 45 52 ? ctx.cfsandbox(record.base!) 46 53 : ctx.sandbox(); 47 54 48 - await sandbox.post(`/v1/sandboxes/${record.id}/start`, undefined, { 49 - headers: { 50 - Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 55 + await sandbox.post( 56 + `/v1/sandboxes/${record.id}/start`, 57 + { 58 + repo: input.repo, 51 59 }, 52 - }); 60 + { 61 + headers: { 62 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 63 + }, 64 + }, 65 + ); 53 66 return {}; 54 67 }; 55 68 server.io.pocketenv.sandbox.startSandbox({ 56 69 auth: ctx.authVerifier, 57 - handler: async ({ params, auth }) => { 58 - const result = await startSandbox(params, auth); 70 + handler: async ({ params, input, auth }) => { 71 + const result = await startSandbox(params, input.body, auth); 59 72 return { 60 73 encoding: "application/json", 61 74 body: result,
+3
apps/cf-sandbox/app.json
··· 1 + { 2 + "expo": {} 3 + }
+21
apps/cf-sandbox/eas.json
··· 1 + { 2 + "cli": { 3 + "version": ">= 18.4.0", 4 + "appVersionSource": "remote" 5 + }, 6 + "build": { 7 + "development": { 8 + "developmentClient": true, 9 + "distribution": "internal" 10 + }, 11 + "preview": { 12 + "distribution": "internal" 13 + }, 14 + "production": { 15 + "autoIncrement": true 16 + } 17 + }, 18 + "submit": { 19 + "production": {} 20 + } 21 + }
+19 -1
apps/cf-sandbox/src/index.ts
··· 24 24 import { and, eq, ExtractTablesWithRelations, isNull, or } from "drizzle-orm"; 25 25 import { getConnection } from "./drizzle"; 26 26 import { env } from "cloudflare:workers"; 27 - import { SandboxConfig, SandboxConfigSchema } from "./types/sandbox"; 27 + import { 28 + SandboxConfig, 29 + SandboxConfigSchema, 30 + StartSandboxConfig, 31 + StartSandboxConfigSchema, 32 + } from "./types/sandbox"; 28 33 import { BaseSandbox, createSandbox } from "./providers"; 29 34 import { SelectSandbox } from "./schema/sandboxes"; 30 35 import { PgTransaction } from "drizzle-orm/pg-core"; ··· 150 155 .values({ 151 156 base: params.base, 152 157 name, 158 + repo: params.repo, 153 159 provider: params.provider, 154 160 publicKey: env.PUBLIC_KEY, 155 161 userId: user?.id, ··· 245 251 c.req.param("sandboxId"), 246 252 ); 247 253 254 + const body = await c.req.json<StartSandboxConfig>(); 255 + const { repo } = StartSandboxConfigSchema.parse(body); 256 + 248 257 if (!record) { 249 258 return c.json({ error: "Sandbox not found" }, 404); 250 259 } ··· 393 402 .clone(record.repo) 394 403 .then(() => 395 404 consola.success(`Git Repository successfully cloned: ${record.repo}`), 405 + ) 406 + .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 407 + } 408 + 409 + if (repo) { 410 + sandbox 411 + .clone(repo) 412 + .then(() => 413 + consola.success(`Git Repository successfully cloned: ${repo}`), 396 414 ) 397 415 .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 398 416 }
+8
apps/cf-sandbox/src/types/sandbox.ts
··· 31 31 name: z.string().optional(), 32 32 description: z.string().optional(), 33 33 provider: z.enum(["cloudflare"]).optional().default("cloudflare"), 34 + repo: z.string().optional(), 34 35 base: z 35 36 .enum([ 36 37 "openclaw", ··· 90 91 ), 91 92 }); 92 93 94 + export const StartSandboxConfigSchema = z.object({ 95 + repo: z.string().optional(), 96 + keepAlive: z.boolean().optional().default(false), 97 + }); 98 + 93 99 export type SandboxConfig = z.infer<typeof SandboxConfigSchema>; 100 + 101 + export type StartSandboxConfig = z.infer<typeof StartSandboxConfigSchema>;
+6 -3
apps/cli/src/cmd/create.ts
··· 11 11 provider, 12 12 ssh, 13 13 base, 14 + repo, 14 15 }: { 15 - provider: string | undefined; 16 - ssh: boolean | undefined; 17 - base: string | undefined; 16 + provider?: string; 17 + ssh?: boolean; 18 + base?: string; 19 + repo?: string; 18 20 }, 19 21 ) { 20 22 const token = await getAccessToken(); ··· 34 36 base ?? 35 37 "at://did:plc:aturpi2ls3yvsmhc6wybomun/io.pocketenv.sandbox/openclaw", 36 38 provider: provider ?? "cloudflare", 39 + repo, 37 40 }, 38 41 { 39 42 headers: {
+16 -7
apps/cli/src/cmd/start.ts
··· 5 5 import { env } from "../lib/env"; 6 6 import connectToSandbox from "./ssh"; 7 7 8 - async function start(name: string, { ssh }: { ssh?: boolean }) { 8 + async function start( 9 + name: string, 10 + { ssh, repo }: { ssh?: boolean; repo?: string }, 11 + ) { 9 12 const token = await getAccessToken(); 10 13 11 14 try { 12 - await client.post("/xrpc/io.pocketenv.sandbox.startSandbox", undefined, { 13 - params: { 14 - id: name, 15 + await client.post( 16 + "/xrpc/io.pocketenv.sandbox.startSandbox", 17 + { 18 + repo, 15 19 }, 16 - headers: { 17 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 20 + { 21 + params: { 22 + id: name, 23 + }, 24 + headers: { 25 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 26 + }, 18 27 }, 19 - }); 28 + ); 20 29 21 30 if (ssh) { 22 31 await connectToSandbox(name);
+8
apps/cli/src/index.ts
··· 77 77 .command("start") 78 78 .argument("<sandbox>", "the sandbox to start") 79 79 .option("--ssh, -s", "connect to the Sandbox and automatically open a shell") 80 + .option( 81 + "--repo, -r <repo>", 82 + "the repository to clone into the sandbox (e.g., github:user/repo, tangled:user/repo, or a Git URL)", 83 + ) 80 84 .description("start the given sandbox") 81 85 .action(start); 82 86 ··· 95 99 "the base sandbox to use for the sandbox, e.g. openclaw, claude-code, codex, copilot ...", 96 100 ) 97 101 .option("--ssh, -s", "connect to the Sandbox and automatically open a shell") 102 + .option( 103 + "--repo, -r <repo>", 104 + "the repository to clone into the sandbox (e.g., github:user/repo, tangled:user/repo, or a Git URL)", 105 + ) 98 106 .argument("[name]", "the name of the sandbox to create") 99 107 .description("create a new sandbox") 100 108 .action(createSandbox);
+18 -1
apps/sandbox/src/index.ts
··· 21 21 } from "unique-username-generator"; 22 22 import { eq, ExtractTablesWithRelations, or } from "drizzle-orm"; 23 23 import { getConnection } from "./drizzle.ts"; 24 - import { SandboxConfig, SandboxConfigSchema } from "./types/sandbox.ts"; 24 + import { 25 + SandboxConfig, 26 + SandboxConfigSchema, 27 + StartSandboxInput, 28 + StartSandboxInputSchema, 29 + } from "./types/sandbox.ts"; 25 30 import { 26 31 BaseSandbox, 27 32 createSandbox, ··· 209 214 return c.json({ error: "Sandbox provider not supported" }, 400); 210 215 } 211 216 217 + const body = await c.req.json<StartSandboxInput>(); 218 + const { repo } = StartSandboxInputSchema.parse(body); 219 + 212 220 sandbox = await getSandboxById( 213 221 record.provider as Provider, 214 222 record.sandboxId!, ··· 260 268 .clone(record.repo) 261 269 .then(() => 262 270 consola.success(`Git Repository successfully cloned: ${record.repo}`), 271 + ) 272 + .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 273 + } 274 + 275 + if (repo) { 276 + sandbox 277 + .clone(repo) 278 + .then(() => 279 + consola.success(`Git Repository successfully cloned: ${repo}`), 263 280 ) 264 281 .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 265 282 }
+6
apps/sandbox/src/types/sandbox.ts
··· 70 70 ), 71 71 }); 72 72 73 + export const StartSandboxInputSchema = z.object({ 74 + repo: z.string().optional(), 75 + }); 76 + 73 77 export type SandboxConfig = z.infer<typeof SandboxConfigSchema>; 78 + 79 + export type StartSandboxInput = z.infer<typeof StartSandboxInputSchema>;
+11 -7
apps/web/src/api/sandbox.ts
··· 88 88 }); 89 89 90 90 export const startSandbox = (id: string) => 91 - client.post("/xrpc/io.pocketenv.sandbox.startSandbox", undefined, { 92 - params: { 93 - id, 94 - }, 95 - headers: { 96 - Authorization: `Bearer ${localStorage.getItem("token")}`, 91 + client.post( 92 + "/xrpc/io.pocketenv.sandbox.startSandbox", 93 + {}, 94 + { 95 + params: { 96 + id, 97 + }, 98 + headers: { 99 + Authorization: `Bearer ${localStorage.getItem("token")}`, 100 + }, 97 101 }, 98 - }); 102 + ); 99 103 100 104 export const putPreferences = () => 101 105 client.post(