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.
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TI
13
13
  var init_defaults = __esm({
14
14
  "src/defaults.ts"() {
15
15
  "use strict";
16
- SDK_VERSION = "0.5.10";
16
+ SDK_VERSION = "0.5.11";
17
17
  API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
18
18
  API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
19
19
  NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
@@ -4459,6 +4459,7 @@ __export(sandbox_image_exports, {
4459
4459
  runCreateSandboxImageCli: () => runCreateSandboxImageCli
4460
4460
  });
4461
4461
  import { readFile, readdir, stat } from "fs/promises";
4462
+ import { homedir } from "os";
4462
4463
  import path from "path";
4463
4464
  import { parseArgs } from "util";
4464
4465
  function defaultRegisteredName(dockerfilePath) {
@@ -4589,12 +4590,6 @@ function shellSplit(input) {
4589
4590
  }
4590
4591
  return tokens;
4591
4592
  }
4592
- function shellQuote(value) {
4593
- if (!value) {
4594
- return "''";
4595
- }
4596
- return `'${value.replace(/'/g, `'\\''`)}'`;
4597
- }
4598
4593
  function stripLeadingFlags(value) {
4599
4594
  const flags = {};
4600
4595
  let remaining = value.trimStart();
@@ -4632,75 +4627,6 @@ function parseFromValue(value, lineNumber) {
4632
4627
  }
4633
4628
  return tokens[0];
4634
4629
  }
4635
- function parseCopyLikeValues(value, lineNumber, keyword) {
4636
- const { flags, remaining } = stripLeadingFlags(value);
4637
- if ("from" in flags) {
4638
- throw new Error(
4639
- `line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
4640
- );
4641
- }
4642
- const payload = remaining.trim();
4643
- if (!payload) {
4644
- throw new Error(
4645
- `line ${lineNumber}: ${keyword} must include source and destination`
4646
- );
4647
- }
4648
- let parts;
4649
- if (payload.startsWith("[")) {
4650
- let parsed;
4651
- try {
4652
- parsed = JSON.parse(payload);
4653
- } catch (error) {
4654
- throw new Error(
4655
- `line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
4656
- );
4657
- }
4658
- if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
4659
- throw new Error(
4660
- `line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
4661
- );
4662
- }
4663
- parts = parsed;
4664
- } else {
4665
- parts = shellSplit(payload);
4666
- if (parts.length < 2) {
4667
- throw new Error(
4668
- `line ${lineNumber}: ${keyword} must include at least one source and one destination`
4669
- );
4670
- }
4671
- }
4672
- return {
4673
- flags,
4674
- sources: parts.slice(0, -1),
4675
- destination: parts[parts.length - 1]
4676
- };
4677
- }
4678
- function parseEnvPairs(value, lineNumber) {
4679
- const tokens = shellSplit(value);
4680
- if (tokens.length === 0) {
4681
- throw new Error(`line ${lineNumber}: ENV must include a key and value`);
4682
- }
4683
- if (tokens.every((token) => token.includes("="))) {
4684
- return tokens.map((token) => {
4685
- const [key, envValue] = token.split(/=(.*)/s, 2);
4686
- if (!key) {
4687
- throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
4688
- }
4689
- return [key, envValue];
4690
- });
4691
- }
4692
- if (tokens.length < 2) {
4693
- throw new Error(`line ${lineNumber}: ENV must include a key and value`);
4694
- }
4695
- return [[tokens[0], tokens.slice(1).join(" ")]];
4696
- }
4697
- function resolveContainerPath(containerPath, workingDir) {
4698
- if (!containerPath) {
4699
- return workingDir;
4700
- }
4701
- const normalized = containerPath.startsWith("/") ? path.posix.normalize(containerPath) : path.posix.normalize(path.posix.join(workingDir, containerPath));
4702
- return normalized.startsWith("/") ? normalized : `/${normalized}`;
4703
- }
4704
4630
  function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
4705
4631
  let baseImage;
4706
4632
  const instructions = [];
@@ -4811,14 +4737,292 @@ function buildContextFromEnv() {
4811
4737
  };
4812
4738
  }
4813
4739
  function createDefaultClient(context) {
4740
+ const useScopeHeaders = context.personalAccessToken != null && context.apiKey == null;
4814
4741
  return new SandboxClient({
4815
4742
  apiUrl: context.apiUrl,
4816
4743
  apiKey: context.apiKey ?? context.personalAccessToken,
4817
- organizationId: context.organizationId,
4818
- projectId: context.projectId,
4744
+ organizationId: useScopeHeaders ? context.organizationId : void 0,
4745
+ projectId: useScopeHeaders ? context.projectId : void 0,
4819
4746
  namespace: context.namespace
4820
4747
  });
4821
4748
  }
4749
+ function baseApiUrl(context) {
4750
+ return context.apiUrl.replace(/\/+$/, "");
4751
+ }
4752
+ function scopedBuildsPath(context) {
4753
+ return `/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-template-builds`;
4754
+ }
4755
+ function platformHeaders(context) {
4756
+ const headers = {
4757
+ Authorization: `Bearer ${context.bearerToken}`,
4758
+ "Content-Type": "application/json"
4759
+ };
4760
+ if (context.useScopeHeaders) {
4761
+ headers["X-Forwarded-Organization-Id"] = context.organizationId;
4762
+ headers["X-Forwarded-Project-Id"] = context.projectId;
4763
+ }
4764
+ return headers;
4765
+ }
4766
+ async function requestJson(url, init, errorPrefix) {
4767
+ const response = await fetch(url, init);
4768
+ if (!response.ok) {
4769
+ throw new Error(
4770
+ `${errorPrefix} (HTTP ${response.status}): ${await response.text()}`
4771
+ );
4772
+ }
4773
+ const text = await response.text();
4774
+ return text ? JSON.parse(text) : {};
4775
+ }
4776
+ async function resolveBuildContext(context) {
4777
+ const bearerToken = context.apiKey ?? context.personalAccessToken;
4778
+ if (!bearerToken) {
4779
+ throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
4780
+ }
4781
+ if (context.apiKey) {
4782
+ const scope = await requestJson(
4783
+ `${baseApiUrl(context)}/platform/v1/keys/introspect`,
4784
+ {
4785
+ method: "POST",
4786
+ headers: {
4787
+ Authorization: `Bearer ${bearerToken}`,
4788
+ "Content-Type": "application/json"
4789
+ }
4790
+ },
4791
+ "API key introspection failed"
4792
+ );
4793
+ if (!scope.organizationId || !scope.projectId) {
4794
+ throw new Error("API key introspection response is missing organizationId or projectId");
4795
+ }
4796
+ return {
4797
+ ...context,
4798
+ bearerToken,
4799
+ organizationId: scope.organizationId,
4800
+ projectId: scope.projectId,
4801
+ useScopeHeaders: false
4802
+ };
4803
+ }
4804
+ if (!context.organizationId || !context.projectId) {
4805
+ throw new Error(
4806
+ "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."
4807
+ );
4808
+ }
4809
+ return {
4810
+ ...context,
4811
+ bearerToken,
4812
+ organizationId: context.organizationId,
4813
+ projectId: context.projectId,
4814
+ useScopeHeaders: true
4815
+ };
4816
+ }
4817
+ async function prepareRootfsBuild(context, plan, isPublic) {
4818
+ if (!plan.baseImage) {
4819
+ throw new Error("Sandbox image builds require a Dockerfile FROM image or Image baseImage");
4820
+ }
4821
+ const spec = await requestJson(
4822
+ `${baseApiUrl(context)}${scopedBuildsPath(context)}`,
4823
+ {
4824
+ method: "POST",
4825
+ headers: platformHeaders(context),
4826
+ body: JSON.stringify({
4827
+ name: plan.registeredName,
4828
+ dockerfile: plan.dockerfileText,
4829
+ baseImage: plan.baseImage,
4830
+ public: isPublic
4831
+ })
4832
+ },
4833
+ "failed to prepare sandbox image build"
4834
+ );
4835
+ return { prepared: parsePreparedBuild(spec), spec };
4836
+ }
4837
+ function parsePreparedBuild(raw) {
4838
+ const builder = raw.builder;
4839
+ if (!builder) {
4840
+ throw new Error("platform API response is missing rootfs builder configuration");
4841
+ }
4842
+ const prepared = {
4843
+ ...raw,
4844
+ buildId: requiredString(raw, "buildId"),
4845
+ snapshotId: requiredString(raw, "snapshotId"),
4846
+ snapshotUri: requiredString(raw, "snapshotUri"),
4847
+ rootfsNodeKind: requiredString(raw, "rootfsNodeKind"),
4848
+ builder: {
4849
+ image: requiredString(builder, "image"),
4850
+ command: requiredString(builder, "command"),
4851
+ cpus: requiredNumber(builder, "cpus"),
4852
+ memoryMb: requiredNumber(builder, "memoryMb"),
4853
+ diskMb: requiredNumber(builder, "diskMb")
4854
+ }
4855
+ };
4856
+ const parent = raw.parent;
4857
+ if (parent != null) {
4858
+ prepared.parent = {
4859
+ parentManifestUri: requiredString(parent, "parentManifestUri"),
4860
+ rootfsDiskBytes: optionalNumber(parent, "rootfsDiskBytes")
4861
+ };
4862
+ }
4863
+ return prepared;
4864
+ }
4865
+ async function completeRootfsBuild(context, buildId, request) {
4866
+ return requestJson(
4867
+ `${baseApiUrl(context)}${scopedBuildsPath(context)}/${encodeURIComponent(buildId)}/complete`,
4868
+ {
4869
+ method: "POST",
4870
+ headers: platformHeaders(context),
4871
+ body: JSON.stringify(request)
4872
+ },
4873
+ "failed to complete sandbox image build"
4874
+ );
4875
+ }
4876
+ async function resolvedDockerConfigJson() {
4877
+ const configDir = process.env.DOCKER_CONFIG ?? path.join(homedir(), ".docker");
4878
+ const configPath = path.join(configDir, "config.json");
4879
+ try {
4880
+ const content = await readFile(configPath, "utf8");
4881
+ const parsed = JSON.parse(content);
4882
+ const auths = parsed.auths;
4883
+ if (auths != null && Object.keys(auths).length > 0) {
4884
+ return JSON.stringify({ auths });
4885
+ }
4886
+ } catch (error) {
4887
+ if (error.code === "ENOENT") {
4888
+ return void 0;
4889
+ }
4890
+ throw error;
4891
+ }
4892
+ return void 0;
4893
+ }
4894
+ function rootfsDiskBytes(diskMb, prepared) {
4895
+ if (diskMb != null) {
4896
+ return diskMb * 1024 * 1024;
4897
+ }
4898
+ if (prepared.parent != null) {
4899
+ if (prepared.parent.rootfsDiskBytes == null) {
4900
+ throw new Error(
4901
+ "platform API did not return parent rootfsDiskBytes for diff build; pass diskMb explicitly or update Platform API"
4902
+ );
4903
+ }
4904
+ return prepared.parent.rootfsDiskBytes;
4905
+ }
4906
+ return DEFAULT_ROOTFS_DISK_MB * 1024 * 1024;
4907
+ }
4908
+ function rootfsDiskBytesToMb(bytes) {
4909
+ return Math.ceil(bytes / (1024 * 1024));
4910
+ }
4911
+ async function buildRootfsSpec(preparedSpec, prepared, plan, diskMb) {
4912
+ const spec = {
4913
+ ...preparedSpec,
4914
+ dockerfile: plan.dockerfileText,
4915
+ contextDir: REMOTE_CONTEXT_DIR,
4916
+ baseImage: plan.baseImage,
4917
+ rootfsDiskBytes: rootfsDiskBytes(diskMb, prepared)
4918
+ };
4919
+ const dockerConfigJson = await resolvedDockerConfigJson();
4920
+ if (dockerConfigJson != null) {
4921
+ spec.dockerConfigJson = dockerConfigJson;
4922
+ }
4923
+ return spec;
4924
+ }
4925
+ function rootfsBuilderExecutable(executable) {
4926
+ return executable === ROOTFS_BUILDER_COMMAND ? `${ROOTFS_BUILDER_BIN_DIR}/${ROOTFS_BUILDER_COMMAND}` : executable;
4927
+ }
4928
+ function rootfsBuilderEnv() {
4929
+ return { PATH: ROOTFS_BUILDER_PATH };
4930
+ }
4931
+ async function runRootfsBuilder(sandbox, command, emit, sleep3) {
4932
+ const parts = shellSplit(command);
4933
+ const [executable, ...commandArgs] = parts;
4934
+ if (!executable) {
4935
+ throw new Error("empty rootfs builder command returned by platform API");
4936
+ }
4937
+ await runStreaming(
4938
+ sandbox,
4939
+ emit,
4940
+ sleep3,
4941
+ rootfsBuilderExecutable(executable),
4942
+ [...commandArgs, "--spec", REMOTE_SPEC_PATH, "--metadata-out", REMOTE_METADATA_PATH],
4943
+ rootfsBuilderEnv(),
4944
+ REMOTE_BUILD_DIR
4945
+ );
4946
+ }
4947
+ function metadataString(metadata, snakeKey, camelKey) {
4948
+ const value = metadata[snakeKey] ?? metadata[camelKey];
4949
+ return typeof value === "string" ? value : void 0;
4950
+ }
4951
+ function metadataNumber(metadata, snakeKey, camelKey) {
4952
+ const value = metadata[snakeKey] ?? metadata[camelKey];
4953
+ if (typeof value === "number") {
4954
+ return value;
4955
+ }
4956
+ if (typeof value === "string" && value.trim()) {
4957
+ const parsed = Number(value);
4958
+ return Number.isFinite(parsed) ? parsed : void 0;
4959
+ }
4960
+ return void 0;
4961
+ }
4962
+ function completeRequestFromMetadata(prepared, metadata) {
4963
+ const rootfsNodeKind = metadataString(metadata, "rootfs_node_kind", "rootfsNodeKind") ?? prepared.rootfsNodeKind;
4964
+ const parentManifestUri = metadataString(metadata, "parent_manifest_uri", "parentManifestUri") ?? (rootfsNodeKind === "diff" ? prepared.parent?.parentManifestUri : void 0);
4965
+ if (rootfsNodeKind === "diff" && parentManifestUri == null) {
4966
+ throw new Error("rootfs diff build completed without parent_manifest_uri");
4967
+ }
4968
+ const snapshotFormatVersion = metadataString(
4969
+ metadata,
4970
+ "snapshot_format_version",
4971
+ "snapshotFormatVersion"
4972
+ );
4973
+ const snapshotSizeBytes = metadataNumber(
4974
+ metadata,
4975
+ "snapshot_size_bytes",
4976
+ "snapshotSizeBytes"
4977
+ );
4978
+ const rootfsDiskBytesValue = metadataNumber(
4979
+ metadata,
4980
+ "rootfs_disk_bytes",
4981
+ "rootfsDiskBytes"
4982
+ );
4983
+ if (!snapshotFormatVersion) {
4984
+ throw new Error("rootfs builder metadata is missing snapshot_format_version");
4985
+ }
4986
+ if (snapshotSizeBytes == null) {
4987
+ throw new Error("rootfs builder metadata is missing numeric snapshot_size_bytes");
4988
+ }
4989
+ if (rootfsDiskBytesValue == null) {
4990
+ throw new Error("rootfs builder metadata is missing numeric rootfs_disk_bytes");
4991
+ }
4992
+ return {
4993
+ snapshotId: metadataString(metadata, "snapshot_id", "snapshotId") ?? prepared.snapshotId,
4994
+ snapshotUri: metadataString(metadata, "snapshot_uri", "snapshotUri") ?? prepared.snapshotUri,
4995
+ snapshotFormatVersion,
4996
+ snapshotSizeBytes,
4997
+ rootfsDiskBytes: rootfsDiskBytesValue,
4998
+ rootfsNodeKind,
4999
+ ...parentManifestUri ? { parentManifestUri } : {}
5000
+ };
5001
+ }
5002
+ function requiredString(object, key) {
5003
+ const value = object[key];
5004
+ if (typeof value !== "string" || value.length === 0) {
5005
+ throw new Error(`expected '${key}' to be a non-empty string`);
5006
+ }
5007
+ return value;
5008
+ }
5009
+ function requiredNumber(object, key) {
5010
+ const value = object[key];
5011
+ if (typeof value !== "number" || !Number.isFinite(value)) {
5012
+ throw new Error(`expected '${key}' to be a finite number`);
5013
+ }
5014
+ return value;
5015
+ }
5016
+ function optionalNumber(object, key) {
5017
+ const value = object[key];
5018
+ if (value == null) {
5019
+ return void 0;
5020
+ }
5021
+ if (typeof value !== "number" || !Number.isFinite(value)) {
5022
+ throw new Error(`expected '${key}' to be a finite number`);
5023
+ }
5024
+ return value;
5025
+ }
4822
5026
  async function runChecked(sandbox, command, args, env, workingDir) {
4823
5027
  const result = await sandbox.run(command, {
4824
5028
  args,
@@ -4878,18 +5082,6 @@ function emitOutputLines(emit, stream, response, seen) {
4878
5082
  emit({ type: "build_log", stream, message: line });
4879
5083
  }
4880
5084
  }
4881
- function isPathWithinContext(contextDir, localPath) {
4882
- const relative = path.relative(contextDir, localPath);
4883
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
4884
- }
4885
- function resolveContextSourcePath(contextDir, source) {
4886
- const resolvedContextDir = path.resolve(contextDir);
4887
- const resolvedSource = path.resolve(resolvedContextDir, source);
4888
- if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
4889
- throw new Error(`Local path escapes the build context: ${source}`);
4890
- }
4891
- return resolvedSource;
4892
- }
4893
5085
  async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
4894
5086
  const fileStats = await stat(localPath).catch(() => null);
4895
5087
  if (!fileStats) {
@@ -4920,168 +5112,7 @@ async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
4920
5112
  }
4921
5113
  }
4922
5114
  }
4923
- async function persistEnvVar(sandbox, processEnv, key, value) {
4924
- const exportLine = `export ${key}=${shellQuote(value)}`;
4925
- await runChecked(
4926
- sandbox,
4927
- "sh",
4928
- ["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
4929
- processEnv
4930
- );
4931
- }
4932
- async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
4933
- const destinationPath = resolveContainerPath(destination, workingDir);
4934
- if (sources.length > 1 && !destinationPath.endsWith("/")) {
4935
- throw new Error(
4936
- `${keyword} with multiple sources requires a directory destination ending in '/'`
4937
- );
4938
- }
4939
- for (const source of sources) {
4940
- const localSource = resolveContextSourcePath(contextDir, source);
4941
- const localStats = await stat(localSource).catch(() => null);
4942
- if (!localStats) {
4943
- throw new Error(`Local path not found: ${localSource}`);
4944
- }
4945
- let remoteDestination = destinationPath;
4946
- if (sources.length > 1) {
4947
- remoteDestination = path.posix.join(
4948
- destinationPath.replace(/\/$/, ""),
4949
- path.posix.basename(source.replace(/\/$/, ""))
4950
- );
4951
- } else if (localStats.isFile() && destinationPath.endsWith("/")) {
4952
- remoteDestination = path.posix.join(
4953
- destinationPath.replace(/\/$/, ""),
4954
- path.basename(source)
4955
- );
4956
- }
4957
- emit({
4958
- type: "status",
4959
- message: `${keyword} ${source} -> ${remoteDestination}`
4960
- });
4961
- await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
4962
- }
4963
- }
4964
- async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep3) {
4965
- let destinationPath = resolveContainerPath(destination, workingDir);
4966
- const parsedUrl = new URL(url);
4967
- const fileName = path.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
4968
- if (destinationPath.endsWith("/")) {
4969
- destinationPath = path.posix.join(destinationPath.replace(/\/$/, ""), fileName);
4970
- }
4971
- const parentDir = path.posix.dirname(destinationPath) || "/";
4972
- emit({
4973
- type: "status",
4974
- message: `ADD ${url} -> ${destinationPath}`
4975
- });
4976
- await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
4977
- await runStreaming(
4978
- sandbox,
4979
- emit,
4980
- sleep3,
4981
- "sh",
4982
- [
4983
- "-c",
4984
- `curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
4985
- ],
4986
- processEnv,
4987
- workingDir
4988
- );
4989
- }
4990
- async function executeDockerfilePlan(sandbox, plan, emit, sleep3) {
4991
- const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
4992
- let workingDir = "/";
4993
- for (const instruction of plan.instructions) {
4994
- const { keyword, value, lineNumber } = instruction;
4995
- if (keyword === "RUN") {
4996
- emit({ type: "status", message: `RUN ${value}` });
4997
- await runStreaming(
4998
- sandbox,
4999
- emit,
5000
- sleep3,
5001
- "sh",
5002
- ["-c", value],
5003
- processEnv,
5004
- workingDir
5005
- );
5006
- continue;
5007
- }
5008
- if (keyword === "WORKDIR") {
5009
- const tokens = shellSplit(value);
5010
- if (tokens.length !== 1) {
5011
- throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
5012
- }
5013
- workingDir = resolveContainerPath(tokens[0], workingDir);
5014
- emit({ type: "status", message: `WORKDIR ${workingDir}` });
5015
- await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
5016
- continue;
5017
- }
5018
- if (keyword === "ENV") {
5019
- for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
5020
- emit({ type: "status", message: `ENV ${key}=${envValue}` });
5021
- processEnv[key] = envValue;
5022
- await persistEnvVar(sandbox, processEnv, key, envValue);
5023
- }
5024
- continue;
5025
- }
5026
- if (keyword === "COPY") {
5027
- const { sources, destination } = parseCopyLikeValues(
5028
- value,
5029
- lineNumber,
5030
- keyword
5031
- );
5032
- await copyFromContext(
5033
- sandbox,
5034
- emit,
5035
- plan.contextDir,
5036
- sources,
5037
- destination,
5038
- workingDir,
5039
- keyword
5040
- );
5041
- continue;
5042
- }
5043
- if (keyword === "ADD") {
5044
- const { sources, destination } = parseCopyLikeValues(
5045
- value,
5046
- lineNumber,
5047
- keyword
5048
- );
5049
- if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
5050
- await addUrlToSandbox(
5051
- sandbox,
5052
- emit,
5053
- sources[0],
5054
- destination,
5055
- workingDir,
5056
- processEnv,
5057
- sleep3
5058
- );
5059
- } else {
5060
- await copyFromContext(
5061
- sandbox,
5062
- emit,
5063
- plan.contextDir,
5064
- sources,
5065
- destination,
5066
- workingDir,
5067
- keyword
5068
- );
5069
- }
5070
- continue;
5071
- }
5072
- if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
5073
- emit({
5074
- type: "warning",
5075
- message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
5076
- });
5077
- continue;
5078
- }
5079
- throw new Error(
5080
- `line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
5081
- );
5082
- }
5083
- }
5084
- async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes, isPublic, snapshotFormatVersion) {
5115
+ async function registerImage(context, name, dockerfile, snapshotId, snapshotSandboxId, snapshotUri, snapshotSizeBytes, rootfsDiskBytes2, isPublic, snapshotFormatVersion) {
5085
5116
  const bearerToken = context.apiKey ?? context.personalAccessToken;
5086
5117
  if (!bearerToken) {
5087
5118
  throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
@@ -5115,7 +5146,7 @@ async function registerImage(context, name, dockerfile, snapshotId, snapshotSand
5115
5146
  snapshotUri,
5116
5147
  ...snapshotFormatVersion ? { snapshotFormatVersion } : {},
5117
5148
  snapshotSizeBytes,
5118
- rootfsDiskBytes,
5149
+ rootfsDiskBytes: rootfsDiskBytes2,
5119
5150
  public: isPublic
5120
5151
  })
5121
5152
  });
@@ -5132,67 +5163,71 @@ async function createSandboxImage(source, options = {}, deps = {}) {
5132
5163
  const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5133
5164
  const context = buildContextFromEnv();
5134
5165
  const clientFactory = deps.createClient ?? createDefaultClient;
5135
- const register = deps.registerImage ?? ((...args) => registerImage(...args));
5136
5166
  const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
5137
5167
  emit({ type: "status", message: `Loading ${sourceLabel}...` });
5138
5168
  const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
5139
5169
  emit({
5140
5170
  type: "status",
5141
- message: plan.baseImage == null ? "Starting build sandbox with the default server image..." : `Starting build sandbox from ${plan.baseImage}...`
5171
+ message: `Selected image name: ${plan.registeredName}`
5172
+ });
5173
+ emit({ type: "status", message: "Preparing rootfs build..." });
5174
+ const resolvedContext = await resolveBuildContext(context);
5175
+ const { prepared, spec: preparedSpec } = await prepareRootfsBuild(
5176
+ resolvedContext,
5177
+ plan,
5178
+ options.isPublic ?? false
5179
+ );
5180
+ emit({
5181
+ type: "status",
5182
+ message: prepared.rootfsNodeKind === "diff" ? "Build mode: RootfsDiff" : "Build mode: RootfsBase"
5142
5183
  });
5143
5184
  const client = clientFactory(context);
5144
5185
  let sandbox;
5145
5186
  try {
5146
- sandbox = await client.createAndConnect({
5147
- ...plan.baseImage == null ? {} : { image: plan.baseImage },
5148
- cpus: options.cpus ?? 2,
5149
- memoryMb: options.memoryMb ?? 4096,
5150
- ...options.diskMb != null ? { diskMb: options.diskMb } : {}
5151
- });
5187
+ const outputRootfsDiskBytes = rootfsDiskBytes(options.diskMb, prepared);
5188
+ const builderDiskMb = Math.max(
5189
+ rootfsDiskBytesToMb(outputRootfsDiskBytes),
5190
+ options.builderDiskMb ?? prepared.builder.diskMb
5191
+ );
5152
5192
  emit({
5153
5193
  type: "status",
5154
- message: `Materializing image in sandbox ${sandbox.sandboxId}...`
5194
+ message: `Creating rootfs builder sandbox from ${prepared.builder.image}...`
5155
5195
  });
5156
- await executeDockerfilePlan(sandbox, plan, emit, sleep3);
5157
- emit({ type: "status", message: "Creating snapshot..." });
5158
- const snapshot = await client.snapshotAndWait(sandbox.sandboxId, {
5159
- snapshotType: "filesystem",
5160
- waitUntil: "completed"
5196
+ sandbox = await client.createAndConnect({
5197
+ image: prepared.builder.image,
5198
+ cpus: options.cpus ?? prepared.builder.cpus,
5199
+ memoryMb: options.memoryMb ?? prepared.builder.memoryMb,
5200
+ diskMb: builderDiskMb
5161
5201
  });
5162
- emit({
5163
- type: "snapshot_created",
5164
- snapshot_id: snapshot.snapshotId
5165
- });
5166
- if (!snapshot.snapshotUri) {
5167
- throw new Error(
5168
- `Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
5169
- );
5170
- }
5171
- if (snapshot.sizeBytes == null) {
5172
- throw new Error(
5173
- `Snapshot ${snapshot.snapshotId} is missing sizeBytes and cannot be registered as a sandbox image.`
5174
- );
5175
- }
5176
- if (snapshot.rootfsDiskBytes == null) {
5177
- throw new Error(
5178
- `Snapshot ${snapshot.snapshotId} is missing rootfsDiskBytes and cannot be registered as a sandbox image.`
5179
- );
5180
- }
5181
5202
  emit({
5182
5203
  type: "status",
5183
- message: `Registering image '${plan.registeredName}'...`
5204
+ message: `Rootfs builder sandbox ${sandbox.sandboxId} is running`
5184
5205
  });
5185
- const result = await register(
5186
- context,
5187
- plan.registeredName,
5188
- plan.dockerfileText,
5189
- snapshot.snapshotId,
5190
- snapshot.sandboxId,
5191
- snapshot.snapshotUri,
5192
- snapshot.sizeBytes,
5193
- snapshot.rootfsDiskBytes,
5194
- options.isPublic ?? false,
5195
- snapshot.snapshotFormatVersion
5206
+ emit({ type: "status", message: "Uploading build context..." });
5207
+ await copyLocalPathToSandbox(sandbox, plan.contextDir, REMOTE_CONTEXT_DIR);
5208
+ const spec = await buildRootfsSpec(
5209
+ preparedSpec,
5210
+ prepared,
5211
+ plan,
5212
+ options.diskMb
5213
+ );
5214
+ await runChecked(sandbox, "mkdir", ["-p", path.posix.dirname(REMOTE_SPEC_PATH)]);
5215
+ await sandbox.writeFile(
5216
+ REMOTE_SPEC_PATH,
5217
+ new TextEncoder().encode(JSON.stringify(spec, null, 2))
5218
+ );
5219
+ emit({ type: "status", message: "Running offline rootfs builder..." });
5220
+ await runRootfsBuilder(sandbox, prepared.builder.command, emit, sleep3);
5221
+ const metadataBytes = await sandbox.readFile(REMOTE_METADATA_PATH);
5222
+ const metadata = JSON.parse(
5223
+ new TextDecoder().decode(metadataBytes)
5224
+ );
5225
+ const completeRequest = completeRequestFromMetadata(prepared, metadata);
5226
+ emit({ type: "status", message: "Completing image registration..." });
5227
+ const result = await completeRootfsBuild(
5228
+ resolvedContext,
5229
+ prepared.buildId,
5230
+ completeRequest
5196
5231
  );
5197
5232
  emit({
5198
5233
  type: "image_registered",
@@ -5220,16 +5255,18 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
5220
5255
  cpus: { type: "string" },
5221
5256
  memory: { type: "string" },
5222
5257
  disk_mb: { type: "string" },
5258
+ builder_disk_mb: { type: "string" },
5223
5259
  public: { type: "boolean", default: false }
5224
5260
  }
5225
5261
  });
5226
5262
  const dockerfilePath = parsed.positionals[0];
5227
5263
  if (!dockerfilePath) {
5228
- throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--public]");
5264
+ throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk_mb MB] [--builder_disk_mb MB] [--public]");
5229
5265
  }
5230
5266
  const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
5231
5267
  const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
5232
5268
  const diskMb = parsed.values.disk_mb != null ? Number(parsed.values.disk_mb) : void 0;
5269
+ const builderDiskMb = parsed.values.builder_disk_mb != null ? Number(parsed.values.builder_disk_mb) : void 0;
5233
5270
  if (cpus != null && !Number.isFinite(cpus)) {
5234
5271
  throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
5235
5272
  }
@@ -5239,6 +5276,11 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
5239
5276
  if (diskMb != null && !Number.isInteger(diskMb)) {
5240
5277
  throw new Error(`Invalid --disk_mb value: ${parsed.values.disk_mb}`);
5241
5278
  }
5279
+ if (builderDiskMb != null && !Number.isInteger(builderDiskMb)) {
5280
+ throw new Error(
5281
+ `Invalid --builder_disk_mb value: ${parsed.values.builder_disk_mb}`
5282
+ );
5283
+ }
5242
5284
  await createSandboxImage(
5243
5285
  dockerfilePath,
5244
5286
  {
@@ -5246,28 +5288,27 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
5246
5288
  cpus,
5247
5289
  memoryMb,
5248
5290
  diskMb,
5291
+ builderDiskMb,
5249
5292
  isPublic: parsed.values.public
5250
5293
  },
5251
5294
  { emit: ndjsonStdoutEmit }
5252
5295
  );
5253
5296
  }
5254
- var BUILD_SANDBOX_PIP_ENV, IGNORED_DOCKERFILE_INSTRUCTIONS, UNSUPPORTED_DOCKERFILE_INSTRUCTIONS;
5297
+ 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;
5255
5298
  var init_sandbox_image = __esm({
5256
5299
  "src/sandbox-image.ts"() {
5257
5300
  "use strict";
5258
5301
  init_models();
5259
5302
  init_client();
5260
5303
  init_image();
5261
- BUILD_SANDBOX_PIP_ENV = { PIP_BREAK_SYSTEM_PACKAGES: "1" };
5262
- IGNORED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
5263
- "CMD",
5264
- "ENTRYPOINT",
5265
- "EXPOSE",
5266
- "HEALTHCHECK",
5267
- "LABEL",
5268
- "STOPSIGNAL",
5269
- "VOLUME"
5270
- ]);
5304
+ DEFAULT_ROOTFS_DISK_MB = 10 * 1024;
5305
+ REMOTE_BUILD_DIR = "/var/lib/tensorlake/rootfs-builder/build";
5306
+ REMOTE_CONTEXT_DIR = "/var/lib/tensorlake/rootfs-builder/build/context";
5307
+ REMOTE_SPEC_PATH = "/var/lib/tensorlake/rootfs-builder/build/spec.json";
5308
+ REMOTE_METADATA_PATH = "/var/lib/tensorlake/rootfs-builder/build/metadata.json";
5309
+ ROOTFS_BUILDER_BIN_DIR = "/usr/local/bin";
5310
+ ROOTFS_BUILDER_PATH = "/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
5311
+ ROOTFS_BUILDER_COMMAND = "tl-rootfs-build";
5271
5312
  UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
5272
5313
  "ARG",
5273
5314
  "ONBUILD",