code-sandboxes 1.9.34__tar.gz → 1.9.36__tar.gz

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.
Files changed (266) hide show
  1. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.github/workflows/environments-live.yml +98 -20
  2. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/PKG-INFO +1 -1
  3. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/adapters/datalayer.py +11 -0
  5. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/adapters/modal.py +88 -26
  6. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/resolve.py +47 -0
  7. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/resolve_conda.py +6 -0
  8. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/modal_sandbox.py +10 -1
  9. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/pyproject.toml +4 -0
  10. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_builders.py +4 -4
  11. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_contract.py +1 -3
  12. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_datalayer_builder.py +21 -0
  13. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_live_matrix.py +5 -18
  14. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_modal_builder.py +106 -14
  15. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_resolve.py +28 -0
  16. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_resolve_conda.py +24 -0
  17. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_modal_google_colab_sandbox.py +51 -0
  18. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.github/workflows/build.yml +0 -0
  19. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.github/workflows/py-code-style.yml +0 -0
  20. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.github/workflows/py-tests.yml +0 -0
  21. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.github/workflows/py-typing.yml +0 -0
  22. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.github/workflows/reusable-python.yml +0 -0
  23. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.gitignore +0 -0
  24. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.licenserc.yaml +0 -0
  25. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.pre-commit-config.yaml +0 -0
  26. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/.vscode/settings.json +0 -0
  27. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/CHANGELOG.md +0 -0
  28. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/Dockerfile +0 -0
  29. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/LICENSE +0 -0
  30. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/Makefile +0 -0
  31. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/README.md +0 -0
  32. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/__init__.py +0 -0
  33. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/base.py +0 -0
  34. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/bridge_mount.py +0 -0
  35. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/builds.py +0 -0
  36. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/cli.py +0 -0
  37. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/client.py +0 -0
  38. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/cloudflare_sandbox.py +0 -0
  39. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/commands.py +0 -0
  40. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/console.py +0 -0
  41. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/contents.py +0 -0
  42. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/coreweave_sandbox.py +0 -0
  43. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/datalayer_sandbox.py +0 -0
  44. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/daytona_sandbox.py +0 -0
  45. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/docker_sandbox.py +0 -0
  46. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/e2b_sandbox.py +0 -0
  47. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/__init__.py +0 -0
  48. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/__main__.py +0 -0
  49. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/accounts.py +0 -0
  50. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/adapters/__init__.py +0 -0
  51. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/adapters/daytona.py +0 -0
  52. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/adapters/e2b.py +0 -0
  53. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/adapters/managed.py +0 -0
  54. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/attest.py +0 -0
  55. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/bases.py +0 -0
  56. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/breaker.py +0 -0
  57. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/build_secrets.py +0 -0
  58. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/builders.py +0 -0
  59. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/canonical.py +0 -0
  60. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/conformance.py +0 -0
  61. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  62. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  63. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  64. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/contract.py +0 -0
  65. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/doctor/__init__.py +0 -0
  66. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/doctor/build.py +0 -0
  67. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  68. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  69. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/errors.py +0 -0
  70. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/files.py +0 -0
  71. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/image_import.py +0 -0
  72. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/lifecycle.py +0 -0
  73. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/policy.py +0 -0
  74. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/redact.py +0 -0
  75. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/schema.py +0 -0
  76. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/environments/spec.py +0 -0
  77. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/eval_sandbox.py +0 -0
  78. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/exceptions.py +0 -0
  79. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/filesystem.py +0 -0
  80. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/google_colab.py +0 -0
  81. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/google_colab_sandbox.py +0 -0
  82. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/interfaces.py +0 -0
  83. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/jupyter_ingress.py +0 -0
  84. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  85. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/kaggle.py +0 -0
  86. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/kaggle_execute.py +0 -0
  87. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/kaggle_live.py +0 -0
  88. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/kaggle_sandbox.py +0 -0
  89. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/lifecycle.py +0 -0
  90. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/manage.py +0 -0
  91. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/models.py +0 -0
  92. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/monty_sandbox.py +0 -0
  93. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/provider_ingress.py +0 -0
  94. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/code_sandboxes/providers.py +0 -0
  95. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docker/Dockerfile +0 -0
  96. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/.gitignore +0 -0
  97. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/LICENSE +0 -0
  98. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/Makefile +0 -0
  99. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/README.md +0 -0
  100. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/babel.config.js +0 -0
  101. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/api-reference/_category_.yml +0 -0
  102. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/api-reference/index.mdx +0 -0
  103. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/cli/_category_.yml +0 -0
  104. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/cli/index.mdx +0 -0
  105. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/contribute/_category_.yml +0 -0
  106. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/contribute/index.mdx +0 -0
  107. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/environments/_category_.yml +0 -0
  108. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/environments/contract.mdx +0 -0
  109. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/environments/index.mdx +0 -0
  110. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/environments/specification.mdx +0 -0
  111. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/environments/spike-2026-09.mdx +0 -0
  112. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/examples/_category_.yml +0 -0
  113. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/examples/index.mdx +0 -0
  114. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/guide/_category_.yml +0 -0
  115. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/guide/index.mdx +0 -0
  116. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/index.mdx +0 -0
  117. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/install/_category_.yml +0 -0
  118. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/install/index.mdx +0 -0
  119. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/provider-ingress.mdx +0 -0
  120. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/_category_.yml +0 -0
  121. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/cloudflare.mdx +0 -0
  122. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/coreweave.mdx +0 -0
  123. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/datalayer.mdx +0 -0
  124. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/daytona.mdx +0 -0
  125. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/docker.mdx +0 -0
  126. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/e2b.mdx +0 -0
  127. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/eval.mdx +0 -0
  128. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/google-colab.mdx +0 -0
  129. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/index.mdx +0 -0
  130. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/jupyter-server.mdx +0 -0
  131. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/kaggle.mdx +0 -0
  132. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/modal.mdx +0 -0
  133. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docs/providers/monty.mdx +0 -0
  134. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/docusaurus.config.js +0 -0
  135. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/package.json +0 -0
  136. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/sidebars.js +0 -0
  137. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/components/HomepageRow1.js +0 -0
  138. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/components/HomepageRow1.module.css +0 -0
  139. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/components/HomepageRow2.js +0 -0
  140. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/components/HomepageRow2.module.css +0 -0
  141. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/components/HomepageRow3.js +0 -0
  142. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/components/HomepageRow3.module.css +0 -0
  143. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/css/custom.css +0 -0
  144. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/pages/index.module.css +0 -0
  145. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/pages/markdown-page.md +0 -0
  146. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/theme/CustomDocItem.tsx +0 -0
  147. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/src/theme/IconsTitle.tsx +0 -0
  148. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/bricks.svg +0 -0
  149. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/building-construction.svg +0 -0
  150. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/cloud-sun.svg +0 -0
  151. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/cloud.svg +0 -0
  152. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/collaboration.svg +0 -0
  153. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/copy-left.svg +0 -0
  154. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/datalayer/logo.png +0 -0
  155. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/datalayer/logo.svg +0 -0
  156. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/datalayer/personas/business.svg +0 -0
  157. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  158. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/datalayer/personas/devops.svg +0 -0
  159. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/datalayer/project/project-line.svg +0 -0
  160. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/favicon.ico +0 -0
  161. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/jupyter.svg +0 -0
  162. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/open-source.svg +0 -0
  163. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/react-js.svg +0 -0
  164. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/react-js2.svg +0 -0
  165. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/ringed-planet.svg +0 -0
  166. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/robot.svg +0 -0
  167. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/rocket.svg +0 -0
  168. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/shield.svg +0 -0
  169. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/sparkles.svg +0 -0
  170. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/streamlit.gif +0 -0
  171. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/super-hero.svg +0 -0
  172. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/target.svg +0 -0
  173. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/docs/static/img/thumbs-up.svg +0 -0
  174. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/README.md +0 -0
  175. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/Makefile +0 -0
  176. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/cloudflare_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/coreweave_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/datalayer_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/daytona_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/docker_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/e2b_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/eval_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/google_colab_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/kaggle_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/modal_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/exec/monty_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/Makefile +0 -0
  189. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/cloudflare_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/coreweave_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/datalayer_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/daytona_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/docker_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/e2b_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/eval_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/google_colab_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/kaggle_sandbox_example.py +0 -0
  199. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/modal_sandbox_example.py +0 -0
  200. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/examples/repl/monty_sandbox_example.py +0 -0
  201. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/schemas/environment-v1alpha1.json +0 -0
  202. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/__init__.py +0 -0
  203. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/conftest.py +0 -0
  204. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  205. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_bridge_mount.py +0 -0
  206. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_builds.py +0 -0
  207. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_cli_exec.py +0 -0
  208. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_cli_repl.py +0 -0
  209. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_client.py +0 -0
  210. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_client_contents.py +0 -0
  211. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_client_files.py +0 -0
  212. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_client_outcome_error_output.py +0 -0
  213. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_cloudflare.py +0 -0
  214. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_commands_envelope.py +0 -0
  215. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_console.py +0 -0
  216. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_contents_contract.py +0 -0
  217. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_contents_live_matrix.py +0 -0
  218. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_coreweave.py +0 -0
  219. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_datalayer_sandbox.py +0 -0
  220. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_daytona.py +0 -0
  221. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_e2b.py +0 -0
  222. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_accounts.py +0 -0
  223. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_attest.py +0 -0
  224. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_bases.py +0 -0
  225. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_breaker.py +0 -0
  226. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_build_secrets.py +0 -0
  227. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_canonical.py +0 -0
  228. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_conformance.py +0 -0
  229. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_constraints.py +0 -0
  230. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_cross_variant_report.py +0 -0
  231. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_daytona_builder.py +0 -0
  232. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_dockerfile_source.py +0 -0
  233. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_doctor.py +0 -0
  234. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_e2b_builder.py +0 -0
  235. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_errors.py +0 -0
  236. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_image_import.py +0 -0
  237. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_lifecycle.py +0 -0
  238. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_managed_builders.py +0 -0
  239. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_organization_policy.py +0 -0
  240. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_redact.py +0 -0
  241. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_spec.py +0 -0
  242. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_environment_spec_organization_policy.py +0 -0
  243. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_eval.py +0 -0
  244. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  245. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_factory.py +0 -0
  246. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_google_colab.py +0 -0
  247. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_integration.py +0 -0
  248. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_jupyter_ingress.py +0 -0
  249. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_jupyter_server.py +0 -0
  250. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_jupyter_server_streaming.py +0 -0
  251. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_kaggle.py +0 -0
  252. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_kaggle_execute.py +0 -0
  253. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_kaggle_live.py +0 -0
  254. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_kaggle_session.py +0 -0
  255. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_kernel_client_compatibility.py +0 -0
  256. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_lifecycle_vocabulary.py +0 -0
  257. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_manage.py +0 -0
  258. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_manage_datalayer.py +0 -0
  259. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_modal_session.py +0 -0
  260. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_models.py +0 -0
  261. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_output_line_fidelity.py +0 -0
  262. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_provider_catalog_names.py +0 -0
  263. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_provider_ingress.py +0 -0
  264. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_providers.py +0 -0
  265. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_sandboxes.py +0 -0
  266. {code_sandboxes-1.9.34 → code_sandboxes-1.9.36}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -4,18 +4,19 @@ name: Environments Live Matrix
4
4
  # providers, nightly (PLAN_ENV.md E2-11): a real, hash-verified lock, a real
5
5
  # build, a real launch, and the formal core tier — tests/test_environment_live_matrix.py.
6
6
  #
7
- # E2B and Modal carry their own already-documented, unticked gaps there
8
- # (E2-03's uid/gid mismatch; modal_sandbox.py's missing setpriv) and are
9
- # `xfail(strict=False)` in that file, so an ordinary failure there is
10
- # expected and does not fail this workflow. Daytona carries no such marker:
11
- # a failure there is a real regression, and this workflow opens (or
12
- # comments on) an issue naming it.
7
+ # E2B carries its own documented, unticked gap there (E2-03: its services run
8
+ # as root) and is `xfail(strict=False)` in that file, so an ordinary failure
9
+ # there is expected and does not fail this workflow. Daytona and Modal carry
10
+ # no such marker: a failure there is a real regression, and this workflow
11
+ # opens (or comments on) an issue naming it.
13
12
  #
14
- # Needs, as repository secrets: E2B_API_KEY, DAYTONA_API_KEY, MODAL_TOKEN_ID,
15
- # MODAL_TOKEN_SECRET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY — the last two
16
- # scoped to read the approved base from ECR only (D-18). A secret that is
17
- # not set is not a workflow failure: the test each secret belongs to skips,
18
- # by name, and says so in the job summary.
13
+ # Needs one repository secret, DATALAYER_API_KEY. The providers' keys are
14
+ # Datalayer secrets of the account it belongs to, read at run time: E2B_API_KEY,
15
+ # DAYTONA_API_KEY, MODAL_TOKEN_ID, MODAL_TOKEN_SECRET, and the AWS pair under
16
+ # CODE_SANDBOXES_CI_AWS_ACCESS_KEY_ID and CODE_SANDBOXES_CI_AWS_SECRET_ACCESS_KEY,
17
+ # a key of the base reader that can only pull the approved base from ECR (D-18).
18
+ # A key that is not there skips the test it belongs to, by name, and the job
19
+ # summary lists each leg's outcome; a Daytona leg that did not run fails the job.
19
20
 
20
21
  on:
21
22
  schedule:
@@ -55,6 +56,48 @@ jobs:
55
56
  uv pip install ".[e2b,daytona,modal,test]"
56
57
  uv pip install boto3
57
58
 
59
+ # Each value is masked before it reaches the job's environment, and none
60
+ # is set on a later step's own `env:`, which would override it with an
61
+ # empty string.
62
+ - name: Read the providers' keys from Datalayer
63
+ env:
64
+ DATALAYER_API_KEY: ${{ secrets.DATALAYER_API_KEY }}
65
+ DATALAYER_IAM_URL: https://prod1.datalayer.run
66
+ run: |
67
+ python - <<'PY'
68
+ import json
69
+ import os
70
+ import urllib.request
71
+
72
+ key = os.environ.get("DATALAYER_API_KEY", "")
73
+ if not key:
74
+ print("DATALAYER_API_KEY is not a secret of this repository: every leg will skip")
75
+ raise SystemExit(0)
76
+ request = urllib.request.Request(
77
+ os.environ["DATALAYER_IAM_URL"] + "/api/iam/v1/secrets/values",
78
+ headers={"Authorization": f"Bearer {key}"},
79
+ )
80
+ with urllib.request.urlopen(request, timeout=60) as response:
81
+ secrets = json.load(response).get("secrets") or {}
82
+ wanted = {
83
+ "DAYTONA_API_KEY": "DAYTONA_API_KEY",
84
+ "E2B_API_KEY": "E2B_API_KEY",
85
+ "MODAL_TOKEN_ID": "MODAL_TOKEN_ID",
86
+ "MODAL_TOKEN_SECRET": "MODAL_TOKEN_SECRET",
87
+ "AWS_ACCESS_KEY_ID": "CODE_SANDBOXES_CI_AWS_ACCESS_KEY_ID",
88
+ "AWS_SECRET_ACCESS_KEY": "CODE_SANDBOXES_CI_AWS_SECRET_ACCESS_KEY",
89
+ }
90
+ with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as environment:
91
+ for variable, name in wanted.items():
92
+ value = str(secrets.get(name) or "")
93
+ if not value or "\n" in value:
94
+ print(f"{variable}: no Datalayer secret {name} for this account")
95
+ continue
96
+ print(f"::add-mask::{value}")
97
+ environment.write(f"{variable}={value}\n")
98
+ print(f"{variable}: read from the Datalayer secret {name}")
99
+ PY
100
+
58
101
  - name: Configure AWS credentials
59
102
  run: |
60
103
  mkdir -p ~/.aws
@@ -63,9 +106,6 @@ jobs:
63
106
  echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}"
64
107
  echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY}"
65
108
  } > ~/.aws/credentials
66
- env:
67
- AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
68
- AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
69
109
 
70
110
  - name: Run the live matrix
71
111
  id: live
@@ -75,13 +115,51 @@ jobs:
75
115
  tests/test_environment_live_matrix.py \
76
116
  --junitxml=live-matrix-results.xml
77
117
  env:
78
- AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
79
- AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
80
118
  AWS_REGION: us-east-1
81
- E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
82
- DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
83
- MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
84
- MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
119
+
120
+ # A leg whose secret is not set skips, and a run where every leg skipped
121
+ # reported success in under a second: the nightly was green every night
122
+ # while it tested nothing (found on 2026-09-18, "3 skipped in 0.61s").
123
+ # Each leg's outcome goes in the job summary, and a Daytona leg that did
124
+ # not run fails the job: it is the one this matrix is required to prove
125
+ # (PLAN_ENV.md, the Datalayer and Daytona target, E2-11).
126
+ - name: Say which legs ran, and refuse a Daytona leg that did not
127
+ if: always()
128
+ run: |
129
+ python - <<'PY'
130
+ import os
131
+ import sys
132
+ import xml.etree.ElementTree as ET
133
+
134
+ try:
135
+ cases = list(ET.parse("live-matrix-results.xml").getroot().iter("testcase"))
136
+ except (OSError, ET.ParseError) as error:
137
+ sys.exit(f"no JUnit report to read: {error}")
138
+ rows, daytona = [], None
139
+ for case in cases:
140
+ leg = (case.get("classname") or "").rsplit(".", 1)[-1]
141
+ skipped = case.find("skipped")
142
+ if skipped is not None:
143
+ outcome = "skipped: " + (skipped.get("message") or "").strip()
144
+ elif case.find("failure") is not None or case.find("error") is not None:
145
+ outcome = "failed"
146
+ else:
147
+ outcome = "passed"
148
+ rows.append(f"| {leg} | {outcome} |")
149
+ if leg == "TestDaytona":
150
+ daytona = outcome
151
+ summary = "| Leg | Outcome |\n|---|---|\n" + "\n".join(rows) + "\n"
152
+ with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle:
153
+ handle.write(summary)
154
+ print(summary)
155
+ if daytona is None or daytona.startswith("skipped"):
156
+ sys.exit(
157
+ f"The Daytona leg did not run ({daytona or 'absent'}): it is the one this "
158
+ "matrix must prove, so a run without it is not a green run. Set the "
159
+ "DATALAYER_API_KEY repository secret, whose account holds the "
160
+ "DAYTONA_API_KEY and CODE_SANDBOXES_CI_AWS_* Datalayer secrets."
161
+ )
162
+ PY
85
163
 
86
164
  - name: Upload the JUnit report
87
165
  if: always()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.34
3
+ Version: 1.9.36
4
4
  Project-URL: Home, https://github.com/datalayer/code-sandboxes
5
5
  Author-email: Datalayer <info@datalayer.io>
6
6
  License: BSD 3-Clause License
@@ -3,4 +3,4 @@
3
3
 
4
4
  """Code Sandboxes."""
5
5
 
6
- __version__ = "1.9.34"
6
+ __version__ = "1.9.36"
@@ -76,6 +76,8 @@ from ..resolve import (
76
76
  WHEELHOUSE_PATH,
77
77
  apt_pins_in,
78
78
  apt_snapshot_in,
79
+ buildkit_proxy,
80
+ buildkit_proxy_options,
79
81
  locked_versions,
80
82
  )
81
83
  from ..resolve_conda import (
@@ -214,6 +216,11 @@ class Builder:
214
216
  importing this module needs no credentials.
215
217
  run
216
218
  How a subprocess is run, so a test can watch the argv without a daemon.
219
+ proxy
220
+ The build pool's egress proxy, as ``buildkitd`` reaches it (E1-06):
221
+ every step of the build goes through it. ``DATALAYER_BUILDKIT_PROXY``
222
+ by default; empty is a ``buildkitd`` whose steps reach the network
223
+ directly.
217
224
  """
218
225
 
219
226
  variant = "datalayer"
@@ -233,6 +240,7 @@ class Builder:
233
240
  run: Callable[..., subprocess.CompletedProcess[str]] | None = None,
234
241
  max_build_seconds: int = DEFAULT_MAX_BUILD_SECONDS,
235
242
  resolve_secret: Callable[..., str] = resolve_build_secret,
243
+ proxy: str | None = None,
236
244
  ) -> None:
237
245
  self._log = log or (lambda _line: None)
238
246
  self._credential = credential
@@ -241,6 +249,8 @@ class Builder:
241
249
  self._tlscert = tlscert or os.environ.get("DATALAYER_BUILDKIT_TLSCERT", "").strip()
242
250
  self._tlskey = tlskey or os.environ.get("DATALAYER_BUILDKIT_TLSKEY", "").strip()
243
251
  self._tlscacert = tlscacert or os.environ.get("DATALAYER_BUILDKIT_TLSCACERT", "").strip()
252
+ #: The build pool's egress proxy (E1-06), or `DATALAYER_BUILDKIT_PROXY`.
253
+ self._proxy = buildkit_proxy(proxy)
244
254
  self._region = region or os.environ.get("AWS_REGION", "us-east-1")
245
255
  self._ecr = ecr
246
256
  self._run = run or subprocess.run
@@ -530,6 +540,7 @@ class Builder:
530
540
  "--metadata-file",
531
541
  str(metadata),
532
542
  *self._cache_options(request),
543
+ *buildkit_proxy_options(self._proxy),
533
544
  *secret_args,
534
545
  ]
535
546
  self._log(f"Building {reference} from {request.resolved_base}")
@@ -96,13 +96,20 @@ end and the container exited before that first real exec ever reached it.
96
96
  The entrypoint now execs `sleep infinity` when it is given no arguments,
97
97
  and `"$@"` when it is — correct either way a caller invokes it.
98
98
 
99
- **A GPU size class is left buildable at `validate()`,** matching an existing
100
- test (`test_a_gpu_spec_is_buildable_on_modal_and_daytona`), and refused at
101
- `build()` instead, naming E2-17 — the same reasoning as Daytona's: the CUDA
102
- base is not published yet, so this cannot be reached in practice, and this
103
- builder does not know the launch-time `gpu=` argument to hand a size class
104
- either (D-20 leaves that to a later item; a GPU is Modal's own launch
105
- option, not an image property, per section 11.4 item 10).
99
+ **And a sandbox with no command is not given no arguments** (found live on
100
+ 2026-09-18): Modal keeps the base's own CMD, `start-jupyter.sh`, under the
101
+ new ENTRYPOINT where Docker would reset it, so the main process was a
102
+ Jupyter server, which exits within a minute and ends the sandbox. A smoke
103
+ test's restarted sandbox, started warm, died between checks 8 and 9. The
104
+ artifact now sets its CMD to `sleep infinity`, and `ModalSandbox` names the
105
+ same command when it launches an artifact, which also covers the ones built
106
+ before.
107
+
108
+ **A GPU is a launch option on Modal, not part of the image** (section 11.4
109
+ item 10, E2-17). A GPU version builds the same image any version does, on the
110
+ CUDA base; the spec's `accelerator` names one of Modal's GPUs, checked at
111
+ `validate`, and the smoke test launches the image on it (`gpu="T4"`,
112
+ `"H100:2"`) and adds check 11 to the core tier.
106
113
 
107
114
  **A build secret is attached to the `postInstall` steps that name it (E3-05).**
108
115
  `run_commands` takes a per-step `secrets=` collection, a mechanism E2B and
@@ -149,14 +156,42 @@ from ..files import files_step
149
156
  from ..redact import redact
150
157
  from ..resolve import WHEELHOUSE_IMAGE_PATH, apt_pins_in
151
158
  from ..resolve_conda import conda_lock_pip_requirements, is_conda_lock
152
- from ..spec import GPU_SIZE_CLASSES, BuildSecret, Environment, command_names_secret
159
+ from ..spec import BuildSecret, Environment, command_names_secret
153
160
  from .managed import ManagedBuilder
154
161
 
155
- __all__ = ["UNIMPLEMENTED_INSTRUCTIONS", "Builder"]
162
+ __all__ = ["MODAL_GPUS", "UNIMPLEMENTED_INSTRUCTIONS", "Builder", "modal_gpu"]
156
163
 
157
164
  #: What Modal's own Dockerfile builder does not implement (§6).
158
165
  UNIMPLEMENTED_INSTRUCTIONS = ("ONBUILD", "STOPSIGNAL", "VOLUME")
159
166
 
167
+ #: The GPUs a Modal sandbox takes by name (E2-17), as its `gpu=` spells them.
168
+ MODAL_GPUS: tuple[str, ...] = (
169
+ "T4",
170
+ "L4",
171
+ "A10G",
172
+ "L40S",
173
+ "A100",
174
+ "A100-40GB",
175
+ "A100-80GB",
176
+ "H100",
177
+ "H200",
178
+ "B200",
179
+ )
180
+
181
+
182
+ def modal_gpu(accelerator_type: str, count: int = 1) -> str | None:
183
+ """Modal's `gpu=` for an accelerator, or `None` when Modal has no such GPU.
184
+
185
+ `t4` and `a100_80gb` name what Modal calls `T4` and `A100-80GB`, and a
186
+ count above one is Modal's `"H100:2"`. An `RTX-4090` is Daytona's
187
+ vocabulary, not Modal's.
188
+ """
189
+ name = accelerator_type.strip().upper().replace("_", "-")
190
+ if name not in MODAL_GPUS:
191
+ return None
192
+ return f"{name}:{count}" if count > 1 else name
193
+
194
+
160
195
  #: `uv`, pinned the same way every other builder's bootstrap is (E1-04/E3-04).
161
196
  _UV_VERSION = "0.12.11"
162
197
 
@@ -180,6 +215,8 @@ _ENTRYPOINT_PATH = "/opt/datalayer/bin/entrypoint.sh"
180
215
  #: is given keeps it alive for that; `exec "$@"` still wins when something
181
216
  #: is, for a caller that does supply a command directly.
182
217
  _ENTRYPOINT_SCRIPT = '#!/bin/sh\nif [ "$#" -eq 0 ]; then exec sleep infinity; fi\nexec "$@"\n'
218
+ #: The artifact's CMD: what a sandbox started with no command of its own runs.
219
+ _KEEP_ALIVE_COMMAND = ("sleep", "infinity")
183
220
 
184
221
 
185
222
  def _modal_sdk() -> Any:
@@ -269,7 +306,10 @@ def _intermediates_of(built: Any) -> tuple[str, ...]:
269
306
  if hasattr(dependency, "deps"):
270
307
  walk(dependency)
271
308
  object_id = getattr(image, "object_id", None)
272
- if object_id and image is not built:
309
+ # Only images: the build's ECR secret is in `deps()` too, and has an
310
+ # `st-` id `ImageDelete` refuses ("not a valid Image ID", found live
311
+ # on 2026-09-18). It is deleted by the build itself, as a secret.
312
+ if object_id and image is not built and str(object_id).startswith("im-"):
273
313
  found.append(str(object_id))
274
314
 
275
315
  try:
@@ -393,6 +433,18 @@ class Builder(ManagedBuilder):
393
433
  field=f"spec.buildSecrets[{index}].mountAs",
394
434
  )
395
435
  )
436
+ accelerator = environment.spec.resources.accelerator
437
+ if accelerator != "none" and modal_gpu(accelerator.type) is None:
438
+ findings.append(
439
+ CapabilityFinding(
440
+ code="DL_ENV_CAPABILITY_UNSUPPORTED",
441
+ message=(
442
+ f"Modal has no GPU called `{accelerator.type}`; it offers "
443
+ + ", ".join(MODAL_GPUS)
444
+ ),
445
+ field="spec.resources.accelerator.type",
446
+ )
447
+ )
396
448
  return findings
397
449
 
398
450
  # -- Building -------------------------------------------------------------
@@ -406,18 +458,6 @@ class Builder(ManagedBuilder):
406
458
  launch, not here, for the same reason.
407
459
  """
408
460
  spec = request.environment.spec
409
- if request.size_class in GPU_SIZE_CLASSES:
410
- # `validate` leaves a GPU class buildable (`gpu = True`, D-20):
411
- # the CUDA base E2-17 has not published yet, so this cannot be
412
- # reached in practice — refused plainly here rather than
413
- # guessing at the launch-time `gpu=` argument this class needs
414
- # (section 11.4 item 10 is a launch concern, not a build one).
415
- raise EnvironmentsError(
416
- CAPABILITY_UNSUPPORTED,
417
- f"Modal runs `{request.size_class}` on its own GPUs, but the CUDA base this "
418
- "needs is E2-17's, not built yet",
419
- detail={"variant": self.variant, "missing": "E2-17"},
420
- )
421
461
  # Resolved before Modal is touched: a secret IAM will not give stops
422
462
  # the build with nothing to clean up in the owner's workspace.
423
463
  declared, values = self._resolved_secrets(request)
@@ -469,7 +509,19 @@ class Builder(ManagedBuilder):
469
509
  for command in files_step(request.environment, variant=self.variant):
470
510
  image = image.run_commands(command)
471
511
  image = _post_install(image, spec.commands.post_install, declared, step_secrets)
472
- image = image.workdir(_CONTENT_DIR).entrypoint([_ENTRYPOINT_PATH])
512
+ # The base's own CMD is `start-jupyter.sh`, and Modal keeps
513
+ # it under a new ENTRYPOINT where Docker would reset it: a
514
+ # sandbox started with no command ran a Jupyter server as its
515
+ # main process, which exits within a minute and takes the
516
+ # sandbox with it (found live on 2026-09-18, the restarted
517
+ # sandbox of a smoke test dying mid-tier). What holds the
518
+ # container is `sleep`; Jupyter is started by exec, when
519
+ # asked (`ModalSandbox.prepare_jupyter_server`).
520
+ image = (
521
+ image.workdir(_CONTENT_DIR)
522
+ .entrypoint([_ENTRYPOINT_PATH])
523
+ .cmd(list(_KEEP_ALIVE_COMMAND))
524
+ )
473
525
 
474
526
  logged: list[str] = []
475
527
  buffer = io.StringIO()
@@ -717,11 +769,13 @@ class Builder(ManagedBuilder):
717
769
  )
718
770
  from ...modal_sandbox import ModalSandbox
719
771
  from ...models import SandboxConfig
720
- from ..conformance import expected_packages, run_core_tier
772
+ from ..conformance import expected_packages, run_accelerator_check, run_core_tier
721
773
 
722
774
  sdk = self._modal_sdk()
775
+ accelerator = environment.spec.resources.accelerator
776
+ gpu = None if accelerator == "none" else modal_gpu(accelerator.type, accelerator.count)
723
777
  sandbox = ModalSandbox(
724
- config=SandboxConfig(name=f"smoke-{artifact.provider_artifact_id}"),
778
+ config=SandboxConfig(name=f"smoke-{artifact.provider_artifact_id}", gpu=gpu),
725
779
  app_name=f"dl-{environment.metadata.name}",
726
780
  image_id=artifact.provider_artifact_id,
727
781
  client=self._client(sdk),
@@ -729,13 +783,21 @@ class Builder(ManagedBuilder):
729
783
  self._log(f"Launching {artifact.provider_artifact_id} to smoke-test it")
730
784
  try:
731
785
  sandbox.start()
732
- return run_core_tier(
786
+ result = run_core_tier(
733
787
  sandbox,
734
788
  python_version=environment.spec.language.version,
735
789
  expected_packages=expected_packages(environment, lock_text or ""),
736
790
  secret_values=tuple(secret_values),
737
791
  restart=lambda: self._restart(sandbox),
738
792
  )
793
+ if gpu is not None:
794
+ # The core tier passes on a machine with no GPU: a GPU version
795
+ # is the version its spec describes only when its GPUs are
796
+ # visible and its CUDA is the spec's (check 11, E2-17).
797
+ result.checks.append(
798
+ run_accelerator_check(sandbox, cuda=accelerator.cuda, count=accelerator.count)
799
+ )
800
+ return result
739
801
  except EnvironmentsError:
740
802
  raise
741
803
  except Exception as error:
@@ -83,6 +83,7 @@ if TYPE_CHECKING:
83
83
  __all__ = [
84
84
  "APT_PIN_PREFIX",
85
85
  "APT_SNAPSHOT_PREFIX",
86
+ "BUILDKIT_PROXY_ENV",
86
87
  "CONSTRAINTS_PATH",
87
88
  "COVERAGE_PREFIX",
88
89
  "DOCKERFILE_COVERAGE",
@@ -98,6 +99,8 @@ __all__ = [
98
99
  "apt_pins",
99
100
  "apt_pins_in",
100
101
  "apt_snapshot_in",
102
+ "buildkit_proxy",
103
+ "buildkit_proxy_options",
101
104
  "lock_document",
102
105
  "locked_versions",
103
106
  "merge_requirements",
@@ -145,6 +148,46 @@ DOCKERFILE_COVERAGE = (
145
148
  APT_SNAPSHOT_PREFIX = "# datalayer-apt-snapshot: "
146
149
  _SNAPSHOT_ID = re.compile(r"^\d{8}T\d{6}Z$")
147
150
 
151
+ #: Where a build's own steps reach the network through, when the build pool
152
+ #: gives them nowhere else to go (E1-06): an HTTP proxy that allows only the
153
+ #: package indexes, the snapshot mirrors and the registries. The address is
154
+ #: the one `buildkitd` itself sees, since a step runs in its network.
155
+ BUILDKIT_PROXY_ENV = "DATALAYER_BUILDKIT_PROXY"
156
+ _PROXY_URL = re.compile(r"^http://[A-Za-z0-9.\-]+:\d{1,5}$")
157
+
158
+
159
+ def buildkit_proxy(proxy: str | None = None) -> str:
160
+ """The proxy a build's steps go through: ``proxy``, or the environment's.
161
+
162
+ Empty is no proxy, which is what a `buildkitd` with open egress needs
163
+ (`plane local`'s own). Anything that is not ``http://host:port`` is
164
+ refused rather than handed to every package manager of every build.
165
+ """
166
+ value = (os.environ.get(BUILDKIT_PROXY_ENV, "") if proxy is None else proxy).strip()
167
+ if value and not _PROXY_URL.match(value):
168
+ raise ValueError(f"{BUILDKIT_PROXY_ENV} must be http://host:port, not {value!r}")
169
+ return value
170
+
171
+
172
+ def buildkit_proxy_options(proxy: str) -> list[str]:
173
+ """The ``buildctl`` options that send a build's steps through ``proxy``.
174
+
175
+ The Dockerfile frontend predefines these build args: a ``RUN`` step sees
176
+ them without an ``ARG``, they never reach the image's config or history,
177
+ and they do not change a cache key. Both spellings, since ``apt`` and
178
+ ``curl`` read only the lower-case one for plain HTTP and ``uv``, ``pip``
179
+ and ``micromamba`` read either.
180
+ """
181
+ if not proxy:
182
+ return []
183
+ options: list[str] = []
184
+ for name in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
185
+ options += ["--opt", f"build-arg:{name}={proxy}"]
186
+ for name in ("NO_PROXY", "no_proxy"):
187
+ options += ["--opt", f"build-arg:{name}=127.0.0.1,localhost"]
188
+ return options
189
+
190
+
148
191
  #: How a protected pin is recorded in the lock.
149
192
  PROTECTED_PIN_PREFIX = "# datalayer-protected: "
150
193
 
@@ -623,8 +666,11 @@ class BuildkitResolveRunner:
623
666
  tlscacert: str | None = None,
624
667
  apt_snapshot: str = "",
625
668
  timeout: float = 900.0,
669
+ proxy: str | None = None,
626
670
  ) -> None:
627
671
  self._buildctl = (shutil.which("buildctl") or "") if buildctl is None else buildctl
672
+ #: The build pool's egress proxy (E1-06), or `DATALAYER_BUILDKIT_PROXY`.
673
+ self._proxy = buildkit_proxy(proxy)
628
674
  #: The build pool's `buildkitd` takes mTLS connections only
629
675
  #: (PLAN_ENV.md E1-06); a plain-socket one, such as `plane local`'s
630
676
  #: own ephemeral daemon, needs none of these three. All or nothing,
@@ -761,6 +807,7 @@ class BuildkitResolveRunner:
761
807
  f"dockerfile={root}",
762
808
  "--output",
763
809
  f"type=local,dest={out}",
810
+ *buildkit_proxy_options(self._proxy),
764
811
  ]
765
812
  say(f"Solving the lock in {request.base_reference}")
766
813
  try:
@@ -68,6 +68,8 @@ from .resolve import (
68
68
  WHEELHOUSE_PATH,
69
69
  MergedRequirements,
70
70
  ProtectedPin,
71
+ buildkit_proxy,
72
+ buildkit_proxy_options,
71
73
  merge_requirements,
72
74
  )
73
75
 
@@ -684,8 +686,11 @@ class BuildkitCondaResolveRunner:
684
686
  tlskey: str | None = None,
685
687
  tlscacert: str | None = None,
686
688
  timeout: float = 1200.0,
689
+ proxy: str | None = None,
687
690
  ) -> None:
688
691
  self._buildctl = (shutil.which("buildctl") or "") if buildctl is None else buildctl
692
+ #: The build pool's egress proxy (E1-06), or `DATALAYER_BUILDKIT_PROXY`.
693
+ self._proxy = buildkit_proxy(proxy)
689
694
  self._tlscert = tlscert or ""
690
695
  self._tlskey = tlskey or ""
691
696
  self._tlscacert = tlscacert or ""
@@ -778,6 +783,7 @@ class BuildkitCondaResolveRunner:
778
783
  f"dockerfile={root}",
779
784
  "--output",
780
785
  f"type=local,dest={out}",
786
+ *buildkit_proxy_options(self._proxy),
781
787
  ]
782
788
  say(f"Solving the conda lock in {request.base_reference}")
783
789
  try:
@@ -441,7 +441,16 @@ class ModalSandbox(Sandbox):
441
441
  for mount_path, volume_id in self._volume_mounts.requested.items()
442
442
  }
443
443
 
444
- self._sandbox = modal.Sandbox.create(**create_kwargs)
444
+ # An Environments artifact's main process only holds the container:
445
+ # everything this class runs in it is an exec, Jupyter included
446
+ # (`prepare_jupyter_server`). Named here rather than left to the
447
+ # image, since an artifact built before code-sandboxes 1.9.36 carries
448
+ # its base's `start-jupyter.sh` as its CMD, which Modal runs under the
449
+ # entrypoint; that server exits within a minute and ends the sandbox
450
+ # (found live on 2026-09-18: a smoke test's restarted sandbox died
451
+ # between checks 8 and 9).
452
+ command = ("sleep", "infinity") if self._image_id else ()
453
+ self._sandbox = modal.Sandbox.create(*command, **create_kwargs)
445
454
  self._volume_mounts.created()
446
455
  self._start_driver()
447
456
 
@@ -105,6 +105,10 @@ filterwarnings = [
105
105
  # use of the client in a test became an exception. It took the live matrix
106
106
  # against a real Datalayer sandbox down before the sandbox was reached.
107
107
  "module:Datalayer is migrating its paths to use standard platformdirs:DeprecationWarning",
108
+ # The Daytona SDK (0.205) reads its own deprecated `DaytonaConfig.server_url`
109
+ # inside `Daytona(config)`: with `error` in force, every live Daytona test
110
+ # failed before reaching Daytona, and a nightly that skipped hid it.
111
+ "ignore:`server_url` is deprecated:DeprecationWarning",
108
112
  ]
109
113
 
110
114
  [tool.mypy]
@@ -326,10 +326,10 @@ def test_files_and_contents_build_are_baked_together() -> None:
326
326
 
327
327
 
328
328
  def test_a_contents_build_entry_without_its_digest_will_not_parse() -> None:
329
- with pytest.raises(Exception):
330
- environment(
331
- contents_build=[{"source": "https://data.example/x", "path": "/opt/x"}]
332
- )
329
+ with pytest.raises(EnvironmentsError) as raised:
330
+ environment(contents_build=[{"source": "https://data.example/x", "path": "/opt/x"}])
331
+ assert raised.value.code.code == "DL_ENV_SPEC_INVALID"
332
+ assert "sha256" in raised.value.message
333
333
 
334
334
 
335
335
  def test_the_neutral_modules_import_no_provider_sdk() -> None:
@@ -163,9 +163,7 @@ def test_a_plain_build_context_passes() -> None:
163
163
  ),
164
164
  ],
165
165
  )
166
- def test_a_forbidden_context_member_is_refused(
167
- entry: BuildContextEntry, message: str
168
- ) -> None:
166
+ def test_a_forbidden_context_member_is_refused(entry: BuildContextEntry, message: str) -> None:
169
167
  findings = validate_build_context([entry])
170
168
  assert any(finding.message == message for finding in findings), findings
171
169
  with pytest.raises(EnvironmentsError) as refused:
@@ -726,6 +726,27 @@ class TestBuildingAndPushing:
726
726
  a_builder(run=buildctl, tlscert="/certs/client/tls.crt").build(a_request())
727
727
  assert not any(arg.startswith("--tls") for arg in buildctl.argv)
728
728
 
729
+ def test_the_steps_go_through_the_pools_proxy(self, monkeypatch) -> None:
730
+ """The build pool lets a step reach the network only through its
731
+ proxy (E1-06), so every step is handed it, in both spellings."""
732
+ monkeypatch.setenv("DATALAYER_BUILDKIT_PROXY", "http://127.0.0.1:3128")
733
+ buildctl = Buildctl()
734
+ a_builder(run=buildctl).build(a_request())
735
+ for name in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
736
+ assert f"build-arg:{name}=http://127.0.0.1:3128" in buildctl.argv
737
+ assert "build-arg:NO_PROXY=127.0.0.1,localhost" in buildctl.argv
738
+
739
+ def test_no_proxy_when_none_is_set(self, monkeypatch) -> None:
740
+ """A `buildkitd` whose steps reach the network directly needs none."""
741
+ monkeypatch.delenv("DATALAYER_BUILDKIT_PROXY", raising=False)
742
+ buildctl = Buildctl()
743
+ a_builder(run=buildctl).build(a_request())
744
+ assert not any("proxy" in arg.lower() for arg in buildctl.argv)
745
+
746
+ def test_a_proxy_that_is_not_http_host_port_is_refused(self) -> None:
747
+ with pytest.raises(ValueError, match="DATALAYER_BUILDKIT_PROXY"):
748
+ a_builder(proxy="socks5://127.0.0.1:1080")
749
+
729
750
  def test_the_docker_config_does_not_outlive_the_build(self) -> None:
730
751
  """A registry password base64'd into a file the worker keeps forever
731
752
  is a credential leak on disk, whichever way the build ends (found on
@@ -243,25 +243,12 @@ class TestDaytona:
243
243
 
244
244
 
245
245
  class TestModal:
246
- """`xfail(strict=False)`: checks 5 (imports) and 6 (filesystem) fail —
247
- confirmed live, 2026-09-13, the session driver refusing to start
248
- against a sandbox that reports itself already shutting down, while 1,
249
- 2, 3, 4, 7, 8 and 9 all pass. This used to be attributed to the
250
- identity gap (checks 1/2 failed too, then) — closed since, live: a
251
- contract-built artifact's `ModalSandbox` now drops its driver to
252
- `1000:100` (`_start_driver`, gated on `image_id` so a plain
253
- `ModalSandbox` is unaffected), and checks 1/2 pass with it. 5/6 turned
254
- out to be a separate, still-unexplained issue: a plain, non-Environments
255
- `ModalSandbox` runs several sequential snippets with no trouble at all
256
- over the same span, so this is specific to a contract-built artifact's
257
- image under repeated `exec`. An `XPASS` here means that's closed too."""
246
+ """No `xfail` since 2026-09-18: a real artifact, built by the production
247
+ builder and launched by image id, passes all nine checks, check 9 included
248
+ with a secret to scan for, right after check 8. The break recorded under
249
+ E2-05 (the exec channel gone after check 8) no longer reproduces; the
250
+ kernel-restart fix of 1.9.28, check 7's own path, is the likeliest cause."""
258
251
 
259
- @pytest.mark.xfail(
260
- reason="checks 5 and 6 fail on a session driver restart the sandbox refuses, "
261
- "specific to a contract-built artifact and not yet root-caused (identity, "
262
- "checks 1 and 2, is fixed)",
263
- strict=False,
264
- )
265
252
  def test_build_launch_and_the_core_tier(self, real_lock: tuple[str, str]) -> None:
266
253
  _skip_unless_available("modal")
267
254
  import modal