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.

Use id for getSandbox and add author filter

Implement getSandbox server handler with DB lookup by id or URI,
including retry, timeout, and error handling. Add client hook and page
UI to fetch and display sandboxes, update lexicons/types, and add new
dependencies for the web app.

+239 -51
+4 -5
apps/api/lexicons/sandbox/getSandbox.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", 7 - "description": "Get a sandbox by uri", 7 + "description": "Get a sandbox by ID or URI", 8 8 "parameters": { 9 9 "type": "params", 10 10 "required": [ 11 - "uri" 11 + "id" 12 12 ], 13 13 "properties": { 14 - "uri": { 14 + "id": { 15 15 "type": "string", 16 - "description": "The sandbox URI.", 17 - "format": "at-uri" 16 + "description": "The sandbox ID or URI to retrieve" 18 17 } 19 18 } 20 19 },
+5
apps/api/lexicons/sandbox/getSandboxes.json
··· 8 8 "parameters": { 9 9 "type": "params", 10 10 "properties": { 11 + "author": { 12 + "type": "string", 13 + "description": "Filter sandboxes by author did or handle", 14 + "format": "at-identifier" 15 + }, 11 16 "limit": { 12 17 "type": "integer", 13 18 "description": "The maximum number of sandboxes to return.",
+4 -5
apps/api/pkl/defs/sandbox/getSandbox.pkl
··· 5 5 defs = new Mapping<String, Query> { 6 6 ["main"] { 7 7 type = "query" 8 - description = "Get a sandbox by uri" 8 + description = "Get a sandbox by ID or URI" 9 9 parameters { 10 10 type = "params" 11 - required = List("uri") 11 + required = List("id") 12 12 properties { 13 - ["uri"] = new StringType { 13 + ["id"] = new StringType { 14 14 type = "string" 15 - description = "The sandbox URI." 16 - format = "at-uri" 15 + description = "The sandbox ID or URI to retrieve" 17 16 } 18 17 } 19 18 }
+5
apps/api/pkl/defs/sandbox/getSandboxes.pkl
··· 9 9 parameters { 10 10 type = "params" 11 11 properties { 12 + ["author"] = new StringType { 13 + type = "string" 14 + description = "Filter sandboxes by author did or handle" 15 + format = "at-identifier" 16 + } 12 17 ["limit"] = new IntegerType { 13 18 type = "integer" 14 19 description = "The maximum number of sandboxes to return."
+9 -5
apps/api/src/lexicon/lexicons.ts
··· 412 412 defs: { 413 413 main: { 414 414 type: "query", 415 - description: "Get a sandbox by uri", 415 + description: "Get a sandbox by ID or URI", 416 416 parameters: { 417 417 type: "params", 418 - required: ["uri"], 418 + required: ["id"], 419 419 properties: { 420 - uri: { 420 + id: { 421 421 type: "string", 422 - description: "The sandbox URI.", 423 - format: "at-uri", 422 + description: "The sandbox ID or URI to retrieve", 424 423 }, 425 424 }, 426 425 }, ··· 444 443 parameters: { 445 444 type: "params", 446 445 properties: { 446 + author: { 447 + type: "string", 448 + description: "Filter sandboxes by author did or handle", 449 + format: "at-identifier", 450 + }, 447 451 limit: { 448 452 type: "integer", 449 453 description: "The maximum number of sandboxes to return.",
+2 -2
apps/api/src/lexicon/types/io/pocketenv/sandbox/getSandbox.ts
··· 10 10 import type * as IoPocketenvSandboxDefs from "./defs"; 11 11 12 12 export interface QueryParams { 13 - /** The sandbox URI. */ 14 - uri: string; 13 + /** The sandbox ID or URI to retrieve */ 14 + id: string; 15 15 } 16 16 17 17 export type InputSchema = undefined;
+2
apps/api/src/lexicon/types/io/pocketenv/sandbox/getSandboxes.ts
··· 10 10 import type * as IoPocketenvSandboxDefs from "./defs"; 11 11 12 12 export interface QueryParams { 13 + /** Filter sandboxes by author did or handle */ 14 + author?: string; 13 15 /** The maximum number of sandboxes to return. */ 14 16 limit?: number; 15 17 /** The number of sandboxes to skip before starting to collect the result set. */
+67 -3
apps/api/src/xrpc/io/pocketenv/sandbox/getSandbox.ts
··· 1 1 import type { HandlerAuth } from "@atproto/xrpc-server"; 2 2 import type { Context } from "context"; 3 3 import type { Server } from "lexicon"; 4 - import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/getSandbox"; 4 + import type { 5 + OutputSchema, 6 + QueryParams, 7 + } from "lexicon/types/io/pocketenv/sandbox/getSandbox"; 8 + import type { SelectSandbox } from "schema/sandboxes"; 9 + import { Effect, pipe } from "effect"; 10 + import schema from "schema"; 11 + import { eq, or } from "drizzle-orm"; 12 + import { consola } from "consola"; 5 13 6 14 export default function (server: Server, ctx: Context) { 7 - const getSandbox = (params: QueryParams, auth: HandlerAuth) => ({}); 15 + const getSandbox = (params: QueryParams, auth: HandlerAuth) => 16 + pipe( 17 + { params, ctx }, 18 + retrieve, 19 + Effect.flatMap(presentation), 20 + Effect.retry({ times: 3 }), 21 + Effect.timeout("10 seconds"), 22 + Effect.catchAll((err) => { 23 + consola.error("Error retrieving sandboxes:", err); 24 + return Effect.succeed({ sandbox: null }); 25 + }), 26 + ); 8 27 server.io.pocketenv.sandbox.getSandbox({ 9 28 auth: ctx.authVerifier, 10 29 handler: async ({ params, auth }) => { 11 - const result = getSandbox(params, auth); 30 + const result = await Effect.runPromise(getSandbox(params, auth)); 12 31 return { 13 32 encoding: "application/json", 14 33 body: result, ··· 16 35 }, 17 36 }); 18 37 } 38 + 39 + const retrieve = ({ 40 + params, 41 + ctx, 42 + }: { 43 + params: QueryParams; 44 + ctx: Context; 45 + }): Effect.Effect<SelectSandbox | undefined, Error> => { 46 + return Effect.tryPromise({ 47 + try: async () => 48 + ctx.db 49 + .select() 50 + .from(schema.sandboxes) 51 + .where( 52 + or( 53 + eq(schema.sandboxes.id, params.id), 54 + eq(schema.sandboxes.uri, params.id), 55 + ), 56 + ) 57 + .execute() 58 + .then(([row]) => row), 59 + catch: (error) => 60 + new Error( 61 + `Failed to retrieve sandbox: ${error instanceof Error ? error.message : String(error)}`, 62 + ), 63 + }); 64 + }; 65 + 66 + const presentation = ( 67 + sandbox: SelectSandbox | undefined, 68 + ): Effect.Effect<OutputSchema, never> => { 69 + return Effect.sync(() => ({ 70 + sandbox: sandbox && { 71 + id: sandbox.id, 72 + name: sandbox.name, 73 + displayName: sandbox.displayName, 74 + description: sandbox.description!, 75 + logo: sandbox.logo!, 76 + readme: sandbox.readme!, 77 + installs: sandbox.installs, 78 + uri: sandbox.uri, 79 + createdAt: sandbox.createdAt.toISOString(), 80 + }, 81 + })); 82 + };
+18 -2
apps/api/src/xrpc/io/pocketenv/sandbox/getSandboxes.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 } from "drizzle-orm"; 12 + import { count, eq, desc, or } from "drizzle-orm"; 13 13 14 14 export default function (server: Server, ctx: Context) { 15 15 const getSandboxes = (params: QueryParams, auth: HandlerAuth) => ··· 50 50 .select() 51 51 .from(schema.sandboxes) 52 52 .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 53 - .where(eq(schema.users.handle, "pocketenv.io")) 53 + .where( 54 + params.author 55 + ? or( 56 + eq(schema.users.did, params.author), 57 + eq(schema.users.handle, params.author), 58 + ) 59 + : eq(schema.users.handle, "pocketenv.io"), 60 + ) 54 61 .orderBy(desc(schema.sandboxes.installs)) 55 62 .limit(params.limit ?? 30) 56 63 .offset(params.offset ?? 0) ··· 59 66 ctx.db 60 67 .select({ count: count() }) 61 68 .from(schema.sandboxes) 69 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 70 + .where( 71 + params.author 72 + ? or( 73 + eq(schema.users.did, params.author), 74 + eq(schema.users.handle, params.author), 75 + ) 76 + : eq(schema.users.handle, "pocketenv.io"), 77 + ) 62 78 .execute() 63 79 .then((result) => result[0]?.count ?? 0), 64 80 ]),
+8
apps/web/bun.lock
··· 5 5 "": { 6 6 "name": "pocketenv", 7 7 "dependencies": { 8 + "@cloudflare/sandbox": "^0.7.4", 8 9 "@iconify/json": "^2.2.438", 9 10 "@iconify/tailwind4": "^1.2.1", 10 11 "@tabler/icons-react": "^3.36.1", ··· 13 14 "@tanstack/react-router": "^1.159.5", 14 15 "@tanstack/react-router-devtools": "^1.159.5", 15 16 "@tanstack/router-plugin": "^1.159.5", 17 + "@xterm/addon-fit": "^0.11.0", 16 18 "@xterm/xterm": "^6.0.0", 17 19 "axios": "^1.13.5", 18 20 "consola": "^3.4.2", ··· 85 87 "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], 86 88 87 89 "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], 90 + 91 + "@cloudflare/containers": ["@cloudflare/containers@0.0.30", "", {}, "sha512-i148xBgmyn/pje82ZIyuTr/Ae0BT/YWwa1/GTJcw6DxEjUHAzZLaBCiX446U9OeuJ2rBh/L/9FIzxX5iYNt1AQ=="], 92 + 93 + "@cloudflare/sandbox": ["@cloudflare/sandbox@0.7.4", "", { "dependencies": { "@cloudflare/containers": "^0.0.30" }, "peerDependencies": { "@openai/agents": "^0.3.3", "@opencode-ai/sdk": "^1.1.40", "@xterm/xterm": ">=5.0.0" }, "optionalPeers": ["@openai/agents", "@opencode-ai/sdk", "@xterm/xterm"] }, "sha512-8DRGlATKH5chOVTEwcntuS9ZtYmjknuIoXRUnIXDwwEEhst4FFXf/DjNxeBr/4J4XH6aWJvOKcdsTlajrhlhHA=="], 88 94 89 95 "@cyberalien/svg-utils": ["@cyberalien/svg-utils@1.1.3", "", { "dependencies": { "@iconify/types": "^2.0.0" } }, "sha512-AoEJO7XEa24esSnkDpIF/Ow1YmUxcylVOq9Ye+oYUE4WjmwDgmWEhc2DB1QKNYl8qBSbuY/sIp/qQIGUl3ZvEw=="], 90 96 ··· 367 373 "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA=="], 368 374 369 375 "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.2.3", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA=="], 376 + 377 + "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], 370 378 371 379 "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], 372 380
+2
apps/web/package.json
··· 10 10 "preview": "vite preview" 11 11 }, 12 12 "dependencies": { 13 + "@cloudflare/sandbox": "^0.7.4", 13 14 "@iconify/json": "^2.2.438", 14 15 "@iconify/tailwind4": "^1.2.1", 15 16 "@tabler/icons-react": "^3.36.1", ··· 18 19 "@tanstack/react-router": "^1.159.5", 19 20 "@tanstack/react-router-devtools": "^1.159.5", 20 21 "@tanstack/router-plugin": "^1.159.5", 22 + "@xterm/addon-fit": "^0.11.0", 21 23 "@xterm/xterm": "^6.0.0", 22 24 "axios": "^1.13.5", 23 25 "consola": "^3.4.2",
+3 -1
apps/web/src/api/sandbox.ts
··· 28 28 ); 29 29 30 30 export const getSandbox = (id: string) => 31 - client.get(`/xrpc/io.pocketenv.sandbox.getSandbox?id=${id}`); 31 + client.get<{ sandbox: Sandbox | undefined }>( 32 + `/xrpc/io.pocketenv.sandbox.getSandbox?id=${id}`, 33 + ); 32 34 33 35 export const getSandboxes = (offset?: number, limit?: number) => 34 36 client.get<{ sandboxes: Sandbox[]; total: number }>(
+13 -1
apps/web/src/hooks/useSandbox.ts
··· 1 1 import { useMutation, useQuery } from "@tanstack/react-query"; 2 - import { claimSandbox, createSandbox, getSandboxes } from "../api/sandbox"; 2 + import { 3 + claimSandbox, 4 + createSandbox, 5 + getSandbox, 6 + getSandboxes, 7 + } from "../api/sandbox"; 3 8 4 9 export const useSandboxesQuery = (offset?: number, limit?: number) => 5 10 useQuery({ 6 11 queryKey: ["sandboxes", offset, limit], 7 12 queryFn: () => getSandboxes(offset, limit), 13 + select: (response) => response.data, 14 + }); 15 + 16 + export const useSandboxQuery = (id: string) => 17 + useQuery({ 18 + queryKey: ["sandbox", id], 19 + queryFn: () => getSandbox(id), 8 20 select: (response) => response.data, 9 21 }); 10 22
+97 -27
apps/web/src/pages/sandbox/Sandbox.tsx
··· 2 2 import Navbar from "../../components/navbar"; 3 3 import SignIn from "../../components/signin/Signin"; 4 4 import { useLocation, useNavigate } from "@tanstack/react-router"; 5 + import { useSandboxQuery } from "../../hooks/useSandbox"; 6 + import { consola } from "consola"; 5 7 6 8 function New() { 7 9 const isAuthenticated = !!localStorage.getItem("token"); 8 10 const [signInModalOpen, setSignInModalOpen] = useState(false); 9 11 const navigate = useNavigate(); 12 + const location = useLocation(); 13 + 14 + const getSandboxIdFromPath = () => { 15 + const path = location.pathname; 16 + 17 + const didMatch = path.match(/\/(did:[^/]+)\/sandbox\/([^/]+)/); 18 + if (didMatch) { 19 + const did = didMatch[1]; 20 + const rkey = didMatch[2]; 21 + return `at://${did}/io.pocketenv.sandbox/${rkey}`; 22 + } 23 + 24 + const idMatch = path.match(/\/sandbox\/([^/]+)/); 25 + if (idMatch) { 26 + return idMatch[1]; 27 + } 28 + 29 + return ""; 30 + }; 31 + 32 + const { data, isLoading } = useSandboxQuery(getSandboxIdFromPath()); 33 + 34 + consola.info("Sandbox data:", data, isLoading); 35 + 10 36 const onClaim = () => { 11 37 if (isAuthenticated) { 12 38 navigate({ ··· 16 42 } 17 43 setSignInModalOpen(true); 18 44 }; 19 - const location = useLocation(); 20 45 21 46 return ( 22 47 <> 23 48 <div className="flex flex-col min-h-screen bg-base-100"> 24 - <Navbar withLogo title="" project="lucky-quietude" /> 25 - {location.pathname.startsWith("/sandbox") && ( 26 - <div 27 - className="alert alert-soft alert-warning flex items-center bg-warning/10 border-none" 28 - role="alert" 29 - > 30 - <div className="flex-1"> 31 - This is a temporary project (what's this?) and will be deleted in 32 - 24 hours. Claim it to make it yours. 33 - </div> 49 + <Navbar withLogo title="" project={data?.sandbox?.name} /> 50 + {data?.sandbox && !isLoading && ( 51 + <> 52 + {location.pathname.startsWith("/sandbox") && ( 53 + <div 54 + className="alert alert-soft alert-warning flex items-center bg-warning/10 border-none" 55 + role="alert" 56 + > 57 + <div className="flex-1"> 58 + This is a temporary project (what's this?) and will be deleted 59 + in 24 hours. Claim it to make it yours. 60 + </div> 34 61 35 - <button 36 - onClick={onClaim} 37 - className="btn btn-md btn-primary font-semibold ml-4" 38 - > 39 - Claim Project 40 - </button> 41 - </div> 62 + <button 63 + onClick={onClaim} 64 + className="btn btn-md btn-primary font-semibold ml-4" 65 + > 66 + Claim Project 67 + </button> 68 + </div> 69 + )} 70 + <div className="p-10"> 71 + <div className="mt-[50px] flex space-between"> 72 + <div className="flex-1"> 73 + <div className="flex items-center"> 74 + <div className="text-xl mr-3 mt-[-5px]"> 75 + {data?.sandbox?.id} 76 + </div> 77 + <span className="badge bg-white/15 rounded-full text-white/80 border-none"> 78 + Sandbox 79 + </span> 80 + </div> 81 + 82 + <div className="w-[50%] overflow-x-auto mt-5 ml-[-18px]"> 83 + <table className="table-borderless table"> 84 + <thead> 85 + <tr> 86 + <th>Status</th> 87 + <th>Started</th> 88 + <th>Timeout</th> 89 + <th>Resources</th> 90 + </tr> 91 + </thead> 92 + <tbody> 93 + <tr> 94 + <td> 95 + <span className="badge badge-soft badge-success rounded-full bg-green-400/10"> 96 + Running 97 + </span> 98 + </td> 99 + <td>1 minute ago</td> 100 + <td>5m</td> 101 + <td> 102 + <span className="badge badge-soft badge-primary bg-blue-400/10 rounded-full"> 103 + 1 CPU 104 + </span> 105 + <span className="badge badge-soft badge-primary bg-blue-400/10 rounded-full ml-2"> 106 + 4 GiB RAM 107 + </span> 108 + </td> 109 + </tr> 110 + </tbody> 111 + </table> 112 + </div> 113 + </div> 114 + <button className="btn btn-outline btn-lg hover:text-white"> 115 + <span className="icon-[tabler--player-stop-filled] size-5 shrink-0"></span> 116 + Stop Sandbox 117 + </button> 118 + </div> 119 + </div> 120 + </> 42 121 )} 43 - <div className="p-4"> 44 - <div className="mt-[50px] flex space-between"> 45 - <div className="flex-1"></div> 46 - <button className="btn btn-outline btn-lg hover:text-white"> 47 - <span className="icon-[tabler--player-stop-filled] size-5 shrink-0"></span> 48 - Stop Sandbox 49 - </button> 50 - </div> 51 - </div> 52 122 </div> 53 123 <SignIn 54 124 isOpen={signInModalOpen}