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