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 isRunning filter and CLI sandbox commands

+331 -51
+4
apps/api/lexicons/actor/getActorSandboxes.json
··· 25 25 "type": "integer", 26 26 "description": "The number of sandboxes to skip before starting to collect the result set.", 27 27 "minimum": 0 28 + }, 29 + "isRunning": { 30 + "type": "boolean", 31 + "description": "Filter sandboxes by their running status." 28 32 } 29 33 } 30 34 },
+4
apps/api/pkl/defs/actor/getActorSandboxes.pkl
··· 24 24 description = "The number of sandboxes to skip before starting to collect the result set." 25 25 minimum = 0 26 26 } 27 + ["isRunning"] = new BooleanType { 28 + type = "boolean" 29 + description = "Filter sandboxes by their running status." 30 + } 27 31 } 28 32 } 29 33 output {
+4
apps/api/src/lexicon/lexicons.ts
··· 73 73 "The number of sandboxes to skip before starting to collect the result set.", 74 74 minimum: 0, 75 75 }, 76 + isRunning: { 77 + type: "boolean", 78 + description: "Filter sandboxes by their running status.", 79 + }, 76 80 }, 77 81 }, 78 82 output: {
+2
apps/api/src/lexicon/types/io/pocketenv/actor/getActorSandboxes.ts
··· 16 16 limit?: number; 17 17 /** The number of sandboxes to skip before starting to collect the result set. */ 18 18 offset?: number; 19 + /** Filter sandboxes by their running status. */ 20 + isRunning?: boolean; 19 21 } 20 22 21 23 export type InputSchema = undefined;
+27 -7
apps/api/src/xrpc/io/pocketenv/actor/getActorSandboxes.ts
··· 9 9 import type { SelectSandbox } from "schema/sandboxes"; 10 10 import { consola } from "consola"; 11 11 import schema from "schema"; 12 - import { count, eq, desc, or } from "drizzle-orm"; 12 + import { count, eq, desc, or, and } from "drizzle-orm"; 13 13 14 14 export default function (server: Server, ctx: Context) { 15 15 const getActorSandboxes = (params: QueryParams, auth: HandlerAuth) => ··· 47 47 .from(schema.sandboxes) 48 48 .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 49 49 .where( 50 - or( 51 - eq(schema.users.did, params.did), 52 - eq(schema.users.handle, params.did), 50 + and( 51 + or( 52 + eq(schema.users.did, params.did), 53 + eq(schema.users.handle, params.did), 54 + ), 55 + ...(params.isRunning !== undefined 56 + ? [ 57 + eq( 58 + schema.sandboxes.status, 59 + params.isRunning ? "RUNNING" : "STOPPED", 60 + ), 61 + ] 62 + : []), 53 63 ), 54 64 ) 55 65 .orderBy(desc(schema.sandboxes.createdAt)) ··· 62 72 .from(schema.sandboxes) 63 73 .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 64 74 .where( 65 - or( 66 - eq(schema.users.did, params.did), 67 - eq(schema.users.handle, params.did), 75 + and( 76 + or( 77 + eq(schema.users.did, params.did), 78 + eq(schema.users.handle, params.did), 79 + ), 80 + ...(params.isRunning !== undefined 81 + ? [ 82 + eq( 83 + schema.sandboxes.status, 84 + params.isRunning ? "RUNNING" : "STOPPED", 85 + ), 86 + ] 87 + : []), 68 88 ), 69 89 ) 70 90 .execute()
+206 -44
apps/cli/src/cmd/copy.ts
··· 2 2 import { c } from "../theme"; 3 3 import { glob, unlink } from "node:fs/promises"; 4 4 import ignore from "ignore"; 5 - import { readFile, lstat } from "node:fs/promises"; 5 + import { readFile, lstat, writeFile } from "node:fs/promises"; 6 6 import { join } from "node:path"; 7 7 import * as tar from "tar"; 8 8 import crypto from "node:crypto"; ··· 16 16 `Copying files from ${c.primary(source)} to ${c.primary(destination)}...`, 17 17 ).start(); 18 18 19 - if (!source.includes(":/") && destination.includes(":/")) { 20 - const sandboxId = destination.split(":/")[0]!; 21 - const token = await getAccessToken(); 22 - 23 - const { data } = await client.get<{ sandbox: Sandbox }>( 24 - "/xrpc/io.pocketenv.sandbox.getSandbox", 25 - { 26 - params: { 27 - id: sandboxId, 28 - }, 29 - headers: { 30 - Authorization: `Bearer ${token}`, 31 - }, 32 - }, 33 - ); 34 - 35 - if (!data.sandbox) { 36 - consola.error(`Sandbox not found: ${c.primary(sandboxId)}`); 37 - process.exit(1); 38 - } 39 - 40 - if (data.sandbox.status !== "RUNNING") { 41 - consola.error(`Sandbox ${c.primary(sandboxId)} is not running.`); 42 - process.exit(1); 43 - } 44 - 45 - const output = await compressDirectory(source); 46 - const uuid = await uploadToStorage(output); 47 - consola.info(`Uploaded to storage with UUID: ${uuid}`); 48 - await unlink(output); 19 + if (source === destination) { 20 + consola.error("Source and destination cannot be the same."); 21 + process.exit(1); 22 + } 49 23 50 - await client.post( 51 - "/xrpc/io.pocketenv.sandbox.pullDirectory", 52 - { 53 - uuid, 54 - sandboxId, 55 - directoryPath: destination.split(":")[1], 56 - }, 57 - { 58 - headers: { 59 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 60 - }, 61 - }, 62 - ); 24 + if (!source.includes(":/") && destination.includes(":/")) { 25 + await localToSandbox(source, destination); 63 26 } 64 27 65 28 if (source.includes(":/") && !destination.includes(":/")) { 29 + await sandboxToLocal(source, destination); 66 30 } 67 31 68 32 if (source.includes(":/") && destination.includes(":/")) { 33 + await sandboxToSandbox(source, destination); 34 + } 35 + 36 + if (!source.includes(":/") && !destination.includes(":/")) { 37 + consola.error("Both source and destination cannot be local paths."); 38 + process.exit(1); 69 39 } 70 40 71 41 spinner.stopAndPersist({ ··· 155 125 consola.error("Failed to upload", error); 156 126 process.exit(1); 157 127 } 128 + } 129 + 130 + async function localToSandbox(source: string, destination: string) { 131 + const sandboxId = destination.split(":/")[0]!; 132 + const token = await getAccessToken(); 133 + 134 + const { data } = await client.get<{ sandbox: Sandbox }>( 135 + "/xrpc/io.pocketenv.sandbox.getSandbox", 136 + { 137 + params: { 138 + id: sandboxId, 139 + }, 140 + headers: { 141 + Authorization: `Bearer ${token}`, 142 + }, 143 + }, 144 + ); 145 + 146 + if (!data.sandbox) { 147 + consola.error(`Sandbox not found: ${c.primary(sandboxId)}`); 148 + process.exit(1); 149 + } 150 + 151 + if (data.sandbox.status !== "RUNNING") { 152 + consola.error(`Sandbox ${c.primary(sandboxId)} is not running.`); 153 + process.exit(1); 154 + } 155 + 156 + const output = await compressDirectory(source); 157 + const uuid = await uploadToStorage(output); 158 + await unlink(output); 159 + 160 + await client.post( 161 + "/xrpc/io.pocketenv.sandbox.pullDirectory", 162 + { 163 + uuid, 164 + sandboxId, 165 + directoryPath: destination.split(":")[1], 166 + }, 167 + { 168 + headers: { 169 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 170 + }, 171 + }, 172 + ); 173 + } 174 + 175 + async function sandboxToLocal(source: string, destination: string) { 176 + const token = await getAccessToken(); 177 + const sandboxId = source.split(":/")[0]!; 178 + 179 + const { data } = await client.get<{ sandbox: Sandbox }>( 180 + "/xrpc/io.pocketenv.sandbox.getSandbox", 181 + { 182 + params: { 183 + id: sandboxId, 184 + }, 185 + headers: { 186 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 187 + }, 188 + }, 189 + ); 190 + 191 + if (!data.sandbox) { 192 + consola.error(`Sandbox not found: ${c.primary(sandboxId)}`); 193 + process.exit(1); 194 + } 195 + 196 + if (data.sandbox.status !== "RUNNING") { 197 + consola.error(`Sandbox ${c.primary(sandboxId)} is not running.`); 198 + process.exit(1); 199 + } 200 + 201 + const response = await client.post<{ uuid: string }>( 202 + "/xrpc/io.pocketenv.sandbox.pushDirectory", 203 + { 204 + sandboxId, 205 + directoryPath: source.split(":")[1], 206 + }, 207 + { 208 + headers: { 209 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 210 + }, 211 + }, 212 + ); 213 + 214 + const { uuid } = response.data; 215 + 216 + const downloadResponse = await fetch( 217 + `https://sandbox.pocketenv.io/cp/${uuid}`, 218 + { 219 + headers: { 220 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 221 + }, 222 + }, 223 + ); 224 + 225 + if (!downloadResponse.ok) { 226 + consola.error(`Failed to download archive: ${downloadResponse.statusText}`); 227 + process.exit(1); 228 + } 229 + 230 + const arrayBuffer = await downloadResponse.arrayBuffer(); 231 + const buffer = Buffer.from(arrayBuffer); 232 + const tempFile = `${crypto.randomBytes(16).toString("hex")}.tar.gz`; 233 + await writeFile(tempFile, buffer); 234 + } 235 + 236 + async function sandboxToSandbox(source: string, destination: string) { 237 + const sourceSandboxId = source.split(":/")[0]!; 238 + const destinationSandboxId = destination.split(":/")[0]!; 239 + 240 + const token = await getAccessToken(); 241 + 242 + const [{ data: sourceSandbox }, { data: destinationSandbox }] = 243 + await Promise.all([ 244 + client.get<{ sandbox: Sandbox }>( 245 + "/xrpc/io.pocketenv.sandbox.getSandbox", 246 + { 247 + params: { 248 + id: sourceSandboxId, 249 + }, 250 + headers: { 251 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 252 + }, 253 + }, 254 + ), 255 + client.get<{ sandbox: Sandbox }>( 256 + "/xrpc/io.pocketenv.sandbox.getSandbox", 257 + { 258 + params: { 259 + id: destinationSandboxId, 260 + }, 261 + headers: { 262 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 263 + }, 264 + }, 265 + ), 266 + ]); 267 + 268 + if (!sourceSandbox.sandbox) { 269 + consola.error(`Source sandbox not found: ${c.primary(sourceSandboxId)}`); 270 + process.exit(1); 271 + } 272 + 273 + if (!destinationSandbox.sandbox) { 274 + consola.error( 275 + `Destination Sandbox not found: ${c.primary(destinationSandboxId)}`, 276 + ); 277 + process.exit(1); 278 + } 279 + 280 + if (sourceSandbox.sandbox.status !== "RUNNING") { 281 + consola.error( 282 + `Source Sandbox ${c.primary(sourceSandboxId)} is not running.`, 283 + ); 284 + process.exit(1); 285 + } 286 + 287 + if (destinationSandbox.sandbox.status !== "RUNNING") { 288 + consola.error( 289 + `Destination Sandbox ${c.primary(destinationSandboxId)} is not running.`, 290 + ); 291 + process.exit(1); 292 + } 293 + 294 + const { data } = await client.post<{ uuid: string }>( 295 + "/xrpc/io.pocketenv.sandbox.pushDirectory", 296 + { 297 + sandboxId: sourceSandboxId, 298 + directoryPath: source.split(":")[1], 299 + }, 300 + { 301 + headers: { 302 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 303 + }, 304 + }, 305 + ); 306 + 307 + await client.post( 308 + "/xrpc/io.pocketenv.sandbox.pullDirectory", 309 + { 310 + uuid: data.uuid, 311 + sandboxId: destinationSandboxId, 312 + directoryPath: destination.split(":")[1], 313 + }, 314 + { 315 + headers: { 316 + Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 317 + }, 318 + }, 319 + ); 158 320 } 159 321 160 322 export default copy;
+81
apps/cli/src/cmd/ps.ts
··· 1 + import { client } from "../client"; 2 + import consola from "consola"; 3 + import { env } from "../lib/env"; 4 + import getAccessToken from "../lib/getAccessToken"; 5 + import type { Sandbox } from "../types/sandbox"; 6 + import Table from "cli-table3"; 7 + import dayjs from "dayjs"; 8 + import relativeTime from "dayjs/plugin/relativeTime"; 9 + import type { Profile } from "../types/profile"; 10 + import { c } from "../theme"; 11 + dayjs.extend(relativeTime); 12 + 13 + async function ps() { 14 + const token = await getAccessToken(); 15 + 16 + const profile = await client.get<Profile>( 17 + "/xrpc/io.pocketenv.actor.getProfile", 18 + { 19 + headers: { 20 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 21 + }, 22 + }, 23 + ); 24 + 25 + const response = await client.get<{ sandboxes: Sandbox[] }>( 26 + "/xrpc/io.pocketenv.actor.getActorSandboxes", 27 + { 28 + params: { 29 + did: profile.data.did, 30 + isRunning: true, 31 + offset: 0, 32 + limit: 100, 33 + }, 34 + }, 35 + ); 36 + 37 + const table = new Table({ 38 + head: [ 39 + c.primary("NAME"), 40 + c.primary("BASE"), 41 + c.primary("STATUS"), 42 + c.primary("CREATED AT"), 43 + ], 44 + chars: { 45 + top: "", 46 + "top-mid": "", 47 + "top-left": "", 48 + "top-right": "", 49 + bottom: "", 50 + "bottom-mid": "", 51 + "bottom-left": "", 52 + "bottom-right": "", 53 + left: "", 54 + "left-mid": "", 55 + mid: "", 56 + "mid-mid": "", 57 + right: "", 58 + "right-mid": "", 59 + middle: " ", 60 + }, 61 + style: { 62 + border: [], 63 + head: [], 64 + }, 65 + }); 66 + 67 + for (const sandbox of response.data.sandboxes) { 68 + table.push([ 69 + c.secondary(sandbox.name), 70 + sandbox.baseSandbox, 71 + sandbox.status === "RUNNING" 72 + ? c.highlight(sandbox.status) 73 + : sandbox.status, 74 + dayjs(sandbox.createdAt).fromNow(), 75 + ]); 76 + } 77 + 78 + consola.log(table.toString()); 79 + } 80 + 81 + export default ps;
+3
apps/cli/src/index.ts
··· 32 32 stopService, 33 33 } from "./cmd/service"; 34 34 import copy from "./cmd/copy"; 35 + import ps from "./cmd/ps"; 35 36 36 37 const program = new Command(); 37 38 ··· 99 100 `, 100 101 ) 101 102 .action(copy); 103 + 104 + program.command("ps").description("list running Sandboxes").action(ps); 102 105 103 106 program 104 107 .command("start")