code-sandboxes 1.9.14__tar.gz → 1.9.18__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 (263) hide show
  1. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/CHANGELOG.md +62 -0
  2. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/PKG-INFO +1 -1
  3. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/client.py +15 -1
  5. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/attest.py +26 -1
  6. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/bases.py +10 -1
  7. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/files.py +18 -6
  8. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/resolve.py +10 -8
  9. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/resolve_conda.py +22 -23
  10. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/spec.py +16 -0
  11. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/jupyter_server_sandbox.py +52 -0
  12. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/schemas/environment-v1alpha1.json +46 -0
  13. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client.py +56 -0
  14. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_attest.py +41 -1
  15. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_bases.py +16 -5
  16. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_builders.py +52 -0
  17. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_resolve.py +15 -5
  18. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_resolve_conda.py +9 -15
  19. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_jupyter_server.py +74 -0
  20. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/build.yml +0 -0
  21. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/environments-live.yml +0 -0
  22. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/py-code-style.yml +0 -0
  23. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/py-tests.yml +0 -0
  24. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/py-typing.yml +0 -0
  25. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/reusable-python.yml +0 -0
  26. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.gitignore +0 -0
  27. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.licenserc.yaml +0 -0
  28. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.pre-commit-config.yaml +0 -0
  29. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.vscode/settings.json +0 -0
  30. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/Dockerfile +0 -0
  31. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/LICENSE +0 -0
  32. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/Makefile +0 -0
  33. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/README.md +0 -0
  34. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/__init__.py +0 -0
  35. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/base.py +0 -0
  36. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/bridge_mount.py +0 -0
  37. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/builds.py +0 -0
  38. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/cli.py +0 -0
  39. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/cloudflare_sandbox.py +0 -0
  40. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/commands.py +0 -0
  41. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/console.py +0 -0
  42. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/contents.py +0 -0
  43. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/coreweave_sandbox.py +0 -0
  44. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/datalayer_sandbox.py +0 -0
  45. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/daytona_sandbox.py +0 -0
  46. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/docker_sandbox.py +0 -0
  47. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/e2b_sandbox.py +0 -0
  48. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/__init__.py +0 -0
  49. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/__main__.py +0 -0
  50. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/accounts.py +0 -0
  51. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/__init__.py +0 -0
  52. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/datalayer.py +0 -0
  53. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/daytona.py +0 -0
  54. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/e2b.py +0 -0
  55. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/managed.py +0 -0
  56. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/modal.py +0 -0
  57. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/breaker.py +0 -0
  58. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/build_secrets.py +0 -0
  59. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/builders.py +0 -0
  60. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/canonical.py +0 -0
  61. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/conformance.py +0 -0
  62. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  63. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  64. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  65. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/contract.py +0 -0
  66. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/__init__.py +0 -0
  67. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/build.py +0 -0
  68. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  69. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  70. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/errors.py +0 -0
  71. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/image_import.py +0 -0
  72. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/lifecycle.py +0 -0
  73. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/policy.py +0 -0
  74. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/redact.py +0 -0
  75. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/schema.py +0 -0
  76. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/eval_sandbox.py +0 -0
  77. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/exceptions.py +0 -0
  78. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/filesystem.py +0 -0
  79. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/google_colab.py +0 -0
  80. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/google_colab_sandbox.py +0 -0
  81. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/interfaces.py +0 -0
  82. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/jupyter_ingress.py +0 -0
  83. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle.py +0 -0
  84. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle_execute.py +0 -0
  85. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle_live.py +0 -0
  86. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle_sandbox.py +0 -0
  87. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/lifecycle.py +0 -0
  88. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/manage.py +0 -0
  89. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/modal_sandbox.py +0 -0
  90. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/models.py +0 -0
  91. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/monty_sandbox.py +0 -0
  92. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/provider_ingress.py +0 -0
  93. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/providers.py +0 -0
  94. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docker/Dockerfile +0 -0
  95. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/.gitignore +0 -0
  96. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/LICENSE +0 -0
  97. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/Makefile +0 -0
  98. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/README.md +0 -0
  99. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/babel.config.js +0 -0
  100. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/api-reference/_category_.yml +0 -0
  101. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/api-reference/index.mdx +0 -0
  102. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/cli/_category_.yml +0 -0
  103. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/cli/index.mdx +0 -0
  104. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/contribute/_category_.yml +0 -0
  105. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/contribute/index.mdx +0 -0
  106. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/_category_.yml +0 -0
  107. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/contract.mdx +0 -0
  108. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/index.mdx +0 -0
  109. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/specification.mdx +0 -0
  110. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/spike-2026-09.mdx +0 -0
  111. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/examples/_category_.yml +0 -0
  112. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/examples/index.mdx +0 -0
  113. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/guide/_category_.yml +0 -0
  114. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/guide/index.mdx +0 -0
  115. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/index.mdx +0 -0
  116. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/install/_category_.yml +0 -0
  117. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/install/index.mdx +0 -0
  118. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/provider-ingress.mdx +0 -0
  119. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/_category_.yml +0 -0
  120. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/cloudflare.mdx +0 -0
  121. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/coreweave.mdx +0 -0
  122. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/datalayer.mdx +0 -0
  123. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/daytona.mdx +0 -0
  124. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/docker.mdx +0 -0
  125. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/e2b.mdx +0 -0
  126. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/eval.mdx +0 -0
  127. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/google-colab.mdx +0 -0
  128. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/index.mdx +0 -0
  129. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/jupyter-server.mdx +0 -0
  130. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/kaggle.mdx +0 -0
  131. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/modal.mdx +0 -0
  132. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/monty.mdx +0 -0
  133. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docusaurus.config.js +0 -0
  134. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/package.json +0 -0
  135. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/sidebars.js +0 -0
  136. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow1.js +0 -0
  137. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow1.module.css +0 -0
  138. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow2.js +0 -0
  139. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow2.module.css +0 -0
  140. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow3.js +0 -0
  141. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow3.module.css +0 -0
  142. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/css/custom.css +0 -0
  143. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/pages/index.module.css +0 -0
  144. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/pages/markdown-page.md +0 -0
  145. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/theme/CustomDocItem.tsx +0 -0
  146. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/theme/IconsTitle.tsx +0 -0
  147. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/bricks.svg +0 -0
  148. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/building-construction.svg +0 -0
  149. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/cloud-sun.svg +0 -0
  150. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/cloud.svg +0 -0
  151. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/collaboration.svg +0 -0
  152. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/copy-left.svg +0 -0
  153. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/logo.png +0 -0
  154. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/logo.svg +0 -0
  155. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/personas/business.svg +0 -0
  156. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  157. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/personas/devops.svg +0 -0
  158. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/project/project-line.svg +0 -0
  159. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/favicon.ico +0 -0
  160. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/jupyter.svg +0 -0
  161. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/open-source.svg +0 -0
  162. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/react-js.svg +0 -0
  163. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/react-js2.svg +0 -0
  164. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/ringed-planet.svg +0 -0
  165. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/robot.svg +0 -0
  166. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/rocket.svg +0 -0
  167. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/shield.svg +0 -0
  168. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/sparkles.svg +0 -0
  169. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/streamlit.gif +0 -0
  170. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/super-hero.svg +0 -0
  171. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/target.svg +0 -0
  172. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/thumbs-up.svg +0 -0
  173. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/README.md +0 -0
  174. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/Makefile +0 -0
  175. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/cloudflare_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/coreweave_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/datalayer_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/daytona_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/docker_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/e2b_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/eval_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/google_colab_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/kaggle_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/modal_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/monty_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/Makefile +0 -0
  188. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/cloudflare_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/coreweave_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/datalayer_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/daytona_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/docker_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/e2b_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/eval_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/google_colab_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/kaggle_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/modal_sandbox_example.py +0 -0
  199. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/monty_sandbox_example.py +0 -0
  200. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/pyproject.toml +0 -0
  201. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/__init__.py +0 -0
  202. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/conftest.py +0 -0
  203. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  204. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_bridge_mount.py +0 -0
  205. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_builds.py +0 -0
  206. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_cli_exec.py +0 -0
  207. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_cli_repl.py +0 -0
  208. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client_contents.py +0 -0
  209. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client_files.py +0 -0
  210. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client_outcome_error_output.py +0 -0
  211. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_cloudflare.py +0 -0
  212. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_commands_envelope.py +0 -0
  213. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_console.py +0 -0
  214. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_contents_contract.py +0 -0
  215. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_contents_live_matrix.py +0 -0
  216. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_coreweave.py +0 -0
  217. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_datalayer_sandbox.py +0 -0
  218. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_daytona.py +0 -0
  219. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_e2b.py +0 -0
  220. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_accounts.py +0 -0
  221. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_breaker.py +0 -0
  222. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_build_secrets.py +0 -0
  223. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_canonical.py +0 -0
  224. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_conformance.py +0 -0
  225. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_constraints.py +0 -0
  226. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_contract.py +0 -0
  227. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_cross_variant_report.py +0 -0
  228. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_datalayer_builder.py +0 -0
  229. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_daytona_builder.py +0 -0
  230. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_doctor.py +0 -0
  231. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_e2b_builder.py +0 -0
  232. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_errors.py +0 -0
  233. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_image_import.py +0 -0
  234. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_lifecycle.py +0 -0
  235. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_live_matrix.py +0 -0
  236. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_managed_builders.py +0 -0
  237. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_modal_builder.py +0 -0
  238. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_redact.py +0 -0
  239. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_spec.py +0 -0
  240. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_eval.py +0 -0
  241. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  242. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_factory.py +0 -0
  243. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_google_colab.py +0 -0
  244. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_integration.py +0 -0
  245. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_jupyter_ingress.py +0 -0
  246. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_jupyter_server_streaming.py +0 -0
  247. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle.py +0 -0
  248. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle_execute.py +0 -0
  249. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle_live.py +0 -0
  250. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle_session.py +0 -0
  251. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kernel_client_compatibility.py +0 -0
  252. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_lifecycle_vocabulary.py +0 -0
  253. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_manage.py +0 -0
  254. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_manage_datalayer.py +0 -0
  255. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_modal_google_colab_sandbox.py +0 -0
  256. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_modal_session.py +0 -0
  257. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_models.py +0 -0
  258. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_output_line_fidelity.py +0 -0
  259. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_provider_catalog_names.py +0 -0
  260. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_provider_ingress.py +0 -0
  261. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_providers.py +0 -0
  262. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_sandboxes.py +0 -0
  263. {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -8,6 +8,68 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.9.18
12
+
13
+ - **A lock no longer carries a wall clock, so the build cache can hit**
14
+ (`environments/resolve.py`, `resolve_conda.py`; PLAN_ENVS.md E1-26, D-12).
15
+ The lock's header carried a `# resolved-at:` line, and its digest is over
16
+ the whole text — so two resolves of the same spec, in the same base,
17
+ pinning the same 320 packages, produced two different digests. Found by
18
+ resolving one environment twice on r1 on 2026-09-16: the texts differed in
19
+ exactly that one line out of 5,388. Section 5's cache key is over the lock
20
+ digest, so D-12's build cache could never hit, and it never had:
21
+ `environments.cache.lookups` read `hit=false` twelve times out of twelve.
22
+ When a lock was resolved is on the lock document Runtimes stores, in its
23
+ `created_at`, which is where it belongs. `resolved_at` is gone from
24
+ `lock_document`, `conda_lock_document`, `resolve_environment` and
25
+ `resolve_conda_environment`; the two tests that asserted determinism by
26
+ freezing the clock now assert it without one.
27
+
28
+ ## 1.9.17
29
+
30
+ - **An artifact's size is read from the registry** (`environments/attest.py`;
31
+ PLAN_ENVS.md E1-25). `attest_artifact` took `size_bytes` from its caller and
32
+ nobody ever passed one — the builder answers a reference, not a weight — so
33
+ every artefact was recorded with `sizeBytes: null` and
34
+ `environments.artifact.bytes`, the series section 14 tracks the artifact
35
+ size in, had no point in it although artifacts had been recorded (seen on r1,
36
+ 2026-09-16, through the OTEL query API). `Attestor.size_of()` asks the
37
+ registry, with the client the scan is already read from, and a size that
38
+ cannot be read is logged rather than raised: a missing number on a dashboard
39
+ is not a reason to refuse an artifact that is otherwise signed. 3 new tests.
40
+
41
+ ## 1.9.15
42
+
43
+ - **A restart restarts the kernel, not just this client's socket**
44
+ (`jupyter_server_sandbox`, `client`; PLAN_ENV.md E0-09, Appendix B check
45
+ 7). `CodeSandboxClient.restart()` was `stop()` then `start()`, which is
46
+ right for a sandbox this process owns — it is destroyed and recreated, and
47
+ nothing survives — and wrong for one *attached* to a Jupyter server
48
+ somebody else runs, which is every Datalayer runtime pod: stopping drops
49
+ the websocket while the kernel process keeps running, so the reconnect
50
+ lands in the same interpreter with every global still set. Check 7 is
51
+ "nothing is assumed to persist across restarts", and it read `state survived the restart ('True')` for exactly this reason — found live on r1,
52
+ 2026-09-16, the first drill whose smoke test reached the check.
53
+ `JupyterServerSandbox.restart_kernel()` now asks the server's own
54
+ `POST /api/kernels/{id}/restart` (the way `_do_interrupt` already uses the
55
+ API rather than the client's lifecycle) and reconnects onto the new
56
+ kernel; `restart()` prefers it and falls back to the lifecycle for every
57
+ variant that draws no such distinction. 7 new tests.
58
+ - **`datalayer/python-cpu:2026.09` repinned** to
59
+ `sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148`:
60
+ the contract layer now sets `MappingKernelManager.root_dir`, so kernels
61
+ start in `/home/datalayer/content`. The image already declared `WORKDIR`
62
+ there and `sandbox-contract/v1`'s User row already required it, but a
63
+ kernel's cwd is the Jupyter server's to choose and jupyter-python's config
64
+ roots it at `$HOME` — so every environment's kernel ran in
65
+ `/home/datalayer` and Appendix B check 2 read `cwd is '/home/datalayer', not '/home/datalayer/content'`. The file browser stays rooted at `$HOME`,
66
+ where a person expects to see everything they have; only the kernel moves.
67
+ - Two assertions that had rotted through three base releases are pinned in
68
+ one place again: the channel's digest and its apt snapshot were duplicated
69
+ across `test_environment_bases.py` and `test_environment_resolve.py`, and
70
+ 2026-09-15's and 2026-09-16's releases left both red rather than catching
71
+ anything.
72
+
11
73
  ## 1.9.14
12
74
 
13
75
  - **`datalayer/python-cpu:2026.09` base channel repinned** to the rebuilt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.14
3
+ Version: 1.9.18
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.14"
6
+ __version__ = "1.9.18"
@@ -508,7 +508,21 @@ class CodeSandboxClient:
508
508
  return self.is_started
509
509
 
510
510
  def restart(self) -> None:
511
- """Restart the wrapped sandbox through its public lifecycle."""
511
+ """Restart the wrapped sandbox, clearing what it was holding.
512
+
513
+ A sandbox this process owns is restarted by its own lifecycle: stop
514
+ and start destroy and recreate it, and nothing survives. One that is
515
+ merely *attached* to a server somebody else runs — a Jupyter server
516
+ in a Datalayer runtime pod — is not: stopping drops this client's
517
+ websocket while the kernel process goes on running, and starting
518
+ reconnects to the same interpreter with every global still set. A
519
+ sandbox that knows how to restart what it is attached to says so with
520
+ `restart_kernel`, and that is used in preference; the lifecycle is
521
+ the fallback for every variant that has no such distinction.
522
+ """
523
+ restart_kernel = getattr(self._sandbox, "restart_kernel", None)
524
+ if callable(restart_kernel) and restart_kernel():
525
+ return
512
526
  self._sandbox.stop()
513
527
  self._sandbox.start()
514
528
 
@@ -536,10 +536,35 @@ class Attestor:
536
536
  signature_ref=signature,
537
537
  sbom_ref=sbom_ref or f"{registry}/{repository}@{digest}.sbom",
538
538
  provenance_ref=provenance_ref or f"{registry}/{repository}@{digest}.att",
539
- size_bytes=size_bytes,
539
+ size_bytes=size_bytes
540
+ if size_bytes is not None
541
+ else self.size_of(repository=repository, digest=digest),
540
542
  signed_now=signed_now,
541
543
  )
542
544
 
545
+ def size_of(self, *, repository: str, digest: str) -> int | None:
546
+ """What the registry says the artifact weighs, or None.
547
+
548
+ Nobody hands the size down: the builder answers a reference, not a
549
+ weight, so until this asked the registry `size_bytes` was always None
550
+ — and with it `environments.artifact.bytes`, the series section 14
551
+ tracks the artifact size in, which had no point in it on 2026-09-16
552
+ although artifacts had been recorded. The registry has known all
553
+ along; the scan is read from the same client.
554
+
555
+ Never a reason to fail an attestation: a size that could not be read
556
+ is a missing number on a dashboard, and the artifact is still signed.
557
+ """
558
+ try:
559
+ images = self._client().describe_images(
560
+ repositoryName=repository, imageIds=[{"imageDigest": digest}]
561
+ )["imageDetails"]
562
+ except Exception as error:
563
+ self._log(f"The artifact's size could not be read: {error}")
564
+ return None
565
+ size = (images[0] or {}).get("imageSizeInBytes") if images else None
566
+ return int(size) if size else None
567
+
543
568
  def _client(self) -> Any:
544
569
  if self._ecr is None:
545
570
  try:
@@ -126,7 +126,16 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
126
126
  # `.spec.VARIANTS`, spelled out: `spec` imports from this
127
127
  # module, so importing it back here would be circular.
128
128
  ("datalayer", "e2b", "daytona", "modal"),
129
- "sha256:aa5413000bb5b6ecd0a0cf03959b107f0d572f65bf230c08bbdf9a4569775545",
129
+ # 2026-09-16: kernels now start in the contract's own
130
+ # working directory. The image already declared `WORKDIR
131
+ # /home/datalayer/content`, but a kernel's cwd is the
132
+ # Jupyter server's to choose, and jupyter-python's config
133
+ # roots it at `$HOME` — so every environment's kernel ran
134
+ # in `/home/datalayer` and Appendix B check 2 read "cwd is
135
+ # '/home/datalayer', not '/home/datalayer/content'". The
136
+ # contract layer now sets `MappingKernelManager.root_dir`,
137
+ # which moves the kernel without moving the file browser.
138
+ "sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148",
130
139
  )
131
140
  },
132
141
  snapshots={"2026.09": "20260916T120000Z"},
@@ -25,12 +25,15 @@ __all__ = ["build_entries", "files_step"]
25
25
  def build_entries(
26
26
  environment: Environment, *, source_of: Callable[[FileEntry], str] | None = None
27
27
  ) -> list[BuildEntry]:
28
- """The spec's files as build entries.
28
+ """The spec's baked files as build entries: ``files`` then ``contentsBuild``.
29
29
 
30
- ``source_of`` turns a ``contentRef`` into a URL the build can fetch —
31
- a presigned URL for a ``blob://`` reference — and defaults to the
32
- reference itself. A file without its sha256 is refused: the build
33
- verifies every byte it bakes.
30
+ ``files`` a user uploaded (referenced by ``contentRef``) and
31
+ ``contentsBuild`` fetched from an external ``source`` are both baked the
32
+ same way, so they are the same kind of entry here. ``source_of`` turns a
33
+ ``contentRef`` into a URL the build can fetch — a presigned URL for a
34
+ ``blob://`` reference — and defaults to the reference itself; a
35
+ ``contentsBuild`` source is already a URL and is used as is. A file without
36
+ its sha256 is refused: the build verifies every byte it bakes.
34
37
  """
35
38
  entries: list[BuildEntry] = []
36
39
  for index, entry in enumerate(environment.spec.files):
@@ -48,6 +51,15 @@ def build_entries(
48
51
  size_bytes=entry.size_bytes,
49
52
  )
50
53
  )
54
+ for built in environment.spec.contents_build:
55
+ entries.append(
56
+ BuildEntry(
57
+ source_uri=built.source,
58
+ destination_path=built.path,
59
+ sha256=built.sha256,
60
+ size_bytes=built.size_bytes,
61
+ )
62
+ )
51
63
  return entries
52
64
 
53
65
 
@@ -63,7 +75,7 @@ def files_step(
63
75
  """
64
76
  if variant not in VARIANTS:
65
77
  raise ValueError(f"{variant!r} is not a variant")
66
- if not environment.spec.files:
78
+ if not environment.spec.files and not environment.spec.contents_build:
67
79
  return []
68
80
  build = EnvironmentBuild(
69
81
  environment=environment.metadata.name,
@@ -889,7 +889,6 @@ def lock_document(
889
889
  python_version: str,
890
890
  base_reference: str,
891
891
  merged: MergedRequirements,
892
- resolved_at: datetime | None = None,
893
892
  ) -> dict[str, Any]:
894
893
  """The stored lock: its text, its digest, and what a reader needs from it.
895
894
 
@@ -897,11 +896,19 @@ def lock_document(
897
896
  output, so the document is still a requirements file — ``pip install -r``
898
897
  reads it, and so does every tool that only knows that format — while
899
898
  saying everything the build installs.
899
+
900
+ **Nothing here is a wall clock.** The header carried a ``# resolved-at:``
901
+ line until 2026-09-16, and since the digest is over the whole text, two
902
+ resolves of the same spec, in the same base, pinning the same 320 packages
903
+ produced two different digests — the texts differed in that one line out
904
+ of 5,388, found by resolving the same environment twice on r1. The cache
905
+ key of section 5 is over the lock digest, so D-12's build cache could
906
+ never hit: `environments.cache.lookups` read `hit=false` 12 times out of
907
+ 12. When the lock was resolved is on the lock document Runtimes stores, in
908
+ its ``created_at``, where it belongs.
900
909
  """
901
- when = (resolved_at or _utcnow()).replace(microsecond=0).isoformat()
902
910
  header = [
903
911
  "# Resolved by Datalayer (PLAN_ENV.md D-9). Do not edit: a change makes a new version.",
904
- f"# resolved-at: {when}",
905
912
  f"# python: {python_version}",
906
913
  f"# base: {base_reference}",
907
914
  ]
@@ -1163,7 +1170,6 @@ def resolve_environment(
1163
1170
  runner: ResolveRunner | None = None,
1164
1171
  conda_runner: CondaResolveRunner | None = None,
1165
1172
  bases: dict[str, ApprovedBase] = APPROVED_BASES,
1166
- resolved_at: datetime | None = None,
1167
1173
  uv: str | None = None,
1168
1174
  pyproject_run: Callable[..., subprocess.CompletedProcess[str]] | None = None,
1169
1175
  image_transport: Any = None,
@@ -1198,8 +1204,6 @@ def resolve_environment(
1198
1204
  bases
1199
1205
  The approved bases, injected by tests and by a plane whose channel is
1200
1206
  published somewhere else.
1201
- resolved_at
1202
- The moment the lock records. Now by default.
1203
1207
  uv, pyproject_run
1204
1208
  A `pyproject` `dependencyFile` source's own verification (E3-01):
1205
1209
  the `uv` to check and export with, and how it is run — injected by
@@ -1274,7 +1278,6 @@ def resolve_environment(
1274
1278
  credential=credential,
1275
1279
  log=say,
1276
1280
  runner=conda_runner,
1277
- resolved_at=resolved_at,
1278
1281
  )
1279
1282
  if dependency_file.source_format == "pyproject":
1280
1283
  # Verified, not re-resolved (E3-01): the author's own uv.lock is
@@ -1321,7 +1324,6 @@ def resolve_environment(
1321
1324
  python_version=environment.spec.language.version,
1322
1325
  base_reference=solving_in,
1323
1326
  merged=merged,
1324
- resolved_at=resolved_at,
1325
1327
  )
1326
1328
  say(
1327
1329
  f"Locked {document['package_count']} packages"
@@ -644,24 +644,28 @@ class BuildkitCondaResolveRunner:
644
644
  carries the whole of what the solve resolved, not the conda layer alone.
645
645
  """
646
646
  micromamba = shlex.quote(MICROMAMBA_BINARY)
647
- return "\n".join(
648
- [
649
- f"FROM {request.base_reference} AS solve",
650
- "USER root",
651
- "WORKDIR /solve",
652
- micromamba_bootstrap_dockerfile_line(),
653
- "COPY environment.yml ./environment.yml",
654
- "ENV PIP_FIND_LINKS=" + shlex.quote(WHEELHOUSE_IMAGE_PATH),
655
- "RUN --mount=type=cache,target=/opt/conda/pkgs "
656
- f"{micromamba} create --yes --prefix /solve/prefix "
657
- f"--platform {shlex.quote(request.platform)} --file environment.yml",
658
- f"RUN {micromamba} env export --explicit --prefix /solve/prefix > /solve/lock.txt",
659
- f"RUN {micromamba} env export --prefix /solve/prefix > /solve/pip-env.yml",
660
- "FROM scratch",
661
- "COPY --from=solve /solve/lock.txt /lock.txt",
662
- "COPY --from=solve /solve/pip-env.yml /pip-env.yml",
663
- ]
664
- ) + "\n"
647
+ return (
648
+ "\n".join(
649
+ [
650
+ f"FROM {request.base_reference} AS solve",
651
+ "USER root",
652
+ "WORKDIR /solve",
653
+ micromamba_bootstrap_dockerfile_line(),
654
+ "COPY environment.yml ./environment.yml",
655
+ "ENV PIP_FIND_LINKS=" + shlex.quote(WHEELHOUSE_IMAGE_PATH),
656
+ "RUN --mount=type=cache,target=/opt/conda/pkgs "
657
+ f"{micromamba} create --yes --prefix /solve/prefix "
658
+ f"--platform {shlex.quote(request.platform)} --file environment.yml",
659
+ f"RUN {micromamba} env export --explicit "
660
+ "--prefix /solve/prefix > /solve/lock.txt",
661
+ f"RUN {micromamba} env export --prefix /solve/prefix > /solve/pip-env.yml",
662
+ "FROM scratch",
663
+ "COPY --from=solve /solve/lock.txt /lock.txt",
664
+ "COPY --from=solve /solve/pip-env.yml /pip-env.yml",
665
+ ]
666
+ )
667
+ + "\n"
668
+ )
665
669
 
666
670
  def solve(
667
671
  self, request: CondaResolveRequest, log: Callable[[str], None] | None = None
@@ -800,7 +804,6 @@ def conda_lock_document(
800
804
  base_reference: str,
801
805
  merged: MergedRequirements,
802
806
  platform: str = CONDA_PLATFORM,
803
- resolved_at: datetime | None = None,
804
807
  ) -> dict[str, Any]:
805
808
  """The stored conda lock: its text, its digest, and what a reader needs.
806
809
 
@@ -814,10 +817,8 @@ def conda_lock_document(
814
817
  could read the prefix back, and the merged requirements otherwise, so it is
815
818
  always complete rather than the protected pins alone.
816
819
  """
817
- when = (resolved_at or _utcnow()).replace(microsecond=0).isoformat()
818
820
  header = [
819
821
  "# Resolved by Datalayer (PLAN_ENV.md D-9). Do not edit: a change makes a new version.",
820
- f"# resolved-at: {when}",
821
822
  f"# python: {python_version}",
822
823
  f"# platform: {platform}",
823
824
  f"# base: {base_reference}",
@@ -854,7 +855,6 @@ def resolve_conda_environment(
854
855
  credential: Any = None,
855
856
  log: Callable[[str], None] | None = None,
856
857
  runner: CondaResolveRunner | None = None,
857
- resolved_at: datetime | None = None,
858
858
  ) -> dict[str, Any]:
859
859
  """A conda version's lock, from its ``environment.yml`` (E3-02).
860
860
 
@@ -892,7 +892,6 @@ def resolve_conda_environment(
892
892
  base_reference=solving_in,
893
893
  merged=merged,
894
894
  platform=platform,
895
- resolved_at=resolved_at,
896
895
  )
897
896
  say(f"Locked {document['package_count']} conda packages as {document['digest']}")
898
897
  return {**document, "resolved_bases": dict(resolved_bases)}
@@ -259,6 +259,21 @@ class FileEntry(_Model):
259
259
  sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
260
260
 
261
261
 
262
+ class ContentsBuildEntry(_Model):
263
+ """One immutable file the Environment bakes from an external source.
264
+
265
+ Unlike ``files`` — which a user uploads, referenced by ``contentRef`` — a
266
+ ``contentsBuild`` entry names an external ``source`` URL fetched at build
267
+ time and verified against ``sha256`` (required: the build verifies every
268
+ byte it bakes). Both are baked the same way, into every provider artifact.
269
+ """
270
+
271
+ source: str
272
+ path: str
273
+ sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
274
+ size_bytes: int | None = Field(default=None, ge=0)
275
+
276
+
262
277
  class Commands(_Model):
263
278
  post_install: list[str] = Field(default_factory=list)
264
279
 
@@ -365,6 +380,7 @@ class EnvironmentSpec(_Model):
365
380
  platform: Platform = Field(default_factory=Platform)
366
381
  packages: Packages = Field(default_factory=Packages)
367
382
  files: list[FileEntry] = Field(default_factory=list)
383
+ contents_build: list[ContentsBuildEntry] = Field(default_factory=list)
368
384
  env: dict[str, str] = Field(default_factory=dict)
369
385
  commands: Commands = Field(default_factory=Commands)
370
386
  build_secrets: list[BuildSecret] = Field(default_factory=list)
@@ -607,6 +607,58 @@ class JupyterServerSandbox(Sandbox):
607
607
  logger.warning(f"Failed to interrupt Jupyter kernel: {e}")
608
608
  return False
609
609
 
610
+ def restart_kernel(self) -> bool:
611
+ """Restart the kernel itself, through the server's own REST API.
612
+
613
+ Not `stop()` then `start()`: those are this *client's* lifecycle, and
614
+ when the server is somebody else's — a Datalayer runtime pod, which
615
+ is every attached sandbox — stopping the client only drops the
616
+ websocket. The kernel is a process on the server and keeps running,
617
+ so a reconnect lands back in the same interpreter with every global
618
+ still set.
619
+
620
+ That is what Appendix B check 7 ("nothing is assumed to persist
621
+ across restarts") measures, and it read `state survived the restart`
622
+ for exactly this reason — found live on r1, 2026-09-16, the first
623
+ drill whose smoke test reached the check. `POST
624
+ /api/kernels/{id}/restart` is the one that restarts the kernel, the
625
+ same way `_do_interrupt` already uses the API rather than the
626
+ client's own lifecycle.
627
+
628
+ Answers whether the server accepted it; never raises, so a caller
629
+ that cannot restart reports a failed check rather than an error.
630
+ """
631
+ if not self._server_url or not self._client:
632
+ return False
633
+ kernel_id = getattr(self._client, "id", None)
634
+ if not kernel_id:
635
+ return False
636
+ try:
637
+ response = requests.post(
638
+ f"{self._server_url}/api/kernels/{kernel_id}/restart",
639
+ params={"token": self._token},
640
+ headers=self._headers or None,
641
+ timeout=30,
642
+ )
643
+ except Exception as error:
644
+ # A restart that cannot even be asked for is a failed check, not
645
+ # an error to raise at the caller, exactly as `_do_interrupt` is.
646
+ logger.warning(f"Failed to restart Jupyter kernel: {error}")
647
+ return False
648
+ if not response.ok:
649
+ logger.warning(
650
+ "Failed to restart Jupyter kernel: the server answered %s", response.status_code
651
+ )
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()
660
+ return True
661
+
610
662
  @marks_execution
611
663
  def run_code( # noqa: C901
612
664
  self,
@@ -152,6 +152,45 @@
152
152
  "title": "Compatibility",
153
153
  "type": "object"
154
154
  },
155
+ "ContentsBuildEntry": {
156
+ "additionalProperties": false,
157
+ "description": "One immutable file the Environment bakes from an external source.\n\nUnlike ``files`` \u2014 which a user uploads, referenced by ``contentRef`` \u2014 a\n``contentsBuild`` entry names an external ``source`` URL fetched at build\ntime and verified against ``sha256`` (required: the build verifies every\nbyte it bakes). Both are baked the same way, into every provider artifact.",
158
+ "properties": {
159
+ "path": {
160
+ "title": "Path",
161
+ "type": "string"
162
+ },
163
+ "sha256": {
164
+ "pattern": "^[0-9a-f]{64}$",
165
+ "title": "Sha256",
166
+ "type": "string"
167
+ },
168
+ "sizeBytes": {
169
+ "anyOf": [
170
+ {
171
+ "minimum": 0,
172
+ "type": "integer"
173
+ },
174
+ {
175
+ "type": "null"
176
+ }
177
+ ],
178
+ "default": null,
179
+ "title": "Sizebytes"
180
+ },
181
+ "source": {
182
+ "title": "Source",
183
+ "type": "string"
184
+ }
185
+ },
186
+ "required": [
187
+ "source",
188
+ "path",
189
+ "sha256"
190
+ ],
191
+ "title": "ContentsBuildEntry",
192
+ "type": "object"
193
+ },
155
194
  "DependencyFileSpec": {
156
195
  "additionalProperties": false,
157
196
  "description": "A `requirements.txt`, a `pyproject.toml` with its `uv.lock`, or a conda\n`environment.yml` (E3-01, E3-02).\n\n``requirements`` resolves the way ``packages`` does \u2014 the protected\nconstraints merged in, the same solve. ``pyproject`` does not resolve at\nall: its own ``uv.lock`` is verified against the current\n``pyproject.toml`` and exported, never re-solved, because a lock the\nauthor already made is the whole point of bringing one. ``conda`` resolves\nthe ``environment.yml`` in its own ``micromamba`` solve into an explicit\nlock, with the protected constraints merged over its ``pip:`` layer.",
@@ -202,6 +241,13 @@
202
241
  "compatibility": {
203
242
  "$ref": "#/$defs/Compatibility"
204
243
  },
244
+ "contentsBuild": {
245
+ "items": {
246
+ "$ref": "#/$defs/ContentsBuildEntry"
247
+ },
248
+ "title": "Contentsbuild",
249
+ "type": "array"
250
+ },
205
251
  "contract": {
206
252
  "default": "sandbox-contract/v1",
207
253
  "title": "Contract",
@@ -157,6 +157,62 @@ def test_variables_interrupt_and_restart_delegate_to_sandbox():
157
157
  assert client.is_alive() is True
158
158
 
159
159
 
160
+ class TestRestartingWhatIsActuallyHoldingTheState:
161
+ """A restart has to clear the interpreter, not just this client's socket.
162
+
163
+ Appendix B check 7 is "nothing is assumed to persist across restarts". A
164
+ sandbox this process owns is cleared by its own lifecycle; one attached
165
+ to somebody else's Jupyter server is not, because the kernel outlives
166
+ the websocket. Found live on r1, 2026-09-16: `state survived the
167
+ restart`.
168
+ """
169
+
170
+ def test_a_sandbox_that_can_restart_its_kernel_is_asked_to(self):
171
+ class _AttachedSandbox(_FakeSandbox):
172
+ def __init__(self):
173
+ super().__init__()
174
+ self.kernel_restarts = 0
175
+
176
+ def restart_kernel(self):
177
+ self.kernel_restarts += 1
178
+ return True
179
+
180
+ sandbox = _AttachedSandbox()
181
+ sandbox.start()
182
+ CodeSandboxClient(sandbox).restart()
183
+
184
+ assert sandbox.kernel_restarts == 1
185
+ # And the lifecycle was left alone: stopping it would have dropped a
186
+ # connection the restarted kernel is still reachable on.
187
+ assert sandbox.is_started is True
188
+
189
+ def test_a_kernel_restart_that_fails_falls_back_to_the_lifecycle(self):
190
+ class _RefusingSandbox(_FakeSandbox):
191
+ def __init__(self):
192
+ super().__init__()
193
+ self.stops = 0
194
+
195
+ def restart_kernel(self):
196
+ return False
197
+
198
+ def stop(self):
199
+ self.stops += 1
200
+ super().stop()
201
+
202
+ sandbox = _RefusingSandbox()
203
+ sandbox.start()
204
+ CodeSandboxClient(sandbox).restart()
205
+
206
+ assert sandbox.stops == 1, "a refused kernel restart still restarts the sandbox"
207
+ assert sandbox.is_started is True
208
+
209
+ def test_a_sandbox_with_no_kernel_of_its_own_restarts_as_it_always_did(self):
210
+ sandbox = _FakeSandbox()
211
+ sandbox.start()
212
+ CodeSandboxClient(sandbox).restart()
213
+ assert sandbox.is_started is True
214
+
215
+
160
216
  def test_a_sandbox_that_knows_it_is_gone_is_believed():
161
217
  """`is_alive` reports what the sandbox can find out, not the local start
162
218
  flag, so a backend that died under us is not reported as ready."""
@@ -80,7 +80,7 @@ def basic(identifier: str, severity: str, *, package: str = "openssl") -> dict:
80
80
 
81
81
 
82
82
  class FakeEcr:
83
- """The two ECR calls the attestor makes, and nothing else."""
83
+ """The three ECR calls the attestor makes, and nothing else."""
84
84
 
85
85
  def __init__(
86
86
  self,
@@ -90,6 +90,7 @@ class FakeEcr:
90
90
  enhanced_findings=True,
91
91
  manifest: dict | None = None,
92
92
  pages: list[list[dict]] | None = None,
93
+ size_bytes: int | None = 2_147_483_648,
93
94
  ) -> None:
94
95
  self.statuses = list(statuses)
95
96
  self.findings = list(findings)
@@ -99,8 +100,12 @@ class FakeEcr:
99
100
  self.pages = pages
100
101
  self.enhanced_findings = enhanced_findings
101
102
  self.manifest = manifest
103
+ #: What `describe_images` answers for the digest; None makes it raise
104
+ #: `ImageNotFoundException`, the way a registry that lost it would.
105
+ self.size_bytes = size_bytes
102
106
  self.asked = 0
103
107
  self.scanned: list[str] = []
108
+ self.sized: list[str] = []
104
109
 
105
110
  def batch_get_image(self, repositoryName, imageIds, acceptedMediaTypes): # noqa: N803 - boto3's spelling
106
111
  manifest = self.manifest or {
@@ -117,6 +122,14 @@ class FakeEcr:
117
122
  ]
118
123
  }
119
124
 
125
+ def describe_images(self, repositoryName, imageIds): # noqa: N803 - boto3's spelling
126
+ self.sized.append(imageIds[0]["imageDigest"])
127
+ if self.size_bytes is None:
128
+ error = Exception("ImageNotFoundException")
129
+ error.response = {"Error": {"Code": "ImageNotFoundException"}}
130
+ raise error
131
+ return {"imageDetails": [{"imageSizeInBytes": self.size_bytes}]}
132
+
120
133
  def describe_image_scan_findings(self, repositoryName, imageId, nextToken=None): # noqa: N803 - boto3's spelling
121
134
  self.asked += 1
122
135
  self.scanned.append(imageId["imageDigest"])
@@ -630,6 +643,33 @@ class TestAttestingAnArtifact:
630
643
  answer = attest_artifact(artifact=self.an_artifact(), attestor=an_attestor())
631
644
  assert answer["scan_summary"]["policy"] == DEFAULT_POLICY.body()
632
645
 
646
+ def test_the_size_is_read_from_the_registry_when_nobody_hands_one_down(self) -> None:
647
+ """Which is every real call: the builder answers a reference, not a
648
+ weight, so `environments.artifact.bytes` — section 14's artifact size
649
+ — had no point in it although artifacts had been recorded (E1-25)."""
650
+ ecr = FakeEcr(size_bytes=2_147_483_648)
651
+ answer = attest_artifact(artifact=self.an_artifact(), attestor=an_attestor(ecr=ecr))
652
+ assert answer["size_bytes"] == 2_147_483_648
653
+ assert ecr.sized == [DIGEST]
654
+
655
+ def test_a_size_handed_down_is_kept_and_the_registry_is_not_asked(self) -> None:
656
+ ecr = FakeEcr()
657
+ answer = attest_artifact(
658
+ artifact=self.an_artifact(), size_bytes=116_183_040, attestor=an_attestor(ecr=ecr)
659
+ )
660
+ assert answer["size_bytes"] == 116_183_040
661
+ assert ecr.sized == []
662
+
663
+ def test_a_size_that_cannot_be_read_is_not_a_reason_to_refuse(self) -> None:
664
+ """A missing number on a dashboard, against an artifact nothing can
665
+ launch: the artifact is signed and the attestation stands."""
666
+ said: list[str] = []
667
+ attestor = an_attestor(ecr=FakeEcr(size_bytes=None), log=said.append)
668
+ answer = attest_artifact(artifact=self.an_artifact(), attestor=attestor)
669
+ assert answer["size_bytes"] is None
670
+ assert answer["signature_ref"] == f"{REGISTRY}/{REPOSITORY}@{DIGEST}"
671
+ assert any("size could not be read" in line for line in said)
672
+
633
673
 
634
674
  def test_nothing_reaches_a_registry_when_nothing_could_sign() -> None:
635
675
  """The order that keeps a refusal cheap and honest (E1-08, E1-09).