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.

Raise minimum port to 1025; add VSCode endpoints

Add POST/DELETE /v1/sandboxes/:sandboxId/vscode endpoints and
Cloudflare provider methods (exposeVscode, unexposeVscode). Update
lexicons, PKL, server and frontend validation to require ports >= 1025.

+125 -40
+1 -1
apps/api/lexicons/port/defs.json
··· 10 10 "type": "integer", 11 11 "description": "The port number.", 12 12 "maximum": 65535, 13 - "minimum": 1024 13 + "minimum": 1025 14 14 }, 15 15 "description": { 16 16 "type": "string",
+3 -7
apps/api/lexicons/sandbox/defs.json
··· 65 65 "items": { 66 66 "type": "integer", 67 67 "maximum": 65535, 68 - "minimum": 1024 68 + "minimum": 1025 69 69 }, 70 70 "maxLength": 100 71 71 }, ··· 158 158 "items": { 159 159 "type": "integer", 160 160 "maximum": 65535, 161 - "minimum": 1024 161 + "minimum": 1025 162 162 }, 163 163 "maxLength": 100 164 164 }, ··· 179 179 }, 180 180 "sandboxDetailsPref": { 181 181 "type": "object", 182 - "nullable": [ 183 - "repo", 184 - "description", 185 - "topics" 186 - ], 182 + "nullable": ["repo", "description", "topics"], 187 183 "properties": { 188 184 "name": { 189 185 "type": "string",
+4 -10
apps/api/lexicons/sandbox/exposePort.json
··· 7 7 "description": "Expose a port for a sandbox.", 8 8 "parameters": { 9 9 "type": "params", 10 - "required": [ 11 - "id" 12 - ], 10 + "required": ["id"], 13 11 "properties": { 14 12 "id": { 15 13 "type": "string", ··· 21 19 "encoding": "application/json", 22 20 "schema": { 23 21 "type": "object", 24 - "required": [ 25 - "port" 26 - ], 22 + "required": ["port"], 27 23 "properties": { 28 24 "port": { 29 25 "type": "integer", 30 26 "description": "The port number to expose.", 31 27 "maximum": 65535, 32 - "minimum": 1024 28 + "minimum": 1025 33 29 }, 34 30 "description": { 35 31 "type": "string", ··· 42 38 "encoding": "application/json", 43 39 "schema": { 44 40 "type": "object", 45 - "nullable": [ 46 - "previewUrl" 47 - ], 41 + "nullable": ["previewUrl"], 48 42 "properties": { 49 43 "previewUrl": { 50 44 "type": "string",
+3 -7
apps/api/lexicons/sandbox/unexposePort.json
··· 7 7 "description": "Unexpose a port for a sandbox.", 8 8 "parameters": { 9 9 "type": "params", 10 - "required": [ 11 - "id" 12 - ], 10 + "required": ["id"], 13 11 "properties": { 14 12 "id": { 15 13 "type": "string", ··· 21 19 "encoding": "application/json", 22 20 "schema": { 23 21 "type": "object", 24 - "required": [ 25 - "port" 26 - ], 22 + "required": ["port"], 27 23 "properties": { 28 24 "port": { 29 25 "type": "integer", 30 26 "description": "The port number to unexpose.", 31 27 "maximum": 65535, 32 - "minimum": 1024 28 + "minimum": 1025 33 29 } 34 30 } 35 31 }
+1 -1
apps/api/pkl/defs/port/defs.pkl
··· 10 10 ["port"] = new IntegerType { 11 11 type = "integer" 12 12 description = "The port number." 13 - minimum = 1024 13 + minimum = 1025 14 14 maximum = 65535 15 15 } 16 16 ["description"] = new StringType {
+2 -2
apps/api/pkl/defs/sandbox/defs.pkl
··· 65 65 type = "array" 66 66 items = new IntegerType { 67 67 type = "integer" 68 - minimum = 1024 68 + minimum = 1025 69 69 maximum = 65535 70 70 } 71 71 maxLength = 100 ··· 159 159 type = "array" 160 160 items = new IntegerType { 161 161 type = "integer" 162 - minimum = 1024 162 + minimum = 1025 163 163 maximum = 65535 164 164 } 165 165 maxLength = 100
+1 -1
apps/api/pkl/defs/sandbox/exposePort.pkl
··· 25 25 ["port"] = new IntegerType { 26 26 type = "integer" 27 27 description = "The port number to expose." 28 - minimum = 1024 28 + minimum = 1025 29 29 maximum = 65535 30 30 } 31 31 ["description"] = new StringType {
+1 -1
apps/api/pkl/defs/sandbox/unexposePort.pkl
··· 25 25 ["port"] = new IntegerType { 26 26 type = "integer" 27 27 description = "The port number to unexpose." 28 - minimum = 1024 28 + minimum = 1025 29 29 maximum = 65535 30 30 } 31 31 }
+5 -5
apps/api/src/lexicon/lexicons.ts
··· 425 425 type: "integer", 426 426 description: "The port number.", 427 427 maximum: 65535, 428 - minimum: 1024, 428 + minimum: 1025, 429 429 }, 430 430 description: { 431 431 type: "string", ··· 663 663 items: { 664 664 type: "integer", 665 665 maximum: 65535, 666 - minimum: 1024, 666 + minimum: 1025, 667 667 }, 668 668 maxLength: 100, 669 669 }, ··· 761 761 items: { 762 762 type: "integer", 763 763 maximum: 65535, 764 - minimum: 1024, 764 + minimum: 1025, 765 765 }, 766 766 maxLength: 100, 767 767 }, ··· 1070 1070 type: "integer", 1071 1071 description: "The port number to expose.", 1072 1072 maximum: 65535, 1073 - minimum: 1024, 1073 + minimum: 1025, 1074 1074 }, 1075 1075 description: { 1076 1076 type: "string", ··· 1680 1680 type: "integer", 1681 1681 description: "The port number to unexpose.", 1682 1682 maximum: 65535, 1683 - minimum: 1024, 1683 + minimum: 1025, 1684 1684 }, 1685 1685 }, 1686 1686 },
+70 -2
apps/cf-sandbox/src/index.ts
··· 739 739 740 740 const { port } = await c.req.json<{ port: number }>(); 741 741 742 - if (!port || port < 1024 || port > 65535 || port == 3000) { 742 + if (!port || port < 1025 || port > 65535 || port == 3000) { 743 743 return c.json({ error: "Invalid port number" }, 400); 744 744 } 745 745 ··· 780 780 781 781 const port = parseInt(c.req.query("port") || "0", 10); 782 782 783 - if (!port || port <= 1024 || port > 65535 || port == 3000) { 783 + if (!port || port <= 1025 || port > 65535 || port == 3000) { 784 784 return c.json({ error: "Invalid port number" }, 400); 785 785 } 786 786 ··· 794 794 errorMessage, 795 795 ); 796 796 return c.json({ error: `Failed to unexpose port: ${errorMessage}` }, 500); 797 + } 798 + }); 799 + 800 + app.post("/v1/sandboxes/:sandboxId/vscode", async (c) => { 801 + const { sandboxes: record } = await getSandboxById( 802 + c.var.db, 803 + c.req.param("sandboxId"), 804 + ); 805 + 806 + if (!record) { 807 + return c.json({ error: "Sandbox not found" }, 404); 808 + } 809 + 810 + if (record.provider !== "cloudflare") { 811 + return c.json({ error: "Sandbox provider not supported" }, 400); 812 + } 813 + 814 + try { 815 + let sandbox: BaseSandbox | null = null; 816 + 817 + sandbox = await createSandbox("cloudflare", { 818 + id: record.name, 819 + }); 820 + 821 + const { hostname } = new URL(c.req.url); 822 + const previewUrl = await sandbox.exposeVscode(hostname); 823 + return c.json({ previewUrl }); 824 + } catch (err) { 825 + const errorMessage = err instanceof Error ? err.message : "Unknown error"; 826 + consola.error( 827 + c.req.param("sandboxId"), 828 + "Failed to expose vscode:", 829 + errorMessage, 830 + ); 831 + return c.json({ error: `Failed to expose vscode: ${errorMessage}` }, 500); 832 + } 833 + }); 834 + 835 + app.delete("/v1/sandboxes/:sandboxId/vscode", async (c) => { 836 + const { sandboxes: record } = await getSandboxById( 837 + c.var.db, 838 + c.req.param("sandboxId"), 839 + ); 840 + 841 + if (!record) { 842 + return c.json({ error: "Sandbox not found" }, 404); 843 + } 844 + 845 + if (record.provider !== "cloudflare") { 846 + return c.json({ error: "Sandbox provider not supported" }, 400); 847 + } 848 + try { 849 + let sandbox: BaseSandbox | null = null; 850 + 851 + sandbox = await createSandbox("cloudflare", { 852 + id: record.name, 853 + }); 854 + 855 + await sandbox.unexposeVscode(); 856 + return c.json({}); 857 + } catch (err) { 858 + const errorMessage = err instanceof Error ? err.message : "Unknown error"; 859 + consola.error( 860 + c.req.param("sandboxId"), 861 + "Failed to unexpose vscode:", 862 + errorMessage, 863 + ); 864 + return c.json({ error: `Failed to unexpose vscode: ${errorMessage}` }, 500); 797 865 } 798 866 }); 799 867
+28 -1
apps/cf-sandbox/src/providers/cloudflare/index.ts
··· 1 1 import { getSandbox, Sandbox } from "@cloudflare/sandbox"; 2 - import BaseProvider, { BaseSandbox, SandboxOptions } from ".."; 2 + import BaseProvider, { BaseSandbox, SandboxOptions, VSCODE_PORT } from ".."; 3 3 import { env } from "cloudflare:workers"; 4 4 import path from "node:path"; 5 5 ··· 138 138 await this.sandbox.unexposePort(port); 139 139 } catch (e) { 140 140 console.log("Failed to unexpose port", e); 141 + } 142 + } 143 + 144 + async exposeVscode(hostname: string): Promise<string | null> { 145 + this.sh`type code-server || curl -L https://coder.com/install.sh | sh`; 146 + this.sandbox.startProcess( 147 + `code-server --bind-addr 0.0.0.0:${VSCODE_PORT} --auth none`, 148 + ); 149 + 150 + try { 151 + const { url } = await this.sandbox.exposePort(VSCODE_PORT, { 152 + hostname: hostname.split(".").slice(-2).join("."), 153 + token: env.PREVIEW_TOKEN, 154 + }); 155 + return url; 156 + } catch (e) { 157 + console.log("Failed to expose vscode port", e); 158 + } 159 + 160 + return ""; 161 + } 162 + 163 + async unexposeVscode(): Promise<void> { 164 + try { 165 + await this.sandbox.unexposePort(VSCODE_PORT); 166 + } catch (e) { 167 + console.log("Failed to unexpose vscode port", e); 141 168 } 142 169 } 143 170 }
+4
apps/cf-sandbox/src/providers/index.ts
··· 1 + export const VSCODE_PORT = 1024; 2 + 1 3 export abstract class BaseSandbox { 2 4 abstract start(): Promise<void>; 3 5 abstract stop(): Promise<void>; ··· 14 16 abstract unmount(path: string): Promise<void>; 15 17 abstract expose(port: number, hostname: string): Promise<string | null>; 16 18 abstract unexpose(port: number): Promise<void>; 19 + abstract exposeVscode(hostname: string): Promise<string | null>; 20 + abstract unexposeVscode(): Promise<void>; 17 21 } 18 22 19 23 abstract class BaseProvider {
+2 -2
apps/web/src/components/contextmenu/ExposePortModal/ExposePortModal.tsx
··· 9 9 port: z.coerce 10 10 .number({ error: "Port is required" }) 11 11 .int() 12 - .min(1024, "Port must be between 1024 and 65535") 13 - .max(65535, "Port must be between 1024 and 65535"), 12 + .min(1025, "Port must be between 1025 and 65535") 13 + .max(65535, "Port must be between 1025 and 65535"), 14 14 description: z.string(), 15 15 }); 16 16