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 secret/env types and control APIs

- Add Secret and EnvVar lexicon/pkl defs and generated TS types; switch
secrets/envs arrays to ref items
- Rename sandbox route params from uri to id across lexicons and
handlers
- Implement create/start/stop/delete handlers to call external sandbox
API via axios; add axios dependency and SANDBOX_API_URL default
- Add claimSandbox endpoint and web client/hooks; small UI updates
(Navbar)

+452 -137
+19
apps/api/bun.lock
··· 20 20 "@types/node": "^25.2.3", 21 21 "@types/prompts": "^2.4.9", 22 22 "@types/ramda": "^0.31.1", 23 + "axios": "^1.13.5", 23 24 "better-sqlite3": "^12.6.2", 24 25 "chalk": "^5.6.2", 25 26 "consola": "^3.4.2", ··· 289 290 290 291 "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], 291 292 293 + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], 294 + 292 295 "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], 293 296 294 297 "await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="], 298 + 299 + "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], 295 300 296 301 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 297 302 ··· 343 348 344 349 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 345 350 351 + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], 352 + 346 353 "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], 347 354 348 355 "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], ··· 372 379 "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], 373 380 374 381 "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], 382 + 383 + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], 375 384 376 385 "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], 377 386 ··· 409 418 410 419 "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 411 420 421 + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], 422 + 412 423 "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 413 424 414 425 "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], ··· 441 452 442 453 "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], 443 454 455 + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], 456 + 457 + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], 458 + 444 459 "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 445 460 446 461 "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], ··· 468 483 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 469 484 470 485 "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 486 + 487 + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], 471 488 472 489 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 473 490 ··· 654 671 "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], 655 672 656 673 "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 674 + 675 + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], 657 676 658 677 "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], 659 678
+1 -2
apps/api/lexicons/sandbox/createSandbox.json
··· 15 15 "properties": { 16 16 "base": { 17 17 "type": "string", 18 - "description": "The base sandbox URI to clone from, e.g. a template or an existing sandbox.", 19 - "format": "at-uri" 18 + "description": "The base sandbox URI to clone from, e.g. a template or an existing sandbox." 20 19 }, 21 20 "name": { 22 21 "type": "string",
+40 -30
apps/api/lexicons/sandbox/defs.json
··· 71 71 } 72 72 } 73 73 }, 74 + "secret": { 75 + "type": "object", 76 + "required": [ 77 + "name", 78 + "value" 79 + ], 80 + "properties": { 81 + "name": { 82 + "type": "string", 83 + "description": "Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc." 84 + }, 85 + "value": { 86 + "type": "string", 87 + "description": "Value of the secret. This will be encrypted at rest and redacted in any API responses." 88 + } 89 + } 90 + }, 91 + "envVar": { 92 + "type": "object", 93 + "required": [ 94 + "name", 95 + "value" 96 + ], 97 + "properties": { 98 + "name": { 99 + "type": "string", 100 + "description": "Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc." 101 + }, 102 + "value": { 103 + "type": "string", 104 + "description": "Value of the environment variable. This will be visible in API responses and should not contain sensitive information." 105 + } 106 + } 107 + }, 74 108 "secrets": { 75 109 "type": "array", 76 110 "items": { 77 - "type": "object", 78 - "required": [ 79 - "name", 80 - "value" 81 - ], 82 - "properties": { 83 - "name": { 84 - "type": "string", 85 - "description": "Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc." 86 - }, 87 - "value": { 88 - "type": "string", 89 - "description": "Value of the secret. This will be encrypted at rest and redacted in any API responses." 90 - } 91 - } 111 + "type": "ref", 112 + "description": "A secret to add to the sandbox", 113 + "ref": "io.pocketenv.sandbox.defs#secret" 92 114 } 93 115 }, 94 116 "envs": { 95 117 "type": "array", 96 118 "items": { 97 - "type": "object", 98 - "required": [ 99 - "name", 100 - "value" 101 - ], 102 - "properties": { 103 - "name": { 104 - "type": "string", 105 - "description": "Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc." 106 - }, 107 - "value": { 108 - "type": "string", 109 - "description": "Value of the environment variable. This will be visible in API responses and should not contain sensitive information." 110 - } 111 - } 119 + "type": "ref", 120 + "description": "An environment variable to add to the sandbox", 121 + "ref": "io.pocketenv.sandbox.defs#envVar" 112 122 } 113 123 } 114 124 }
+3 -4
apps/api/lexicons/sandbox/deleteSandbox.json
··· 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." 18 17 } 19 18 } 20 19 },
+3 -4
apps/api/lexicons/sandbox/startSandbox.json
··· 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." 18 17 } 19 18 } 20 19 },
+3 -4
apps/api/lexicons/sandbox/stopSandbox.json
··· 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." 18 17 } 19 18 } 20 19 },
+1
apps/api/package.json
··· 29 29 "@types/node": "^25.2.3", 30 30 "@types/prompts": "^2.4.9", 31 31 "@types/ramda": "^0.31.1", 32 + "axios": "^1.13.5", 32 33 "better-sqlite3": "^12.6.2", 33 34 "chalk": "^5.6.2", 34 35 "consola": "^3.4.2",
-1
apps/api/pkl/defs/sandbox/createSandbox.pkl
··· 16 16 type = "string" 17 17 description = 18 18 "The base sandbox URI to clone from, e.g. a template or an existing sandbox." 19 - format = "at-uri" 20 19 } 21 20 ["name"] = new StringType { 22 21 type = "string"
+38 -28
apps/api/pkl/defs/sandbox/defs.pkl
··· 72 72 } 73 73 } 74 74 } 75 + ["secret"] = new ObjectType { 76 + type = "object" 77 + required = List("name", "value") 78 + properties { 79 + ["name"] = new StringType { 80 + type = "string" 81 + description = "Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc." 82 + } 83 + ["value"] = new StringType { 84 + type = "string" 85 + description = 86 + "Value of the secret. This will be encrypted at rest and redacted in any API responses." 87 + } 88 + } 89 + } 90 + ["envVar"] = new ObjectType { 91 + type = "object" 92 + required = List("name", "value") 93 + properties { 94 + ["name"] = new StringType { 95 + type = "string" 96 + description = "Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc." 97 + } 98 + ["value"] = new StringType { 99 + type = "string" 100 + description = 101 + "Value of the environment variable. This will be visible in API responses and should not contain sensitive information." 102 + } 103 + } 104 + } 75 105 ["secrets"] = new Array { 76 106 type = "array" 77 - items = new ObjectType { 78 - type = "object" 79 - required = List("name", "value") 80 - properties { 81 - ["name"] = new StringType { 82 - type = "string" 83 - description = "Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc." 84 - } 85 - ["value"] = new StringType { 86 - type = "string" 87 - description = 88 - "Value of the secret. This will be encrypted at rest and redacted in any API responses." 89 - } 90 - } 107 + items = new Ref { 108 + type = "ref" 109 + ref = "io.pocketenv.sandbox.defs#secret" 110 + description = "A secret to add to the sandbox" 91 111 } 92 112 } 93 113 ["envs"] = new Array { 94 114 type = "array" 95 - items = new ObjectType { 96 - type = "object" 97 - required = List("name", "value") 98 - properties { 99 - ["name"] = new StringType { 100 - type = "string" 101 - description = "Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc." 102 - } 103 - ["value"] = new StringType { 104 - type = "string" 105 - description = 106 - "Value of the environment variable. This will be visible in API responses and should not contain sensitive information." 107 - } 108 - } 115 + items = new Ref { 116 + type = "ref" 117 + ref = "io.pocketenv.sandbox.defs#envVar" 118 + description = "An environment variable to add to the sandbox" 109 119 } 110 120 } 111 121 }
+3 -4
apps/api/pkl/defs/sandbox/deleteSandbox.pkl
··· 8 8 description = "Delete a sandbox by 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." 17 16 } 18 17 } 19 18 }
+3 -4
apps/api/pkl/defs/sandbox/startSandbox.pkl
··· 8 8 description = "Start a sandbox" 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." 17 16 } 18 17 } 19 18 }
+3 -4
apps/api/pkl/defs/sandbox/stopSandbox.pkl
··· 8 8 description = "Stop a sandbox" 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." 17 16 } 18 17 } 19 18 }
+1 -1
apps/api/pkl/schema/lexicon.pkl
··· 30 30 31 31 class Array extends BaseType { 32 32 type: "array" 33 - items: StringType | IntegerType | ObjectType | Blob | Ref | Union 33 + items: StringType | IntegerType | Blob | Ref | Union 34 34 maxLength: Int? 35 35 } 36 36
+4
apps/api/src/context.ts
··· 9 9 import authVerifier from "lib/authVerfifier"; 10 10 import redis from "redis"; 11 11 import type { RequestHandler } from "express"; 12 + import axios from "axios"; 12 13 13 14 const { DB_PATH } = env; 14 15 export const db = createDb(DB_PATH); ··· 36 37 }) 37 38 .connect(), 38 39 kv: new Map<string, string>(), 40 + sandbox: axios.create({ 41 + baseURL: env.SANDBOX_API_URL, 42 + }), 39 43 }; 40 44 41 45 export const contextMiddleware: RequestHandler = (req, _res, next) => {
+10 -7
apps/api/src/index.ts
··· 5 5 import bsky from "bsky"; 6 6 import { contextMiddleware, ctx } from "context"; 7 7 import { createServer } from "lexicon"; 8 + import chalk from "chalk"; 8 9 import API from "./xrpc"; 9 10 10 11 let server = createServer({ ··· 22 23 23 24 app.use(contextMiddleware); 24 25 app.use(cors()); 25 - app.use(express.json()); 26 26 app.use(morgan("dev")); 27 27 28 + const banner = ` 29 + ___ __ __ 30 + / _ \\___ ____/ /_____ / /____ ___ _ __ 31 + / ___/ _ \\/ __/ '_/ -_) __/ -_) _ \\ |/ / 32 + /_/ \\___/\\__/_/\\_\\__/\\__/\\__/_/ /_/___/ 33 + 34 + `; 35 + 28 36 app.get("/", (req, res) => { 29 37 const accept = req.headers.accept || ""; 30 38 const wantsHTML = accept.includes("text/html"); 31 - const banner = ` 32 - ___ __ __ 33 - / _ \\___ ____/ /_____ / /____ ___ _ __ 34 - / ___/ _ \\/ __/ '_/ -_) __/ -_) _ \\ |/ / 35 - /_/ \\___/\\__/_/\\_\\__/\\__/\\__/_/ /_/___/ 36 39 37 - `; 38 40 if (wantsHTML) { 39 41 res.contentType("text/html"); 40 42 res.send(`<pre>${banner}</pre>`); ··· 48 50 app.use(server.xrpc.router); 49 51 50 52 app.listen(process.env.POCKETENV_XPRC_PORT || 8789, () => { 53 + console.log(chalk.greenBright(banner)); 51 54 consola.info( 52 55 `Pocketenv XRPC API is running on port ${process.env.POCKETENV_XRPC_PORT || 8789}`, 53 56 );
+136 -13
apps/api/src/lexicon/lexicons.ts
··· 103 103 type: "string", 104 104 description: 105 105 "The base sandbox URI to clone from, e.g. a template or an existing sandbox.", 106 - format: "at-uri", 107 106 }, 108 107 name: { 109 108 type: "string", ··· 176 175 }, 177 176 }, 178 177 }, 178 + IoPocketenvSandboxDefs: { 179 + lexicon: 1, 180 + id: "io.pocketenv.sandbox.defs", 181 + defs: { 182 + sandboxViewBasic: { 183 + type: "object", 184 + properties: { 185 + name: { 186 + type: "string", 187 + description: "Name of the sandbox", 188 + maxLength: 50, 189 + }, 190 + provider: { 191 + type: "string", 192 + description: 193 + "The provider of the sandbox, e.g. 'daytona', 'vercel', 'cloudflare', etc.", 194 + maxLength: 50, 195 + }, 196 + description: { 197 + type: "string", 198 + maxGraphemes: 300, 199 + maxLength: 3000, 200 + }, 201 + website: { 202 + type: "string", 203 + description: "Any URI related to the sandbox", 204 + format: "uri", 205 + }, 206 + logo: { 207 + type: "string", 208 + description: "URI to an image logo for the sandbox", 209 + format: "uri", 210 + }, 211 + topics: { 212 + type: "array", 213 + items: { 214 + type: "string", 215 + minLength: 1, 216 + maxLength: 50, 217 + }, 218 + maxLength: 50, 219 + }, 220 + repo: { 221 + type: "string", 222 + description: 223 + "A git repository URL to clone into the sandbox, e.g. a GitHub/Tangled repo.", 224 + format: "uri", 225 + }, 226 + readme: { 227 + type: "string", 228 + description: "A URI to a README for the sandbox.", 229 + format: "uri", 230 + }, 231 + vcpus: { 232 + type: "integer", 233 + description: "Number of virtual CPUs allocated to the sandbox", 234 + }, 235 + memory: { 236 + type: "integer", 237 + description: "Amount of memory in GB allocated to the sandbox", 238 + }, 239 + disk: { 240 + type: "integer", 241 + description: "Amount of disk space in GB allocated to the sandbox", 242 + }, 243 + installs: { 244 + type: "integer", 245 + description: 246 + "Number of times the sandbox has been installed by users.", 247 + }, 248 + createdAt: { 249 + type: "string", 250 + format: "datetime", 251 + }, 252 + }, 253 + }, 254 + secret: { 255 + type: "object", 256 + required: ["name", "value"], 257 + properties: { 258 + name: { 259 + type: "string", 260 + description: 261 + "Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc.", 262 + }, 263 + value: { 264 + type: "string", 265 + description: 266 + "Value of the secret. This will be encrypted at rest and redacted in any API responses.", 267 + }, 268 + }, 269 + }, 270 + envVar: { 271 + type: "object", 272 + required: ["name", "value"], 273 + properties: { 274 + name: { 275 + type: "string", 276 + description: 277 + "Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc.", 278 + }, 279 + value: { 280 + type: "string", 281 + description: 282 + "Value of the environment variable. This will be visible in API responses and should not contain sensitive information.", 283 + }, 284 + }, 285 + }, 286 + secrets: { 287 + type: "array", 288 + items: { 289 + type: "ref", 290 + description: "A secret to add to the sandbox", 291 + ref: "lex:io.pocketenv.sandbox.defs#secret", 292 + }, 293 + }, 294 + envs: { 295 + type: "array", 296 + items: { 297 + type: "ref", 298 + description: "An environment variable to add to the sandbox", 299 + ref: "lex:io.pocketenv.sandbox.defs#envVar", 300 + }, 301 + }, 302 + }, 303 + }, 179 304 IoPocketenvSandboxDeleteSandbox: { 180 305 lexicon: 1, 181 306 id: "io.pocketenv.sandbox.deleteSandbox", ··· 185 310 description: "Delete a sandbox by uri", 186 311 parameters: { 187 312 type: "params", 188 - required: ["uri"], 313 + required: ["id"], 189 314 properties: { 190 - uri: { 315 + id: { 191 316 type: "string", 192 - description: "The sandbox URI.", 193 - format: "at-uri", 317 + description: "The sandbox ID.", 194 318 }, 195 319 }, 196 320 }, ··· 372 496 description: "Start a sandbox", 373 497 parameters: { 374 498 type: "params", 375 - required: ["uri"], 499 + required: ["id"], 376 500 properties: { 377 - uri: { 501 + id: { 378 502 type: "string", 379 - description: "The sandbox URI.", 380 - format: "at-uri", 503 + description: "The sandbox ID.", 381 504 }, 382 505 }, 383 506 }, ··· 400 523 description: "Stop a sandbox", 401 524 parameters: { 402 525 type: "params", 403 - required: ["uri"], 526 + required: ["id"], 404 527 properties: { 405 - uri: { 528 + id: { 406 529 type: "string", 407 - description: "The sandbox URI.", 408 - format: "at-uri", 530 + description: "The sandbox ID.", 409 531 }, 410 532 }, 411 533 }, ··· 478 600 AppBskyActorProfile: "app.bsky.actor.profile", 479 601 IoPocketenvSandboxClaimSandbox: "io.pocketenv.sandbox.claimSandbox", 480 602 IoPocketenvSandboxCreateSandbox: "io.pocketenv.sandbox.createSandbox", 603 + IoPocketenvSandboxDefs: "io.pocketenv.sandbox.defs", 481 604 IoPocketenvSandboxDeleteSandbox: "io.pocketenv.sandbox.deleteSandbox", 482 605 IoPocketenvSandboxGetSandbox: "io.pocketenv.sandbox.getSandbox", 483 606 IoPocketenvSandboxGetSandboxes: "io.pocketenv.sandbox.getSandboxes",
+89
apps/api/src/lexicon/types/io/pocketenv/sandbox/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + 9 + export interface SandboxViewBasic { 10 + /** Name of the sandbox */ 11 + name?: string; 12 + /** The provider of the sandbox, e.g. 'daytona', 'vercel', 'cloudflare', etc. */ 13 + provider?: string; 14 + description?: string; 15 + /** Any URI related to the sandbox */ 16 + website?: string; 17 + /** URI to an image logo for the sandbox */ 18 + logo?: string; 19 + topics?: string[]; 20 + /** A git repository URL to clone into the sandbox, e.g. a GitHub/Tangled repo. */ 21 + repo?: string; 22 + /** A URI to a README for the sandbox. */ 23 + readme?: string; 24 + /** Number of virtual CPUs allocated to the sandbox */ 25 + vcpus?: number; 26 + /** Amount of memory in GB allocated to the sandbox */ 27 + memory?: number; 28 + /** Amount of disk space in GB allocated to the sandbox */ 29 + disk?: number; 30 + /** Number of times the sandbox has been installed by users. */ 31 + installs?: number; 32 + createdAt?: string; 33 + [k: string]: unknown; 34 + } 35 + 36 + export function isSandboxViewBasic(v: unknown): v is SandboxViewBasic { 37 + return ( 38 + isObj(v) && 39 + hasProp(v, "$type") && 40 + v.$type === "io.pocketenv.sandbox.defs#sandboxViewBasic" 41 + ); 42 + } 43 + 44 + export function validateSandboxViewBasic(v: unknown): ValidationResult { 45 + return lexicons.validate("io.pocketenv.sandbox.defs#sandboxViewBasic", v); 46 + } 47 + 48 + export interface Secret { 49 + /** Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc. */ 50 + name: string; 51 + /** Value of the secret. This will be encrypted at rest and redacted in any API responses. */ 52 + value: string; 53 + [k: string]: unknown; 54 + } 55 + 56 + export function isSecret(v: unknown): v is Secret { 57 + return ( 58 + isObj(v) && 59 + hasProp(v, "$type") && 60 + v.$type === "io.pocketenv.sandbox.defs#secret" 61 + ); 62 + } 63 + 64 + export function validateSecret(v: unknown): ValidationResult { 65 + return lexicons.validate("io.pocketenv.sandbox.defs#secret", v); 66 + } 67 + 68 + export interface EnvVar { 69 + /** Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc. */ 70 + name: string; 71 + /** Value of the environment variable. This will be visible in API responses and should not contain sensitive information. */ 72 + value: string; 73 + [k: string]: unknown; 74 + } 75 + 76 + export function isEnvVar(v: unknown): v is EnvVar { 77 + return ( 78 + isObj(v) && 79 + hasProp(v, "$type") && 80 + v.$type === "io.pocketenv.sandbox.defs#envVar" 81 + ); 82 + } 83 + 84 + export function validateEnvVar(v: unknown): ValidationResult { 85 + return lexicons.validate("io.pocketenv.sandbox.defs#envVar", v); 86 + } 87 + 88 + export type Secrets = Secret[]; 89 + export type Envs = EnvVar[];
+2 -2
apps/api/src/lexicon/types/io/pocketenv/sandbox/deleteSandbox.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. */ 14 + id: string; 15 15 } 16 16 17 17 export type InputSchema = undefined;
+2 -2
apps/api/src/lexicon/types/io/pocketenv/sandbox/startSandbox.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. */ 14 + id: string; 15 15 } 16 16 17 17 export type InputSchema = undefined;
+2 -2
apps/api/src/lexicon/types/io/pocketenv/sandbox/stopSandbox.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. */ 14 + id: string; 15 15 } 16 16 17 17 export type InputSchema = undefined;
+1
apps/api/src/lib/env.ts
··· 25 25 PRIVATE_KEY_3: str({}), 26 26 PUBLIC_KEY: str({}), 27 27 PRIVATE_KEY: str({}), 28 + SANDBOX_API_URL: str({ default: "http://localhost:8788" }), 28 29 });
+1 -3
apps/api/src/schema/sandboxes.ts
··· 9 9 import users from "./users"; 10 10 11 11 const sandboxes = pgTable("sandboxes", { 12 - id: text("id") 13 - .primaryKey() 14 - .default(sql`sandbox_id()`), 12 + id: text("id").primaryKey().default(sql`sandbox_id()`), 15 13 base: text("base"), 16 14 name: text("name").unique().notNull(), 17 15 displayName: text("display_name"),
+4 -2
apps/api/src/xrpc/index.ts
··· 6 6 import getSandboxes from "./io/pocketenv/sandbox/getSandboxes"; 7 7 import startSandbox from "./io/pocketenv/sandbox/startSandbox"; 8 8 import stopSandbox from "./io/pocketenv/sandbox/stopSandbox"; 9 + import claimSandbox from "./io/pocketenv/sandbox/claimSandbox"; 9 10 10 11 export default function (server: Server, ctx: Context) { 11 12 // io.pocketenv 12 - createSandbox(server, ctx); 13 - deleteSandbox(server, ctx); 14 13 getSandbox(server, ctx); 15 14 getSandboxes(server, ctx); 15 + createSandbox(server, ctx); 16 + deleteSandbox(server, ctx); 16 17 startSandbox(server, ctx); 17 18 stopSandbox(server, ctx); 19 + claimSandbox(server, ctx); 18 20 19 21 return server; 20 22 }
+21 -2
apps/api/src/xrpc/io/pocketenv/sandbox/createSandbox.ts
··· 1 1 import type { HandlerAuth } from "@atproto/xrpc-server"; 2 + import { consola } from "consola"; 2 3 import type { Context } from "context"; 3 4 import type { Server } from "lexicon"; 4 5 import type { HandlerInput } from "lexicon/types/io/pocketenv/sandbox/createSandbox"; 5 6 6 7 export default function (server: Server, ctx: Context) { 7 - const createSandbox = (input: HandlerInput, auth: HandlerAuth) => ({}); 8 + const createSandbox = async (input: HandlerInput, auth: HandlerAuth) => { 9 + const res = await ctx.sandbox.post("/v1/sandboxes", { 10 + provider: "daytona", 11 + }); 12 + return { 13 + id: res.data.id, 14 + name: input.body.name || "Unnamed Sandbox", 15 + provider: "daytona", // or whatever provider you're using 16 + description: input.body.description, 17 + topics: input.body.topics, 18 + repo: input.body.repo, 19 + vcpus: input.body.vcpus, 20 + memory: input.body.memory, 21 + disk: input.body.disk, 22 + readme: input.body.readme, 23 + createdAt: new Date().toISOString(), 24 + // Add other required fields 25 + }; 26 + }; 8 27 server.io.pocketenv.sandbox.createSandbox({ 9 28 auth: ctx.authVerifier, 10 29 handler: async ({ input, auth }) => { 11 - const result = createSandbox(input, auth); 30 + const result = await createSandbox(input, auth); 12 31 return { 13 32 encoding: "application/json", 14 33 body: result,
+5 -2
apps/api/src/xrpc/io/pocketenv/sandbox/deleteSandbox.ts
··· 4 4 import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/deleteSandbox"; 5 5 6 6 export default function (server: Server, ctx: Context) { 7 - const deleteSandbox = (params: QueryParams, auth: HandlerAuth) => ({}); 7 + const deleteSandbox = async (params: QueryParams, auth: HandlerAuth) => { 8 + await ctx.sandbox.delete(`/v1/sandboxes/${params.id}`); 9 + return {}; 10 + }; 8 11 server.io.pocketenv.sandbox.deleteSandbox({ 9 12 auth: ctx.authVerifier, 10 13 handler: async ({ params, auth }) => { 11 - const result = deleteSandbox(params, auth); 14 + const result = await deleteSandbox(params, auth); 12 15 return { 13 16 encoding: "application/json", 14 17 body: result,
+3 -3
apps/api/src/xrpc/io/pocketenv/sandbox/getSandboxes.ts
··· 78 78 id: sandbox.id, 79 79 name: sandbox.name, 80 80 displayName: sandbox.displayName, 81 - description: sandbox.description, 82 - logo: sandbox.logo, 83 - readme: sandbox.readme, 81 + description: sandbox.description!, 82 + logo: sandbox.logo!, 83 + readme: sandbox.readme!, 84 84 installs: sandbox.installs, 85 85 uri: sandbox.uri, 86 86 createdAt: sandbox.createdAt.toISOString(),
+5 -2
apps/api/src/xrpc/io/pocketenv/sandbox/startSandbox.ts
··· 4 4 import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/startSandbox"; 5 5 6 6 export default function (server: Server, ctx: Context) { 7 - const startSandbox = (params: QueryParams, auth: HandlerAuth) => ({}); 7 + const startSandbox = async (params: QueryParams, auth: HandlerAuth) => { 8 + await ctx.sandbox.post(`/v1/sandboxes/${params.id}/start`); 9 + return {}; 10 + }; 8 11 server.io.pocketenv.sandbox.startSandbox({ 9 12 auth: ctx.authVerifier, 10 13 handler: async ({ params, auth }) => { 11 - const result = startSandbox(params, auth); 14 + const result = await startSandbox(params, auth); 12 15 return { 13 16 encoding: "application/json", 14 17 body: result,
+6 -3
apps/api/src/xrpc/io/pocketenv/sandbox/stopSandbox.ts
··· 4 4 import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/stopSandbox"; 5 5 6 6 export default function (server: Server, ctx: Context) { 7 - const stopSandbox = (params: QueryParams, auth: HandlerAuth) => ({}); 8 - server.io.pocketenv.sandbox.deleteSandbox({ 7 + const stopSandbox = async (params: QueryParams, auth: HandlerAuth) => { 8 + await ctx.sandbox.post(`/v1/sandboxes/${params.id}/stop`); 9 + return {}; 10 + }; 11 + server.io.pocketenv.sandbox.stopSandbox({ 9 12 auth: ctx.authVerifier, 10 13 handler: async ({ params, auth }) => { 11 - const result = stopSandbox(params, auth); 14 + const result = await stopSandbox(params, auth); 12 15 return { 13 16 encoding: "application/json", 14 17 body: result,
+9 -2
apps/web/src/api/sandbox.ts
··· 1 1 import { client } from "."; 2 2 import type { Sandbox } from "../types/sandbox"; 3 3 4 - export const createSandbox = () => 5 - client.post("/xrpc/io.pocketenv.sandbox.createSandbox"); 4 + export const createSandbox = ({ base }: { base: string }) => 5 + client.post("/xrpc/io.pocketenv.sandbox.createSandbox", { 6 + base, 7 + }); 8 + 9 + export const claimSandbox = ({ id }: { id: string }) => 10 + client.post("/xrpc/io.pocketenv.sandbox.claimSandbox", { 11 + id, 12 + }); 6 13 7 14 export const getSandbox = (id: string) => 8 15 client.get(`/xrpc/io.pocketenv.sandbox.getSandbox?id=${id}`);
+9 -3
apps/web/src/components/newproject/NewProject.tsx
··· 1 1 import { useEffect, useRef, useState } from "react"; 2 - import { useSandboxesQuery } from "../../hooks/useSandbox"; 2 + import { 3 + useCreateSandboxMutation, 4 + useSandboxesQuery, 5 + } from "../../hooks/useSandbox"; 3 6 import { useNavigate } from "@tanstack/react-router"; 7 + import consola from "consola"; 4 8 5 9 export type NewProjectProps = { 6 10 isOpen: boolean; ··· 12 16 const [filter, setFilter] = useState(""); 13 17 const { data, isLoading } = useSandboxesQuery(); 14 18 const navigate = useNavigate(); 19 + const { mutateAsync } = useCreateSandboxMutation(); 15 20 16 21 const sandboxes = data?.sandboxes.filter((sandbox) => 17 22 filter ··· 51 56 }; 52 57 53 58 const onSelect = async (id: string) => { 54 - await navigate({ to: `/sandbox/${id}` }); 59 + const res = await mutateAsync(id); 60 + await navigate({ to: `/sandbox/${res.data.id}` }); 55 61 onClose(); 56 62 setFilter(""); 57 63 }; ··· 91 97 <div 92 98 key={item.id} 93 99 className="p-3 hover:bg-white/7 cursor-pointer rounded-md" 94 - onClick={() => onSelect(item.id)} 100 + onClick={() => onSelect(item.uri)} 95 101 > 96 102 <div className="font-semibold">{item.displayName}</div> 97 103 </div>
+20 -2
apps/web/src/hooks/useSandbox.ts
··· 1 - import { useQuery } from "@tanstack/react-query"; 2 - import { getSandboxes } from "../api/sandbox"; 1 + import { useMutation, useQuery } from "@tanstack/react-query"; 2 + import { claimSandbox, createSandbox, getSandboxes } from "../api/sandbox"; 3 3 4 4 export const useSandboxesQuery = (offset?: number, limit?: number) => 5 5 useQuery({ ··· 7 7 queryFn: () => getSandboxes(offset, limit), 8 8 select: (response) => response.data, 9 9 }); 10 + 11 + export const useCreateSandboxMutation = () => 12 + useMutation({ 13 + mutationKey: ["createSandbox"], 14 + mutationFn: async (base: string) => 15 + createSandbox({ 16 + base, 17 + }), 18 + }); 19 + 20 + export const useClaimSandboxMutation = () => 21 + useMutation({ 22 + mutationKey: ["claimSandbox"], 23 + mutationFn: async (id: string) => 24 + claimSandbox({ 25 + id, 26 + }), 27 + });
+4 -1
apps/web/src/pages/home/Navbar/Navbar.tsx
··· 2 2 import NewProject from "../../../components/newproject"; 3 3 import Logo from "../../../assets/logo.png"; 4 4 import { useState } from "react"; 5 + import { Link } from "@tanstack/react-router"; 5 6 6 7 function Navbar() { 7 8 const [modalOpen, setModalOpen] = useState(false); 8 9 return ( 9 10 <nav className="navbar bg-base-100 h-20 max-w-[80%] m-auto mt-0"> 10 - <img src={Logo} className="max-h-[40px] mr-[15px]" /> 11 + <Link to="/"> 12 + <img src={Logo} className="max-h-[40px] mr-[15px]" /> 13 + </Link> 11 14 <div className="flex flex-1 items-center"></div> 12 15 <div className="navbar-end flex items-center gap-4"> 13 16 <div className="ml-[10px]">
+1
apps/web/src/types/sandbox.ts
··· 2 2 id: string; 3 3 name: string; 4 4 displayName: string; 5 + uri: string; 5 6 description?: string; 6 7 logo?: string; 7 8 readme?: string;