code-sandboxes 1.9.19__tar.gz → 1.9.21__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.19 → code_sandboxes-1.9.21}/PKG-INFO +1 -1
  2. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/__version__.py +1 -1
  3. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/daytona.py +20 -2
  4. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/e2b.py +18 -1
  5. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/managed.py +37 -1
  6. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/modal.py +1 -1
  7. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/attest.py +30 -1
  8. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/spec.py +49 -13
  9. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/schemas/environment-v1alpha1.json +24 -0
  10. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_attest.py +54 -15
  11. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_daytona_builder.py +18 -2
  12. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_managed_builders.py +54 -15
  13. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_spec.py +35 -3
  14. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/build.yml +0 -0
  15. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/environments-live.yml +0 -0
  16. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/py-code-style.yml +0 -0
  17. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/py-tests.yml +0 -0
  18. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/py-typing.yml +0 -0
  19. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/reusable-python.yml +0 -0
  20. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.gitignore +0 -0
  21. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.licenserc.yaml +0 -0
  22. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.pre-commit-config.yaml +0 -0
  23. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.vscode/settings.json +0 -0
  24. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/CHANGELOG.md +0 -0
  25. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/Dockerfile +0 -0
  26. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/LICENSE +0 -0
  27. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/Makefile +0 -0
  28. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/README.md +0 -0
  29. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/__init__.py +0 -0
  30. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/base.py +0 -0
  31. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/bridge_mount.py +0 -0
  32. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/builds.py +0 -0
  33. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/cli.py +0 -0
  34. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/client.py +0 -0
  35. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/cloudflare_sandbox.py +0 -0
  36. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/commands.py +0 -0
  37. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/console.py +0 -0
  38. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/contents.py +0 -0
  39. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/coreweave_sandbox.py +0 -0
  40. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/datalayer_sandbox.py +0 -0
  41. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/daytona_sandbox.py +0 -0
  42. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/docker_sandbox.py +0 -0
  43. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/e2b_sandbox.py +0 -0
  44. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/__init__.py +0 -0
  45. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/__main__.py +0 -0
  46. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/accounts.py +0 -0
  47. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/__init__.py +0 -0
  48. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/datalayer.py +0 -0
  49. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/bases.py +0 -0
  50. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/breaker.py +0 -0
  51. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/build_secrets.py +0 -0
  52. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/builders.py +0 -0
  53. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/canonical.py +0 -0
  54. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/conformance.py +0 -0
  55. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  56. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  57. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  58. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/contract.py +0 -0
  59. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/__init__.py +0 -0
  60. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/build.py +0 -0
  61. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  62. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  63. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/errors.py +0 -0
  64. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/files.py +0 -0
  65. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/image_import.py +0 -0
  66. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/lifecycle.py +0 -0
  67. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/policy.py +0 -0
  68. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/redact.py +0 -0
  69. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/resolve.py +0 -0
  70. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/resolve_conda.py +0 -0
  71. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/schema.py +0 -0
  72. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/eval_sandbox.py +0 -0
  73. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/exceptions.py +0 -0
  74. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/filesystem.py +0 -0
  75. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/google_colab.py +0 -0
  76. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/google_colab_sandbox.py +0 -0
  77. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/interfaces.py +0 -0
  78. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/jupyter_ingress.py +0 -0
  79. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  80. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle.py +0 -0
  81. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle_execute.py +0 -0
  82. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle_live.py +0 -0
  83. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle_sandbox.py +0 -0
  84. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/lifecycle.py +0 -0
  85. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/manage.py +0 -0
  86. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/modal_sandbox.py +0 -0
  87. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/models.py +0 -0
  88. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/monty_sandbox.py +0 -0
  89. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/provider_ingress.py +0 -0
  90. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/providers.py +0 -0
  91. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docker/Dockerfile +0 -0
  92. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/.gitignore +0 -0
  93. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/LICENSE +0 -0
  94. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/Makefile +0 -0
  95. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/README.md +0 -0
  96. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/babel.config.js +0 -0
  97. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/api-reference/_category_.yml +0 -0
  98. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/api-reference/index.mdx +0 -0
  99. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/cli/_category_.yml +0 -0
  100. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/cli/index.mdx +0 -0
  101. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/contribute/_category_.yml +0 -0
  102. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/contribute/index.mdx +0 -0
  103. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/_category_.yml +0 -0
  104. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/contract.mdx +0 -0
  105. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/index.mdx +0 -0
  106. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/specification.mdx +0 -0
  107. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/spike-2026-09.mdx +0 -0
  108. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/examples/_category_.yml +0 -0
  109. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/examples/index.mdx +0 -0
  110. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/guide/_category_.yml +0 -0
  111. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/guide/index.mdx +0 -0
  112. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/index.mdx +0 -0
  113. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/install/_category_.yml +0 -0
  114. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/install/index.mdx +0 -0
  115. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/provider-ingress.mdx +0 -0
  116. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/_category_.yml +0 -0
  117. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/cloudflare.mdx +0 -0
  118. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/coreweave.mdx +0 -0
  119. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/datalayer.mdx +0 -0
  120. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/daytona.mdx +0 -0
  121. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/docker.mdx +0 -0
  122. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/e2b.mdx +0 -0
  123. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/eval.mdx +0 -0
  124. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/google-colab.mdx +0 -0
  125. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/index.mdx +0 -0
  126. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/jupyter-server.mdx +0 -0
  127. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/kaggle.mdx +0 -0
  128. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/modal.mdx +0 -0
  129. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/monty.mdx +0 -0
  130. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docusaurus.config.js +0 -0
  131. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/package.json +0 -0
  132. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/sidebars.js +0 -0
  133. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow1.js +0 -0
  134. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow1.module.css +0 -0
  135. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow2.js +0 -0
  136. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow2.module.css +0 -0
  137. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow3.js +0 -0
  138. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow3.module.css +0 -0
  139. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/css/custom.css +0 -0
  140. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/pages/index.module.css +0 -0
  141. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/pages/markdown-page.md +0 -0
  142. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/theme/CustomDocItem.tsx +0 -0
  143. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/theme/IconsTitle.tsx +0 -0
  144. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/bricks.svg +0 -0
  145. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/building-construction.svg +0 -0
  146. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/cloud-sun.svg +0 -0
  147. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/cloud.svg +0 -0
  148. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/collaboration.svg +0 -0
  149. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/copy-left.svg +0 -0
  150. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/logo.png +0 -0
  151. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/logo.svg +0 -0
  152. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/personas/business.svg +0 -0
  153. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  154. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/personas/devops.svg +0 -0
  155. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/project/project-line.svg +0 -0
  156. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/favicon.ico +0 -0
  157. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/jupyter.svg +0 -0
  158. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/open-source.svg +0 -0
  159. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/react-js.svg +0 -0
  160. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/react-js2.svg +0 -0
  161. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/ringed-planet.svg +0 -0
  162. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/robot.svg +0 -0
  163. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/rocket.svg +0 -0
  164. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/shield.svg +0 -0
  165. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/sparkles.svg +0 -0
  166. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/streamlit.gif +0 -0
  167. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/super-hero.svg +0 -0
  168. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/target.svg +0 -0
  169. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/thumbs-up.svg +0 -0
  170. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/README.md +0 -0
  171. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/Makefile +0 -0
  172. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/cloudflare_sandbox_example.py +0 -0
  173. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/coreweave_sandbox_example.py +0 -0
  174. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/datalayer_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/daytona_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/docker_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/e2b_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/eval_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/google_colab_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/kaggle_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/modal_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/monty_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/Makefile +0 -0
  185. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/cloudflare_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/coreweave_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/datalayer_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/daytona_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/docker_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/e2b_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/eval_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/google_colab_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/kaggle_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/modal_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/monty_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/pyproject.toml +0 -0
  198. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/__init__.py +0 -0
  199. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/conftest.py +0 -0
  200. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  201. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_bridge_mount.py +0 -0
  202. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_builds.py +0 -0
  203. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_cli_exec.py +0 -0
  204. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_cli_repl.py +0 -0
  205. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client.py +0 -0
  206. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client_contents.py +0 -0
  207. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client_files.py +0 -0
  208. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client_outcome_error_output.py +0 -0
  209. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_cloudflare.py +0 -0
  210. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_commands_envelope.py +0 -0
  211. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_console.py +0 -0
  212. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_contents_contract.py +0 -0
  213. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_contents_live_matrix.py +0 -0
  214. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_coreweave.py +0 -0
  215. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_datalayer_sandbox.py +0 -0
  216. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_daytona.py +0 -0
  217. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_e2b.py +0 -0
  218. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_accounts.py +0 -0
  219. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_bases.py +0 -0
  220. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_breaker.py +0 -0
  221. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_build_secrets.py +0 -0
  222. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_builders.py +0 -0
  223. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_canonical.py +0 -0
  224. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_conformance.py +0 -0
  225. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_constraints.py +0 -0
  226. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_contract.py +0 -0
  227. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_cross_variant_report.py +0 -0
  228. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_datalayer_builder.py +0 -0
  229. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_doctor.py +0 -0
  230. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_e2b_builder.py +0 -0
  231. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_errors.py +0 -0
  232. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_image_import.py +0 -0
  233. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_lifecycle.py +0 -0
  234. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_live_matrix.py +0 -0
  235. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_modal_builder.py +0 -0
  236. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_redact.py +0 -0
  237. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_resolve.py +0 -0
  238. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_resolve_conda.py +0 -0
  239. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_eval.py +0 -0
  240. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  241. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_factory.py +0 -0
  242. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_google_colab.py +0 -0
  243. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_integration.py +0 -0
  244. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_jupyter_ingress.py +0 -0
  245. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_jupyter_server.py +0 -0
  246. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_jupyter_server_streaming.py +0 -0
  247. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle.py +0 -0
  248. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle_execute.py +0 -0
  249. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle_live.py +0 -0
  250. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle_session.py +0 -0
  251. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kernel_client_compatibility.py +0 -0
  252. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_lifecycle_vocabulary.py +0 -0
  253. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_manage.py +0 -0
  254. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_manage_datalayer.py +0 -0
  255. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_modal_google_colab_sandbox.py +0 -0
  256. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_modal_session.py +0 -0
  257. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_models.py +0 -0
  258. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_output_line_fidelity.py +0 -0
  259. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_provider_catalog_names.py +0 -0
  260. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_provider_ingress.py +0 -0
  261. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_providers.py +0 -0
  262. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_sandboxes.py +0 -0
  263. {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.19
3
+ Version: 1.9.21
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.19"
6
+ __version__ = "1.9.21"
@@ -179,7 +179,12 @@ class Builder(ManagedBuilder):
179
179
  title = "Daytona"
180
180
  #: A `packages` list and, for conda (E3-02), an `environment.yml`
181
181
  #: dependency file installed with `micromamba`.
182
- build_sources = ("packages", "dependencyFile")
182
+ build_sources = ("packages", "dependencyFile", "dockerfile")
183
+ #: None beyond the contract's own (E3-03): `Image.from_dockerfile` keeps
184
+ #: the Dockerfile text as it is and Daytona builds it on a real Docker
185
+ #: builder, so the grammar it accepts is Docker's. Checked in the SDK on
186
+ #: 2026-09-17.
187
+ forbidden_instructions = ()
183
188
  dependency_formats = ("conda",)
184
189
  #: Daytona runs GPUs, on its own hardware and the owner's account (E2-17).
185
190
  #: This builder does not build one yet: see `_own_findings`.
@@ -419,6 +424,11 @@ class Builder(ManagedBuilder):
419
424
  self._log(line)
420
425
 
421
426
  resources = self._resources(sdk, request.size_class)
427
+ # A snapshot is region-scoped, and the region that scopes it is
428
+ # Daytona's, not this platform's. `validate` already refuses
429
+ # more than one, so the first is the only one.
430
+ declared = list(request.environment.spec.compatibility.regions)
431
+ region = declared[0] if declared else None
422
432
  try:
423
433
  snapshot = client.snapshot.create(
424
434
  sdk.CreateSnapshotParams(
@@ -426,7 +436,15 @@ class Builder(ManagedBuilder):
426
436
  image=image,
427
437
  resources=resources,
428
438
  entrypoint=_CONTRACT_ENTRYPOINT,
429
- region_id=request.region,
439
+ # Only a region Daytona knows. `request.region` is
440
+ # *Datalayer's* — `r1` — and sending it answered
441
+ # "Region not found" on the first real Daytona
442
+ # build (2026-09-17). The owner names a Daytona
443
+ # region in `compatibility.regions`; with none,
444
+ # the field is left out and the account's own
445
+ # default decides, which is what every snapshot
446
+ # in the owner's account already has.
447
+ **({"region_id": region} if region else {}),
430
448
  ),
431
449
  on_logs=on_logs,
432
450
  timeout=self.max_build_seconds,
@@ -199,7 +199,24 @@ class Builder(ManagedBuilder):
199
199
  title = "E2B"
200
200
  #: A `packages` list and, for conda (E3-02), an `environment.yml`
201
201
  #: dependency file installed with `micromamba`.
202
- build_sources = ("packages", "dependencyFile")
202
+ build_sources = ("packages", "dependencyFile", "dockerfile")
203
+ #: What E2B's own Dockerfile parser does not handle (E3-03). Read from the
204
+ #: SDK on 2026-09-17: `e2b.template.dockerfile_parser` branches on FROM,
205
+ #: RUN, COPY, ADD, WORKDIR, USER, ENV, ARG, CMD and ENTRYPOINT, and for
206
+ #: anything else **prints `Unsupported instruction` and carries on**. So a
207
+ #: template built from a Dockerfile naming one of these comes back without
208
+ #: it and reports success — which is exactly what a capability report
209
+ #: exists to prevent.
210
+ forbidden_instructions = (
211
+ "VOLUME",
212
+ "EXPOSE",
213
+ "HEALTHCHECK",
214
+ "SHELL",
215
+ "ONBUILD",
216
+ "STOPSIGNAL",
217
+ "LABEL",
218
+ "MAINTAINER",
219
+ )
203
220
  dependency_formats = ("conda",)
204
221
  #: Firecracker microVMs: no GPU passthrough.
205
222
  gpu = False
@@ -73,7 +73,11 @@ class ManagedBuilder:
73
73
  #: built for a managed variant yet (E3-01), so it is not listed here.
74
74
  dependency_formats: tuple[str, ...] = ()
75
75
  package_managers: tuple[str, ...] = ("uv", "pip")
76
- #: Dockerfile instructions its own builder does not implement (§6).
76
+ #: Dockerfile instructions its own builder does not implement (§6, E3-03).
77
+ #: Checked against a `dockerfile`-sourced spec at `validate`, before
78
+ #: anything is queued — the point being to refuse an instruction a variant
79
+ #: would otherwise drop, rather than hand back an image quietly missing
80
+ #: whatever it asked for.
77
81
  forbidden_instructions: tuple[str, ...] = ()
78
82
  #: Where its artifacts live; empty when the variant is regionless.
79
83
  regions: tuple[str, ...] = ()
@@ -128,6 +132,8 @@ class ManagedBuilder:
128
132
  )
129
133
  elif spec.build.source == "dependencyFile":
130
134
  findings.extend(self._dependency_file_findings(spec))
135
+ elif spec.build.source == "dockerfile":
136
+ findings.extend(self._dockerfile_findings(spec))
131
137
  if spec.packages.python.manager not in self.package_managers:
132
138
  findings.append(
133
139
  CapabilityFinding(
@@ -269,6 +275,36 @@ class ManagedBuilder:
269
275
 
270
276
  # -- helpers for the subclasses -------------------------------------------
271
277
 
278
+ def _dockerfile_findings(self, spec: Any) -> list[CapabilityFinding]:
279
+ """Every instruction in the spec's Dockerfile this variant would not honour.
280
+
281
+ The contract's own refusals are `spec.py`'s to make and apply to every
282
+ variant alike; this is the narrower question of what *this* builder
283
+ does with a Dockerfile it accepts. Each is named with its line, since
284
+ a Dockerfile is somebody's file.
285
+ """
286
+ if not self.forbidden_instructions:
287
+ return []
288
+ dockerfile = getattr(spec.build, "dockerfile", None)
289
+ content = getattr(dockerfile, "content", "") or ""
290
+ if not content.strip():
291
+ return []
292
+ from ..contract import parse_dockerfile
293
+
294
+ refused = set(self.forbidden_instructions)
295
+ return [
296
+ CapabilityFinding(
297
+ code=CAPABILITY_UNSUPPORTED.code,
298
+ message=(
299
+ f"line {instruction.line}: {self.title} does not implement "
300
+ f"`{instruction.keyword}`"
301
+ ),
302
+ field="spec.build.dockerfile.content",
303
+ )
304
+ for instruction in parse_dockerfile(content)
305
+ if instruction.keyword in refused
306
+ ]
307
+
272
308
  @staticmethod
273
309
  def _spec_finding(message: str, field: str) -> CapabilityFinding:
274
310
  return CapabilityFinding(code=SPEC_INVALID.code, message=message, field=field)
@@ -286,7 +286,7 @@ class Builder(ManagedBuilder):
286
286
  title = "Modal"
287
287
  #: A `packages` list and, for conda (E3-02), an `environment.yml`
288
288
  #: dependency file installed with `micromamba_install`.
289
- build_sources = ("packages", "dependencyFile")
289
+ build_sources = ("packages", "dependencyFile", "dockerfile")
290
290
  dependency_formats = ("conda",)
291
291
  #: Modal runs GPUs, in the owner's workspace (E2-17). This builder does
292
292
  #: not build one yet: see `build`'s own guard.
@@ -675,14 +675,43 @@ def attest_artifact(
675
675
  answers the mapping the workflow stores: the scan's decision, the
676
676
  signature, the SBOM and provenance references, and the size.
677
677
  """
678
+ variant = str(getattr(artifact, "variant", "") or "")
678
679
  reference = str(getattr(artifact, "immutable_reference", "") or "")
680
+ if variant and variant != "datalayer":
681
+ # D-11 is about the Datalayer artifact: it lives in this platform's
682
+ # registry, the scanner reads it there, cosign signs that digest, and
683
+ # the Operator refuses to start what is unsigned. A managed artifact
684
+ # is none of those things — it lives in the owner's own provider
685
+ # account (D-8) and is referenced the way that provider names it: a
686
+ # Daytona snapshot uuid, an E2B build id, a Modal `im-…`. There is no
687
+ # digest in a repository to scan or sign, and no Operator starting it.
688
+ #
689
+ # Attesting one anyway is what the first real Daytona build did, and
690
+ # it failed *after* the snapshot was built — the work done, the
691
+ # artifact live at the provider, and the build recorded as failed
692
+ # (2026-09-17).
693
+ #
694
+ # Publishing is not weakened by this: E2-15's gate requires the
695
+ # **Datalayer** artifact to have passed its scan, and that one is
696
+ # still attested here.
697
+ if log:
698
+ log(f"{variant} artifacts are not attested: {reference} is not a digest in a registry")
699
+ return {
700
+ "scan_summary": {},
701
+ "sbom_ref": "",
702
+ "provenance_ref": "",
703
+ "signature_ref": "",
704
+ "size_bytes": size_bytes,
705
+ "signed_now": False,
706
+ "licenses": [],
707
+ }
679
708
  registry, _, rest = reference.partition("/")
680
709
  repository, _, digest = rest.partition("@")
681
710
  if not (registry and repository and _DIGEST.match(digest)):
682
711
  raise EnvironmentsError(
683
712
  PROVIDER_ERROR,
684
713
  f"`{reference}` is not a digest in a repository, so it cannot be attested",
685
- detail={"reference": reference},
714
+ detail={"reference": reference, "variant": variant},
686
715
  )
687
716
  use = attestor or Attestor(
688
717
  key=str(getattr(credential, "signing_key", "") or ""),
@@ -55,10 +55,10 @@ __all__ = [
55
55
  "BUILD_SOURCES",
56
56
  "GPU_SIZE_CLASSES",
57
57
  "KIND",
58
+ "PUBLIC_PACKAGE_INDEX_HOSTS",
58
59
  "SIZE_CLASSES",
59
60
  "SUPPORTED_BUILD_SOURCES",
60
61
  "VARIANTS",
61
- "PUBLIC_PACKAGE_INDEX_HOSTS",
62
62
  "Accelerator",
63
63
  "ArtifactStatus",
64
64
  "Base",
@@ -174,14 +174,12 @@ class Platform(_Model):
174
174
  #: reached with a credential the public does not hold. Matched on host, so the
175
175
  #: trailing `/simple` or its absence never decides it. `pypi.org` is the index;
176
176
  #: `files.pythonhosted.org` is where its wheels are served from.
177
- PUBLIC_PACKAGE_INDEX_HOSTS = frozenset(
178
- {"pypi.org", "files.pythonhosted.org"}
179
- )
177
+ PUBLIC_PACKAGE_INDEX_HOSTS = frozenset({"pypi.org", "files.pythonhosted.org"})
180
178
 
181
179
 
182
180
  def _package_index_host(url: str) -> str:
183
181
  """The host an index URL names, lower-cased and without its port, or `""`."""
184
- from urllib.parse import urlsplit # noqa: PLC0415
182
+ from urllib.parse import urlsplit
185
183
 
186
184
  try:
187
185
  return (urlsplit(url).hostname or "").lower()
@@ -214,9 +212,7 @@ PUBLIC_CONDA_CHANNELS = frozenset(
214
212
  "msys2",
215
213
  }
216
214
  )
217
- PUBLIC_CONDA_CHANNEL_HOSTS = frozenset(
218
- {"conda.anaconda.org", "repo.anaconda.com", "anaconda.org"}
219
- )
215
+ PUBLIC_CONDA_CHANNEL_HOSTS = frozenset({"conda.anaconda.org", "repo.anaconda.com", "anaconda.org"})
220
216
 
221
217
 
222
218
  def channel_is_public(channel: str) -> bool:
@@ -344,6 +340,25 @@ class DependencyFileSpec(_Model):
344
340
  lock_content: str = ""
345
341
 
346
342
 
343
+ class DockerfileSpec(_Model):
344
+ """The Dockerfile a `dockerfile`-sourced version builds from (E3-03).
345
+
346
+ Inline, the way ``DependencyFileSpec`` carries a ``requirements.txt``: a
347
+ file the author brings lives in the spec, so ``check_dockerfile`` and
348
+ every variant's capability report can read it **before** anything is
349
+ queued — which is the whole point of refusing an instruction a variant
350
+ does not implement at ``validate`` rather than halfway through a build.
351
+
352
+ The build *context* — the extra files a ``COPY`` needs — is the separate
353
+ upload E3-03 describes, bounded and checked by ``validate_build_context``.
354
+ A Dockerfile with no context is the common case and needs no upload at
355
+ all.
356
+ """
357
+
358
+ #: The Dockerfile text.
359
+ content: str = ""
360
+
361
+
347
362
  class ImageSourceSpec(_Model):
348
363
  """An existing OCI image, imported as the build's base (E3-04).
349
364
 
@@ -370,6 +385,7 @@ class ImageSourceSpec(_Model):
370
385
  class BuildSpec(_Model):
371
386
  source: Literal["packages", "dependencyFile", "dockerfile", "image"] = "packages"
372
387
  dependency_file: DependencyFileSpec | None = None
388
+ dockerfile: DockerfileSpec | None = None
373
389
  image: ImageSourceSpec | None = None
374
390
 
375
391
 
@@ -486,6 +502,28 @@ def parse_requirements_txt(text: str) -> list[str]:
486
502
  return lines
487
503
 
488
504
 
505
+ def _dockerfile_findings(dockerfile: DockerfileSpec | None) -> list[SpecFinding]:
506
+ """What a `dockerfile`-sourced version must carry, and what the contract refuses.
507
+
508
+ The contract's own refusals are reported here, at `validate`, rather than
509
+ at build time: a `FROM` that is not an approved base, a privileged build,
510
+ the host network, a Docker socket mount, a host bind mount, and every
511
+ instruction §6 does not allow. Each is named with its line, because a
512
+ Dockerfile is somebody's file and "it was refused" is not a reason.
513
+ """
514
+ field = "spec.build.dockerfile"
515
+ if dockerfile is None:
516
+ return [SpecFinding(field, "is required when `spec.build.source` is `dockerfile`")]
517
+ if not dockerfile.content.strip():
518
+ return [SpecFinding(f"{field}.content", "is empty; it is the Dockerfile text")]
519
+ from .contract import validate_dockerfile
520
+
521
+ return [
522
+ SpecFinding(f"{field}.content", f"line {finding.line}: {finding.message}")
523
+ for finding in validate_dockerfile(dockerfile.content)
524
+ ]
525
+
526
+
489
527
  def _dependency_file_findings(dependency_file: DependencyFileSpec | None) -> list[SpecFinding]:
490
528
  field = "spec.build.dependencyFile"
491
529
  if dependency_file is None:
@@ -644,6 +682,8 @@ def spec_findings(
644
682
  )
645
683
  if spec.build.source == "dependencyFile":
646
684
  findings.extend(_dependency_file_findings(spec.build.dependency_file))
685
+ elif spec.build.source == "dockerfile":
686
+ findings.extend(_dockerfile_findings(spec.build.dockerfile))
647
687
  elif spec.build.source == "image":
648
688
  findings.extend(_image_findings(spec.build.image))
649
689
 
@@ -951,11 +991,7 @@ def publication_findings(environment: Environment) -> list[SpecFinding]:
951
991
  PUBLICATION_BLOCKED,
952
992
  )
953
993
  )
954
- private = [
955
- url
956
- for url in environment.spec.packages.python.indexes
957
- if not index_is_public(url)
958
- ]
994
+ private = [url for url in environment.spec.packages.python.indexes if not index_is_public(url)]
959
995
  if private:
960
996
  findings.append(
961
997
  SpecFinding(
@@ -95,6 +95,17 @@
95
95
  ],
96
96
  "default": null
97
97
  },
98
+ "dockerfile": {
99
+ "anyOf": [
100
+ {
101
+ "$ref": "#/$defs/DockerfileSpec"
102
+ },
103
+ {
104
+ "type": "null"
105
+ }
106
+ ],
107
+ "default": null
108
+ },
98
109
  "image": {
99
110
  "anyOf": [
100
111
  {
@@ -219,6 +230,19 @@
219
230
  "title": "DependencyFileSpec",
220
231
  "type": "object"
221
232
  },
233
+ "DockerfileSpec": {
234
+ "additionalProperties": false,
235
+ "description": "The Dockerfile a `dockerfile`-sourced version builds from (E3-03).\n\nInline, the way ``DependencyFileSpec`` carries a ``requirements.txt``: a\nfile the author brings lives in the spec, so ``check_dockerfile`` and\nevery variant's capability report can read it **before** anything is\nqueued \u2014 which is the whole point of refusing an instruction a variant\ndoes not implement at ``validate`` rather than halfway through a build.\n\nThe build *context* \u2014 the extra files a ``COPY`` needs \u2014 is the separate\nupload E3-03 describes, bounded and checked by ``validate_build_context``.\nA Dockerfile with no context is the common case and needs no upload at\nall.",
236
+ "properties": {
237
+ "content": {
238
+ "default": "",
239
+ "title": "Content",
240
+ "type": "string"
241
+ }
242
+ },
243
+ "title": "DockerfileSpec",
244
+ "type": "object"
245
+ },
222
246
  "EnvironmentSpec": {
223
247
  "additionalProperties": false,
224
248
  "properties": {
@@ -19,12 +19,13 @@ from __future__ import annotations
19
19
 
20
20
  import json
21
21
  import subprocess
22
+ from types import SimpleNamespace
22
23
 
23
24
  import pytest
24
25
 
25
26
  from code_sandboxes.environments.attest import Attestor, attest_artifact, licenses_of, signature_tag
26
27
  from code_sandboxes.environments.builders import ArtifactReference
27
- from code_sandboxes.environments.errors import EnvironmentsError
28
+ from code_sandboxes.environments.errors import PROVIDER_ERROR, EnvironmentsError
28
29
  from code_sandboxes.environments.policy import (
29
30
  DEFAULT_POLICY,
30
31
  Finding,
@@ -613,27 +614,25 @@ class TestAttestingAnArtifact:
613
614
  assert cosign.argv == [], "a blocked artifact must not be signed"
614
615
 
615
616
  def test_a_reference_that_is_not_a_digest_cannot_be_attested(self) -> None:
616
- artifact = ArtifactReference(
617
- variant="modal",
618
- immutable_reference="im-1234567890",
619
- provider_artifact_id="im-1234567890",
620
- contract_version="sandbox-contract/v1",
621
- )
617
+ """A `datalayer` artifact must be a digest in this platform's registry.
618
+
619
+ `attest_artifact` takes `artifact: Any`, so nothing guarantees every
620
+ caller went through the model validator that would have refused this.
621
+ """
622
+ artifact = SimpleNamespace(variant="datalayer", immutable_reference="im-1234567890")
622
623
  with pytest.raises(EnvironmentsError) as raised:
623
624
  attest_artifact(artifact=artifact, attestor=an_attestor())
624
625
  assert raised.value.code.code == "DL_ENV_PROVIDER_ERROR"
625
626
 
626
627
  def test_a_malformed_digest_cannot_be_attested_either(self) -> None:
627
628
  """`sha256:bad` starts with `sha256:` too: only a whole one is
628
- accepted (found on PR #27's Copilot review). `e2b` rather than
629
- `datalayer`, whose own model validator already refuses a malformed
630
- digest before this function ever sees it — `attest_artifact` takes
631
- `artifact: Any`, so nothing guarantees every caller went through it."""
632
- artifact = ArtifactReference(
633
- variant="e2b",
629
+ accepted (found on PR #27's Copilot review). Asked of `datalayer`,
630
+ since that is the variant this check is for — `attest_artifact` takes
631
+ `artifact: Any`, so nothing guarantees every caller went through the
632
+ model validator that would have refused it."""
633
+ artifact = SimpleNamespace(
634
+ variant="datalayer",
634
635
  immutable_reference=f"{REGISTRY}/{REPOSITORY}@sha256:bad",
635
- provider_artifact_id="sha256:bad",
636
- contract_version="sandbox-contract/v1",
637
636
  )
638
637
  with pytest.raises(EnvironmentsError) as raised:
639
638
  attest_artifact(artifact=artifact, attestor=an_attestor())
@@ -749,3 +748,43 @@ class TestLicencesFromTheSbom:
749
748
  """Never a reason to fail a build: a licence list is worth having, not dying for."""
750
749
  for document in (None, {}, {"packages": None}, {"components": [1, 2]}, "spdx"):
751
750
  assert licenses_of(document) == []
751
+
752
+
753
+ class TestWhatIsAttestedAndWhatIsNot:
754
+ """D-11 is about the Datalayer artifact, not every artifact.
755
+
756
+ It lives in this platform's registry: the scanner reads it there, cosign
757
+ signs that digest, and the Operator refuses to start what is unsigned. A
758
+ managed artifact is none of those things — it lives in the owner's own
759
+ provider account (D-8), named the way that provider names it.
760
+ """
761
+
762
+ def _artifact(self, variant: str, reference: str):
763
+ return SimpleNamespace(variant=variant, immutable_reference=reference)
764
+
765
+ def test_a_daytona_snapshot_is_not_attested(self) -> None:
766
+ """Its reference is a uuid, and attesting it anyway failed the first
767
+ real Daytona build *after* the snapshot was already built
768
+ (2026-09-17)."""
769
+ answer = attest_artifact(
770
+ artifact=self._artifact("daytona", "51d10ab0-d98d-4117-bdb5-918e98646c92"),
771
+ size_bytes=1234,
772
+ )
773
+ assert answer["scan_summary"] == {}
774
+ assert answer["signature_ref"] == ""
775
+ assert answer["signed_now"] is False
776
+ # What the provider told us is still recorded.
777
+ assert answer["size_bytes"] == 1234
778
+
779
+ def test_an_e2b_build_id_and_a_modal_image_id_are_not_either(self) -> None:
780
+ for variant, reference in (("e2b", "bld-123"), ("modal", "im-abc123")):
781
+ answer = attest_artifact(artifact=self._artifact(variant, reference))
782
+ assert answer["signed_now"] is False, variant
783
+ assert answer["scan_summary"] == {}, variant
784
+
785
+ def test_a_datalayer_artifact_that_is_not_a_digest_still_fails(self) -> None:
786
+ """The check that matters is kept where it means something."""
787
+ with pytest.raises(EnvironmentsError) as raised:
788
+ attest_artifact(artifact=self._artifact("datalayer", "not-a-digest"))
789
+ assert raised.value.code is PROVIDER_ERROR
790
+ assert "cannot be attested" in str(raised.value)
@@ -568,12 +568,28 @@ class TestBuildingASnapshot:
568
568
  resources = daytona.client.snapshot.create_calls[0].args[0].resources
569
569
  assert (resources.cpu, resources.memory, resources.disk) == (cpu, memory, disk)
570
570
 
571
- def test_the_region_is_passed_through(self) -> None:
571
+ def test_the_region_sent_is_daytonas_own_not_this_platforms(self) -> None:
572
+ """`request.region` is Datalayer's — `r1` — and Daytona answered
573
+ "Region not found" for it on the first real build (2026-09-17).
574
+
575
+ The owner names a Daytona region in `compatibility.regions`; that is
576
+ the one that scopes the snapshot.
577
+ """
572
578
  daytona = FakeDaytonaModule()
573
- a_builder(daytona=daytona).build(a_request(region="eu"))
579
+ a_builder(daytona=daytona).build(
580
+ a_request(region="r1", spec={"compatibility": {"regions": ["eu"]}})
581
+ )
574
582
  params = daytona.client.snapshot.create_calls[0].args[0]
575
583
  assert params.region_id == "eu"
576
584
 
585
+ def test_with_no_region_named_the_account_default_decides(self) -> None:
586
+ """The field is left out rather than filled with something Daytona
587
+ does not know — which is what every snapshot in a real account has."""
588
+ daytona = FakeDaytonaModule()
589
+ a_builder(daytona=daytona).build(a_request(region="r1"))
590
+ params = daytona.client.snapshot.create_calls[0].args[0]
591
+ assert params.region_id is None
592
+
577
593
  def test_the_snapshot_is_named_after_the_environment_and_version(self) -> None:
578
594
  daytona = FakeDaytonaModule()
579
595
  a_builder(daytona=daytona).build(a_request())
@@ -55,11 +55,7 @@ def environment(**spec: Any) -> Environment:
55
55
  #: A `dependencyFile` conda source: an `environment.yml` a managed variant
56
56
  #: builds with `micromamba` (E3-02).
57
57
  CONDA_ENVIRONMENT_YML = (
58
- "name: geo\n"
59
- "channels: [conda-forge]\n"
60
- "dependencies:\n"
61
- " - python=3.13\n"
62
- " - gdal\n"
58
+ "name: geo\nchannels: [conda-forge]\ndependencies:\n - python=3.13\n - gdal\n"
63
59
  )
64
60
 
65
61
 
@@ -102,15 +98,33 @@ class TestTheCapabilitySets:
102
98
  assert get_builder("modal").capabilities().supports_gpu is True
103
99
  assert get_builder("daytona").capabilities().supports_gpu is True
104
100
 
105
- def test_only_modal_forbids_instructions_its_builder_never_implemented(self) -> None:
106
- """Modal implements its own Dockerfile builder; the others hand a
107
- Dockerfile to BuildKit, which implements all of them."""
101
+ def test_each_variant_forbids_what_its_own_builder_will_not_honour(self) -> None:
102
+ """Modal implements its own Dockerfile builder, and E2B parses a
103
+ Dockerfile into Template SDK calls; Daytona hands the text to a real
104
+ Docker builder, which implements all of them.
105
+
106
+ E2B's list is read from its SDK (E3-03, 2026-09-17):
107
+ `e2b.template.dockerfile_parser` branches on FROM, RUN, COPY, ADD,
108
+ WORKDIR, USER, ENV, ARG, CMD and ENTRYPOINT, and for anything else
109
+ **prints `Unsupported instruction` and carries on** — so a template
110
+ built from a Dockerfile naming one of these comes back without it and
111
+ reports success. That is the case a capability report exists for.
112
+ """
108
113
  assert set(get_builder("modal").capabilities().forbidden_instructions) == {
109
114
  "ONBUILD",
110
115
  "STOPSIGNAL",
111
116
  "VOLUME",
112
117
  }
113
- assert get_builder("e2b").capabilities().forbidden_instructions == ()
118
+ assert set(get_builder("e2b").capabilities().forbidden_instructions) == {
119
+ "VOLUME",
120
+ "EXPOSE",
121
+ "HEALTHCHECK",
122
+ "SHELL",
123
+ "ONBUILD",
124
+ "STOPSIGNAL",
125
+ "LABEL",
126
+ "MAINTAINER",
127
+ }
114
128
  assert get_builder("daytona").capabilities().forbidden_instructions == ()
115
129
 
116
130
  def test_each_one_bounds_how_long_a_build_may_take(self) -> None:
@@ -120,10 +134,13 @@ class TestTheCapabilitySets:
120
134
  seconds = get_builder(variant).capabilities().max_build_seconds
121
135
  assert seconds and 0 < seconds <= 60 * 60, variant
122
136
 
123
- def test_this_phase_builds_a_package_list_and_a_conda_file(self) -> None:
137
+ def test_this_phase_builds_a_package_list_a_conda_file_and_a_dockerfile(self) -> None:
138
+ """All three take a Dockerfile (E3-03), each through its own door:
139
+ E2B parses one into Template SDK calls, Daytona hands the text to a
140
+ real Docker builder, Modal builds it with its own frontend."""
124
141
  for variant in MANAGED:
125
142
  sources = get_builder(variant).capabilities().build_sources
126
- assert sources == ("packages", "dependencyFile"), variant
143
+ assert sources == ("packages", "dependencyFile", "dockerfile"), variant
127
144
 
128
145
 
129
146
  # -- what each one refuses ------------------------------------------------------
@@ -230,10 +247,29 @@ class TestWhatIsSaidBeforeAnythingIsQueued:
230
247
 
231
248
  def test_a_source_this_phase_does_not_build_is_refused_by_name(self) -> None:
232
249
  for variant in MANAGED:
233
- report = get_builder(variant).validate(environment(build={"source": "dockerfile"}))
250
+ report = get_builder(variant).validate(environment(build={"source": "image"}))
234
251
  assert report.supported is False, variant
235
- assert "`dockerfile` is not built for" in messages(report)
236
- assert "it builds packages, dependencyFile" in messages(report)
252
+ assert "`image` is not built for" in messages(report)
253
+ assert "it builds packages, dependencyFile, dockerfile" in messages(report)
254
+
255
+ def test_an_instruction_a_variant_would_drop_is_refused_with_its_line(self) -> None:
256
+ """The case this exists for: E2B's parser prints `Unsupported
257
+ instruction` and carries on, so without this the template comes back
258
+ missing what the Dockerfile asked for and the build reports success."""
259
+ dockerfile = "FROM datalayer/python-cpu:2026.09\nRUN true\nVOLUME /data\n"
260
+ report = get_builder("e2b").validate(
261
+ environment(build={"source": "dockerfile", "dockerfile": {"content": dockerfile}})
262
+ )
263
+ assert report.supported is False
264
+ assert "line 3: E2B does not implement `VOLUME`" in messages(report)
265
+
266
+ def test_a_dockerfile_a_variant_can_honour_is_accepted(self) -> None:
267
+ """Daytona builds the text as it is, so Docker's own grammar is the limit."""
268
+ dockerfile = "FROM datalayer/python-cpu:2026.09\nVOLUME /data\nEXPOSE 8888\n"
269
+ report = get_builder("daytona").validate(
270
+ environment(build={"source": "dockerfile", "dockerfile": {"content": dockerfile}})
271
+ )
272
+ assert report.supported is True, messages(report)
237
273
 
238
274
  def test_a_conda_dependency_file_is_buildable_on_every_managed_variant(self) -> None:
239
275
  for variant in MANAGED:
@@ -415,8 +451,11 @@ def test_every_variant_validates_with_no_provider_sdk_installed() -> None:
415
451
  assert builder.capabilities().variant == variant
416
452
  report = builder.validate(environment())
417
453
  assert report.supported is True, f"{variant}: {messages(report)}"
454
+ # A refusal each variant still makes, and makes without an SDK.
455
+ # `dockerfile` stopped being one when E3-03 gave all three a door
456
+ # into it, so a managed variant is asked about `image` instead.
418
457
  refused = get_builder(variant).validate(
419
- environment(build={"source": "dockerfile"})
458
+ environment(build={"source": "image"})
420
459
  if variant != "datalayer"
421
460
  else environment(
422
461
  resources={"sizeClass": "gpu-small", "accelerator": {"type": "A10G"}}
@@ -322,14 +322,46 @@ def test_an_invalid_field_outranks_something_unsupported() -> None:
322
322
  }
323
323
 
324
324
 
325
- def test_a_dockerfile_source_is_accepted_and_keeps_its_own_base() -> None:
326
- # E3-03: the base is the `FROM` its uploaded Dockerfile names, so
327
- # `spec.base` is not checked against the approved table (as for `image`).
325
+ def a_dockerfile_document(content: str) -> dict[str, Any]:
328
326
  data = mutated("spec.build.source", "dockerfile")
327
+ data["spec"]["build"]["dockerfile"] = {"content": content}
328
+ return data
329
+
330
+
331
+ def test_a_dockerfile_source_is_accepted_and_keeps_its_own_base() -> None:
332
+ # E3-03: the base is the `FROM` the Dockerfile names, so `spec.base` is
333
+ # not checked against the approved table (as for `image`).
334
+ data = a_dockerfile_document("FROM datalayer/python-cpu:2026.09\nRUN true\n")
329
335
  data["spec"]["base"]["ref"] = "python"
330
336
  assert _codes(data) == {}
331
337
 
332
338
 
339
+ def test_a_dockerfile_source_without_a_dockerfile_is_refused() -> None:
340
+ """`source: dockerfile` with nowhere to read the file from is not a spec."""
341
+ data = mutated("spec.build.source", "dockerfile")
342
+ assert _codes(data) == {"spec.build.dockerfile": "DL_ENV_SPEC_INVALID"}
343
+
344
+
345
+ def test_an_empty_dockerfile_is_refused() -> None:
346
+ assert _codes(a_dockerfile_document(" \n")) == {
347
+ "spec.build.dockerfile.content": "DL_ENV_SPEC_INVALID"
348
+ }
349
+
350
+
351
+ def test_the_contract_refuses_a_dockerfile_at_validate_naming_the_line() -> None:
352
+ """A Dockerfile is somebody's file: "it was refused" is not a reason.
353
+
354
+ The refusals are the contract's own — an unapproved base, the host
355
+ network, a privileged build, a Docker socket mount — and they are made
356
+ here, before anything is queued, rather than partway through a build.
357
+ """
358
+ data = a_dockerfile_document("FROM ubuntu:22.04\nRUN --network=host apt-get update\n")
359
+ assert _codes(data) == {"spec.build.dockerfile.content": "DL_ENV_SPEC_INVALID"}
360
+ messages = [finding.message for finding in spec_findings(parse_environment(data))]
361
+ assert any("line 1" in message and "approved" in message for message in messages)
362
+ assert any("line 2" in message and "host network" in message for message in messages)
363
+
364
+
333
365
  # -- Dependency files (E3-01) --------------------------------------------------
334
366
 
335
367