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 sandbox lookup and improve CLI file/volume

Validate sandbox ownership when adding files and volumes (allow lookup
by ID or name and ensure the caller's DID matches). Enhance CLI:
putFile accepts stdin/local path or opens editor, encrypts content and
uploads; list views show IDs and improved coloring; deletes call API
endpoints by ID. Tidy up some schema formatting.

+182 -45
+1 -3
apps/api/src/schema/sandbox-files.ts
··· 6 6 const sandboxFiles = pgTable( 7 7 "sandbox_files", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`file_id()`), 9 + id: text("id").primaryKey().default(sql`file_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id),
+1 -3
apps/api/src/schema/sandbox-secrets.ts
··· 6 6 const sandboxSecrets = pgTable( 7 7 "sandbox_secrets", 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),
+1 -3
apps/api/src/schema/sandbox-variables.ts
··· 6 6 const sandboxVariables = pgTable( 7 7 "sandbox_variables", 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),
+1 -3
apps/api/src/schema/sandbox-volumes.ts
··· 6 6 const sandboxVolumes = pgTable( 7 7 "sandbox_volumes", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`volume_id()`), 9 + id: text("id").primaryKey().default(sql`volume_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id),
+23 -1
apps/api/src/xrpc/io/pocketenv/file/addFile.ts
··· 1 1 import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 2 import type { Context } from "context"; 3 + import { and, eq, or } from "drizzle-orm"; 3 4 import type { Server } from "lexicon"; 4 5 import type { HandlerInput } from "lexicon/types/io/pocketenv/file/addFile"; 5 6 import files from "schema/files"; 6 7 import sandboxFiles from "schema/sandbox-files"; 8 + import sandboxes from "schema/sandboxes"; 9 + import users from "schema/users"; 7 10 8 11 export default function (server: Server, ctx: Context) { 9 12 const addFile = async (input: HandlerInput, auth: HandlerAuth) => { ··· 26 29 return; 27 30 } 28 31 32 + const [sandbox] = await tx 33 + .select() 34 + .from(sandboxes) 35 + .leftJoin(users, eq(sandboxes.userId, users.id)) 36 + .where( 37 + and( 38 + or( 39 + eq(sandboxes.id, input.body.file.sandboxId), 40 + eq(sandboxes.name, input.body.file.sandboxId), 41 + ), 42 + eq(users.did, auth.credentials.did), 43 + ), 44 + ) 45 + .execute(); 46 + 47 + if (!sandbox) { 48 + throw new XRPCError(404, "Sandbox not found"); 49 + } 50 + 29 51 await tx 30 52 .insert(sandboxFiles) 31 53 .values({ 32 54 fileId: file.id, 33 - sandboxId: input.body.file.sandboxId, 55 + sandboxId: sandbox.sandboxes.id, 34 56 path: input.body.file.path, 35 57 }) 36 58 .execute();
+32 -10
apps/api/src/xrpc/io/pocketenv/volume/addVolume.ts
··· 1 1 import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 2 import type { Context } from "context"; 3 - import { eq } from "drizzle-orm"; 3 + import { and, eq, or } from "drizzle-orm"; 4 4 import type { Server } from "lexicon"; 5 5 import type { HandlerInput } from "lexicon/types/io/pocketenv/volume/addVolume"; 6 6 import sandboxVolumes from "schema/sandbox-volumes"; ··· 14 14 } from "unique-username-generator"; 15 15 import { consola } from "consola"; 16 16 import { createAgent } from "lib/agent"; 17 + import users from "schema/users"; 17 18 18 19 export default function (server: Server, ctx: Context) { 19 20 const addVolume = async (input: HandlerInput, auth: HandlerAuth) => { ··· 60 61 return; 61 62 } 62 63 63 - await tx 64 - .insert(sandboxVolumes) 65 - .values({ 66 - volumeId: volume.id, 67 - sandboxId: input.body.volume.sandboxId, 68 - path: input.body.volume.path, 69 - name: input.body.volume.name, 70 - }) 71 - .execute(); 64 + if (input.body.volume.sandboxId) { 65 + const [sandbox] = await tx 66 + .select() 67 + .from(sandboxes) 68 + .leftJoin(users, eq(sandboxes.userId, users.id)) 69 + .where( 70 + and( 71 + eq(users.did, auth.credentials.did), 72 + or( 73 + eq(sandboxes.id, input.body.volume.sandboxId), 74 + eq(sandboxes.name, input.body.volume.sandboxId), 75 + ), 76 + ), 77 + ) 78 + .execute(); 79 + 80 + if (!sandbox) { 81 + throw new XRPCError(404, "Sandbox not found"); 82 + } 83 + 84 + await tx 85 + .insert(sandboxVolumes) 86 + .values({ 87 + volumeId: volume.id, 88 + sandboxId: sandbox.sandboxes.id, 89 + path: input.body.volume.path, 90 + name: input.body.volume.name, 91 + }) 92 + .execute(); 93 + } 72 94 }); 73 95 74 96 if (input.body.volume.sandboxId) {
+71 -8
apps/cli/src/cmd/file.ts
··· 8 8 import CliTable3 from "cli-table3"; 9 9 import type { File } from "../types/file"; 10 10 import { c } from "../theme"; 11 + import { editor } from "@inquirer/prompts"; 12 + import fs from "fs/promises"; 13 + import path from "path"; 14 + import encrypt from "../lib/sodium"; 11 15 12 16 dayjs.extend(relativeTime); 13 17 14 - export async function putFile(sandbox: string, path: string) { 18 + export async function putFile( 19 + sandbox: string, 20 + remotePath: string, 21 + localPath?: string, 22 + ) { 15 23 const token = await getAccessToken(); 16 24 17 - consola.success( 18 - `File ${chalk.rgb(0, 232, 198)(path)} successfully created in sandbox ${chalk.rgb(0, 232, 198)(sandbox)}`, 19 - ); 25 + let content: string; 26 + if (!process.stdin.isTTY) { 27 + const chunks: Buffer[] = []; 28 + for await (const chunk of process.stdin) chunks.push(chunk); 29 + content = Buffer.concat(chunks).toString().trim(); 30 + } else if (localPath) { 31 + const resolvedPath = path.resolve(localPath); 32 + try { 33 + await fs.access(resolvedPath); 34 + } catch (err) { 35 + consola.error(`No such file: ${chalk.redBright(localPath)}`); 36 + process.exit(1); 37 + } 38 + content = await fs.readFile(resolvedPath, "utf-8"); 39 + } else { 40 + content = ( 41 + await editor({ 42 + message: "File content (opens in $EDITOR):", 43 + waitForUserInput: false, 44 + }) 45 + ).trim(); 46 + } 47 + 48 + try { 49 + await client.post( 50 + "/xrpc/io.pocketenv.file.addFile", 51 + { 52 + file: { 53 + sandbox, 54 + path, 55 + content: await encrypt(content), 56 + }, 57 + }, 58 + { 59 + headers: { 60 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 61 + }, 62 + }, 63 + ); 64 + 65 + consola.success( 66 + `File ${chalk.rgb(0, 232, 198)(remotePath)} successfully created in sandbox ${chalk.rgb(0, 232, 198)(sandbox)}`, 67 + ); 68 + } catch (error) { 69 + consola.error(`Failed to create file: ${error}`); 70 + } 20 71 } 21 72 22 73 export async function listFiles(sandboxId: string) { ··· 70 121 consola.log(table.toString()); 71 122 } 72 123 73 - export async function deleteFile(sandbox: string, id: string) { 124 + export async function deleteFile(id: string) { 74 125 const token = await getAccessToken(); 75 126 76 - consola.success( 77 - `File ${chalk.rgb(0, 232, 198)(id)} successfully deleted from sandbox ${chalk.rgb(0, 232, 198)(sandbox)}`, 78 - ); 127 + try { 128 + await client.post(`/xrpc/io.pocketenv.file.deleteFile`, undefined, { 129 + params: { 130 + id, 131 + }, 132 + headers: { 133 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 134 + }, 135 + }); 136 + consola.success( 137 + `File ${chalk.rgb(0, 232, 198)(id)} successfully deleted from sandbox`, 138 + ); 139 + } catch (error) { 140 + consola.error(`Failed to delete file: ${error}`); 141 + } 79 142 }
+4 -3
apps/cli/src/cmd/secret.ts
··· 10 10 import relativeTime from "dayjs/plugin/relativeTime"; 11 11 import { env } from "../lib/env"; 12 12 import encrypt from "../lib/sodium"; 13 + import { c } from "../theme"; 13 14 14 15 dayjs.extend(relativeTime); 15 16 ··· 41 42 ); 42 43 43 44 const table = new Table({ 44 - head: [chalk.cyan("ID"), chalk.cyan("NAME"), chalk.cyan("CREATED AT")], 45 + head: [c.primary("ID"), c.primary("NAME"), c.primary("CREATED AT")], 45 46 chars: { 46 47 top: "", 47 48 "top-mid": "", ··· 67 68 68 69 for (const secret of response.data.secrets) { 69 70 table.push([ 70 - chalk.greenBright(secret.id), 71 - chalk.greenBright(secret.name), 71 + c.secondary(secret.id), 72 + c.highlight(secret.name), 72 73 dayjs(secret.createdAt).fromNow(), 73 74 ]); 74 75 }
+47 -7
apps/cli/src/cmd/volume.ts
··· 27 27 ); 28 28 29 29 const table = new CliTable3({ 30 - head: [c.primary("NAME"), c.primary("PATH"), c.primary("CREATED AT")], 30 + head: [ 31 + c.primary("ID"), 32 + c.primary("NAME"), 33 + c.primary("PATH"), 34 + c.primary("CREATED AT"), 35 + ], 31 36 chars: { 32 37 top: "", 33 38 "top-mid": "", ··· 53 58 54 59 for (const volume of response.data.volumes) { 55 60 table.push([ 56 - c.secondary(volume.name), 61 + c.secondary(volume.id), 62 + volume.name, 57 63 volume.path, 58 64 dayjs(volume.createdAt).fromNow(), 59 65 ]); ··· 69 75 ) { 70 76 const token = await getAccessToken(); 71 77 72 - consola.success( 73 - `Volume ${chalk.rgb(0, 232, 198)(name)} successfully mounted in sandbox ${chalk.rgb(0, 232, 198)(sandbox)} at path ${chalk.rgb(0, 232, 198)(path)}`, 74 - ); 78 + try { 79 + await client.post( 80 + "/xrpc/io.pocketenv.volume.addVolume", 81 + { 82 + volume: { 83 + sandbox, 84 + name, 85 + path, 86 + }, 87 + }, 88 + { 89 + headers: { 90 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 91 + }, 92 + }, 93 + ); 94 + 95 + consola.success( 96 + `Volume ${chalk.rgb(0, 232, 198)(name)} successfully mounted in sandbox ${chalk.rgb(0, 232, 198)(sandbox)} at path ${chalk.rgb(0, 232, 198)(path)}`, 97 + ); 98 + } catch (error) { 99 + consola.error("Failed to create volume:", error); 100 + } 75 101 } 76 102 77 - export async function deleteVolume(sandbox: string, name: string) { 103 + export async function deleteVolume(id: string) { 78 104 const token = await getAccessToken(); 79 105 106 + try { 107 + await client.post(`/xrpc/io.pocketenv.volume.deleteVolume`, undefined, { 108 + params: { 109 + id, 110 + }, 111 + headers: { 112 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 113 + }, 114 + }); 115 + } catch (error) { 116 + consola.error(`Failed to delete volume: ${error}`); 117 + return; 118 + } 119 + 80 120 consola.success( 81 - `Volume ${chalk.rgb(0, 232, 198)(name)} successfully deleted from sandbox ${chalk.rgb(0, 232, 198)(sandbox)}`, 121 + `Volume ${chalk.rgb(0, 232, 198)(id)} successfully deleted from sandbo}`, 82 122 ); 83 123 }
+1 -4
apps/cli/src/index.ts
··· 167 167 .command("put") 168 168 .argument("<sandbox>", "the sandbox to put the file in") 169 169 .argument("<path>", "the remote path to upload the file to") 170 - .option( 171 - "--local-path, -f <localPath>", 172 - "the local path of the file to upload", 173 - ) 170 + .argument("[localPath]", "the local path of the file to upload") 174 171 .description("upload a file to the given sandbox") 175 172 .action(putFile); 176 173