code-sandboxes 1.9.14__tar.gz → 1.9.17__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.17}/CHANGELOG.md +45 -0
  2. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/PKG-INFO +1 -1
  3. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/client.py +15 -1
  5. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/attest.py +26 -1
  6. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/bases.py +10 -1
  7. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/files.py +18 -6
  8. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/spec.py +16 -0
  9. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/jupyter_server_sandbox.py +52 -0
  10. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/schemas/environment-v1alpha1.json +46 -0
  11. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_client.py +56 -0
  12. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_attest.py +41 -1
  13. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_bases.py +16 -5
  14. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_builders.py +52 -0
  15. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_resolve.py +5 -1
  16. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_jupyter_server.py +74 -0
  17. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.github/workflows/build.yml +0 -0
  18. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.github/workflows/environments-live.yml +0 -0
  19. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.github/workflows/py-code-style.yml +0 -0
  20. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.github/workflows/py-tests.yml +0 -0
  21. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.github/workflows/py-typing.yml +0 -0
  22. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.github/workflows/reusable-python.yml +0 -0
  23. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.gitignore +0 -0
  24. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.licenserc.yaml +0 -0
  25. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.pre-commit-config.yaml +0 -0
  26. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/.vscode/settings.json +0 -0
  27. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/Dockerfile +0 -0
  28. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/LICENSE +0 -0
  29. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/Makefile +0 -0
  30. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/README.md +0 -0
  31. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/__init__.py +0 -0
  32. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/base.py +0 -0
  33. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/bridge_mount.py +0 -0
  34. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/builds.py +0 -0
  35. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/cli.py +0 -0
  36. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/cloudflare_sandbox.py +0 -0
  37. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/commands.py +0 -0
  38. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/console.py +0 -0
  39. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/contents.py +0 -0
  40. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/coreweave_sandbox.py +0 -0
  41. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/datalayer_sandbox.py +0 -0
  42. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/daytona_sandbox.py +0 -0
  43. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/docker_sandbox.py +0 -0
  44. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/e2b_sandbox.py +0 -0
  45. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/__init__.py +0 -0
  46. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/__main__.py +0 -0
  47. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/accounts.py +0 -0
  48. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/__init__.py +0 -0
  49. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/datalayer.py +0 -0
  50. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/daytona.py +0 -0
  51. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/e2b.py +0 -0
  52. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/managed.py +0 -0
  53. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/modal.py +0 -0
  54. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/breaker.py +0 -0
  55. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/build_secrets.py +0 -0
  56. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/builders.py +0 -0
  57. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/canonical.py +0 -0
  58. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/conformance.py +0 -0
  59. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  60. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  61. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  62. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/contract.py +0 -0
  63. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/__init__.py +0 -0
  64. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/build.py +0 -0
  65. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  66. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  67. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/errors.py +0 -0
  68. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/image_import.py +0 -0
  69. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/lifecycle.py +0 -0
  70. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/policy.py +0 -0
  71. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/redact.py +0 -0
  72. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/resolve.py +0 -0
  73. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/resolve_conda.py +0 -0
  74. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/environments/schema.py +0 -0
  75. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/eval_sandbox.py +0 -0
  76. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/exceptions.py +0 -0
  77. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/filesystem.py +0 -0
  78. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/google_colab.py +0 -0
  79. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/google_colab_sandbox.py +0 -0
  80. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/interfaces.py +0 -0
  81. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/jupyter_ingress.py +0 -0
  82. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/kaggle.py +0 -0
  83. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/kaggle_execute.py +0 -0
  84. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/kaggle_live.py +0 -0
  85. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/kaggle_sandbox.py +0 -0
  86. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/lifecycle.py +0 -0
  87. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/manage.py +0 -0
  88. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/modal_sandbox.py +0 -0
  89. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/models.py +0 -0
  90. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/monty_sandbox.py +0 -0
  91. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/provider_ingress.py +0 -0
  92. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/code_sandboxes/providers.py +0 -0
  93. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docker/Dockerfile +0 -0
  94. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/.gitignore +0 -0
  95. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/LICENSE +0 -0
  96. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/Makefile +0 -0
  97. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/README.md +0 -0
  98. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/babel.config.js +0 -0
  99. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/api-reference/_category_.yml +0 -0
  100. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/api-reference/index.mdx +0 -0
  101. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/cli/_category_.yml +0 -0
  102. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/cli/index.mdx +0 -0
  103. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/contribute/_category_.yml +0 -0
  104. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/contribute/index.mdx +0 -0
  105. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/environments/_category_.yml +0 -0
  106. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/environments/contract.mdx +0 -0
  107. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/environments/index.mdx +0 -0
  108. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/environments/specification.mdx +0 -0
  109. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/environments/spike-2026-09.mdx +0 -0
  110. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/examples/_category_.yml +0 -0
  111. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/examples/index.mdx +0 -0
  112. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/guide/_category_.yml +0 -0
  113. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/guide/index.mdx +0 -0
  114. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/index.mdx +0 -0
  115. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/install/_category_.yml +0 -0
  116. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/install/index.mdx +0 -0
  117. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/provider-ingress.mdx +0 -0
  118. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/_category_.yml +0 -0
  119. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/cloudflare.mdx +0 -0
  120. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/coreweave.mdx +0 -0
  121. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/datalayer.mdx +0 -0
  122. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/daytona.mdx +0 -0
  123. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/docker.mdx +0 -0
  124. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/e2b.mdx +0 -0
  125. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/eval.mdx +0 -0
  126. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/google-colab.mdx +0 -0
  127. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/index.mdx +0 -0
  128. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/jupyter-server.mdx +0 -0
  129. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/kaggle.mdx +0 -0
  130. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/modal.mdx +0 -0
  131. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docs/providers/monty.mdx +0 -0
  132. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/docusaurus.config.js +0 -0
  133. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/package.json +0 -0
  134. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/sidebars.js +0 -0
  135. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow1.js +0 -0
  136. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow1.module.css +0 -0
  137. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow2.js +0 -0
  138. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow2.module.css +0 -0
  139. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow3.js +0 -0
  140. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow3.module.css +0 -0
  141. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/css/custom.css +0 -0
  142. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/pages/index.module.css +0 -0
  143. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/pages/markdown-page.md +0 -0
  144. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/theme/CustomDocItem.tsx +0 -0
  145. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/src/theme/IconsTitle.tsx +0 -0
  146. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/bricks.svg +0 -0
  147. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/building-construction.svg +0 -0
  148. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/cloud-sun.svg +0 -0
  149. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/cloud.svg +0 -0
  150. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/collaboration.svg +0 -0
  151. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/copy-left.svg +0 -0
  152. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/datalayer/logo.png +0 -0
  153. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/datalayer/logo.svg +0 -0
  154. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/datalayer/personas/business.svg +0 -0
  155. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  156. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/datalayer/personas/devops.svg +0 -0
  157. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/datalayer/project/project-line.svg +0 -0
  158. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/favicon.ico +0 -0
  159. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/jupyter.svg +0 -0
  160. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/open-source.svg +0 -0
  161. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/react-js.svg +0 -0
  162. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/react-js2.svg +0 -0
  163. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/ringed-planet.svg +0 -0
  164. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/robot.svg +0 -0
  165. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/rocket.svg +0 -0
  166. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/shield.svg +0 -0
  167. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/sparkles.svg +0 -0
  168. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/streamlit.gif +0 -0
  169. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/super-hero.svg +0 -0
  170. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/target.svg +0 -0
  171. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/docs/static/img/thumbs-up.svg +0 -0
  172. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/README.md +0 -0
  173. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/Makefile +0 -0
  174. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/cloudflare_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/coreweave_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/datalayer_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/daytona_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/docker_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/e2b_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/eval_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/google_colab_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/kaggle_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/modal_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/exec/monty_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/Makefile +0 -0
  187. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/cloudflare_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/coreweave_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/datalayer_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/daytona_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/docker_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/e2b_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/eval_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/google_colab_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/kaggle_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/modal_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/examples/repl/monty_sandbox_example.py +0 -0
  199. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/pyproject.toml +0 -0
  200. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/__init__.py +0 -0
  201. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/conftest.py +0 -0
  202. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  203. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_bridge_mount.py +0 -0
  204. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_builds.py +0 -0
  205. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_cli_exec.py +0 -0
  206. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_cli_repl.py +0 -0
  207. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_client_contents.py +0 -0
  208. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_client_files.py +0 -0
  209. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_client_outcome_error_output.py +0 -0
  210. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_cloudflare.py +0 -0
  211. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_commands_envelope.py +0 -0
  212. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_console.py +0 -0
  213. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_contents_contract.py +0 -0
  214. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_contents_live_matrix.py +0 -0
  215. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_coreweave.py +0 -0
  216. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_datalayer_sandbox.py +0 -0
  217. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_daytona.py +0 -0
  218. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_e2b.py +0 -0
  219. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_accounts.py +0 -0
  220. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_breaker.py +0 -0
  221. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_build_secrets.py +0 -0
  222. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_canonical.py +0 -0
  223. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_conformance.py +0 -0
  224. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_constraints.py +0 -0
  225. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_contract.py +0 -0
  226. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_cross_variant_report.py +0 -0
  227. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_datalayer_builder.py +0 -0
  228. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_daytona_builder.py +0 -0
  229. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_doctor.py +0 -0
  230. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_e2b_builder.py +0 -0
  231. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_errors.py +0 -0
  232. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_image_import.py +0 -0
  233. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_lifecycle.py +0 -0
  234. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_live_matrix.py +0 -0
  235. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_managed_builders.py +0 -0
  236. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_modal_builder.py +0 -0
  237. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_redact.py +0 -0
  238. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_resolve_conda.py +0 -0
  239. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_environment_spec.py +0 -0
  240. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_eval.py +0 -0
  241. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  242. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_factory.py +0 -0
  243. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_google_colab.py +0 -0
  244. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_integration.py +0 -0
  245. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_jupyter_ingress.py +0 -0
  246. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_jupyter_server_streaming.py +0 -0
  247. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_kaggle.py +0 -0
  248. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_kaggle_execute.py +0 -0
  249. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_kaggle_live.py +0 -0
  250. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_kaggle_session.py +0 -0
  251. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_kernel_client_compatibility.py +0 -0
  252. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_lifecycle_vocabulary.py +0 -0
  253. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_manage.py +0 -0
  254. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_manage_datalayer.py +0 -0
  255. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_modal_google_colab_sandbox.py +0 -0
  256. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_modal_session.py +0 -0
  257. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_models.py +0 -0
  258. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_output_line_fidelity.py +0 -0
  259. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_provider_catalog_names.py +0 -0
  260. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_provider_ingress.py +0 -0
  261. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_providers.py +0 -0
  262. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_sandboxes.py +0 -0
  263. {code_sandboxes-1.9.14 → code_sandboxes-1.9.17}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -8,6 +8,51 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.9.17
12
+
13
+ - **An artifact's size is read from the registry** (`environments/attest.py`;
14
+ PLAN_ENVS.md E1-25). `attest_artifact` took `size_bytes` from its caller and
15
+ nobody ever passed one — the builder answers a reference, not a weight — so
16
+ every artefact was recorded with `sizeBytes: null` and
17
+ `environments.artifact.bytes`, the series section 14 tracks the artifact
18
+ size in, had no point in it although artifacts had been recorded (seen on r1,
19
+ 2026-09-16, through the OTEL query API). `Attestor.size_of()` asks the
20
+ registry, with the client the scan is already read from, and a size that
21
+ cannot be read is logged rather than raised: a missing number on a dashboard
22
+ is not a reason to refuse an artifact that is otherwise signed. 3 new tests.
23
+
24
+ ## 1.9.15
25
+
26
+ - **A restart restarts the kernel, not just this client's socket**
27
+ (`jupyter_server_sandbox`, `client`; PLAN_ENV.md E0-09, Appendix B check
28
+ 7). `CodeSandboxClient.restart()` was `stop()` then `start()`, which is
29
+ right for a sandbox this process owns — it is destroyed and recreated, and
30
+ nothing survives — and wrong for one *attached* to a Jupyter server
31
+ somebody else runs, which is every Datalayer runtime pod: stopping drops
32
+ the websocket while the kernel process keeps running, so the reconnect
33
+ lands in the same interpreter with every global still set. Check 7 is
34
+ "nothing is assumed to persist across restarts", and it read `state survived the restart ('True')` for exactly this reason — found live on r1,
35
+ 2026-09-16, the first drill whose smoke test reached the check.
36
+ `JupyterServerSandbox.restart_kernel()` now asks the server's own
37
+ `POST /api/kernels/{id}/restart` (the way `_do_interrupt` already uses the
38
+ API rather than the client's lifecycle) and reconnects onto the new
39
+ kernel; `restart()` prefers it and falls back to the lifecycle for every
40
+ variant that draws no such distinction. 7 new tests.
41
+ - **`datalayer/python-cpu:2026.09` repinned** to
42
+ `sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148`:
43
+ the contract layer now sets `MappingKernelManager.root_dir`, so kernels
44
+ start in `/home/datalayer/content`. The image already declared `WORKDIR`
45
+ there and `sandbox-contract/v1`'s User row already required it, but a
46
+ kernel's cwd is the Jupyter server's to choose and jupyter-python's config
47
+ roots it at `$HOME` — so every environment's kernel ran in
48
+ `/home/datalayer` and Appendix B check 2 read `cwd is '/home/datalayer', not '/home/datalayer/content'`. The file browser stays rooted at `$HOME`,
49
+ where a person expects to see everything they have; only the kernel moves.
50
+ - Two assertions that had rotted through three base releases are pinned in
51
+ one place again: the channel's digest and its apt snapshot were duplicated
52
+ across `test_environment_bases.py` and `test_environment_resolve.py`, and
53
+ 2026-09-15's and 2026-09-16's releases left both red rather than catching
54
+ anything.
55
+
11
56
  ## 1.9.14
12
57
 
13
58
  - **`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.17
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.17"
@@ -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,
@@ -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).
@@ -52,10 +52,17 @@ def test_the_2026_09_channel_of_python_cuda_has_no_digest_until_it_is_pushed(
52
52
  def test_the_2026_09_channel_of_python_cpu_resolves_the_digest_its_release_pushed(
53
53
  variant: str,
54
54
  ) -> None:
55
- """PLAN_ENV.md, E1-05: released 2026-09-14 (jupyter-python 0.2.2, E1-08's
56
- scan fix), same digest for every variant."""
55
+ """PLAN_ENV.md, E1-05: the digest the channel's last release pushed, the
56
+ same one for every variant.
57
+
58
+ **This moves with every base release**, and `bases.py` is where it moves
59
+ first: two releases (2026-09-15's and 2026-09-16's) changed the channel
60
+ and left this assertion on 2026-09-14's digest, so it sat red rather than
61
+ catching anything. Current: released 2026-09-16, the contract layer that
62
+ starts kernels in `/home/datalayer/content` (E1-05, Appendix B check 2).
63
+ """
57
64
  ref = "datalayer/python-cpu"
58
- digest = "sha256:334adf6c2714c8919ef60beeca1db12e3531a391c9dde41932c782f81c432b36"
65
+ digest = "sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148"
59
66
  assert APPROVED_BASES[ref].channels == {"2026.09": dict.fromkeys(VARIANTS, digest)}
60
67
  assert resolve_base(ref, "2026.09", variant) == digest
61
68
 
@@ -114,8 +121,12 @@ def test_each_base_is_published_under_its_own_repository() -> None:
114
121
 
115
122
 
116
123
  def test_the_2026_09_channel_of_python_cpu_pins_apt_to_its_snapshot() -> None:
117
- """D-9: the moment just after the channel's image upgraded its packages."""
118
- assert channel_snapshot("datalayer/python-cpu", "2026.09") == "20260914T150000Z"
124
+ """D-9: the moment just after the channel's image upgraded its packages.
125
+
126
+ Moves with the channel, like the digest above, and had rotted the same
127
+ way — left on 2026-09-14's id after the channel moved to 2026-09-16's.
128
+ """
129
+ assert channel_snapshot("datalayer/python-cpu", "2026.09") == "20260916T120000Z"
119
130
  assert channel_snapshot("datalayer/python-cuda", "2026.09") == ""
120
131
  assert channel_snapshot("datalayer/nothing", "2026.09") == ""
121
132
 
@@ -280,6 +280,58 @@ def test_a_file_is_not_baked_without_its_digest() -> None:
280
280
  files_step(environment(), variant="kaggle")
281
281
 
282
282
 
283
+ CONTENTS_SHA = "c" * 64
284
+
285
+
286
+ def test_the_files_step_bakes_a_contents_build_manifest_from_an_external_source() -> None:
287
+ env = environment(
288
+ files=[],
289
+ contents_build=[
290
+ {
291
+ "source": "https://data.example/iris.csv",
292
+ "path": "/opt/datalayer/contents/iris.csv",
293
+ "sha256": CONTENTS_SHA,
294
+ }
295
+ ],
296
+ )
297
+ commands = files_step(env, variant="modal")
298
+ # The external source is fetched as it is (no contentRef to sign) and verified.
299
+ assert (
300
+ "curl -fsSL https://data.example/iris.csv -o /opt/datalayer/contents/iris.csv"
301
+ in commands[0]
302
+ )
303
+ assert f'echo "{CONTENTS_SHA} /opt/datalayer/contents/iris.csv" | sha256sum -c' in commands[0]
304
+ assert "environment-contents.json" in commands[-1]
305
+
306
+
307
+ def test_files_and_contents_build_are_baked_together() -> None:
308
+ env = environment(
309
+ contents_build=[
310
+ {
311
+ "source": "https://data.example/iris.csv",
312
+ "path": "/opt/datalayer/contents/iris.csv",
313
+ "sha256": CONTENTS_SHA,
314
+ }
315
+ ]
316
+ )
317
+ entries = build_entries(env, source_of=lambda entry: "https://signed.example/notes.md")
318
+ # The uploaded file first, then the external build entry — both baked, one engine.
319
+ assert [entry.destination_path for entry in entries] == [
320
+ "/home/datalayer/content/notes.md",
321
+ "/opt/datalayer/contents/iris.csv",
322
+ ]
323
+ assert entries[0].source_uri == "https://signed.example/notes.md"
324
+ assert entries[1].source_uri == "https://data.example/iris.csv"
325
+ assert entries[1].sha256 == CONTENTS_SHA
326
+
327
+
328
+ def test_a_contents_build_entry_without_its_digest_will_not_parse() -> None:
329
+ with pytest.raises(Exception):
330
+ environment(
331
+ contents_build=[{"source": "https://data.example/x", "path": "/opt/x"}]
332
+ )
333
+
334
+
283
335
  def test_the_neutral_modules_import_no_provider_sdk() -> None:
284
336
  """What a service may import must not drag a provider in."""
285
337
  code = (
@@ -998,8 +998,12 @@ def test_the_solve_is_asked_to_pin_apt_at_the_base_channels_snapshot() -> None:
998
998
  runner = RecordedRunner(A_LOCK)
999
999
  resolve_environment(spec=a_spec(), variants=["datalayer"], runner=runner)
1000
1000
  assert runner.request is not None
1001
+ # What this test is about: the solve is pinned at *the channel's* snapshot,
1002
+ # whatever that is. The id itself is pinned once, in
1003
+ # `test_environment_bases.py`, where it moves with the channel — repeating
1004
+ # it here only meant a base release left two tests red instead of one.
1001
1005
  assert runner.request.apt_snapshot == channel_snapshot("datalayer/python-cpu", "2026.09")
1002
- assert runner.request.apt_snapshot == "20260914T150000Z"
1006
+ assert runner.request.apt_snapshot
1003
1007
 
1004
1008
 
1005
1009
  class TestTheBuildkitRunner: