@blaxel/core 0.3.31 → 0.3.32-preview.330

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,8 +24,8 @@ function missingCredentialsMessage() {
24
24
  return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
25
25
  }
26
26
  // Build info - these placeholders are replaced at build time by build:replace-imports
27
- const BUILD_VERSION = "0.3.31";
28
- const BUILD_COMMIT = "e08b96df13cebc750a05401ee4d824d42f37a191";
27
+ const BUILD_VERSION = "0.3.32-preview.330";
28
+ const BUILD_COMMIT = "4461bded7579866abc4dcbb166189e2ed15a7ea6";
29
29
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
30
30
  const BLAXEL_API_VERSION = "2026-09-22";
31
31
  // Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
@@ -0,0 +1,56 @@
1
+ // The control plane masks every env value it reads back, so values can only be compared when it did not.
2
+ const MASKED_ENV_VALUE = "****";
3
+ // The control plane keeps the image reference as submitted: "app" and "app:latest" are the same image.
4
+ function withTag(image) {
5
+ if (image.includes("@"))
6
+ return image;
7
+ return image.indexOf(":", image.lastIndexOf("/") + 1) === -1 ? `${image}:latest` : image;
8
+ }
9
+ function envDrift(requested, existing) {
10
+ const byName = new Map(existing.map((env) => [env.name, env]));
11
+ const missing = [];
12
+ const changed = [];
13
+ for (const env of requested) {
14
+ const current = byName.get(env.name);
15
+ if (!current)
16
+ missing.push(env.name ?? "");
17
+ else if (current.value !== undefined && current.value !== MASKED_ENV_VALUE && current.value !== env.value)
18
+ changed.push(env.name ?? "");
19
+ }
20
+ // Names only: env values are often secrets and never go into a log line.
21
+ const parts = [
22
+ missing.length ? `not set on the existing sandbox: ${missing.join(", ")}` : "",
23
+ changed.length ? `different value: ${changed.join(", ")}` : "",
24
+ ].filter(Boolean);
25
+ return parts.length ? `envs (${parts.join("; ")})` : undefined;
26
+ }
27
+ /**
28
+ * Describes how an existing sandbox differs from the configuration that was
29
+ * asked for, one entry per field (image, memory, region, envs). Empty when
30
+ * nothing differs. A field the existing sandbox does not report is not drift.
31
+ */
32
+ export function describeDrift(requested, existing) {
33
+ const drift = [];
34
+ const runtime = existing.spec?.runtime;
35
+ if (requested.image && runtime?.image && withTag(requested.image) !== withTag(runtime.image)) {
36
+ drift.push(`image (requested ${requested.image}, existing ${runtime.image})`);
37
+ }
38
+ if (requested.memory !== undefined && runtime?.memory !== undefined && requested.memory !== runtime.memory) {
39
+ drift.push(`memory (requested ${requested.memory} MB, existing ${runtime.memory} MB)`);
40
+ }
41
+ if (requested.region && existing.spec?.region && requested.region !== existing.spec.region) {
42
+ drift.push(`region (requested ${requested.region}, existing ${existing.spec.region})`);
43
+ }
44
+ if (requested.envs?.length && runtime?.envs) {
45
+ const envs = envDrift(requested.envs, runtime.envs);
46
+ if (envs)
47
+ drift.push(envs);
48
+ }
49
+ return drift;
50
+ }
51
+ /** The single warning createIfNotExists logs when the sandbox it returned is not the one that was asked for. */
52
+ export function driftWarning(name, drift) {
53
+ return (`SandboxInstance.createIfNotExists: sandbox ${name ? `"${name}" ` : ""}already exists, so it was returned as is and the requested configuration was not applied. ` +
54
+ `It differs on: ${drift.join("; ")}. ` +
55
+ "Delete the sandbox and create it again to apply the new configuration.");
56
+ }
@@ -5,6 +5,7 @@ import { createPaginatedList } from "../common/pagination.js";
5
5
  import { settings } from "../common/settings.js";
6
6
  import { SandboxCodegen } from "./codegen/index.js";
7
7
  import { SandboxDrive } from "./drive/index.js";
8
+ import { describeDrift, driftWarning } from "./drift.js";
8
9
  import { SandboxFileSystem } from "./filesystem/index.js";
9
10
  import { SandboxNetwork } from "./network/index.js";
10
11
  import { SandboxPreviews } from "./preview.js";
@@ -299,6 +300,9 @@ export class SandboxInstance {
299
300
  // creations become eligible for warm sandbox pools (ENG-3931).
300
301
  const defaultImage = `blaxel/base-image:latest`;
301
302
  const defaultMemory = 4096;
303
+ // What the caller asked for, read before the defaults below fill the gaps:
304
+ // createIfNotExists compares only these with the sandbox it gets back.
305
+ let requested = {};
302
306
  // Handle SandboxCreateConfiguration or simple dict with name/image/memory/ports/envs/volumes keys
303
307
  if (!sandbox ||
304
308
  'name' in sandbox ||
@@ -316,6 +320,7 @@ export class SandboxInstance {
316
320
  'standbyGracePeriodSeconds' in sandbox) {
317
321
  if (!sandbox)
318
322
  sandbox = {};
323
+ requested = { image: sandbox.image, memory: sandbox.memory, region: sandbox.region, envs: normalizeEnvs(sandbox.envs) };
319
324
  if (!sandbox.image)
320
325
  sandbox.image = defaultImage;
321
326
  if (!sandbox.memory)
@@ -362,6 +367,10 @@ export class SandboxInstance {
362
367
  sandbox.spec.runtime.expires = expires.toISOString();
363
368
  }
364
369
  }
370
+ else {
371
+ const { region, runtime } = sandbox.spec ?? {};
372
+ requested = { image: runtime?.image, memory: runtime?.memory, region, envs: runtime?.envs };
373
+ }
365
374
  sandbox = sandbox;
366
375
  if (!sandbox.metadata) {
367
376
  // Leave name unset so the server assigns one (ENG-3931).
@@ -404,6 +413,8 @@ export class SandboxInstance {
404
413
  throw createResult.error;
405
414
  }
406
415
  }
416
+ if (createIfNotExist)
417
+ SandboxInstance.warnOnDrift(requested, data);
407
418
  // Inject the H2 session into the config so subsystems can use it
408
419
  const config = { ...data, h2Session, h2Domain: settings.disableH2 ? null : edgeDomain };
409
420
  const instance = new SandboxInstance(config);
@@ -641,6 +652,21 @@ export class SandboxInstance {
641
652
  static async createIfNotExists(sandbox, options = {}) {
642
653
  return this.create(sandbox, { ...options, createIfNotExist: true });
643
654
  }
655
+ // createIfNotExists hands back the sandbox already holding the name, whatever
656
+ // it was created with. Say so when it is not what was asked for; this only
657
+ // logs and never throws or changes what is returned.
658
+ static warnOnDrift(requested, existing) {
659
+ if (!existing)
660
+ return;
661
+ try {
662
+ const drift = describeDrift(requested, existing);
663
+ if (drift.length)
664
+ console.warn(driftWarning(existing.metadata?.name, drift));
665
+ }
666
+ catch {
667
+ // Best-effort diagnostics: a failed comparison must not fail the create.
668
+ }
669
+ }
644
670
  // Poll the record after a create was cut by the edge with a 504 while the
645
671
  // control plane is still deploying it. Resolves with the sandbox once it
646
672
  // reaches DEPLOYED; throws on FAILED or once the wait budget is spent.