@blaxel/core 0.3.26-preview.314 → 0.3.26

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.26-preview.314";
28
- const BUILD_COMMIT = "2df08257850abd6854feb6b7c412bd74c06f3592";
27
+ const BUILD_VERSION = "0.3.26";
28
+ const BUILD_COMMIT = "18c2c73526c47b9ebc09aa3e16497b19854a02d0";
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
@@ -11,6 +11,37 @@ function ensureNodeEnvironment() {
11
11
  }
12
12
  export const SANDBOX_API_IMAGE = "ghcr.io/blaxel-ai/sandbox";
13
13
  export const SANDBOX_API_PATH = "/usr/local/bin/sandbox-api";
14
+ // Build settings travel as sandbox labels. The control plane signs the allowed
15
+ // ones into the upload URL, and the upload must send them back as matching
16
+ // x-amz-meta-* headers or storage rejects it.
17
+ const BUILD_UPLOAD_LABELS = [
18
+ "x-blaxel-builder",
19
+ "x-blaxel-experimental",
20
+ "x-blaxel-build-memory",
21
+ "x-blaxel-build-volume",
22
+ "x-blaxel-build-region",
23
+ "x-blaxel-build-cache",
24
+ ];
25
+ const MAX_BUILD_UPLOAD_LABEL_VALUE = 64;
26
+ function buildSizeLabel(option, value, min, max) {
27
+ if (value === undefined) {
28
+ return undefined;
29
+ }
30
+ if (!Number.isInteger(value) || value < min || value > max) {
31
+ throw new Error(`${option} must be an integer between ${min} and ${max} MiB`);
32
+ }
33
+ return String(value);
34
+ }
35
+ function buildUploadMetadata(labels) {
36
+ const metadata = {};
37
+ for (const name of BUILD_UPLOAD_LABELS) {
38
+ const value = labels?.[name];
39
+ if (typeof value === "string" && value !== "" && value.length <= MAX_BUILD_UPLOAD_LABEL_VALUE) {
40
+ metadata[name] = value;
41
+ }
42
+ }
43
+ return metadata;
44
+ }
14
45
  function generateDockerfile(context) {
15
46
  // Build the raw Dockerfile content
16
47
  const lines = [`FROM ${context.baseImage}`];
@@ -616,9 +647,10 @@ export class ImageInstance {
616
647
  void archive.finalize();
617
648
  });
618
649
  }
619
- _createSandboxPayload(name, memory = 4096) {
650
+ _createSandboxPayload(name, memory = 4096, buildLabels = {}) {
620
651
  const labels = {
621
652
  "x-blaxel-auto-generated": "true",
653
+ ...buildLabels,
622
654
  };
623
655
  const metadata = {
624
656
  name,
@@ -660,12 +692,16 @@ export class ImageInstance {
660
692
  const uploadUrl = response.headers.get("x-blaxel-upload-url");
661
693
  return { response, uploadUrl };
662
694
  }
663
- async _uploadZip(uploadUrl, zipContent) {
695
+ async _uploadZip(uploadUrl, zipContent, metadata = {}) {
696
+ const headers = {
697
+ "Content-Type": "application/zip",
698
+ };
699
+ for (const [name, value] of Object.entries(metadata)) {
700
+ headers[`x-amz-meta-${name}`] = value;
701
+ }
664
702
  const response = await fetch(uploadUrl, {
665
703
  method: "PUT",
666
- headers: {
667
- "Content-Type": "application/zip",
668
- },
704
+ headers,
669
705
  body: zipContent,
670
706
  });
671
707
  if (response.status >= 400) {
@@ -736,6 +772,16 @@ export class ImageInstance {
736
772
  */
737
773
  async build(options) {
738
774
  const { name, memory = 4096, timeout = 900000, onStatusChange, sandboxVersion = "latest" } = options;
775
+ const buildLabels = {};
776
+ const buildMemory = buildSizeLabel("buildMemoryMb", options.buildMemoryMb, 1, 32768);
777
+ if (buildMemory !== undefined) {
778
+ buildLabels["x-blaxel-build-memory"] = buildMemory;
779
+ }
780
+ // Keep an explicit 0: it asks for memory-backed scratch instead of the default.
781
+ const buildVolume = buildSizeLabel("buildVolumeMb", options.buildVolumeMb, 0, 131072);
782
+ if (buildVolume !== undefined) {
783
+ buildLabels["x-blaxel-build-volume"] = buildVolume;
784
+ }
739
785
  // Prepare image for sandbox deployment (add sandbox-api and entrypoint)
740
786
  const preparedImage = this._prepareForSandbox(sandboxVersion);
741
787
  // Write the image folder
@@ -744,7 +790,7 @@ export class ImageInstance {
744
790
  // Create zip
745
791
  const zipContent = await this._createZip(buildDir);
746
792
  // Create sandbox payload
747
- const sandboxPayload = this._createSandboxPayload(name, memory);
793
+ const sandboxPayload = this._createSandboxPayload(name, memory, buildLabels);
748
794
  // Create/update sandbox and get upload URL
749
795
  const { response, uploadUrl } = await this._createSandboxWithUpload(sandboxPayload);
750
796
  if (response.status >= 400) {
@@ -754,8 +800,18 @@ export class ImageInstance {
754
800
  if (!uploadUrl) {
755
801
  throw new Error("No upload URL returned from API");
756
802
  }
803
+ // Sign-matching headers come from the labels the control plane stored,
804
+ // which can include build settings already on an existing sandbox.
805
+ let storedLabels = sandboxPayload.metadata?.labels;
806
+ try {
807
+ const created = (await response.json());
808
+ storedLabels = created.metadata?.labels ?? storedLabels;
809
+ }
810
+ catch {
811
+ // Fall back to the labels we sent
812
+ }
757
813
  // Upload the zip
758
- await this._uploadZip(uploadUrl, zipContent);
814
+ await this._uploadZip(uploadUrl, zipContent, buildUploadMetadata(storedLabels));
759
815
  // Wait for deployment to complete
760
816
  await this._waitForDeployment(name, timeout, 3000, onStatusChange);
761
817
  // Get the final sandbox state
@@ -4,7 +4,8 @@
4
4
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
5
5
  import { tmpdir } from "os";
6
6
  import { join } from "path";
7
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
+ import { settings } from "../common/settings.js";
8
9
  import { ImageInstance } from "./image.js";
9
10
  // Test fixtures
10
11
  let tempDir;
@@ -541,3 +542,76 @@ describe("Dockerfile instruction order", () => {
541
542
  expect(runIdx).toBeLessThan(thirdIdx);
542
543
  });
543
544
  });
545
+ describe("ImageInstance.build worker resources", () => {
546
+ async function buildWith(options, storedLabels) {
547
+ const calls = [];
548
+ vi.spyOn(settings, "authenticate").mockResolvedValue(undefined);
549
+ vi.spyOn(settings, "headers", "get").mockReturnValue({});
550
+ vi.spyOn(settings, "baseUrl", "get").mockReturnValue("https://api.test");
551
+ vi.stubGlobal("fetch", (url, init = {}) => {
552
+ const headers = init.headers;
553
+ const body = typeof init.body === "string" ? JSON.parse(init.body) : undefined;
554
+ calls.push({ url, method: init.method ?? "GET", headers, body });
555
+ if (url.includes("upload=true")) {
556
+ const sent = body.metadata?.labels;
557
+ return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img", labels: storedLabels ?? sent } }), {
558
+ status: 200,
559
+ headers: { "x-blaxel-upload-url": "https://upload.test/object" },
560
+ }));
561
+ }
562
+ if (url.startsWith("https://upload.test")) {
563
+ return Promise.resolve(new Response("", { status: 200 }));
564
+ }
565
+ return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img" }, status: "DEPLOYED" }), { status: 200 }));
566
+ });
567
+ const image = ImageInstance.fromRegistry("alpine:3.20");
568
+ // @ts-expect-error - private method
569
+ vi.spyOn(image, "_prepareForSandbox").mockReturnValue(image);
570
+ // @ts-expect-error - private method
571
+ vi.spyOn(image, "_waitForDeployment").mockResolvedValue("DEPLOYED");
572
+ await image.build({ name: "img", ...options });
573
+ return {
574
+ create: calls.find((call) => call.url.includes("upload=true")),
575
+ upload: calls.find((call) => call.url.startsWith("https://upload.test")),
576
+ };
577
+ }
578
+ afterEach(() => {
579
+ vi.restoreAllMocks();
580
+ vi.unstubAllGlobals();
581
+ });
582
+ it("sends build memory and volume as labels and signed upload headers", async () => {
583
+ const { create, upload } = await buildWith({ buildMemoryMb: 16384, buildVolumeMb: 32768 });
584
+ const labels = create.body.metadata?.labels;
585
+ expect(labels?.["x-blaxel-build-memory"]).toBe("16384");
586
+ expect(labels?.["x-blaxel-build-volume"]).toBe("32768");
587
+ expect(create.body.spec?.runtime?.memory).toBe(4096);
588
+ expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBe("16384");
589
+ expect(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("32768");
590
+ expect(upload.headers["x-amz-meta-x-blaxel-auto-generated"]).toBeUndefined();
591
+ });
592
+ it("keeps an explicit zero volume", async () => {
593
+ const { upload } = await buildWith({ buildVolumeMb: 0 });
594
+ expect(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("0");
595
+ expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBeUndefined();
596
+ });
597
+ it("sends no build headers by default", async () => {
598
+ const { upload } = await buildWith({});
599
+ expect(Object.keys(upload.headers).filter((name) => name.startsWith("x-amz-meta-"))).toEqual([]);
600
+ });
601
+ it("mirrors build labels the control plane already stored", async () => {
602
+ const { upload } = await buildWith({}, { "x-blaxel-auto-generated": "true", "x-blaxel-build-memory": "8192" });
603
+ expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBe("8192");
604
+ });
605
+ it.each([
606
+ [{ buildMemoryMb: 0 }, "buildMemoryMb"],
607
+ [{ buildMemoryMb: 32769 }, "buildMemoryMb"],
608
+ [{ buildMemoryMb: 1.5 }, "buildMemoryMb"],
609
+ [{ buildVolumeMb: -1 }, "buildVolumeMb"],
610
+ [{ buildVolumeMb: 131073 }, "buildVolumeMb"],
611
+ ])("rejects out-of-range %o before any request", async (options, option) => {
612
+ const fetchSpy = vi.fn();
613
+ vi.stubGlobal("fetch", fetchSpy);
614
+ await expect(ImageInstance.fromRegistry("alpine:3.20").build({ name: "img", ...options })).rejects.toThrow(option);
615
+ expect(fetchSpy).not.toHaveBeenCalled();
616
+ });
617
+ });