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.

Lookup variables by name and add CLI env/secret

+430 -12
+5 -2
apps/api/src/xrpc/io/pocketenv/variable/getVariable.ts
··· 1 1 import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 2 import type { Context } from "context"; 3 - import { eq, and, count } from "drizzle-orm"; 3 + import { eq, and, count, or } from "drizzle-orm"; 4 4 import type { Server } from "lexicon"; 5 5 import type { 6 6 QueryParams, ··· 64 64 .where( 65 65 and( 66 66 eq(users.did, auth.credentials.did), 67 - eq(sandboxVariables.variableId, params.id), 67 + or( 68 + eq(sandboxVariables.variableId, params.id), 69 + eq(variables.name, params.id), 70 + ), 68 71 ), 69 72 ) 70 73 .execute()
+126 -3
apps/cli/src/cmd/env.ts
··· 1 - export async function listEnvs(sandbox: string) {} 1 + import { client } from "../client"; 2 + import getAccessToken from "../lib/getAccessToken"; 3 + import type { Sandbox } from "../types/sandbox"; 4 + import type { Variable } from "../types/variable"; 5 + import chalk from "chalk"; 6 + import dayjs from "dayjs"; 7 + import consola from "consola"; 8 + import Table from "cli-table3"; 9 + import { env } from "../lib/env"; 10 + 11 + export async function listEnvs(sandbox: string) { 12 + const token = await getAccessToken(); 13 + const { data } = await client.get<{ sandbox: Sandbox }>( 14 + "/xrpc/io.pocketenv.sandbox.getSandbox", 15 + { 16 + params: { 17 + id: sandbox, 18 + }, 19 + headers: { 20 + Authorization: `Bearer ${token}`, 21 + }, 22 + }, 23 + ); 24 + const response = await client.get<{ variables: Variable[] }>( 25 + "/xrpc/io.pocketenv.variable.getVariables", 26 + { 27 + params: { 28 + sandboxId: data.sandbox.id, 29 + offset: 0, 30 + limit: 100, 31 + }, 32 + headers: { 33 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 34 + }, 35 + }, 36 + ); 37 + 38 + const table = new Table({ 39 + head: [chalk.cyan("NAME"), chalk.cyan("VALUE"), chalk.cyan("CREATED AT")], 40 + chars: { 41 + top: "", 42 + "top-mid": "", 43 + "top-left": "", 44 + "top-right": "", 45 + bottom: "", 46 + "bottom-mid": "", 47 + "bottom-left": "", 48 + "bottom-right": "", 49 + left: "", 50 + "left-mid": "", 51 + mid: "", 52 + "mid-mid": "", 53 + right: "", 54 + "right-mid": "", 55 + middle: " ", 56 + }, 57 + style: { 58 + border: [], 59 + head: [], 60 + }, 61 + }); 62 + 63 + for (const variable of response.data.variables) { 64 + table.push([ 65 + chalk.greenBright(variable.name), 66 + variable.value, 67 + dayjs(variable.createdAt).fromNow(), 68 + ]); 69 + } 2 70 3 - export async function putEnv(sandbox: string, key: string, value: string) {} 71 + consola.log(table.toString()); 72 + } 4 73 5 - export async function deleteEnv(sandbox: string, key: string) {} 74 + export async function putEnv(sandbox: string, key: string, value: string) { 75 + const token = await getAccessToken(); 76 + const { data } = await client.get<{ sandbox: Sandbox }>( 77 + "/xrpc/io.pocketenv.sandbox.getSandbox", 78 + { 79 + params: { 80 + id: sandbox, 81 + }, 82 + headers: { 83 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 84 + }, 85 + }, 86 + ); 87 + 88 + await client.post( 89 + "/xrpc/io.pocketenv.variable.addVariable", 90 + { 91 + variable: { sandboxId: data.sandbox.id, name: key, value: value }, 92 + }, 93 + { 94 + headers: { 95 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 96 + }, 97 + }, 98 + ); 99 + 100 + consola.success("Variable updated successfully"); 101 + } 102 + 103 + export async function deleteEnv(sandbox: string, key: string) { 104 + const token = await getAccessToken(); 105 + const { data } = await client.get<{ sandbox: Sandbox }>( 106 + "/xrpc/io.pocketenv.sandbox.getSandbox", 107 + { 108 + params: { 109 + id: sandbox, 110 + }, 111 + headers: { 112 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 113 + }, 114 + }, 115 + ); 116 + 117 + await client.post("/xrpc/io.pocketenv.variable.deleteVariable", { 118 + params: { 119 + sandboxId: data.sandbox.id, 120 + name: key, 121 + }, 122 + headers: { 123 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 124 + }, 125 + }); 126 + 127 + consola.success("Variable deleted successfully"); 128 + }
+108 -3
apps/cli/src/cmd/secret.ts
··· 1 - export async function listSecrets() {} 1 + import { client } from "../client"; 2 + import getAccessToken from "../lib/getAccessToken"; 3 + import { password } from "@inquirer/prompts"; 4 + import type { Sandbox } from "../types/sandbox"; 5 + import type { Secret } from "../types/secret"; 6 + import chalk from "chalk"; 7 + import consola from "consola"; 8 + import Table from "cli-table3"; 9 + import dayjs from "dayjs"; 10 + import relativeTime from "dayjs/plugin/relativeTime"; 11 + import { env } from "../lib/env"; 12 + import encrypt from "../lib/sodium"; 13 + 14 + dayjs.extend(relativeTime); 15 + 16 + export async function listSecrets(sandbox: string) { 17 + const token = await getAccessToken(); 18 + const { data } = await client.get<{ sandbox: Sandbox }>( 19 + "/xrpc/io.pocketenv.sandbox.getSandbox", 20 + { 21 + params: { 22 + id: sandbox, 23 + }, 24 + headers: { 25 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 26 + }, 27 + }, 28 + ); 29 + const response = await client.get<{ secrets: Secret[] }>( 30 + "/xrpc/io.pocketenv.secret.getSecrets", 31 + { 32 + params: { 33 + sandboxId: data.sandbox.id, 34 + offset: 0, 35 + limit: 100, 36 + }, 37 + headers: { 38 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 39 + }, 40 + }, 41 + ); 42 + 43 + const table = new Table({ 44 + head: [chalk.cyan("NAME"), chalk.cyan("CREATED AT")], 45 + chars: { 46 + top: "", 47 + "top-mid": "", 48 + "top-left": "", 49 + "top-right": "", 50 + bottom: "", 51 + "bottom-mid": "", 52 + "bottom-left": "", 53 + "bottom-right": "", 54 + left: "", 55 + "left-mid": "", 56 + mid: "", 57 + "mid-mid": "", 58 + right: "", 59 + "right-mid": "", 60 + middle: " ", 61 + }, 62 + style: { 63 + border: [], 64 + head: [], 65 + }, 66 + }); 67 + 68 + for (const secret of response.data.secrets) { 69 + table.push([ 70 + chalk.greenBright(secret.name), 71 + dayjs(secret.createdAt).fromNow(), 72 + ]); 73 + } 74 + 75 + consola.log(table.toString()); 76 + } 77 + 78 + export async function putSecret(sandbox: string, key: string) { 79 + const token = await getAccessToken(); 80 + const value = await password({ message: "Enter secret value" }); 81 + 82 + const { data } = await client.get("/xrpc/io.pocketenv.sandbox.getSandbox", { 83 + params: { 84 + id: sandbox, 85 + }, 86 + headers: { 87 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 88 + }, 89 + }); 2 90 3 - export async function putSecret() {} 91 + await client.post( 92 + "/xrpc/io.pocketenv.secret.addSecret", 93 + { 94 + secret: { 95 + sandboxId: data.sandbox.id, 96 + name: key, 97 + value: await encrypt(value), 98 + }, 99 + }, 100 + { 101 + headers: { 102 + Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 103 + }, 104 + }, 105 + ); 106 + } 4 107 5 - export async function deleteSecret() {} 108 + export async function deleteSecret(sandbox: string, key: string) { 109 + const token = await getAccessToken(); 110 + }
+9 -2
apps/cli/src/cmd/sshkeys.ts
··· 1 - export async function getSshKey(sandbox: string) {} 1 + import { input } from "@inquirer/prompts"; 2 + import getAccessToken from "../lib/getAccessToken"; 3 + 4 + export async function getSshKey(sandbox: string) { 5 + const token = await getAccessToken(); 6 + } 2 7 3 - export async function putKeys(sandbox: string) {} 8 + export async function putKeys(sandbox: string) { 9 + const token = await getAccessToken(); 10 + }
+9 -2
apps/cli/src/cmd/tailscale.ts
··· 1 - export async function putAuthKey(sandbox: string) {} 1 + import { input } from "@inquirer/prompts"; 2 + import getAccessToken from "../lib/getAccessToken"; 3 + 4 + export async function putAuthKey(sandbox: string) { 5 + const token = await getAccessToken(); 6 + } 2 7 3 - export async function getTailscaleAuthKey(sandbox: string) {} 8 + export async function getTailscaleAuthKey(sandbox: string) { 9 + const token = await getAccessToken(); 10 + }
+3
apps/cli/src/lib/env.ts
··· 5 5 POCKETENV_API_URL: str({ default: "https://api.pocketenv.io" }), 6 6 POCKETENV_CF_URL: str({ default: "https://sbx.pocketenv.io" }), 7 7 POCKETENV_TTY_URL: str({ default: "https://api.pocketenv.io" }), 8 + POCKETENV_PUBLIC_KEY: str({ 9 + default: "2bf96e12d109e6948046a7803ef1696e12c11f04f20a6ce64dbd4bcd93db9341", 10 + }), 8 11 });
+15
apps/cli/src/lib/sodium.ts
··· 1 + import sodium from "libsodium-wrappers"; 2 + import { env } from "./env"; 3 + 4 + async function encrypt(message: string): Promise<string> { 5 + await sodium.ready; 6 + 7 + const sealed = sodium.crypto_box_seal( 8 + sodium.from_string(message), 9 + sodium.from_hex(env.POCKETENV_PUBLIC_KEY), 10 + ); 11 + 12 + return sodium.to_base64(sealed, sodium.base64_variants.URLSAFE_NO_PADDING); 13 + } 14 + 15 + export default encrypt;
+122
apps/cli/src/lib/sshKeys.ts
··· 1 + import sodium from "libsodium-wrappers"; 2 + 3 + export type SSHKeyPair = { 4 + publicKey: string; 5 + privateKey: string; 6 + seedBase64: string; 7 + }; 8 + 9 + function u32(n: number): Uint8Array { 10 + return new Uint8Array([ 11 + (n >>> 24) & 0xff, 12 + (n >>> 16) & 0xff, 13 + (n >>> 8) & 0xff, 14 + n & 0xff, 15 + ]); 16 + } 17 + 18 + function concatBytes(...arrays: Uint8Array[]): Uint8Array { 19 + const total = arrays.reduce((sum, arr) => sum + arr.length, 0); 20 + const out = new Uint8Array(total); 21 + let offset = 0; 22 + for (const arr of arrays) { 23 + out.set(arr, offset); 24 + offset += arr.length; 25 + } 26 + return out; 27 + } 28 + 29 + function sshString(bytes: Uint8Array): Uint8Array { 30 + return concatBytes(u32(bytes.length), bytes); 31 + } 32 + 33 + function text(value: string): Uint8Array { 34 + return new TextEncoder().encode(value); 35 + } 36 + 37 + function wrapPem(label: string, bytes: Uint8Array): string { 38 + const base64 = sodium.to_base64(bytes, sodium.base64_variants.ORIGINAL); 39 + const lines = base64.match(/.{1,70}/g)?.join("\n") ?? base64; 40 + return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----\n`; 41 + } 42 + 43 + function buildEd25519PublicKeyBlob(publicKey: Uint8Array): Uint8Array { 44 + return concatBytes(sshString(text("ssh-ed25519")), sshString(publicKey)); 45 + } 46 + 47 + function publicLineFromPublicKey( 48 + publicKey: Uint8Array, 49 + comment: string, 50 + ): string { 51 + const blob = buildEd25519PublicKeyBlob(publicKey); 52 + return `ssh-ed25519 ${sodium.to_base64(blob, sodium.base64_variants.ORIGINAL)} ${comment}`; 53 + } 54 + 55 + function buildOpenSSHEd25519PrivateKey( 56 + publicKey: Uint8Array, 57 + seed: Uint8Array, 58 + comment: string, 59 + ): string { 60 + if (publicKey.length !== 32) throw new Error("Invalid public key length"); 61 + if (seed.length !== 32) throw new Error("Invalid seed length"); 62 + 63 + const privateKey64 = concatBytes(seed, publicKey); 64 + const publicBlob = buildEd25519PublicKeyBlob(publicKey); 65 + const checkint = crypto.getRandomValues(new Uint32Array(1))[0]!; 66 + const commentBytes = text(comment); 67 + 68 + const privateSectionWithoutPadding = concatBytes( 69 + u32(checkint), 70 + u32(checkint), 71 + sshString(text("ssh-ed25519")), 72 + sshString(publicKey), 73 + sshString(privateKey64), 74 + sshString(commentBytes), 75 + ); 76 + 77 + const blockSize = 8; 78 + const remainder = privateSectionWithoutPadding.length % blockSize; 79 + const padLen = remainder === 0 ? 0 : blockSize - remainder; 80 + 81 + const padding = new Uint8Array(padLen); 82 + for (let i = 0; i < padLen; i++) padding[i] = i + 1; 83 + 84 + const privateSection = concatBytes(privateSectionWithoutPadding, padding); 85 + 86 + const opensshKey = concatBytes( 87 + text("openssh-key-v1\0"), 88 + sshString(text("none")), 89 + sshString(text("none")), 90 + sshString(new Uint8Array()), 91 + u32(1), 92 + sshString(publicBlob), 93 + sshString(privateSection), 94 + ); 95 + 96 + return wrapPem("OPENSSH PRIVATE KEY", opensshKey); 97 + } 98 + 99 + export async function generateEd25519SSHKeyPair( 100 + comment = "user@browser", 101 + ): Promise<SSHKeyPair> { 102 + await sodium.ready; 103 + 104 + const seed = new Uint8Array(32); 105 + crypto.getRandomValues(seed); 106 + 107 + const kp = sodium.crypto_sign_seed_keypair(seed); 108 + const publicKey = new Uint8Array(kp.publicKey); 109 + 110 + const publicKeyOpenSSH = publicLineFromPublicKey(publicKey, comment); 111 + const privateKeyOpenSSH = buildOpenSSHEd25519PrivateKey( 112 + publicKey, 113 + seed, 114 + comment, 115 + ); 116 + 117 + return { 118 + publicKey: publicKeyOpenSSH, 119 + privateKey: privateKeyOpenSSH, 120 + seedBase64: sodium.to_base64(seed, sodium.base64_variants.ORIGINAL), 121 + }; 122 + }
+5
apps/cli/src/types/file.ts
··· 1 + export type File = { 2 + id: string; 3 + path: string; 4 + createdAt: string; 5 + };
+5
apps/cli/src/types/secret.ts
··· 1 + export type Secret = { 2 + id: string; 3 + name: string; 4 + createdAt: string; 5 + };
+6
apps/cli/src/types/sshkeys.ts
··· 1 + export type SshKeys = { 2 + id: string; 3 + privateKey: string; 4 + publicKey: string; 5 + createdAt: string; 6 + };
+5
apps/cli/src/types/tailscale-auth-key.ts
··· 1 + export type TailscaleAuthKey = { 2 + id: string; 3 + authKey: string; 4 + createdAt: string; 5 + };
+6
apps/cli/src/types/variable.ts
··· 1 + export type Variable = { 2 + id: string; 3 + name: string; 4 + value: string; 5 + createdAt: string; 6 + };
+6
apps/cli/src/types/volume.ts
··· 1 + export type Volume = { 2 + id: string; 3 + name: string; 4 + path: string; 5 + createdAt: string; 6 + };