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 exec API and CLI command

Add lexicon, types, xrpc handler and pkl for
io.pocketenv.sandbox.exec and wire it into the server. Add CLI
exec command to call the endpoint. Standardize BaseSandbox.sh return
shape to {stdout, stderr, exitCode} and update provider implementations
accordingly

+447 -12
+57
apps/api/lexicons/sandbox/exec.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.sandbox.exec", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Execute a command in a sandbox.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "id" 12 + ], 13 + "properties": { 14 + "id": { 15 + "type": "string", 16 + "description": "The sandbox ID." 17 + } 18 + } 19 + }, 20 + "input": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "command" 26 + ], 27 + "properties": { 28 + "command": { 29 + "type": "string", 30 + "description": "The command to execute in the sandbox." 31 + } 32 + } 33 + } 34 + }, 35 + "output": { 36 + "encoding": "application/json", 37 + "schema": { 38 + "type": "object", 39 + "properties": { 40 + "stdout": { 41 + "type": "string", 42 + "description": "The output of the executed command." 43 + }, 44 + "stderr": { 45 + "type": "string", 46 + "description": "The error output of the executed command, if any." 47 + }, 48 + "exitCode": { 49 + "type": "integer", 50 + "description": "The exit code of the executed command." 51 + } 52 + } 53 + } 54 + } 55 + } 56 + } 57 + }
+53
apps/api/pkl/defs/sandbox/exec.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.sandbox.exec" 5 + defs = new Mapping<String, Procedure> { 6 + ["main"] { 7 + type = "procedure" 8 + description = "Execute a command in a sandbox." 9 + parameters { 10 + type = "params" 11 + required = List("id") 12 + properties { 13 + ["id"] = new StringType { 14 + type = "string" 15 + description = "The sandbox ID." 16 + } 17 + } 18 + } 19 + input { 20 + encoding = "application/json" 21 + schema { 22 + type = "object" 23 + required = List("command") 24 + properties { 25 + ["command"] = new StringType { 26 + type = "string" 27 + description = "The command to execute in the sandbox." 28 + } 29 + } 30 + } 31 + } 32 + output { 33 + encoding = "application/json" 34 + schema = new ObjectType { 35 + type = "object" 36 + properties { 37 + ["stdout"] = new StringType { 38 + type = "string" 39 + description = "The output of the executed command." 40 + } 41 + ["stderr"] = new StringType { 42 + type = "string" 43 + description = "The error output of the executed command, if any." 44 + } 45 + ["exitCode"] = new IntegerType { 46 + type = "integer" 47 + description = "The exit code of the executed command." 48 + } 49 + } 50 + } 51 + } 52 + } 53 + }
+12
apps/api/src/lexicon/index.ts
··· 21 21 import type * as IoPocketenvSandboxCreateIntegration from "./types/io/pocketenv/sandbox/createIntegration"; 22 22 import type * as IoPocketenvSandboxCreateSandbox from "./types/io/pocketenv/sandbox/createSandbox"; 23 23 import type * as IoPocketenvSandboxDeleteSandbox from "./types/io/pocketenv/sandbox/deleteSandbox"; 24 + import type * as IoPocketenvSandboxExec from "./types/io/pocketenv/sandbox/exec"; 24 25 import type * as IoPocketenvSandboxExposePort from "./types/io/pocketenv/sandbox/exposePort"; 25 26 import type * as IoPocketenvSandboxExposeVscode from "./types/io/pocketenv/sandbox/exposeVscode"; 26 27 import type * as IoPocketenvSandboxGetExposedPorts from "./types/io/pocketenv/sandbox/getExposedPorts"; ··· 255 256 >, 256 257 ) { 257 258 const nsid = "io.pocketenv.sandbox.deleteSandbox"; // @ts-ignore 259 + return this._server.xrpc.method(nsid, cfg); 260 + } 261 + 262 + exec<AV extends AuthVerifier>( 263 + cfg: ConfigOf< 264 + AV, 265 + IoPocketenvSandboxExec.Handler<ExtractAuth<AV>>, 266 + IoPocketenvSandboxExec.HandlerReqCtx<ExtractAuth<AV>> 267 + >, 268 + ) { 269 + const nsid = "io.pocketenv.sandbox.exec"; // @ts-ignore 258 270 return this._server.xrpc.method(nsid, cfg); 259 271 } 260 272
+55
apps/api/src/lexicon/lexicons.ts
··· 1043 1043 }, 1044 1044 }, 1045 1045 }, 1046 + IoPocketenvSandboxExec: { 1047 + lexicon: 1, 1048 + id: "io.pocketenv.sandbox.exec", 1049 + defs: { 1050 + main: { 1051 + type: "procedure", 1052 + description: "Execute a command in a sandbox.", 1053 + parameters: { 1054 + type: "params", 1055 + required: ["id"], 1056 + properties: { 1057 + id: { 1058 + type: "string", 1059 + description: "The sandbox ID.", 1060 + }, 1061 + }, 1062 + }, 1063 + input: { 1064 + encoding: "application/json", 1065 + schema: { 1066 + type: "object", 1067 + required: ["command"], 1068 + properties: { 1069 + command: { 1070 + type: "string", 1071 + description: "The command to execute in the sandbox.", 1072 + }, 1073 + }, 1074 + }, 1075 + }, 1076 + output: { 1077 + encoding: "application/json", 1078 + schema: { 1079 + type: "object", 1080 + properties: { 1081 + stdout: { 1082 + type: "string", 1083 + description: "The output of the executed command.", 1084 + }, 1085 + stderr: { 1086 + type: "string", 1087 + description: 1088 + "The error output of the executed command, if any.", 1089 + }, 1090 + exitCode: { 1091 + type: "integer", 1092 + description: "The exit code of the executed command.", 1093 + }, 1094 + }, 1095 + }, 1096 + }, 1097 + }, 1098 + }, 1099 + }, 1046 1100 IoPocketenvSandboxExposePort: { 1047 1101 lexicon: 1, 1048 1102 id: "io.pocketenv.sandbox.exposePort", ··· 2518 2572 IoPocketenvSandboxCreateSandbox: "io.pocketenv.sandbox.createSandbox", 2519 2573 IoPocketenvSandboxDefs: "io.pocketenv.sandbox.defs", 2520 2574 IoPocketenvSandboxDeleteSandbox: "io.pocketenv.sandbox.deleteSandbox", 2575 + IoPocketenvSandboxExec: "io.pocketenv.sandbox.exec", 2521 2576 IoPocketenvSandboxExposePort: "io.pocketenv.sandbox.exposePort", 2522 2577 IoPocketenvSandboxExposeVscode: "io.pocketenv.sandbox.exposeVscode", 2523 2578 IoPocketenvSandboxGetExposedPorts: "io.pocketenv.sandbox.getExposedPorts",
+59
apps/api/src/lexicon/types/io/pocketenv/sandbox/exec.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + 11 + export interface QueryParams { 12 + /** The sandbox ID. */ 13 + id: string; 14 + } 15 + 16 + export interface InputSchema { 17 + /** The command to execute in the sandbox. */ 18 + command: string; 19 + [k: string]: unknown; 20 + } 21 + 22 + export interface OutputSchema { 23 + /** The output of the executed command. */ 24 + stdout?: string; 25 + /** The error output of the executed command, if any. */ 26 + stderr?: string; 27 + /** The exit code of the executed command. */ 28 + exitCode?: number; 29 + [k: string]: unknown; 30 + } 31 + 32 + export interface HandlerInput { 33 + encoding: "application/json"; 34 + body: InputSchema; 35 + } 36 + 37 + export interface HandlerSuccess { 38 + encoding: "application/json"; 39 + body: OutputSchema; 40 + headers?: { [key: string]: string }; 41 + } 42 + 43 + export interface HandlerError { 44 + status: number; 45 + message?: string; 46 + } 47 + 48 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 49 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 50 + auth: HA; 51 + params: QueryParams; 52 + input: HandlerInput; 53 + req: express.Request; 54 + res: express.Response; 55 + resetRouteRateLimits: () => Promise<void>; 56 + }; 57 + export type Handler<HA extends HandlerAuth = never> = ( 58 + ctx: HandlerReqCtx<HA>, 59 + ) => Promise<HandlerOutput> | HandlerOutput;
+2
apps/api/src/xrpc/index.ts
··· 42 42 import getExposedPorts from "./io/pocketenv/sandbox/getExposedPorts"; 43 43 import unexposePort from "./io/pocketenv/sandbox/unexposePort"; 44 44 import exposeVscode from "./io/pocketenv/sandbox/exposeVscode"; 45 + import exec from "./io/pocketenv/sandbox/exec"; 45 46 46 47 export default function (server: Server, ctx: Context) { 47 48 // io.pocketenv ··· 90 91 getExposedPorts(server, ctx); 91 92 unexposePort(server, ctx); 92 93 exposeVscode(server, ctx); 94 + exec(server, ctx); 93 95 94 96 return server; 95 97 }
+84
apps/api/src/xrpc/io/pocketenv/sandbox/exec.ts
··· 1 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 + import { Providers } from "consts"; 3 + import type { Context } from "context"; 4 + import { and, eq, isNull, or } from "drizzle-orm"; 5 + import type { Server } from "lexicon"; 6 + import type { 7 + QueryParams, 8 + InputSchema, 9 + OutputSchema, 10 + } from "lexicon/types/io/pocketenv/sandbox/exec"; 11 + import generateJwt from "lib/generateJwt"; 12 + import schema from "schema"; 13 + 14 + export default function (server: Server, ctx: Context) { 15 + const exec = async ( 16 + params: QueryParams, 17 + input: InputSchema, 18 + auth: HandlerAuth, 19 + ) => { 20 + let userId: string | undefined; 21 + if (auth.credentials) { 22 + const [user] = await ctx.db 23 + .select() 24 + .from(schema.users) 25 + .where(eq(schema.users.did, auth.credentials.did)) 26 + .execute(); 27 + userId = user?.id; 28 + } 29 + 30 + const record = await ctx.db 31 + .select() 32 + .from(schema.sandboxes) 33 + .where( 34 + and( 35 + or( 36 + eq(schema.sandboxes.id, params.id), 37 + eq(schema.sandboxes.name, params.id), 38 + ), 39 + userId 40 + ? eq(schema.sandboxes.userId, userId) 41 + : isNull(schema.sandboxes.userId), 42 + ), 43 + ) 44 + .execute() 45 + .then(([row]) => row); 46 + 47 + if (!record) { 48 + throw new XRPCError(404, "Sandbox not found", "SandboxNotFound"); 49 + } 50 + 51 + const sandbox = 52 + record.provider === Providers.CLOUDFLARE 53 + ? ctx.cfsandbox(record.base!) 54 + : ctx.sandbox(); 55 + 56 + const result = await sandbox.post<{ 57 + stderr: string; 58 + stdout: string; 59 + exitCode: number; 60 + }>( 61 + `/v1/sandboxes/${record.id}/runs`, 62 + { 63 + command: input.command, 64 + }, 65 + { 66 + headers: { 67 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 68 + }, 69 + }, 70 + ); 71 + 72 + return result.data; 73 + }; 74 + server.io.pocketenv.sandbox.exec({ 75 + auth: ctx.authVerifier, 76 + handler: async ({ params, input, auth }) => { 77 + const result = await exec(params, input.body, auth); 78 + return { 79 + encoding: "application/json", 80 + body: result satisfies OutputSchema, 81 + }; 82 + }, 83 + }); 84 + }
+8 -1
apps/cf-sandbox/src/providers/cloudflare/index.ts
··· 51 51 await this.sandbox.destroy(); 52 52 } 53 53 54 - async sh(strings: TemplateStringsArray, ...values: any[]): Promise<any> { 54 + async sh( 55 + strings: TemplateStringsArray, 56 + ...values: any[] 57 + ): Promise<{ 58 + stdout: string; 59 + stderr: string; 60 + exitCode: number; 61 + }> { 55 62 const command = strings.reduce((acc, str, i) => { 56 63 return acc + str + (values[i] || ""); 57 64 }, "");
+8 -1
apps/cf-sandbox/src/providers/index.ts
··· 4 4 abstract start(): Promise<void>; 5 5 abstract stop(): Promise<void>; 6 6 abstract delete(): Promise<void>; 7 - abstract sh(strings: TemplateStringsArray, ...values: any[]): Promise<any>; 7 + abstract sh( 8 + strings: TemplateStringsArray, 9 + ...values: any[] 10 + ): Promise<{ 11 + stdout: string; 12 + stderr: string; 13 + exitCode: number; 14 + }>; 8 15 abstract id(): Promise<string | null>; 9 16 abstract setEnvs(envVars: Record<string, string>): Promise<void>; 10 17 abstract mkdir(dir: string): Promise<void>;
+41
apps/cli/src/cmd/exec.ts
··· 1 + import consola from "consola"; 2 + import getAccessToken from "../lib/getAccessToken"; 3 + import { client } from "../client"; 4 + import { env } from "../lib/env"; 5 + 6 + export async function exec(sandbox: string, command: string[]) { 7 + const token = await getAccessToken(); 8 + 9 + try { 10 + const [cmd, ...args] = command; 11 + const response = await client.post<{ 12 + stderr: string; 13 + stdout: string; 14 + exitCode: number; 15 + }>( 16 + "/xrpc/io.pocketenv.sandbox.startSandbox", 17 + { 18 + command: `${cmd} ${args.join(" ")}`, 19 + }, 20 + { 21 + params: { 22 + id: sandbox, 23 + }, 24 + headers: { 25 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 26 + }, 27 + }, 28 + ); 29 + 30 + process.stdout.write(response.data.stdout); 31 + process.stderr.write(response.data.stderr); 32 + 33 + if (response.data.exitCode !== 0) { 34 + consola.error(`Command exited with code ${response.data.exitCode}`); 35 + } 36 + 37 + process.exit(response.data.exitCode); 38 + } catch (error) { 39 + consola.error("Failed to execute command:", error); 40 + } 41 + }
+12 -3
apps/cli/src/index.ts
··· 22 22 import { listPorts } from "./cmd/ports"; 23 23 import { c } from "./theme"; 24 24 import { exposeVscode } from "./cmd/vscode"; 25 + import { exec } from "./cmd/exec"; 25 26 26 27 const program = new Command(); 27 28 ··· 112 113 113 114 program 114 115 .command("vscode") 116 + .aliases(["code", "code-server"]) 115 117 .argument("<sandbox>", "the sandbox to expose VS Code for") 116 - .description( 117 - "expose a VS Code Server instance running in the given sandbox to the internet", 118 - ) 118 + .description("expose a visual code server to the internet") 119 119 .action(exposeVscode); 120 + 121 + program 122 + .enablePositionalOptions() 123 + .command("exec") 124 + .argument("<sandbox>", "the sandbox to execute the command in") 125 + .argument("<command...>", "the command to execute") 126 + .description("execute a command in the given sandbox") 127 + .passThroughOptions() 128 + .action(exec); 120 129 121 130 program 122 131 .command("expose")
+14 -2
apps/sandbox/src/providers/daytona/mod.ts
··· 3 3 import process from "node:process"; 4 4 import consola from "consola"; 5 5 import path from "node:path"; 6 + import { Buffer } from "node:buffer"; 6 7 7 8 export class DaytonaSandbox implements BaseSandbox { 8 9 constructor(private sandbox: Sandbox) {} ··· 25 26 } 26 27 27 28 // deno-lint-ignore no-explicit-any 28 - sh(strings: TemplateStringsArray, ...values: any[]): Promise<any> { 29 + async sh( 30 + strings: TemplateStringsArray, 31 + ...values: any[] 32 + ): Promise<{ 33 + stdout?: string | Buffer<ArrayBufferLike>; 34 + stderr?: string | Buffer<ArrayBufferLike>; 35 + exitCode: number; 36 + }> { 29 37 const command = strings.reduce((acc, str, i) => { 30 38 return acc + str + (values[i] || ""); 31 39 }, ""); 32 - return this.sandbox.process.executeCommand(command); 40 + const result = await this.sandbox.process.executeCommand(command); 41 + return { 42 + stdout: result.result, 43 + exitCode: result.exitCode, 44 + }; 33 45 } 34 46 35 47 id(): Promise<string | null> {
+15 -2
apps/sandbox/src/providers/deno/mod.ts
··· 3 3 import process from "node:process"; 4 4 import consola from "consola"; 5 5 import path from "node:path"; 6 + import { Buffer } from "node:buffer"; 6 7 7 8 export class DenoSandbox implements BaseSandbox { 8 9 constructor(private sandbox: Sandbox) {} ··· 26 27 await this.sandbox.kill(); 27 28 } 28 29 29 - async sh(strings: TemplateStringsArray, ...values: any[]): Promise<any> { 30 + async sh( 31 + strings: TemplateStringsArray, 32 + ...values: any[] 33 + ): Promise<{ 34 + stdout?: string | Buffer<ArrayBufferLike>; 35 + stderr?: string | Buffer<ArrayBufferLike>; 36 + exitCode: number; 37 + }> { 30 38 const command = strings.reduce((acc, str, i) => { 31 39 return acc + str + (values[i] || ""); 32 40 }, ""); ··· 37 45 stderr: "piped", 38 46 }); 39 47 const output = await result.output(); 40 - return output; 48 + const decoder = new TextDecoder(); 49 + return { 50 + stdout: decoder.decode(output.stdout || new Uint8Array()), 51 + stderr: decoder.decode(output.stderr || new Uint8Array()), 52 + exitCode: (await result.status).code, 53 + }; 41 54 } 42 55 43 56 id(): Promise<string | null> {
+9 -1
apps/sandbox/src/providers/mod.ts
··· 1 1 import { Memory } from "@deno/sandbox"; 2 + import { Buffer } from "node:buffer"; 2 3 import process from "node:process"; 3 4 4 5 export abstract class BaseSandbox { 5 6 abstract start(): Promise<void>; 6 7 abstract stop(): Promise<void>; 7 8 abstract delete(): Promise<void>; 8 - abstract sh(strings: TemplateStringsArray, ...values: any[]): Promise<any>; 9 + abstract sh( 10 + strings: TemplateStringsArray, 11 + ...values: any[] 12 + ): Promise<{ 13 + stdout?: string | Buffer<ArrayBufferLike>; 14 + stderr?: string | Buffer<ArrayBufferLike>; 15 + exitCode: number; 16 + }>; 9 17 abstract id(): Promise<string | null>; 10 18 abstract ssh(): Promise<any>; 11 19 abstract mkdir(dir: string): Promise<void>;
+9 -1
apps/sandbox/src/providers/sprites/mod.ts
··· 3 3 import consola from "consola"; 4 4 import { Sprite, SpritesClient } from "@fly/sprites"; 5 5 import path from "node:path"; 6 + import { Buffer } from "node:buffer"; 6 7 7 8 export class SpriteSandbox implements BaseSandbox { 8 9 constructor(private sprite: Sprite) {} ··· 29 30 await this.sprite.delete(); 30 31 } 31 32 32 - async sh(strings: TemplateStringsArray, ...values: any[]): Promise<any> { 33 + async sh( 34 + strings: TemplateStringsArray, 35 + ...values: any[] 36 + ): Promise<{ 37 + stdout: string | Buffer<ArrayBufferLike>; 38 + stderr: string | Buffer<ArrayBufferLike>; 39 + exitCode: number; 40 + }> { 33 41 const command = strings.reduce((acc, str, i) => { 34 42 return acc + str + (values[i] || ""); 35 43 }, "");
+9 -1
apps/sandbox/src/providers/vercel/mod.ts
··· 3 3 import process from "node:process"; 4 4 import consola from "consola"; 5 5 import path from "node:path"; 6 + import { Buffer } from "node:buffer"; 6 7 7 8 export class VercelSandbox implements BaseSandbox { 8 9 constructor(private sandbox: Sandbox) {} ··· 25 26 // Vercel's sandbox does not have a separate delete method, so we just stop it. 26 27 } 27 28 28 - async sh(strings: TemplateStringsArray, ...values: any[]): Promise<any> { 29 + async sh( 30 + strings: TemplateStringsArray, 31 + ...values: any[] 32 + ): Promise<{ 33 + stdout?: string | Buffer<ArrayBufferLike>; 34 + stderr?: string | Buffer<ArrayBufferLike>; 35 + exitCode: number; 36 + }> { 29 37 const command = strings.reduce((acc, str, i) => { 30 38 return acc + str + (values[i] || ""); 31 39 }, "");