Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"generate:icons": "bun run scripts/generate-file-icons.ts",
"bundle:cli": "bun run scripts/build-bundled-cli.ts",
"predev": "cross-env NODE_ENV=development bun run clean:dev && bun run generate:icons && bun run bundle:cli && cross-env NODE_ENV=development bun run scripts/clean-launch-services.ts && cross-env NODE_ENV=development bun run scripts/patch-dev-protocol.ts",
"dev": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=8192 electron-vite dev --watch",
"dev": "bun scripts/dev.ts",
"compile:app": "cross-env NODE_OPTIONS=--max-old-space-size=8192 electron-vite build && bun run bundle:cli && bun run scripts/check-pty-daemon-bundle.ts",
"check:pty-daemon-bundle": "bun run scripts/check-pty-daemon-bundle.ts",
"profile:terminal-flood": "bun run scripts/cdp-terminal-flood-profile.ts",
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/scripts/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// `bun dev` runs this on a laptop and inside a cloud sandbox alike. The one
// thing Electron forces apart is root: it refuses to start as root unless
// `--no-sandbox` is on its command line (an env var arrives too late), and the
// sandbox runs everything as root. electron-vite's `--noSandbox` puts it there.
const asRoot = process.platform === "linux" && process.getuid?.() === 0;
const child = Bun.spawn(
[
"electron-vite",
"dev",
"--watch",
...(asRoot ? ["--noSandbox"] : []),
...process.argv.slice(2),
],
{
stdio: ["inherit", "inherit", "inherit"],
env: {
...process.env,
NODE_ENV: "development",
NODE_OPTIONS: "--max-old-space-size=8192",
},
},
);
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
process.on(signal, () => child.kill(signal));
}
process.exit(await child.exited);
19 changes: 19 additions & 0 deletions docs/cloud-sandbox-mismatches.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,25 @@ not the product (which is how a 5s path got reported here as 40s), and most of
the weight is packages host-service imports at module load and never calls, so
the lever is that import graph rather than anything about sandboxes.

**The writable disk is half of memory, and there is no disk-size parameter.**
Documented, not a quirk: "Blaxel sandboxes reserve, when possible, approximately
50% of the available memory for the tmpfs filesystem" (Sandboxes → Overview,
"Memory and filesystem"). The root is an overlay over a read-only EROFS image
with a tmpfs upper layer, so 8 GB of memory gives 3.9 GB of disk, 16 GB gives
7.9 GB, 32 GB gives 16 GB (and 8 CPUs), and every file written also occupies
RAM. The `storageMb` our code used to pass was never a Blaxel field — the SDK
has `memory`, `region`, `ttl`, `expires` and `volumes`, nothing for disk — and
the `as never` cast let it through unnoticed (measured: 40960 still gave
3.9 GB). `/bl` is the provider's control mount, not storage. A checkout plus
`bun install` of this monorepo is ~6 GB, so a golden that carries
`node_modules` and expects to run the dev stack needs 32 GB of memory, and
every fork inherits that size along with the files. Filling the disk is what a
"wedged" sandbox looks like: every `process.exec`, even `echo`, returns
`status: failed` with empty logs from then on. The documented ways to get more
space are volumes (one per sandbox, attached at creation, not forkable) and
Agent Drive; neither fits the golden-and-fork model, which is why memory is the
lever.

**host-service has no HTTP health route.** Readiness is the `health.check` tRPC
procedure; `GET /health` 404s. A probe on the wrong path looks exactly like a
sandbox that never came up, which cost an afternoon here.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"db:seed-dev": "NODE_ENV=development bun run packages/auth/src/seed-dev.ts",
"db:generate:desktop": "bun run --cwd packages/local-db generate",
"db:seed-shared-environments": "bun run scripts/sandbox/seed-environments.ts",
"sandbox:smoke": "bun packages/trpc/scripts/sandbox-smoke.ts"
"sandbox:smoke": "bun packages/trpc/scripts/sandbox-smoke.ts",
"sandbox:release": "bun scripts/sandbox/release-environments.ts"
},
"type": "module",
"workspaces": [
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/environment-secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export function validateSecretKey(
export function validateSecretValue(
value: string,
): { valid: true } | { valid: false; error: string } {
if (Buffer.byteLength(value) > MAX_VALUE_SIZE)
// TextEncoder, not Buffer: this runs in the desktop renderer, which has no
// Node globals, and a ReferenceError here silently killed every save.
if (new TextEncoder().encode(value).byteLength > MAX_VALUE_SIZE)
return {
valid: false,
error: `Value must be <= ${MAX_VALUE_SIZE / 1024}KB`,
Expand Down
16 changes: 14 additions & 2 deletions packages/trpc/scripts/sandbox-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
*
* bun run sandbox:smoke # from the repo root; reads .env
* bun run sandbox:smoke -- --keep # leave the sandbox up for poking at
* SMOKE_SOURCE_KIND=fork SMOKE_SOURCE_REF=env-internal-… bun run sandbox:smoke
* # provision from a golden, as the internal
* # environment does; also checks dependencies
*
* Skips the API's own wrapper on purpose: the workspace row, the QStash
* delivery and the GitHub App token mint. The mint needs the production App
Expand All @@ -27,6 +30,8 @@ import {
const REPO_URL = "https://github.com/superset-sh/superset.git";
const BRANCH = process.env.SMOKE_BRANCH ?? "main";
const KEEP = process.argv.includes("--keep");
const SOURCE_KIND = process.env.SMOKE_SOURCE_KIND === "fork" ? "fork" : "image";
const SOURCE_REF = process.env.SMOKE_SOURCE_REF ?? SANDBOX_IMAGE_NAME;
const HEALTH_ATTEMPTS = 40;
const VNC_TIMEOUT_MS = 30_000;

Expand Down Expand Up @@ -70,8 +75,8 @@ try {
environment: {
id: "smoke",
provider: "blaxel",
sourceKind: "image",
sourceRef: SANDBOX_IMAGE_NAME,
sourceKind: SOURCE_KIND,
sourceRef: SOURCE_REF,
envs: {},
},
workspaceEnv: {
Expand Down Expand Up @@ -118,6 +123,7 @@ try {
`git -C ${SANDBOX_WORKSPACE_PATH} remote get-url origin`,
`git -C ${SANDBOX_WORKSPACE_PATH} rev-parse --abbrev-ref HEAD`,
"test -f /data/.workspace-bootstrapped && echo bootstrapped",
`test -d ${SANDBOX_WORKSPACE_PATH}/node_modules && echo deps`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Adding the node_modules probe shifts the inspect output indices that the display check's detail message relies on. lines.slice(3).join(" ") now starts at the new deps marker instead of the xvfb/x11vnc lines, so on a display failure the printed detail no longer reliably shows the missing display process. Adjust the slice offset or filter the detail to the xvfb/x11vnc markers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/trpc/scripts/sandbox-smoke.ts, line 126:

<comment>Adding the node_modules probe shifts the inspect output indices that the display check's detail message relies on. `lines.slice(3).join(" ")` now starts at the new `deps` marker instead of the xvfb/x11vnc lines, so on a display failure the printed detail no longer reliably shows the missing display process. Adjust the slice offset or filter the detail to the xvfb/x11vnc markers.</comment>

<file context>
@@ -118,6 +123,7 @@ try {
 		`git -C ${SANDBOX_WORKSPACE_PATH} remote get-url origin`,
 		`git -C ${SANDBOX_WORKSPACE_PATH} rev-parse --abbrev-ref HEAD`,
 		"test -f /data/.workspace-bootstrapped && echo bootstrapped",
+		`test -d ${SANDBOX_WORKSPACE_PATH}/node_modules && echo deps`,
 		"pgrep -x Xvfb >/dev/null && echo xvfb",
 		"pgrep -x x11vnc >/dev/null && echo x11vnc",
</file context>

"pgrep -x Xvfb >/dev/null && echo xvfb",
"pgrep -x x11vnc >/dev/null && echo x11vnc",
].join("; ");
Expand All @@ -133,6 +139,12 @@ try {
check("repo", lines[0] === REPO_URL, lines[0] ?? "(no origin)");
check("branch", lines[1] === BRANCH, lines[1] ?? "(none)");
check("bootstrap", lines.includes("bootstrapped"), "marker present");
if (SOURCE_KIND === "fork")
check("dependencies", lines.includes("deps"), "node_modules present");
else
console.log(
`${at()} info dependencies: ${lines.includes("deps") ? "present" : "absent (base image bakes none)"}`,
);
check(
"display",
lines.includes("xvfb") && lines.includes("x11vnc"),
Expand Down
5 changes: 2 additions & 3 deletions packages/trpc/src/lib/blaxel/blaxel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,9 @@ export async function provisionSandbox(args: {
: await SandboxInstance.createIfNotExists({
name: args.name,
image: args.environment.sourceRef,
// The writable root is tmpfs sized at half of this; there is no
// separate disk (docs/cloud-sandbox-mismatches.md).
memory: memoryMb,
// Without disk-backed root the writable layer is tmpfs in RAM, and a
// checkout plus node_modules is write-heavy enough to exhaust it.
storageMb: 20480,
ports: [{ target: HOST_SERVICE_PORT, protocol: "HTTP" }],
region,
envs,
Expand Down
22 changes: 17 additions & 5 deletions packages/trpc/src/lib/blaxel/clone-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,21 @@ export async function resolveCloneTarget(
return { cloneUrl, token: null, defaultBranch: repo.defaultBranch };
}

const octokit = await installationOctokit(installation.installationId);
const { token } = (await octokit.auth({ type: "installation" })) as {
token: string;
};
return { cloneUrl, token, defaultBranch: repo.defaultBranch };
try {
const octokit = await installationOctokit(installation.installationId);
const { token } = (await octokit.auth({ type: "installation" })) as {
token: string;
};
return { cloneUrl, token, defaultBranch: repo.defaultBranch };
} catch (error) {
// A public repo clones without a token, so a mint that fails (an App
// that isn't installed here, a stale installation) shouldn't fail the
// workspace. Private repos still need the token, so they still throw.
if (row.isPrivate) throw error;
console.warn(
`[cloud-workspace] GitHub App token mint failed for public ${repo.owner}/${repo.name}; cloning without one`,
error instanceof Error ? error.message : error,
);
return { cloneUrl, token: null, defaultBranch: repo.defaultBranch };
}
}
64 changes: 0 additions & 64 deletions scripts/sandbox/build-internal-environment.ts

This file was deleted.

7 changes: 6 additions & 1 deletion scripts/sandbox/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ function pinnedVersion(dep: string): string {
return version;
}

const BUN_VERSION = "1.4.0";
// The repo pins bun once, in .bun-version; a sandbox on any other version
// rejects the frozen lockfile and every dependency install fails.
const BUN_VERSION = readFileSync(
join(import.meta.dir, "..", "..", ".bun-version"),
"utf8",
).trim();

const AGENT_CLI_VERSIONS = {
claudeCode: "2.1.257",
Expand Down
Loading
Loading