code-sandboxes 1.9.27__tar.gz → 1.9.29__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.27 → code_sandboxes-1.9.29}/PKG-INFO +1 -1
  2. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/__version__.py +1 -1
  3. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/adapters/datalayer.py +41 -10
  4. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/adapters/daytona.py +34 -6
  5. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/contract.py +190 -8
  6. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/resolve.py +41 -18
  7. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/spec.py +15 -3
  8. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/jupyter_server_sandbox.py +11 -7
  9. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_datalayer_builder.py +1 -1
  10. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_daytona_builder.py +5 -0
  11. code_sandboxes-1.9.29/tests/test_environment_dockerfile_source.py +222 -0
  12. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_resolve.py +0 -11
  13. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_jupyter_server.py +28 -0
  14. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.github/workflows/build.yml +0 -0
  15. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.github/workflows/environments-live.yml +0 -0
  16. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.github/workflows/py-code-style.yml +0 -0
  17. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.github/workflows/py-tests.yml +0 -0
  18. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.github/workflows/py-typing.yml +0 -0
  19. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.github/workflows/reusable-python.yml +0 -0
  20. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.gitignore +0 -0
  21. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.licenserc.yaml +0 -0
  22. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.pre-commit-config.yaml +0 -0
  23. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/.vscode/settings.json +0 -0
  24. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/CHANGELOG.md +0 -0
  25. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/Dockerfile +0 -0
  26. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/LICENSE +0 -0
  27. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/Makefile +0 -0
  28. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/README.md +0 -0
  29. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/__init__.py +0 -0
  30. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/base.py +0 -0
  31. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/bridge_mount.py +0 -0
  32. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/builds.py +0 -0
  33. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/cli.py +0 -0
  34. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/client.py +0 -0
  35. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/cloudflare_sandbox.py +0 -0
  36. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/commands.py +0 -0
  37. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/console.py +0 -0
  38. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/contents.py +0 -0
  39. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/coreweave_sandbox.py +0 -0
  40. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/datalayer_sandbox.py +0 -0
  41. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/daytona_sandbox.py +0 -0
  42. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/docker_sandbox.py +0 -0
  43. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/e2b_sandbox.py +0 -0
  44. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/__init__.py +0 -0
  45. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/__main__.py +0 -0
  46. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/accounts.py +0 -0
  47. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/adapters/__init__.py +0 -0
  48. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/adapters/e2b.py +0 -0
  49. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/adapters/managed.py +0 -0
  50. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/adapters/modal.py +0 -0
  51. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/attest.py +0 -0
  52. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/bases.py +0 -0
  53. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/breaker.py +0 -0
  54. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/build_secrets.py +0 -0
  55. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/builders.py +0 -0
  56. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/canonical.py +0 -0
  57. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/conformance.py +0 -0
  58. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  59. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  60. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  61. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/doctor/__init__.py +0 -0
  62. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/doctor/build.py +0 -0
  63. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  64. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  65. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/errors.py +0 -0
  66. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/files.py +0 -0
  67. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/image_import.py +0 -0
  68. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/lifecycle.py +0 -0
  69. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/policy.py +0 -0
  70. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/redact.py +0 -0
  71. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/resolve_conda.py +0 -0
  72. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/environments/schema.py +0 -0
  73. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/eval_sandbox.py +0 -0
  74. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/exceptions.py +0 -0
  75. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/filesystem.py +0 -0
  76. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/google_colab.py +0 -0
  77. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/google_colab_sandbox.py +0 -0
  78. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/interfaces.py +0 -0
  79. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/jupyter_ingress.py +0 -0
  80. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/kaggle.py +0 -0
  81. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/kaggle_execute.py +0 -0
  82. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/kaggle_live.py +0 -0
  83. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/kaggle_sandbox.py +0 -0
  84. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/lifecycle.py +0 -0
  85. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/manage.py +0 -0
  86. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/modal_sandbox.py +0 -0
  87. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/models.py +0 -0
  88. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/monty_sandbox.py +0 -0
  89. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/provider_ingress.py +0 -0
  90. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/code_sandboxes/providers.py +0 -0
  91. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docker/Dockerfile +0 -0
  92. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/.gitignore +0 -0
  93. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/LICENSE +0 -0
  94. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/Makefile +0 -0
  95. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/README.md +0 -0
  96. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/babel.config.js +0 -0
  97. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/api-reference/_category_.yml +0 -0
  98. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/api-reference/index.mdx +0 -0
  99. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/cli/_category_.yml +0 -0
  100. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/cli/index.mdx +0 -0
  101. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/contribute/_category_.yml +0 -0
  102. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/contribute/index.mdx +0 -0
  103. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/environments/_category_.yml +0 -0
  104. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/environments/contract.mdx +0 -0
  105. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/environments/index.mdx +0 -0
  106. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/environments/specification.mdx +0 -0
  107. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/environments/spike-2026-09.mdx +0 -0
  108. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/examples/_category_.yml +0 -0
  109. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/examples/index.mdx +0 -0
  110. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/guide/_category_.yml +0 -0
  111. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/guide/index.mdx +0 -0
  112. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/index.mdx +0 -0
  113. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/install/_category_.yml +0 -0
  114. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/install/index.mdx +0 -0
  115. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/provider-ingress.mdx +0 -0
  116. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/_category_.yml +0 -0
  117. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/cloudflare.mdx +0 -0
  118. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/coreweave.mdx +0 -0
  119. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/datalayer.mdx +0 -0
  120. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/daytona.mdx +0 -0
  121. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/docker.mdx +0 -0
  122. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/e2b.mdx +0 -0
  123. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/eval.mdx +0 -0
  124. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/google-colab.mdx +0 -0
  125. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/index.mdx +0 -0
  126. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/jupyter-server.mdx +0 -0
  127. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/kaggle.mdx +0 -0
  128. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/modal.mdx +0 -0
  129. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docs/providers/monty.mdx +0 -0
  130. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/docusaurus.config.js +0 -0
  131. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/package.json +0 -0
  132. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/sidebars.js +0 -0
  133. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/components/HomepageRow1.js +0 -0
  134. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/components/HomepageRow1.module.css +0 -0
  135. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/components/HomepageRow2.js +0 -0
  136. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/components/HomepageRow2.module.css +0 -0
  137. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/components/HomepageRow3.js +0 -0
  138. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/components/HomepageRow3.module.css +0 -0
  139. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/css/custom.css +0 -0
  140. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/pages/index.module.css +0 -0
  141. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/pages/markdown-page.md +0 -0
  142. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/theme/CustomDocItem.tsx +0 -0
  143. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/src/theme/IconsTitle.tsx +0 -0
  144. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/bricks.svg +0 -0
  145. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/building-construction.svg +0 -0
  146. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/cloud-sun.svg +0 -0
  147. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/cloud.svg +0 -0
  148. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/collaboration.svg +0 -0
  149. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/copy-left.svg +0 -0
  150. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/datalayer/logo.png +0 -0
  151. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/datalayer/logo.svg +0 -0
  152. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/datalayer/personas/business.svg +0 -0
  153. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  154. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/datalayer/personas/devops.svg +0 -0
  155. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/datalayer/project/project-line.svg +0 -0
  156. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/favicon.ico +0 -0
  157. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/jupyter.svg +0 -0
  158. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/open-source.svg +0 -0
  159. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/react-js.svg +0 -0
  160. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/react-js2.svg +0 -0
  161. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/ringed-planet.svg +0 -0
  162. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/robot.svg +0 -0
  163. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/rocket.svg +0 -0
  164. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/shield.svg +0 -0
  165. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/sparkles.svg +0 -0
  166. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/streamlit.gif +0 -0
  167. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/super-hero.svg +0 -0
  168. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/target.svg +0 -0
  169. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/docs/static/img/thumbs-up.svg +0 -0
  170. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/README.md +0 -0
  171. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/Makefile +0 -0
  172. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/cloudflare_sandbox_example.py +0 -0
  173. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/coreweave_sandbox_example.py +0 -0
  174. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/datalayer_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/daytona_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/docker_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/e2b_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/eval_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/google_colab_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/kaggle_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/modal_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/exec/monty_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/Makefile +0 -0
  185. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/cloudflare_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/coreweave_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/datalayer_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/daytona_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/docker_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/e2b_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/eval_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/google_colab_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/kaggle_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/modal_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/examples/repl/monty_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/pyproject.toml +0 -0
  198. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/schemas/environment-v1alpha1.json +0 -0
  199. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/__init__.py +0 -0
  200. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/conftest.py +0 -0
  201. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  202. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_bridge_mount.py +0 -0
  203. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_builds.py +0 -0
  204. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_cli_exec.py +0 -0
  205. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_cli_repl.py +0 -0
  206. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_client.py +0 -0
  207. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_client_contents.py +0 -0
  208. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_client_files.py +0 -0
  209. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_client_outcome_error_output.py +0 -0
  210. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_cloudflare.py +0 -0
  211. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_commands_envelope.py +0 -0
  212. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_console.py +0 -0
  213. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_contents_contract.py +0 -0
  214. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_contents_live_matrix.py +0 -0
  215. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_coreweave.py +0 -0
  216. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_datalayer_sandbox.py +0 -0
  217. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_daytona.py +0 -0
  218. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_e2b.py +0 -0
  219. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_accounts.py +0 -0
  220. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_attest.py +0 -0
  221. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_bases.py +0 -0
  222. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_breaker.py +0 -0
  223. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_build_secrets.py +0 -0
  224. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_builders.py +0 -0
  225. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_canonical.py +0 -0
  226. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_conformance.py +0 -0
  227. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_constraints.py +0 -0
  228. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_contract.py +0 -0
  229. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_cross_variant_report.py +0 -0
  230. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_doctor.py +0 -0
  231. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_e2b_builder.py +0 -0
  232. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_errors.py +0 -0
  233. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_image_import.py +0 -0
  234. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_lifecycle.py +0 -0
  235. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_live_matrix.py +0 -0
  236. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_managed_builders.py +0 -0
  237. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_modal_builder.py +0 -0
  238. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_organization_policy.py +0 -0
  239. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_redact.py +0 -0
  240. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_resolve_conda.py +0 -0
  241. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_spec.py +0 -0
  242. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_environment_spec_organization_policy.py +0 -0
  243. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_eval.py +0 -0
  244. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  245. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_factory.py +0 -0
  246. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_google_colab.py +0 -0
  247. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_integration.py +0 -0
  248. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_jupyter_ingress.py +0 -0
  249. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_jupyter_server_streaming.py +0 -0
  250. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_kaggle.py +0 -0
  251. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_kaggle_execute.py +0 -0
  252. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_kaggle_live.py +0 -0
  253. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_kaggle_session.py +0 -0
  254. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_kernel_client_compatibility.py +0 -0
  255. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_lifecycle_vocabulary.py +0 -0
  256. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_manage.py +0 -0
  257. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_manage_datalayer.py +0 -0
  258. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_modal_google_colab_sandbox.py +0 -0
  259. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_modal_session.py +0 -0
  260. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_models.py +0 -0
  261. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_output_line_fidelity.py +0 -0
  262. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_provider_catalog_names.py +0 -0
  263. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_provider_ingress.py +0 -0
  264. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_providers.py +0 -0
  265. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_sandboxes.py +0 -0
  266. {code_sandboxes-1.9.27 → code_sandboxes-1.9.29}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.27
3
+ Version: 1.9.29
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.27"
6
+ __version__ = "1.9.29"
@@ -61,7 +61,7 @@ from ..builders import (
61
61
  owner_cache_repository,
62
62
  owner_repository,
63
63
  )
64
- from ..contract import SANDBOX_CONTRACT_V1
64
+ from ..contract import SANDBOX_CONTRACT_V1, pin_dockerfile_base
65
65
  from ..errors import (
66
66
  ARTIFACT_MISSING,
67
67
  BUILD_FAILED,
@@ -145,6 +145,41 @@ def _secret_mount(secret: BuildSecret) -> str:
145
145
  return f"--mount=type=secret,id={secret.id},env={secret.name}"
146
146
 
147
147
 
148
+ def _head(request: BuildRequest, authored: Any) -> list[str]:
149
+ """The Dockerfile's first lines: the frontend, what built it, and its base.
150
+
151
+ For a `dockerfile` source (E3-03) the base is the author's own
152
+ Dockerfile, its `FROM` pinned to the digest the resolver chose; the
153
+ contract's lines follow it, whatever came before them.
154
+ """
155
+ lines = [
156
+ f"# syntax={DOCKERFILE_FRONTEND}",
157
+ f"# Generated by Datalayer for {request.environment.metadata.name} "
158
+ f"v{request.version}, build {request.build_uid}.",
159
+ f"# Lock: {request.lock_digest}",
160
+ ]
161
+ if authored is not None:
162
+ lines.append(pin_dockerfile_base(authored.content, request.resolved_base).rstrip("\n"))
163
+ else:
164
+ lines.append(f"FROM {request.resolved_base}")
165
+ return lines
166
+
167
+
168
+ def _pip_lock_command(find_links: str, *, authored: bool) -> str:
169
+ """Install the pip lock: `sync` to it, or `install` it over an authored Dockerfile.
170
+
171
+ `sync` removes whatever the lock does not name, and a `dockerfile`
172
+ source's lock does not name what the Dockerfile itself installed
173
+ (`DOCKERFILE_COVERAGE`). The lock's pins, the protected ones included,
174
+ win either way.
175
+ """
176
+ verb, target = ("install", "-r ") if authored else ("sync", "")
177
+ return (
178
+ f"uv pip {verb} --system --require-hashes --find-links {find_links} "
179
+ f"{target}/opt/datalayer/lock.txt"
180
+ )
181
+
182
+
148
183
  class Builder:
149
184
  """The Datalayer variant: BuildKit into the owner's ECR repository.
150
185
 
@@ -224,7 +259,7 @@ class Builder:
224
259
  # `image` resolves from an imported reference instead of an
225
260
  # approved base (E3-04). Neither changes how this builder itself
226
261
  # builds, once the resolver has done its part.
227
- build_sources=("packages", "dependencyFile", "image"),
262
+ build_sources=("packages", "dependencyFile", "image", "dockerfile"),
228
263
  package_managers=("uv", "pip"),
229
264
  # Nothing is forbidden by the builder itself: BuildKit is the
230
265
  # reference implementation of a Dockerfile, and what the contract
@@ -301,12 +336,9 @@ class Builder:
301
336
  """
302
337
  spec = request.environment.spec
303
338
  apt = apt_pins_in(request.lock_text)
304
- lines = [
305
- f"# syntax={DOCKERFILE_FRONTEND}",
306
- f"# Generated by Datalayer for {request.environment.metadata.name} "
307
- f"v{request.version}, build {request.build_uid}.",
308
- f"# Lock: {request.lock_digest}",
309
- f"FROM {request.resolved_base}",
339
+ authored = spec.build.dockerfile if spec.build.source == "dockerfile" else None
340
+ lines = _head(request, authored)
341
+ lines += [
310
342
  f'LABEL io.datalayer.environment="{request.environment.metadata.name}" \\',
311
343
  f' io.datalayer.environment.uid="{request.environment_uid}" \\',
312
344
  f' io.datalayer.version="{request.version}" \\',
@@ -385,8 +417,7 @@ class Builder:
385
417
  # `--find-links` for what no index has — a protected pin's
386
418
  # own wheel, the fork's local version above all (E1-04).
387
419
  "RUN --mount=type=cache,target=/root/.cache/uv "
388
- f"uv pip sync --system --require-hashes --find-links {find_links} "
389
- "/opt/datalayer/lock.txt",
420
+ + _pip_lock_command(find_links, authored=authored is not None),
390
421
  ]
391
422
  )
392
423
  # A build secret is mounted on the postInstall commands that name it
@@ -102,7 +102,7 @@ from ..builders import (
102
102
  CapabilityFinding,
103
103
  ValidationResult,
104
104
  )
105
- from ..contract import SANDBOX_CONTRACT_V1
105
+ from ..contract import SANDBOX_CONTRACT_V1, pin_dockerfile_base
106
106
  from ..errors import (
107
107
  ARTIFACT_MISSING,
108
108
  BUILD_FAILED,
@@ -148,6 +148,19 @@ _CPU_RESOURCES: dict[str, dict[str, int]] = {
148
148
  }
149
149
 
150
150
 
151
+ def _pip_lock_command(*, authored: bool) -> str:
152
+ """Install the pip lock: `sync` to it, or `install` it over an authored Dockerfile.
153
+
154
+ `sync` would remove what the Dockerfile installed and the lock does not
155
+ name (E3-03, `DOCKERFILE_COVERAGE`).
156
+ """
157
+ verb, target = ("install", "-r ") if authored else ("sync", "")
158
+ return (
159
+ f"uv pip {verb} --system --require-hashes "
160
+ f"--find-links {WHEELHOUSE_IMAGE_PATH} {target}{_LOCK_PATH}"
161
+ )
162
+
163
+
151
164
  def _daytona_sdk() -> Any:
152
165
  try:
153
166
  import daytona
@@ -339,7 +352,8 @@ class Builder(ManagedBuilder):
339
352
  lock_file = Path(scratch) / "lock.txt"
340
353
  lock_file.write_text(request.lock_text, encoding="utf-8")
341
354
 
342
- image = sdk.Image.base(request.resolved_base)
355
+ authored = spec.build.dockerfile if spec.build.source == "dockerfile" else None
356
+ image = self._starting_image(sdk, request, authored, Path(scratch))
343
357
  # `env` before anything installs, the same order the
344
358
  # Datalayer and E2B builders keep: a package that compiles
345
359
  # against a library found through an env var behaves
@@ -394,10 +408,7 @@ class Builder(ManagedBuilder):
394
408
  # Datalayer and E2B builders give: a user install lands
395
409
  # under the content directory's own home, which the
396
410
  # runtime mounts over.
397
- image = image.run_commands(
398
- "uv pip sync --system --require-hashes "
399
- f"--find-links {WHEELHOUSE_IMAGE_PATH} {_LOCK_PATH}"
400
- )
411
+ image = image.run_commands(_pip_lock_command(authored=authored is not None))
401
412
  image = image.dockerfile_commands([f"USER 1000:100\nWORKDIR {_CONTENT_DIR}"])
402
413
  for command in files_step(request.environment, variant=self.variant):
403
414
  image = image.run_commands(command)
@@ -477,6 +488,23 @@ class Builder(ManagedBuilder):
477
488
  contract_version=spec.contract or SANDBOX_CONTRACT_V1.version,
478
489
  )
479
490
 
491
+ @staticmethod
492
+ def _starting_image(sdk: Any, request: BuildRequest, authored: Any, scratch: Path) -> Any:
493
+ """The image the chain starts from: the base, or the author's Dockerfile on it.
494
+
495
+ A Dockerfile source (E3-03) starts from the author's own Dockerfile,
496
+ its base pinned to the digest the resolver chose — which is also what
497
+ the build's registry entry lets Daytona pull. The contract's steps are
498
+ chained after it either way.
499
+ """
500
+ if authored is None:
501
+ return sdk.Image.base(request.resolved_base)
502
+ dockerfile = scratch / "Dockerfile"
503
+ dockerfile.write_text(
504
+ pin_dockerfile_base(authored.content, request.resolved_base), encoding="utf-8"
505
+ )
506
+ return sdk.Image.from_dockerfile(str(dockerfile))
507
+
480
508
  def _resources(self, sdk: Any, size_class: str) -> Any:
481
509
  """The CPU resources a size class bakes into the snapshot (§11.3 item 4)."""
482
510
  shape = _CPU_RESOURCES.get(size_class, _CPU_RESOURCES["small"])
@@ -24,13 +24,13 @@ import argparse
24
24
  import re
25
25
  import shlex
26
26
  import sys
27
- from collections.abc import Sequence
27
+ from collections.abc import Mapping, Sequence
28
28
  from dataclasses import dataclass
29
29
  from pathlib import Path
30
30
 
31
31
  from pydantic import BaseModel, ConfigDict
32
32
 
33
- from .bases import is_approved_repository
33
+ from .bases import APPROVED_BASES, ApprovedBase, is_approved_repository
34
34
  from .errors import CAPABILITY_UNSUPPORTED, SPEC_INVALID, EnvironmentsError
35
35
 
36
36
  __all__ = [
@@ -43,14 +43,18 @@ __all__ = [
43
43
  "BuildContextEntry",
44
44
  "BuildContextFinding",
45
45
  "ContractRow",
46
+ "DockerfileBase",
46
47
  "DockerfileFinding",
47
48
  "DockerfileInstruction",
48
49
  "SandboxContract",
49
50
  "check_build_context",
50
51
  "check_dockerfile",
51
52
  "contract_markdown",
53
+ "dockerfile_base",
54
+ "dockerfile_findings_for_build",
52
55
  "get_contract",
53
56
  "parse_dockerfile",
57
+ "pin_dockerfile_base",
54
58
  "validate_build_context",
55
59
  "validate_dockerfile",
56
60
  ]
@@ -461,6 +465,188 @@ def check_dockerfile(text: str, *, contract: SandboxContract = SANDBOX_CONTRACT_
461
465
  )
462
466
 
463
467
 
468
+ # --- A Dockerfile source's base, and how it is built (E3-03) -----------------------------
469
+
470
+
471
+ @dataclass(frozen=True)
472
+ class DockerfileBase:
473
+ """The approved base a Dockerfile builds on: its channel, and the `FROM` lines naming it."""
474
+
475
+ ref: str
476
+ channel: str
477
+ lines: tuple[int, ...]
478
+
479
+
480
+ @dataclass(frozen=True)
481
+ class _ApprovedFrom:
482
+ line: int
483
+ image: str
484
+ ref: str
485
+ tag: str
486
+ digest: str
487
+
488
+
489
+ def _from_image(instruction: DockerfileInstruction) -> tuple[str, str | None]:
490
+ """A `FROM`'s image and its stage name, flags skipped."""
491
+ tokens = _tokens(instruction.arguments)
492
+ for index, argument in enumerate(tokens):
493
+ if argument.startswith("--"):
494
+ continue
495
+ rest = tokens[index + 1 :]
496
+ alias = rest[1].lower() if len(rest) >= 2 and rest[0].upper() == "AS" else None
497
+ return argument, alias
498
+ return "", None
499
+
500
+
501
+ def _approved_froms(text: str, bases: Mapping[str, ApprovedBase]) -> list[_ApprovedFrom]:
502
+ """Every `FROM` naming an approved base, in order; a stage name is not a base."""
503
+ stages: set[str] = set()
504
+ found: list[_ApprovedFrom] = []
505
+ for instruction in parse_dockerfile(text):
506
+ if instruction.keyword != "FROM":
507
+ continue
508
+ image, alias = _from_image(instruction)
509
+ if image and image.lower() not in stages and "$" not in image:
510
+ repository, _, digest = image.partition("@")
511
+ last = repository.rsplit("/", 1)[-1]
512
+ name, _, tag = last.partition(":")
513
+ repository = repository[: len(repository) - len(last)] + name
514
+ for base in bases.values():
515
+ if any(
516
+ repository == known or repository.endswith("/" + known)
517
+ for known in (base.ref, base.repository)
518
+ ):
519
+ found.append(_ApprovedFrom(instruction.line, image, base.ref, tag, digest))
520
+ break
521
+ if alias:
522
+ stages.add(alias)
523
+ return found
524
+
525
+
526
+ def dockerfile_findings_for_build(
527
+ text: str, bases: Mapping[str, ApprovedBase] = APPROVED_BASES
528
+ ) -> list[DockerfileFinding]:
529
+ """What a Dockerfile source must also be to be built, beyond what the contract allows.
530
+
531
+ - **One base, named by its channel.** Every `FROM` of an approved base
532
+ names the same base, and its channel as the tag
533
+ (`datalayer/python-cpu:2026.09`): the build pins that channel to its
534
+ digest, the way a `packages` source's base is pinned (D-9), and the
535
+ lock is solved in it. A digest is refused rather than trusted — which
536
+ channel it belongs to is what the build must know.
537
+ - **No build context yet.** A `COPY` or `ADD` from the context needs the
538
+ upload this item also describes, which is not taken yet; without it
539
+ the file is absent and the build fails halfway. From another stage
540
+ (`--from=`), from a heredoc, and `ADD` of a URL need none.
541
+ - **The default escape character.** The build appends its own lines,
542
+ continued with a backslash.
543
+ """
544
+ findings: list[DockerfileFinding] = []
545
+ for raw in text.splitlines():
546
+ directive = _DIRECTIVE.match(raw.strip())
547
+ if not directive:
548
+ break
549
+ if directive.group(1).lower() == "escape" and directive.group(2) != "\\":
550
+ findings.append(
551
+ DockerfileFinding(
552
+ 1, "escape", "keep the default escape character: the build appends lines to it"
553
+ )
554
+ )
555
+ named: list[_ApprovedFrom] = []
556
+ for found in _approved_froms(text, bases):
557
+ channels = bases[found.ref].channels
558
+ if found.digest:
559
+ message = f"name `{found.ref}` by its channel, not a digest: the build pins the channel"
560
+ elif not found.tag:
561
+ example = next(iter(channels), "2026.09")
562
+ message = f"name `{found.ref}`'s channel as its tag, such as `{found.ref}:{example}`"
563
+ elif found.tag not in channels:
564
+ message = f"`{found.ref}` has no channel `{found.tag}`; channels: " + (
565
+ ", ".join(channels) or "none"
566
+ )
567
+ else:
568
+ named.append(found)
569
+ continue
570
+ findings.append(DockerfileFinding(found.line, "FROM", message))
571
+ if len({(found.ref, found.tag) for found in named}) > 1:
572
+ findings.append(
573
+ DockerfileFinding(
574
+ named[1].line,
575
+ "FROM",
576
+ "every approved base is the same base and channel: the build pins one",
577
+ )
578
+ )
579
+ for instruction in parse_dockerfile(text):
580
+ if instruction.keyword not in ("COPY", "ADD"):
581
+ continue
582
+ tokens = _tokens(instruction.arguments)
583
+ if "<<" in instruction.arguments or any(t.startswith("--from=") for t in tokens):
584
+ continue
585
+ sources = [t for t in tokens if not t.startswith("--")][:-1]
586
+ local = [s for s in sources if not re.match(r"^(https?|git)://|^git@", s, re.IGNORECASE)]
587
+ if local:
588
+ findings.append(
589
+ DockerfileFinding(
590
+ instruction.line,
591
+ instruction.keyword,
592
+ f"copies `{local[0]}` from the build context, which this deployment does not "
593
+ "take yet: bake it with a `RUN` or a heredoc, or copy it from another stage",
594
+ )
595
+ )
596
+ return sorted(findings, key=lambda finding: finding.line)
597
+
598
+
599
+ def dockerfile_base(
600
+ text: str, bases: Mapping[str, ApprovedBase] = APPROVED_BASES
601
+ ) -> DockerfileBase:
602
+ """The approved base a Dockerfile source is built on, or a refusal naming the line."""
603
+ refused = [
604
+ finding
605
+ for finding in dockerfile_findings_for_build(text, bases)
606
+ if finding.instruction in ("FROM", "escape")
607
+ ]
608
+ if refused:
609
+ raise EnvironmentsError(
610
+ SPEC_INVALID,
611
+ f"line {refused[0].line}: {refused[0].message}",
612
+ detail={"field": "spec.build.dockerfile", "line": refused[0].line},
613
+ )
614
+ found = _approved_froms(text, bases)
615
+ if not found:
616
+ raise EnvironmentsError(
617
+ SPEC_INVALID,
618
+ "the Dockerfile builds on no approved base",
619
+ detail={"field": "spec.build.dockerfile"},
620
+ )
621
+ return DockerfileBase(
622
+ ref=found[0].ref,
623
+ channel=found[0].tag,
624
+ lines=tuple(item.line for item in found),
625
+ )
626
+
627
+
628
+ def pin_dockerfile_base(
629
+ text: str, reference: str, bases: Mapping[str, ApprovedBase] = APPROVED_BASES
630
+ ) -> str:
631
+ """The Dockerfile as it is built: each `FROM` of its base pinned to `reference`.
632
+
633
+ Only the image of those lines changes; comments, the author's own
634
+ continuations and the stage names stay as written. The parser directives
635
+ go, because the build states its own frontend first (`# syntax=`), and a
636
+ directive anywhere but the very top is only a comment.
637
+ """
638
+ images = {found.line: found.image for found in _approved_froms(text, bases)}
639
+ rewritten: list[str] = []
640
+ in_directives = True
641
+ for number, raw in enumerate(text.splitlines(), start=1):
642
+ if in_directives and _DIRECTIVE.match(raw.strip()):
643
+ continue
644
+ in_directives = False
645
+ image = images.get(number)
646
+ rewritten.append(raw.replace(image, reference, 1) if image else raw)
647
+ return "\n".join(rewritten) + "\n"
648
+
649
+
464
650
  # --- The build context (E3-03) -------------------------------------------------------
465
651
 
466
652
  #: A `dockerfile` source uploads a build context to object storage. These bound
@@ -515,9 +701,7 @@ def validate_build_context(
515
701
  elif ".." in components:
516
702
  findings.append(BuildContextFinding(path, "escapes the context with `..`"))
517
703
  if entry.is_symlink:
518
- findings.append(
519
- BuildContextFinding(path, "is a symlink, which could read a host file")
520
- )
704
+ findings.append(BuildContextFinding(path, "is a symlink, which could read a host file"))
521
705
  if entry.size_bytes > MAX_CONTEXT_FILE_BYTES:
522
706
  findings.append(
523
707
  BuildContextFinding(
@@ -526,9 +710,7 @@ def validate_build_context(
526
710
  )
527
711
  total += entry.size_bytes
528
712
  if len(entries) > MAX_CONTEXT_FILES:
529
- findings.append(
530
- BuildContextFinding("", f"has more than {MAX_CONTEXT_FILES} files")
531
- )
713
+ findings.append(BuildContextFinding("", f"has more than {MAX_CONTEXT_FILES} files"))
532
714
  if total > MAX_CONTEXT_TOTAL_BYTES:
533
715
  findings.append(
534
716
  BuildContextFinding("", f"is over the {MAX_CONTEXT_TOTAL_BYTES}-byte total limit")
@@ -83,6 +83,8 @@ __all__ = [
83
83
  "APT_PIN_PREFIX",
84
84
  "APT_SNAPSHOT_PREFIX",
85
85
  "CONSTRAINTS_PATH",
86
+ "COVERAGE_PREFIX",
87
+ "DOCKERFILE_COVERAGE",
86
88
  "LOCK_FORMAT",
87
89
  "PROTECTED_PIN_PREFIX",
88
90
  "BuildkitResolveRunner",
@@ -129,6 +131,13 @@ WHEELHOUSE_PATH = Path(__file__).parent / "constraints" / "wheelhouse"
129
131
  #: How an apt pin is written in the lock. A comment, so every reader of a
130
132
  #: ``pip`` requirements file — the CLI's diff included — ignores it.
131
133
  APT_PIN_PREFIX = "# datalayer-apt: "
134
+ #: What a lock says it does not cover, as a header line.
135
+ COVERAGE_PREFIX = "# datalayer-coverage: "
136
+ #: A `dockerfile` source's lock (E3-03): what the resolver saw, and no more.
137
+ DOCKERFILE_COVERAGE = (
138
+ "the declared packages and Datalayer's protected pins only; what the Dockerfile's own "
139
+ "instructions install is not locked, so a rebuild may install different versions of it"
140
+ )
132
141
  #: The Ubuntu snapshot the apt pins were taken from, as the lock records it.
133
142
  #: The builder installs the pins from the same snapshot, since a pinned
134
143
  #: version can leave the live mirror (D-9).
@@ -889,6 +898,7 @@ def lock_document(
889
898
  python_version: str,
890
899
  base_reference: str,
891
900
  merged: MergedRequirements,
901
+ coverage: str | None = None,
892
902
  ) -> dict[str, Any]:
893
903
  """The stored lock: its text, its digest, and what a reader needs from it.
894
904
 
@@ -912,6 +922,10 @@ def lock_document(
912
922
  f"# python: {python_version}",
913
923
  f"# base: {base_reference}",
914
924
  ]
925
+ if coverage:
926
+ # What this lock does not cover, in the lock itself: it travels with
927
+ # the version, and a reader of the lock is who needs to know (E3-03).
928
+ header.append(f"{COVERAGE_PREFIX}{coverage}")
915
929
  for pin in outcome.apt_pins.items():
916
930
  header.append(f"{APT_PIN_PREFIX}{pin[0]}={pin[1]}")
917
931
  if outcome.apt_pins and outcome.apt_source:
@@ -938,6 +952,9 @@ def resolve_bases(
938
952
  variants: Sequence[str],
939
953
  bases: dict[str, ApprovedBase] = APPROVED_BASES,
940
954
  registry: str | None = None,
955
+ *,
956
+ ref: str | None = None,
957
+ channel: str | None = None,
941
958
  ) -> dict[str, str]:
942
959
  """Each variant's base, pinned by digest (D-9, §4).
943
960
 
@@ -947,15 +964,15 @@ def resolve_bases(
947
964
  Hub. Bare ``<repository>@sha256:…`` otherwise, which is what every test
948
965
  and fixture that never passes a credential still gets.
949
966
  """
950
- base = bases.get(environment.spec.base.ref)
951
- repository = base.repository if base is not None else environment.spec.base.ref
967
+ ref = ref or environment.spec.base.ref
968
+ channel = channel or environment.spec.base.channel
969
+ base = bases.get(ref)
970
+ repository = base.repository if base is not None else ref
952
971
  if registry:
953
972
  repository = f"{registry}/{repository}"
954
973
  resolved: dict[str, str] = {}
955
974
  for variant in variants:
956
- digest = resolve_base(
957
- environment.spec.base.ref, environment.spec.base.channel, variant, bases
958
- )
975
+ digest = resolve_base(ref, channel, variant, bases)
959
976
  resolved[variant] = f"{repository}@{digest}"
960
977
  return resolved
961
978
 
@@ -1238,13 +1255,15 @@ def resolve_environment(
1238
1255
  environment = parse_environment(spec)
1239
1256
  python = environment.spec.packages.python
1240
1257
  source = environment.spec.build.source
1241
- if source not in ("packages", "dependencyFile", "image"):
1242
- raise EnvironmentsError(
1243
- CAPABILITY_UNSUPPORTED,
1244
- f"`{source}` is not resolved yet: only `packages`, `dependencyFile` and `image` "
1245
- "are, in this phase",
1246
- detail={"field": "spec.build.source", "source": source},
1247
- )
1258
+ # A Dockerfile names its own base in `FROM` (E3-03): that is what is
1259
+ # pinned and solved in, never `spec.base`, which the schema still asks for.
1260
+ dockerfile = environment.spec.build.dockerfile if source == "dockerfile" else None
1261
+ base_ref, base_channel = environment.spec.base.ref, environment.spec.base.channel
1262
+ if dockerfile is not None:
1263
+ from .contract import dockerfile_base
1264
+
1265
+ named = dockerfile_base(dockerfile.content, bases)
1266
+ base_ref, base_channel = named.ref, named.channel
1248
1267
  if python.manager == "conda":
1249
1268
  raise EnvironmentsError(
1250
1269
  CAPABILITY_UNSUPPORTED,
@@ -1262,7 +1281,14 @@ def resolve_environment(
1262
1281
  resolve_secret=resolve_secret,
1263
1282
  )
1264
1283
  if source == "image"
1265
- else resolve_bases(environment, wanted, bases, registry=_registry_of(credential))
1284
+ else resolve_bases(
1285
+ environment,
1286
+ wanted,
1287
+ bases,
1288
+ registry=_registry_of(credential),
1289
+ ref=base_ref,
1290
+ channel=base_channel,
1291
+ )
1266
1292
  )
1267
1293
  dependency_file = environment.spec.build.dependency_file
1268
1294
  if source == "dependencyFile" and dependency_file is not None:
@@ -1312,11 +1338,7 @@ def resolve_environment(
1312
1338
  registry_auth=_registry_auth(credential),
1313
1339
  bootstrap_uv=(source == "image"),
1314
1340
  # An imported image is not an approved base, and names no channel.
1315
- apt_snapshot=(
1316
- ""
1317
- if source == "image"
1318
- else channel_snapshot(environment.spec.base.ref, environment.spec.base.channel, bases)
1319
- ),
1341
+ apt_snapshot=("" if source == "image" else channel_snapshot(base_ref, base_channel, bases)),
1320
1342
  )
1321
1343
  outcome = (runner or BuildkitResolveRunner()).solve(request, say)
1322
1344
  document = lock_document(
@@ -1324,6 +1346,7 @@ def resolve_environment(
1324
1346
  python_version=environment.spec.language.version,
1325
1347
  base_reference=solving_in,
1326
1348
  merged=merged,
1349
+ coverage=DOCKERFILE_COVERAGE if dockerfile is not None else None,
1327
1350
  )
1328
1351
  say(
1329
1352
  f"Locked {document['package_count']} packages"
@@ -535,11 +535,23 @@ def _dockerfile_findings(dockerfile: DockerfileSpec | None) -> list[SpecFinding]
535
535
  return [SpecFinding(field, "is required when `spec.build.source` is `dockerfile`")]
536
536
  if not dockerfile.content.strip():
537
537
  return [SpecFinding(f"{field}.content", "is empty; it is the Dockerfile text")]
538
- from .contract import validate_dockerfile
539
-
538
+ from .contract import dockerfile_findings_for_build, validate_dockerfile
539
+
540
+ # The contract's refusals, then what building it needs on top (E3-03):
541
+ # one approved base named by its channel, and no build context yet. The
542
+ # second set is not the contract's — a variant could honour both — so it
543
+ # is its own list, and a line the contract already refused is not named
544
+ # twice.
545
+ findings = validate_dockerfile(dockerfile.content)
546
+ refused = {finding.line for finding in findings}
547
+ findings += [
548
+ finding
549
+ for finding in dockerfile_findings_for_build(dockerfile.content)
550
+ if finding.line not in refused
551
+ ]
540
552
  return [
541
553
  SpecFinding(f"{field}.content", f"line {finding.line}: {finding.message}")
542
- for finding in validate_dockerfile(dockerfile.content)
554
+ for finding in sorted(findings, key=lambda finding: finding.line)
543
555
  ]
544
556
 
545
557
 
@@ -650,13 +650,17 @@ class JupyterServerSandbox(Sandbox):
650
650
  "Failed to restart Jupyter kernel: the server answered %s", response.status_code
651
651
  )
652
652
  return False
653
- # The websocket the client holds is to the kernel that has just been
654
- # replaced; reconnecting is what makes the next execution land in the
655
- # new interpreter rather than on a channel nobody is reading.
656
- with contextlib.suppress(Exception):
657
- self._client.stop()
658
- with contextlib.suppress(Exception):
659
- self._client.start()
653
+ # **The websocket is kept, never reconnected.** jupyter-server binds a
654
+ # kernel's websocket to the kernel id, not to its process, and moves
655
+ # it onto the restarted kernel itself — which is how JupyterLab's own
656
+ # restart works. Reconnecting here broke every restart instead:
657
+ # `jupyter_kernel_client` caches its shell and IOPub channels bound to
658
+ # the socket they were built with, so after `stop()` and `start()` the
659
+ # client held a new socket while its channels still wrote to the
660
+ # closed one. The next execution answered `ok` with no output — found
661
+ # on r1, 2026-09-18, as checks 7, 8 and 9 all failing together, and
662
+ # reproduced against the approved base itself, with the deployed
663
+ # client. Swallowing both calls' errors is what hid it.
660
664
  return True
661
665
 
662
666
  @marks_execution
@@ -475,7 +475,7 @@ class TestWhatItSaysItCannotBuild:
475
475
  def test_this_variant_has_no_gpu_yet(self) -> None:
476
476
  capabilities = a_builder().capabilities()
477
477
  assert capabilities.supports_gpu is False
478
- assert capabilities.build_sources == ("packages", "dependencyFile", "image")
478
+ assert capabilities.build_sources == ("packages", "dependencyFile", "image", "dockerfile")
479
479
  assert capabilities.package_managers == ("uv", "pip")
480
480
 
481
481
 
@@ -114,6 +114,11 @@ class FakeImage:
114
114
  def base(cls, ref: str) -> FakeImage:
115
115
  return cls([Call("base", (ref,))])
116
116
 
117
+ @classmethod
118
+ def from_dockerfile(cls, path: str) -> FakeImage:
119
+ """The text is read now: the builder's scratch file is gone by the time a test looks."""
120
+ return cls([Call("from_dockerfile", (path,), {"content": Path(path).read_text()})])
121
+
117
122
  def env(self, env_vars: dict[str, str]) -> FakeImage:
118
123
  self.calls.append(Call("env", (env_vars,)))
119
124
  return self