tensorlake 0.5.10 → 0.5.11

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.
@@ -61,7 +61,7 @@ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TI
61
61
  var init_defaults = __esm({
62
62
  "src/defaults.ts"() {
63
63
  "use strict";
64
- SDK_VERSION = "0.5.10";
64
+ SDK_VERSION = "0.5.11";
65
65
  API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
66
66
  API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
67
67
  NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
@@ -4293,6 +4293,7 @@ var init_image = __esm({
4293
4293
 
4294
4294
  // src/sandbox-image.ts
4295
4295
  import { readFile, readdir, stat } from "fs/promises";
4296
+ import { homedir } from "os";
4296
4297
  import path from "path";
4297
4298
  import { parseArgs } from "util";
4298
4299
  function defaultRegisteredName(dockerfilePath) {
@@ -4423,12 +4424,6 @@ function shellSplit(input) {
4423
4424
  }
4424
4425
  return tokens;
4425
4426
  }
4426
- function shellQuote(value) {
4427
- if (!value) {
4428
- return "''";
4429
- }
4430
- return `'${value.replace(/'/g, `'\\''`)}'`;
4431
- }
4432
4427
  function stripLeadingFlags(value) {
4433
4428
  const flags = {};
4434
4429
  let remaining = value.trimStart();
@@ -4466,75 +4461,6 @@ function parseFromValue(value, lineNumber) {
4466
4461
  }
4467
4462
  return tokens[0];
4468
4463
  }
4469
- function parseCopyLikeValues(value, lineNumber, keyword) {
4470
- const { flags, remaining } = stripLeadingFlags(value);
4471
- if ("from" in flags) {
4472
- throw new Error(
4473
- `line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
4474
- );
4475
- }
4476
- const payload = remaining.trim();
4477
- if (!payload) {
4478
- throw new Error(
4479
- `line ${lineNumber}: ${keyword} must include source and destination`
4480
- );
4481
- }
4482
- let parts;
4483
- if (payload.startsWith("[")) {
4484
- let parsed;
4485
- try {
4486
- parsed = JSON.parse(payload);
4487
- } catch (error) {
4488
- throw new Error(
4489
- `line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
4490
- );
4491
- }
4492
- if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
4493
- throw new Error(
4494
- `line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
4495
- );
4496
- }
4497
- parts = parsed;
4498
- } else {
4499
- parts = shellSplit(payload);
4500
- if (parts.length < 2) {
4501
- throw new Error(
4502
- `line ${lineNumber}: ${keyword} must include at least one source and one destination`
4503
- );
4504
- }
4505
- }
4506
- return {
4507
- flags,
4508
- sources: parts.slice(0, -1),
4509
- destination: parts[parts.length - 1]
4510
- };
4511
- }
4512
- function parseEnvPairs(value, lineNumber) {
4513
- const tokens = shellSplit(value);
4514
- if (tokens.length === 0) {
4515
- throw new Error(`line ${lineNumber}: ENV must include a key and value`);
4516
- }
4517
- if (tokens.every((token) => token.includes("="))) {
4518
- return tokens.map((token) => {
4519
- const [key, envValue] = token.split(/=(.*)/s, 2);
4520
- if (!key) {
4521
- throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
4522
- }
4523
- return [key, envValue];
4524
- });
4525
- }
4526
- if (tokens.length < 2) {
4527
- throw new Error(`line ${lineNumber}: ENV must include a key and value`);
4528
- }
4529
- return [[tokens[0], tokens.slice(1).join(" ")]];
4530
- }
4531
- function resolveContainerPath(containerPath, workingDir) {
4532
- if (!containerPath) {
4533
- return workingDir;
4534
- }
4535
- const normalized = containerPath.startsWith("/") ? path.posix.normalize(containerPath) : path.posix.normalize(path.posix.join(workingDir, containerPath));
4536
- return normalized.startsWith("/") ? normalized : `/${normalized}`;
4537
- }
4538
4464
  function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
4539
4465
  let baseImage;
4540
4466
  const instructions = [];
@@ -4645,14 +4571,292 @@ function buildContextFromEnv() {
4645
4571
  };
4646
4572
  }
4647
4573
  function createDefaultClient(context) {
4574
+ const useScopeHeaders = context.personalAccessToken != null && context.apiKey == null;
4648
4575
  return new SandboxClient({
4649
4576
  apiUrl: context.apiUrl,
4650
4577
  apiKey: context.apiKey ?? context.personalAccessToken,
4651
- organizationId: context.organizationId,
4652
- projectId: context.projectId,
4578
+ organizationId: useScopeHeaders ? context.organizationId : void 0,
4579
+ projectId: useScopeHeaders ? context.projectId : void 0,
4653
4580
  namespace: context.namespace
4654
4581
  });
4655
4582
  }
4583
+ function baseApiUrl(context) {
4584
+ return context.apiUrl.replace(/\/+$/, "");
4585
+ }
4586
+ function scopedBuildsPath(context) {
4587
+ return `/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-template-builds`;
4588
+ }
4589
+ function platformHeaders(context) {
4590
+ const headers = {
4591
+ Authorization: `Bearer ${context.bearerToken}`,
4592
+ "Content-Type": "application/json"
4593
+ };
4594
+ if (context.useScopeHeaders) {
4595
+ headers["X-Forwarded-Organization-Id"] = context.organizationId;
4596
+ headers["X-Forwarded-Project-Id"] = context.projectId;
4597
+ }
4598
+ return headers;
4599
+ }
4600
+ async function requestJson(url, init, errorPrefix) {
4601
+ const response = await fetch(url, init);
4602
+ if (!response.ok) {
4603
+ throw new Error(
4604
+ `${errorPrefix} (HTTP ${response.status}): ${await response.text()}`
4605
+ );
4606
+ }
4607
+ const text = await response.text();
4608
+ return text ? JSON.parse(text) : {};
4609
+ }
4610
+ async function resolveBuildContext(context) {
4611
+ const bearerToken = context.apiKey ?? context.personalAccessToken;
4612
+ if (!bearerToken) {
4613
+ throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
4614
+ }
4615
+ if (context.apiKey) {
4616
+ const scope = await requestJson(
4617
+ `${baseApiUrl(context)}/platform/v1/keys/introspect`,
4618
+ {
4619
+ method: "POST",
4620
+ headers: {
4621
+ Authorization: `Bearer ${bearerToken}`,
4622
+ "Content-Type": "application/json"
4623
+ }
4624
+ },
4625
+ "API key introspection failed"
4626
+ );
4627
+ if (!scope.organizationId || !scope.projectId) {
4628
+ throw new Error("API key introspection response is missing organizationId or projectId");
4629
+ }
4630
+ return {
4631
+ ...context,
4632
+ bearerToken,
4633
+ organizationId: scope.organizationId,
4634
+ projectId: scope.projectId,
4635
+ useScopeHeaders: false
4636
+ };
4637
+ }
4638
+ if (!context.organizationId || !context.projectId) {
4639
+ throw new Error(
4640
+ "Personal Access Token authentication requires TENSORLAKE_ORGANIZATION_ID and TENSORLAKE_PROJECT_ID to be set (e.g. via 'tl login && tl init'). To skip this requirement, authenticate with TENSORLAKE_API_KEY instead \u2014 API keys are bound to a single project at creation."
4641
+ );
4642
+ }
4643
+ return {
4644
+ ...context,
4645
+ bearerToken,
4646
+ organizationId: context.organizationId,
4647
+ projectId: context.projectId,
4648
+ useScopeHeaders: true
4649
+ };
4650
+ }
4651
+ async function prepareRootfsBuild(context, plan, isPublic) {
4652
+ if (!plan.baseImage) {
4653
+ throw new Error("Sandbox image builds require a Dockerfile FROM image or Image baseImage");
4654
+ }
4655
+ const spec = await requestJson(
4656
+ `${baseApiUrl(context)}${scopedBuildsPath(context)}`,
4657
+ {
4658
+ method: "POST",
4659
+ headers: platformHeaders(context),
4660
+ body: JSON.stringify({
4661
+ name: plan.registeredName,
4662
+ dockerfile: plan.dockerfileText,
4663
+ baseImage: plan.baseImage,
4664
+ public: isPublic
4665
+ })
4666
+ },
4667
+ "failed to prepare sandbox image build"
4668
+ );
4669
+ return { prepared: parsePreparedBuild(spec), spec };
4670
+ }
4671
+ function parsePreparedBuild(raw) {
4672
+ const builder = raw.builder;
4673
+ if (!builder) {
4674
+ throw new Error("platform API response is missing rootfs builder configuration");
4675
+ }
4676
+ const prepared = {
4677
+ ...raw,
4678
+ buildId: requiredString(raw, "buildId"),
4679
+ snapshotId: requiredString(raw, "snapshotId"),
4680
+ snapshotUri: requiredString(raw, "snapshotUri"),
4681
+ rootfsNodeKind: requiredString(raw, "rootfsNodeKind"),
4682
+ builder: {
4683
+ image: requiredString(builder, "image"),
4684
+ command: requiredString(builder, "command"),
4685
+ cpus: requiredNumber(builder, "cpus"),
4686
+ memoryMb: requiredNumber(builder, "memoryMb"),
4687
+ diskMb: requiredNumber(builder, "diskMb")
4688
+ }
4689
+ };
4690
+ const parent = raw.parent;
4691
+ if (parent != null) {
4692
+ prepared.parent = {
4693
+ parentManifestUri: requiredString(parent, "parentManifestUri"),
4694
+ rootfsDiskBytes: optionalNumber(parent, "rootfsDiskBytes")
4695
+ };
4696
+ }
4697
+ return prepared;
4698
+ }
4699
+ async function completeRootfsBuild(context, buildId, request) {
4700
+ return requestJson(
4701
+ `${baseApiUrl(context)}${scopedBuildsPath(context)}/${encodeURIComponent(buildId)}/complete`,
4702
+ {
4703
+ method: "POST",
4704
+ headers: platformHeaders(context),
4705
+ body: JSON.stringify(request)
4706
+ },
4707
+ "failed to complete sandbox image build"
4708
+ );
4709
+ }
4710
+ async function resolvedDockerConfigJson() {
4711
+ const configDir = process.env.DOCKER_CONFIG ?? path.join(homedir(), ".docker");
4712
+ const configPath = path.join(configDir, "config.json");
4713
+ try {
4714
+ const content = await readFile(configPath, "utf8");
4715
+ const parsed = JSON.parse(content);
4716
+ const auths = parsed.auths;
4717
+ if (auths != null && Object.keys(auths).length > 0) {
4718
+ return JSON.stringify({ auths });
4719
+ }
4720
+ } catch (error) {
4721
+ if (error.code === "ENOENT") {
4722
+ return void 0;
4723
+ }
4724
+ throw error;
4725
+ }
4726
+ return void 0;
4727
+ }
4728
+ function rootfsDiskBytes(diskMb, prepared) {
4729
+ if (diskMb != null) {
4730
+ return diskMb * 1024 * 1024;
4731
+ }
4732
+ if (prepared.parent != null) {
4733
+ if (prepared.parent.rootfsDiskBytes == null) {
4734
+ throw new Error(
4735
+ "platform API did not return parent rootfsDiskBytes for diff build; pass diskMb explicitly or update Platform API"
4736
+ );
4737
+ }
4738
+ return prepared.parent.rootfsDiskBytes;
4739
+ }
4740
+ return DEFAULT_ROOTFS_DISK_MB * 1024 * 1024;
4741
+ }
4742
+ function rootfsDiskBytesToMb(bytes) {
4743
+ return Math.ceil(bytes / (1024 * 1024));
4744
+ }
4745
+ async function buildRootfsSpec(preparedSpec, prepared, plan, diskMb) {
4746
+ const spec = {
4747
+ ...preparedSpec,
4748
+ dockerfile: plan.dockerfileText,
4749
+ contextDir: REMOTE_CONTEXT_DIR,
4750
+ baseImage: plan.baseImage,
4751
+ rootfsDiskBytes: rootfsDiskBytes(diskMb, prepared)
4752
+ };
4753
+ const dockerConfigJson = await resolvedDockerConfigJson();
4754
+ if (dockerConfigJson != null) {
4755
+ spec.dockerConfigJson = dockerConfigJson;
4756
+ }
4757
+ return spec;
4758
+ }
4759
+ function rootfsBuilderExecutable(executable) {
4760
+ return executable === ROOTFS_BUILDER_COMMAND ? `${ROOTFS_BUILDER_BIN_DIR}/${ROOTFS_BUILDER_COMMAND}` : executable;
4761
+ }
4762
+ function rootfsBuilderEnv() {
4763
+ return { PATH: ROOTFS_BUILDER_PATH };
4764
+ }
4765
+ async function runRootfsBuilder(sandbox, command, emit, sleep3) {
4766
+ const parts = shellSplit(command);
4767
+ const [executable, ...commandArgs] = parts;
4768
+ if (!executable) {
4769
+ throw new Error("empty rootfs builder command returned by platform API");
4770
+ }
4771
+ await runStreaming(
4772
+ sandbox,
4773
+ emit,
4774
+ sleep3,
4775
+ rootfsBuilderExecutable(executable),
4776
+ [...commandArgs, "--spec", REMOTE_SPEC_PATH, "--metadata-out", REMOTE_METADATA_PATH],
4777
+ rootfsBuilderEnv(),
4778
+ REMOTE_BUILD_DIR
4779
+ );
4780
+ }
4781
+ function metadataString(metadata, snakeKey, camelKey) {
4782
+ const value = metadata[snakeKey] ?? metadata[camelKey];
4783
+ return typeof value === "string" ? value : void 0;
4784
+ }
4785
+ function metadataNumber(metadata, snakeKey, camelKey) {
4786
+ const value = metadata[snakeKey] ?? metadata[camelKey];
4787
+ if (typeof value === "number") {
4788
+ return value;
4789
+ }
4790
+ if (typeof value === "string" && value.trim()) {
4791
+ const parsed = Number(value);
4792
+ return Number.isFinite(parsed) ? parsed : void 0;
4793
+ }
4794
+ return void 0;
4795
+ }
4796
+ function completeRequestFromMetadata(prepared, metadata) {
4797
+ const rootfsNodeKind = metadataString(metadata, "rootfs_node_kind", "rootfsNodeKind") ?? prepared.rootfsNodeKind;
4798
+ const parentManifestUri = metadataString(metadata, "parent_manifest_uri", "parentManifestUri") ?? (rootfsNodeKind === "diff" ? prepared.parent?.parentManifestUri : void 0);
4799
+ if (rootfsNodeKind === "diff" && parentManifestUri == null) {
4800
+ throw new Error("rootfs diff build completed without parent_manifest_uri");
4801
+ }
4802
+ const snapshotFormatVersion = metadataString(
4803
+ metadata,
4804
+ "snapshot_format_version",
4805
+ "snapshotFormatVersion"
4806
+ );
4807
+ const snapshotSizeBytes = metadataNumber(
4808
+ metadata,
4809
+ "snapshot_size_bytes",
4810
+ "snapshotSizeBytes"
4811
+ );
4812
+ const rootfsDiskBytesValue = metadataNumber(
4813
+ metadata,
4814
+ "rootfs_disk_bytes",
4815
+ "rootfsDiskBytes"
4816
+ );
4817
+ if (!snapshotFormatVersion) {
4818
+ throw new Error("rootfs builder metadata is missing snapshot_format_version");
4819
+ }
4820
+ if (snapshotSizeBytes == null) {
4821
+ throw new Error("rootfs builder metadata is missing numeric snapshot_size_bytes");
4822
+ }
4823
+ if (rootfsDiskBytesValue == null) {
4824
+ throw new Error("rootfs builder metadata is missing numeric rootfs_disk_bytes");
4825
+ }
4826
+ return {
4827
+ snapshotId: metadataString(metadata, "snapshot_id", "snapshotId") ?? prepared.snapshotId,
4828
+ snapshotUri: metadataString(metadata, "snapshot_uri", "snapshotUri") ?? prepared.snapshotUri,
4829
+ snapshotFormatVersion,
4830
+ snapshotSizeBytes,
4831
+ rootfsDiskBytes: rootfsDiskBytesValue,
4832
+ rootfsNodeKind,
4833
+ ...parentManifestUri ? { parentManifestUri } : {}
4834
+ };
4835
+ }
4836
+ function requiredString(object, key) {
4837
+ const value = object[key];
4838
+ if (typeof value !== "string" || value.length === 0) {
4839
+ throw new Error(`expected '${key}' to be a non-empty string`);
4840
+ }
4841
+ return value;
4842
+ }
4843
+ function requiredNumber(object, key) {
4844
+ const value = object[key];
4845
+ if (typeof value !== "number" || !Number.isFinite(value)) {
4846
+ throw new Error(`expected '${key}' to be a finite number`);
4847
+ }
4848
+ return value;
4849
+ }
4850
+ function optionalNumber(object, key) {
4851
+ const value = object[key];
4852
+ if (value == null) {
4853
+ return void 0;
4854
+ }
4855
+ if (typeof value !== "number" || !Number.isFinite(value)) {
4856
+ throw new Error(`expected '${key}' to be a finite number`);
4857
+ }
4858
+ return value;
4859
+ }
4656
4860
  async function runChecked(sandbox, command, args, env, workingDir) {
4657
4861
  const result = await sandbox.run(command, {
4658
4862
  args,
@@ -4712,18 +4916,6 @@ function emitOutputLines(emit, stream, response, seen) {
4712
4916
  emit({ type: "build_log", stream, message: line });
4713
4917
  }
4714
4918
  }
4715
- function isPathWithinContext(contextDir, localPath) {
4716
- const relative = path.relative(contextDir, localPath);
4717
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
4718
- }
4719
- function resolveContextSourcePath(contextDir, source) {
4720
- const resolvedContextDir = path.resolve(contextDir);
4721
- const resolvedSource = path.resolve(resolvedContextDir, source);
4722
- if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
4723
- throw new Error(`Local path escapes the build context: ${source}`);
4724
- }
4725
- return resolvedSource;
4726
- }
4727
4919
  async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
4728
4920
  const fileStats = await stat(localPath).catch(() => null);
4729
4921
  if (!fileStats) {
@@ -4754,168 +4946,7 @@ async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
4754
4946
  }
4755
4947
  }
4756
4948
  }
4757
- async function persistEnvVar(sandbox, processEnv, key, value) {
4758
- const exportLine = `export ${key}=${shellQuote(value)}`;
4759
- await runChecked(
4760
- sandbox,
4761
- "sh",
4762
- ["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
4763
- processEnv
4764
- );
4765
- }
4766
- async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
4767
- const destinationPath = resolveContainerPath(destination, workingDir);
4768
- if (sources.length > 1 && !destinationPath.endsWith("/")) {
4769
- throw new Error(
4770
- `${keyword} with multiple sources requires a directory destination ending in '/'`
4771
- );
4772
- }
4773
- for (const source of sources) {
4774
- const localSource = resolveContextSourcePath(contextDir, source);
4775
- const localStats = await stat(localSource).catch(() => null);
4776
- if (!localStats) {
4777
- throw new Error(`Local path not found: ${localSource}`);
4778
- }
4779
- let remoteDestination = destinationPath;
4780
- if (sources.length > 1) {
4781
- remoteDestination = path.posix.join(
4782
- destinationPath.replace(/\/$/, ""),
4783
- path.posix.basename(source.replace(/\/$/, ""))
4784
- );
4785
- } else if (localStats.isFile() && destinationPath.endsWith("/")) {
4786
- remoteDestination = path.posix.join(
4787
- destinationPath.replace(/\/$/, ""),
4788
- path.basename(source)
4789
- );
4790
- }
4791
- emit({
4792
- type: "status",
4793
- message: `${keyword} ${source} -> ${remoteDestination}`
4794
- });
4795
- await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
4796
- }
4797
- }
4798
- async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep3) {
4799
- let destinationPath = resolveContainerPath(destination, workingDir);
4800
- const parsedUrl = new URL(url);
4801
- const fileName = path.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
4802
- if (destinationPath.endsWith("/")) {
4803
- destinationPath = path.posix.join(destinationPath.replace(/\/$/, ""), fileName);
4804
- }
4805
- const parentDir = path.posix.dirname(destinationPath) || "/";
4806
- emit({
4807
- type: "status",
4808
- message: `ADD ${url} -> ${destinationPath}`
4809
- });
4810
- await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
4811
- await runStreaming(
4812
- sandbox,
4813
- emit,
4814
- sleep3,
4815
- "sh",
4816
- [
4817
- "-c",
4818
- `curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
4819
- ],
4820
- processEnv,
4821
- workingDir
4822
- );
4823
- }
4824
- async function executeDockerfilePlan(sandbox, plan, emit, sleep3) {
4825
- const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
4826
- let workingDir = "/";
4827
- for (const instruction of plan.instructions) {
4828
- const { keyword, value, lineNumber } = instruction;
4829
- if (keyword === "RUN") {
4830
- emit({ type: "status", message: `RUN ${value}` });
4831
- await runStreaming(
4832
- sandbox,
4833
- emit,
4834
- sleep3,
4835
- "sh",
4836
- ["-c", value],
4837
- processEnv,
4838
- workingDir
4839
- );
4840
- continue;
4841
- }
4842
- if (keyword === "WORKDIR") {
4843
- const tokens = shellSplit(value);
4844
- if (tokens.length !== 1) {
4845
- throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
4846
- }
4847
- workingDir = resolveContainerPath(tokens[0], workingDir);
4848
- emit({ type: "status", message: `WORKDIR ${workingDir}` });
4849
- await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
4850
- continue;
4851
- }
4852
- if (keyword === "ENV") {
4853
- for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
4854
- emit({ type: "status", message: `ENV ${key}=${envValue}` });
4855
- processEnv[key] = envValue;
4856
- await persistEnvVar(sandbox, processEnv, key, envValue);
4857
- }
4858
- continue;
4859
- }
4860
- if (keyword === "COPY") {
4861
- const { sources, destination } = parseCopyLikeValues(
4862
- value,
4863
- lineNumber,
4864
- keyword
4865
- );
4866
- await copyFromContext(
4867
- sandbox,
4868
- emit,
4869
- plan.contextDir,
4870
- sources,
4871
- destination,
4872
- workingDir,
4873
- keyword
4874
- );
4875
- continue;
4876
- }
4877
- if (keyword === "ADD") {
4878
- const { sources, destination } = parseCopyLikeValues(
4879
- value,
4880
- lineNumber,
4881
- keyword
4882
- );
4883
- if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
4884
- await addUrlToSandbox(
4885
- sandbox,
4886
- emit,
4887
- sources[0],
4888
- destination,
4889
- workingDir,
4890
- processEnv,
4891
- sleep3
4892
- );
4893
- } else {
4894
- await copyFromContext(
4895
- sandbox,
4896
- emit,
4897
- plan.contextDir,
4898
- sources,
4899
- destination,
4900
- workingDir,
4901
- keyword
4902
- );
4903
- }
4904
- continue;
4905
- }
4906
- if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
4907
- emit({
4908
- type: "warning",
4909
- message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
4910
- });
4911
- continue;
4912
- }
4913
- throw new Error(
4914
- `line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
4915
- );
4916
- }
4917
- }
4918
- async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes, isPublic, snapshotFormatVersion) {
4949
+ async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes2, isPublic, snapshotFormatVersion) {
4919
4950
  const bearerToken = context.apiKey ?? context.personalAccessToken;
4920
4951
  if (!bearerToken) {
4921
4952
  throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
@@ -4949,7 +4980,7 @@ async function registerImage(context, name, dockerfile, snapshotId, snapshotSand
4949
4980
  snapshotUri,
4950
4981
  ...snapshotFormatVersion ? { snapshotFormatVersion } : {},
4951
4982
  snapshotSizeBytes,
4952
- rootfsDiskBytes,
4983
+ rootfsDiskBytes: rootfsDiskBytes2,
4953
4984
  public: isPublic
4954
4985
  })
4955
4986
  });
@@ -4966,67 +4997,71 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4966
4997
  const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4967
4998
  const context = buildContextFromEnv();
4968
4999
  const clientFactory = deps.createClient ?? createDefaultClient;
4969
- const register = deps.registerImage ?? ((...args) => registerImage(...args));
4970
5000
  const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
4971
5001
  emit({ type: "status", message: `Loading ${sourceLabel}...` });
4972
5002
  const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
4973
5003
  emit({
4974
5004
  type: "status",
4975
- message: plan.baseImage == null ? "Starting build sandbox with the default server image..." : `Starting build sandbox from ${plan.baseImage}...`
5005
+ message: `Selected image name: ${plan.registeredName}`
5006
+ });
5007
+ emit({ type: "status", message: "Preparing rootfs build..." });
5008
+ const resolvedContext = await resolveBuildContext(context);
5009
+ const { prepared, spec: preparedSpec } = await prepareRootfsBuild(
5010
+ resolvedContext,
5011
+ plan,
5012
+ options.isPublic ?? false
5013
+ );
5014
+ emit({
5015
+ type: "status",
5016
+ message: prepared.rootfsNodeKind === "diff" ? "Build mode: RootfsDiff" : "Build mode: RootfsBase"
4976
5017
  });
4977
5018
  const client = clientFactory(context);
4978
5019
  let sandbox;
4979
5020
  try {
4980
- sandbox = await client.createAndConnect({
4981
- ...plan.baseImage == null ? {} : { image: plan.baseImage },
4982
- cpus: options.cpus ?? 2,
4983
- memoryMb: options.memoryMb ?? 4096,
4984
- ...options.diskMb != null ? { diskMb: options.diskMb } : {}
4985
- });
5021
+ const outputRootfsDiskBytes = rootfsDiskBytes(options.diskMb, prepared);
5022
+ const builderDiskMb = Math.max(
5023
+ rootfsDiskBytesToMb(outputRootfsDiskBytes),
5024
+ options.builderDiskMb ?? prepared.builder.diskMb
5025
+ );
4986
5026
  emit({
4987
5027
  type: "status",
4988
- message: `Materializing image in sandbox ${sandbox.sandboxId}...`
5028
+ message: `Creating rootfs builder sandbox from ${prepared.builder.image}...`
4989
5029
  });
4990
- await executeDockerfilePlan(sandbox, plan, emit, sleep3);
4991
- emit({ type: "status", message: "Creating snapshot..." });
4992
- const snapshot = await client.snapshotAndWait(sandbox.sandboxId, {
4993
- snapshotType: "filesystem",
4994
- waitUntil: "completed"
5030
+ sandbox = await client.createAndConnect({
5031
+ image: prepared.builder.image,
5032
+ cpus: options.cpus ?? prepared.builder.cpus,
5033
+ memoryMb: options.memoryMb ?? prepared.builder.memoryMb,
5034
+ diskMb: builderDiskMb
4995
5035
  });
4996
- emit({
4997
- type: "snapshot_created",
4998
- snapshot_id: snapshot.snapshotId
4999
- });
5000
- if (!snapshot.snapshotUri) {
5001
- throw new Error(
5002
- `Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
5003
- );
5004
- }
5005
- if (snapshot.sizeBytes == null) {
5006
- throw new Error(
5007
- `Snapshot ${snapshot.snapshotId} is missing sizeBytes and cannot be registered as a sandbox image.`
5008
- );
5009
- }
5010
- if (snapshot.rootfsDiskBytes == null) {
5011
- throw new Error(
5012
- `Snapshot ${snapshot.snapshotId} is missing rootfsDiskBytes and cannot be registered as a sandbox image.`
5013
- );
5014
- }
5015
5036
  emit({
5016
5037
  type: "status",
5017
- message: `Registering image '${plan.registeredName}'...`
5038
+ message: `Rootfs builder sandbox ${sandbox.sandboxId} is running`
5018
5039
  });
5019
- const result = await register(
5020
- context,
5021
- plan.registeredName,
5022
- plan.dockerfileText,
5023
- snapshot.snapshotId,
5024
- snapshot.sandboxId,
5025
- snapshot.snapshotUri,
5026
- snapshot.sizeBytes,
5027
- snapshot.rootfsDiskBytes,
5028
- options.isPublic ?? false,
5029
- snapshot.snapshotFormatVersion
5040
+ emit({ type: "status", message: "Uploading build context..." });
5041
+ await copyLocalPathToSandbox(sandbox, plan.contextDir, REMOTE_CONTEXT_DIR);
5042
+ const spec = await buildRootfsSpec(
5043
+ preparedSpec,
5044
+ prepared,
5045
+ plan,
5046
+ options.diskMb
5047
+ );
5048
+ await runChecked(sandbox, "mkdir", ["-p", path.posix.dirname(REMOTE_SPEC_PATH)]);
5049
+ await sandbox.writeFile(
5050
+ REMOTE_SPEC_PATH,
5051
+ new TextEncoder().encode(JSON.stringify(spec, null, 2))
5052
+ );
5053
+ emit({ type: "status", message: "Running offline rootfs builder..." });
5054
+ await runRootfsBuilder(sandbox, prepared.builder.command, emit, sleep3);
5055
+ const metadataBytes = await sandbox.readFile(REMOTE_METADATA_PATH);
5056
+ const metadata = JSON.parse(
5057
+ new TextDecoder().decode(metadataBytes)
5058
+ );
5059
+ const completeRequest = completeRequestFromMetadata(prepared, metadata);
5060
+ emit({ type: "status", message: "Completing image registration..." });
5061
+ const result = await completeRootfsBuild(
5062
+ resolvedContext,
5063
+ prepared.buildId,
5064
+ completeRequest
5030
5065
  );
5031
5066
  emit({
5032
5067
  type: "image_registered",
@@ -5054,16 +5089,18 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
5054
5089
  cpus: { type: "string" },
5055
5090
  memory: { type: "string" },
5056
5091
  disk_mb: { type: "string" },
5092
+ builder_disk_mb: { type: "string" },
5057
5093
  public: { type: "boolean", default: false }
5058
5094
  }
5059
5095
  });
5060
5096
  const dockerfilePath = parsed.positionals[0];
5061
5097
  if (!dockerfilePath) {
5062
- throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--public]");
5098
+ throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--builder_disk_mb MB] [--public]");
5063
5099
  }
5064
5100
  const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
5065
5101
  const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
5066
5102
  const diskMb = parsed.values.disk_mb != null ? Number(parsed.values.disk_mb) : void 0;
5103
+ const builderDiskMb = parsed.values.builder_disk_mb != null ? Number(parsed.values.builder_disk_mb) : void 0;
5067
5104
  if (cpus != null && !Number.isFinite(cpus)) {
5068
5105
  throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
5069
5106
  }
@@ -5073,6 +5110,11 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
5073
5110
  if (diskMb != null && !Number.isInteger(diskMb)) {
5074
5111
  throw new Error(`Invalid --disk_mb value: ${parsed.values.disk_mb}`);
5075
5112
  }
5113
+ if (builderDiskMb != null && !Number.isInteger(builderDiskMb)) {
5114
+ throw new Error(
5115
+ `Invalid --builder_disk_mb value: ${parsed.values.builder_disk_mb}`
5116
+ );
5117
+ }
5076
5118
  await createSandboxImage(
5077
5119
  dockerfilePath,
5078
5120
  {
@@ -5080,27 +5122,26 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
5080
5122
  cpus,
5081
5123
  memoryMb,
5082
5124
  diskMb,
5125
+ builderDiskMb,
5083
5126
  isPublic: parsed.values.public
5084
5127
  },
5085
5128
  { emit: ndjsonStdoutEmit }
5086
5129
  );
5087
5130
  }
5088
- var BUILD_SANDBOX_PIP_ENV, IGNORED_DOCKERFILE_INSTRUCTIONS, UNSUPPORTED_DOCKERFILE_INSTRUCTIONS;
5131
+ var DEFAULT_ROOTFS_DISK_MB, REMOTE_BUILD_DIR, REMOTE_CONTEXT_DIR, REMOTE_SPEC_PATH, REMOTE_METADATA_PATH, ROOTFS_BUILDER_BIN_DIR, ROOTFS_BUILDER_PATH, ROOTFS_BUILDER_COMMAND, UNSUPPORTED_DOCKERFILE_INSTRUCTIONS;
5089
5132
  var init_sandbox_image = __esm({
5090
5133
  "src/sandbox-image.ts"() {
5091
5134
  init_models();
5092
5135
  init_client();
5093
5136
  init_image();
5094
- BUILD_SANDBOX_PIP_ENV = { PIP_BREAK_SYSTEM_PACKAGES: "1" };
5095
- IGNORED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
5096
- "CMD",
5097
- "ENTRYPOINT",
5098
- "EXPOSE",
5099
- "HEALTHCHECK",
5100
- "LABEL",
5101
- "STOPSIGNAL",
5102
- "VOLUME"
5103
- ]);
5137
+ DEFAULT_ROOTFS_DISK_MB = 10 * 1024;
5138
+ REMOTE_BUILD_DIR = "/var/lib/tensorlake/rootfs-builder/build";
5139
+ REMOTE_CONTEXT_DIR = "/var/lib/tensorlake/rootfs-builder/build/context";
5140
+ REMOTE_SPEC_PATH = "/var/lib/tensorlake/rootfs-builder/build/spec.json";
5141
+ REMOTE_METADATA_PATH = "/var/lib/tensorlake/rootfs-builder/build/metadata.json";
5142
+ ROOTFS_BUILDER_BIN_DIR = "/usr/local/bin";
5143
+ ROOTFS_BUILDER_PATH = "/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
5144
+ ROOTFS_BUILDER_COMMAND = "tl-rootfs-build";
5104
5145
  UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
5105
5146
  "ARG",
5106
5147
  "ONBUILD",