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 service endpoints and sandboxId param

+428 -6
+12
apps/api/lexicons/service/addService.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "procedure", 7 + "parameters": { 8 + "type": "params", 9 + "required": [ 10 + "sandboxId" 11 + ], 12 + "properties": { 13 + "sandboxId": { 14 + "type": "string", 15 + "description": "The ID of the sandbox to which the service belongs." 16 + } 17 + } 18 + }, 7 19 "input": { 8 20 "encoding": "application/json", 9 21 "schema": {
+10
apps/api/pkl/defs/service/addService.pkl
··· 5 5 defs = new Mapping<String, Procedure> { 6 6 ["main"] { 7 7 type = "procedure" 8 + parameters { 9 + type = "params" 10 + required = List("sandboxId") 11 + properties { 12 + ["sandboxId"] = new StringType { 13 + type = "string" 14 + description = "The ID of the sandbox to which the service belongs." 15 + } 16 + } 17 + } 8 18 input { 9 19 encoding = "application/json" 10 20 schema {
+11
apps/api/src/lexicon/lexicons.ts
··· 2104 2104 defs: { 2105 2105 main: { 2106 2106 type: "procedure", 2107 + parameters: { 2108 + type: "params", 2109 + required: ["sandboxId"], 2110 + properties: { 2111 + sandboxId: { 2112 + type: "string", 2113 + description: 2114 + "The ID of the sandbox to which the service belongs.", 2115 + }, 2116 + }, 2117 + }, 2107 2118 input: { 2108 2119 encoding: "application/json", 2109 2120 schema: {
+4 -1
apps/api/src/lexicon/types/io/pocketenv/service/addService.ts
··· 9 9 import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as IoPocketenvServiceDefs from "./defs"; 11 11 12 - export type QueryParams = {}; 12 + export interface QueryParams { 13 + /** The ID of the sandbox to which the service belongs. */ 14 + sandboxId: string; 15 + } 13 16 14 17 export interface InputSchema { 15 18 service: IoPocketenvServiceDefs.Service;
+2
apps/api/src/schema/index.ts
··· 12 12 import tailscaleAuthKeys from "./tailscale-auth-keys"; 13 13 import sshKeys from "./ssh-keys"; 14 14 import sandboxPorts from "./sandbox-ports"; 15 + import services from "./services"; 15 16 16 17 export default { 17 18 sandboxes, ··· 28 29 tailscaleAuthKeys, 29 30 sshKeys, 30 31 sandboxPorts, 32 + services, 31 33 };
+133 -1
apps/api/src/xrpc/io/pocketenv/service/addService.ts
··· 1 1 import type { Server } from "lexicon"; 2 2 import type { Context } from "context"; 3 + import type { 4 + HandlerInput, 5 + QueryParams, 6 + } from "lexicon/types/io/pocketenv/service/addService"; 7 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 8 + import schema from "schema"; 9 + import type { InsertService } from "schema/services"; 10 + import { consola } from "consola"; 11 + import type { InsertSandboxPort } from "schema/sandbox-ports"; 12 + import { and, eq } from "drizzle-orm"; 13 + import { Providers } from "consts"; 14 + import generateJwt from "lib/generateJwt"; 3 15 4 - export default function (server: Server, ctx: Context) {} 16 + export default function (server: Server, ctx: Context) { 17 + const addService = async ( 18 + input: HandlerInput, 19 + params: QueryParams, 20 + auth: HandlerAuth, 21 + ) => { 22 + if (!auth.credentials) { 23 + throw new XRPCError(401, "Unauthorized"); 24 + } 25 + 26 + const [record] = await ctx.db 27 + .select() 28 + .from(schema.sandboxes) 29 + .leftJoin(schema.users, eq(schema.users.id, schema.sandboxes.userId)) 30 + .where( 31 + and( 32 + eq(schema.sandboxes.id, params.sandboxId), 33 + eq(schema.users.did, auth.credentials.did), 34 + ), 35 + ) 36 + .execute() 37 + .then((results) => results.map(({ sandboxes }) => sandboxes)); 38 + 39 + if (!record) { 40 + consola.error("Sandbox not found for service", { input, params, auth }); 41 + throw new XRPCError(404, "Sandbox not found"); 42 + } 43 + 44 + const service = await ctx.db.transaction(async (tx) => { 45 + const [service] = await tx 46 + .insert(schema.services) 47 + .values({ 48 + sandboxId: params.sandboxId, 49 + name: input.body.service.name, 50 + description: input.body.service.description, 51 + command: input.body.service.command, 52 + } satisfies InsertService) 53 + .returning() 54 + .execute(); 55 + 56 + if (!service) { 57 + consola.error("Failed to create service", { input, params, auth }); 58 + throw new XRPCError(500, "Failed to create service"); 59 + } 60 + 61 + await Promise.all( 62 + (input.body.service.ports || []).map((port) => 63 + tx 64 + .insert(schema.sandboxPorts) 65 + .values({ 66 + sandboxId: params.sandboxId, 67 + serviceId: service.id, 68 + exposedPort: port, 69 + description: `Port ${port} for service ${service.name}`, 70 + } satisfies InsertSandboxPort) 71 + .execute(), 72 + ), 73 + ); 74 + 75 + return service; 76 + }); 77 + 78 + // start service 79 + const sandbox = 80 + record.provider === Providers.CLOUDFLARE 81 + ? ctx.cfsandbox(record.base!) 82 + : ctx.sandbox(); 83 + 84 + await sandbox.post( 85 + `/v1/sandboxes/${record.id}/services/${service.id}`, 86 + {}, 87 + { 88 + headers: { 89 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 90 + }, 91 + }, 92 + ); 93 + 94 + // expose service ports 95 + const responses = await Promise.all( 96 + (input.body.service.ports || []).map(async (port) => 97 + sandbox.post<{ previewUrl: string }>( 98 + `/v1/sandboxes/${record.id}/ports`, 99 + { 100 + port, 101 + }, 102 + { 103 + headers: { 104 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 105 + }, 106 + }, 107 + ), 108 + ), 109 + ); 110 + 111 + responses.map(async ({ data }, index) => { 112 + if (data.previewUrl) { 113 + await ctx.db 114 + .update(schema.sandboxPorts) 115 + .set({ previewUrl: data.previewUrl }) 116 + .where( 117 + and( 118 + eq(schema.sandboxPorts.sandboxId, record.id), 119 + eq( 120 + schema.sandboxPorts.exposedPort, 121 + input.body.service.ports![index]!, 122 + ), 123 + ), 124 + ) 125 + .execute(); 126 + } 127 + }); 128 + }; 129 + 130 + server.io.pocketenv.service.addService({ 131 + auth: ctx.authVerifier, 132 + handler: async ({ input, params, auth }) => { 133 + await addService(input, params, auth); 134 + }, 135 + }); 136 + }
+65 -1
apps/api/src/xrpc/io/pocketenv/service/deleteService.ts
··· 1 1 import type { Server } from "lexicon"; 2 2 import type { Context } from "context"; 3 + import type { QueryParams } from "lexicon/types/io/pocketenv/service/deleteService"; 4 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 5 + import schema from "schema"; 6 + import { and, eq } from "drizzle-orm"; 7 + import { Providers } from "consts"; 8 + import generateJwt from "lib/generateJwt"; 3 9 4 - export default function (server: Server, ctx: Context) {} 10 + export default function (server: Server, ctx: Context) { 11 + const deleteService = async (params: QueryParams, auth: HandlerAuth) => { 12 + if (!auth.credentials) { 13 + throw new XRPCError(401, "Unauthorized"); 14 + } 15 + 16 + const [record] = await ctx.db 17 + .select() 18 + .from(schema.services) 19 + .leftJoin( 20 + schema.sandboxes, 21 + eq(schema.sandboxes.id, schema.services.sandboxId), 22 + ) 23 + .leftJoin(schema.users, eq(schema.users.id, schema.sandboxes.userId)) 24 + .where( 25 + and( 26 + eq(schema.services.id, params.serviceId), 27 + eq(schema.users.did, auth.credentials.did), 28 + ), 29 + ) 30 + .execute(); 31 + 32 + if (!record?.sandboxes) { 33 + throw new XRPCError(404, "Service not found"); 34 + } 35 + 36 + // stop service 37 + const sandbox = 38 + record.sandboxes.provider === Providers.CLOUDFLARE 39 + ? ctx.cfsandbox(record.sandboxes.base!) 40 + : ctx.sandbox(); 41 + await sandbox.delete( 42 + `/v1/sandboxes/${record.sandboxes.id}/services/${params.serviceId}`, 43 + { 44 + headers: { 45 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 46 + }, 47 + }, 48 + ); 49 + 50 + await ctx.db.transaction(async (tx) => { 51 + await tx 52 + .delete(schema.sandboxPorts) 53 + .where(eq(schema.sandboxPorts.serviceId, params.serviceId)) 54 + .execute(); 55 + await tx 56 + .delete(schema.services) 57 + .where(eq(schema.services.id, params.serviceId)) 58 + .execute(); 59 + }); 60 + }; 61 + 62 + server.io.pocketenv.service.deleteService({ 63 + auth: ctx.authVerifier, 64 + handler: async ({ params, auth }) => { 65 + await deleteService(params, auth); 66 + }, 67 + }); 68 + }
+40 -1
apps/api/src/xrpc/io/pocketenv/service/getServices.ts
··· 1 1 import type { Server } from "lexicon"; 2 2 import type { Context } from "context"; 3 + import type { 4 + QueryParams, 5 + OutputSchema, 6 + } from "lexicon/types/io/pocketenv/service/getServices"; 7 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 8 + import schema from "schema"; 9 + import { eq } from "drizzle-orm"; 3 10 4 - export default function (server: Server, ctx: Context) {} 11 + export default function (server: Server, ctx: Context) { 12 + const getServices = async (params: QueryParams, auth: HandlerAuth) => { 13 + if (!auth.credentials) { 14 + throw new XRPCError(401, "Unauthorized"); 15 + } 16 + 17 + const services = await ctx.db 18 + .select() 19 + .from(schema.services) 20 + .where(eq(schema.services.sandboxId, params.sandboxId)) 21 + .execute(); 22 + 23 + return { 24 + services: services.map((record) => ({ 25 + id: record.id, 26 + name: record.name, 27 + description: record.description!, 28 + command: record.command, 29 + })), 30 + } satisfies OutputSchema; 31 + }; 32 + 33 + server.io.pocketenv.service.getServices({ 34 + auth: ctx.authVerifier, 35 + handler: async ({ params, auth }) => { 36 + const result = await getServices(params, auth); 37 + return { 38 + encoding: "application/json", 39 + body: result satisfies OutputSchema, 40 + }; 41 + }, 42 + }); 43 + }
+70 -1
apps/api/src/xrpc/io/pocketenv/service/restartService.ts
··· 1 1 import type { Server } from "lexicon"; 2 2 import type { Context } from "context"; 3 + import type { QueryParams } from "lexicon/types/io/pocketenv/service/restartService"; 4 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 5 + import schema from "schema"; 6 + import { and, eq } from "drizzle-orm"; 7 + import { Providers } from "consts"; 8 + import generateJwt from "lib/generateJwt"; 9 + 10 + export default function (server: Server, ctx: Context) { 11 + const restartService = async (params: QueryParams, auth: HandlerAuth) => { 12 + if (!auth.credentials) { 13 + throw new XRPCError(401, "Unauthorized"); 14 + } 15 + 16 + // get service by id and verify ownership 17 + // stop service 18 + // start service 19 + 20 + const records = await ctx.db 21 + .select() 22 + .from(schema.services) 23 + .leftJoin( 24 + schema.sandboxes, 25 + eq(schema.sandboxes.id, schema.services.sandboxId), 26 + ) 27 + .leftJoin(schema.users, eq(schema.users.id, schema.sandboxes.userId)) 28 + .where( 29 + and( 30 + eq(schema.services.id, params.serviceId), 31 + eq(schema.users.did, auth.credentials.did), 32 + ), 33 + ) 34 + .execute(); 35 + 36 + if (records.length === 0) { 37 + throw new XRPCError(404, "Service not found"); 38 + } 3 39 4 - export default function (server: Server, ctx: Context) {} 40 + // stop service 41 + const [record] = records; 42 + const sandbox = 43 + record!.sandboxes!.provider === Providers.CLOUDFLARE 44 + ? ctx.cfsandbox(record!.sandboxes!.base!) 45 + : ctx.sandbox(); 46 + await sandbox.delete( 47 + `/v1/sandboxes/${record!.sandboxes!.id}/services/${params.serviceId}`, 48 + { 49 + headers: { 50 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 51 + }, 52 + }, 53 + ); 54 + 55 + // start service 56 + await sandbox.post( 57 + `/v1/sandboxes/${record!.sandboxes!.id}/services/${params.serviceId}`, 58 + {}, 59 + { 60 + headers: { 61 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 62 + }, 63 + }, 64 + ); 65 + }; 66 + 67 + server.io.pocketenv.service.restartService({ 68 + auth: ctx.authVerifier, 69 + handler: async ({ params, auth }) => { 70 + await restartService(params, auth); 71 + }, 72 + }); 73 + }
+77 -1
apps/api/src/xrpc/io/pocketenv/service/updateService.ts
··· 1 1 import type { Server } from "lexicon"; 2 2 import type { Context } from "context"; 3 + import type { 4 + HandlerInput, 5 + QueryParams, 6 + } from "lexicon/types/io/pocketenv/service/updateService"; 7 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 8 + import schema from "schema"; 9 + import { and, eq } from "drizzle-orm"; 3 10 4 - export default function (server: Server, ctx: Context) {} 11 + export default function (server: Server, ctx: Context) { 12 + const updateService = async ( 13 + input: HandlerInput, 14 + params: QueryParams, 15 + auth: HandlerAuth, 16 + ) => { 17 + if (!auth.credentials) { 18 + throw new XRPCError(401, "Unauthorized"); 19 + } 20 + 21 + const records = await ctx.db 22 + .select() 23 + .from(schema.services) 24 + .leftJoin( 25 + schema.sandboxes, 26 + eq(schema.sandboxes.id, schema.services.sandboxId), 27 + ) 28 + .leftJoin(schema.users, eq(schema.users.id, schema.sandboxes.userId)) 29 + .where( 30 + and( 31 + eq(schema.services.id, params.serviceId), 32 + eq(schema.users.did, auth.credentials.did), 33 + ), 34 + ) 35 + .execute(); 36 + 37 + if (records.length === 0) { 38 + throw new XRPCError(404, "Service not found"); 39 + } 40 + 41 + await ctx.db.transaction(async (tx) => { 42 + await tx 43 + .update(schema.services) 44 + .set({ 45 + name: input.body.service.name, 46 + description: input.body.service.description, 47 + command: input.body.service.command, 48 + }) 49 + .where(and(eq(schema.services.id, params.serviceId))); 50 + 51 + if (input.body.service.ports) { 52 + await tx 53 + .delete(schema.sandboxPorts) 54 + .where(eq(schema.sandboxPorts.serviceId, params.serviceId)) 55 + .execute(); 56 + 57 + await Promise.all( 58 + input.body.service.ports.map((port) => 59 + tx 60 + .insert(schema.sandboxPorts) 61 + .values({ 62 + sandboxId: records[0]!.services.sandboxId, 63 + serviceId: params.serviceId, 64 + exposedPort: port, 65 + description: `Port ${port} for service ${input.body.service.name}`, 66 + }) 67 + .execute(), 68 + ), 69 + ); 70 + } 71 + }); 72 + }; 73 + 74 + server.io.pocketenv.service.updateService({ 75 + auth: ctx.authVerifier, 76 + handler: async ({ input, params, auth }) => { 77 + await updateService(input, params, auth); 78 + }, 79 + }); 80 + }
+2
apps/cf-sandbox/src/schema/index.ts
··· 12 12 import tailscaleAuthKeys from "./tailscale-auth-keys"; 13 13 import sshKeys from "./ssh-keys"; 14 14 import sandboxPorts from "./sandbox-ports"; 15 + import services from "./services"; 15 16 16 17 export { 17 18 sandboxes, ··· 28 29 tailscaleAuthKeys, 29 30 sshKeys, 30 31 sandboxPorts, 32 + services, 31 33 };
+2
apps/sandbox/src/schema/mod.ts
··· 12 12 import sshKeys from "./ssh-keys.ts"; 13 13 import tailscaleAuthKeys from "./tailscale-auth-keys.ts"; 14 14 import sandboxPorts from "./sandbox-ports.ts"; 15 + import services from "./services.ts"; 15 16 16 17 export { 17 18 sandboxes, ··· 28 29 sshKeys, 29 30 tailscaleAuthKeys, 30 31 sandboxPorts, 32 + services, 31 33 };