code-sandboxes 1.9.18__tar.gz → 1.9.20__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.18 → code_sandboxes-1.9.20}/PKG-INFO +1 -1
  2. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/__version__.py +1 -1
  3. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/adapters/daytona.py +20 -2
  4. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/adapters/e2b.py +18 -1
  5. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/adapters/managed.py +37 -1
  6. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/adapters/modal.py +87 -3
  7. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/attest.py +81 -1
  8. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/build_secrets.py +144 -1
  9. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/builders.py +8 -0
  10. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/spec.py +49 -13
  11. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/schemas/environment-v1alpha1.json +24 -0
  12. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_attest.py +61 -1
  13. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_build_secrets.py +117 -1
  14. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_daytona_builder.py +18 -2
  15. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_managed_builders.py +58 -19
  16. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_modal_builder.py +149 -3
  17. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_spec.py +35 -3
  18. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.github/workflows/build.yml +0 -0
  19. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.github/workflows/environments-live.yml +0 -0
  20. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.github/workflows/py-code-style.yml +0 -0
  21. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.github/workflows/py-tests.yml +0 -0
  22. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.github/workflows/py-typing.yml +0 -0
  23. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.github/workflows/reusable-python.yml +0 -0
  24. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.gitignore +0 -0
  25. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.licenserc.yaml +0 -0
  26. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.pre-commit-config.yaml +0 -0
  27. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/.vscode/settings.json +0 -0
  28. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/CHANGELOG.md +0 -0
  29. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/Dockerfile +0 -0
  30. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/LICENSE +0 -0
  31. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/Makefile +0 -0
  32. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/README.md +0 -0
  33. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/__init__.py +0 -0
  34. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/base.py +0 -0
  35. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/bridge_mount.py +0 -0
  36. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/builds.py +0 -0
  37. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/cli.py +0 -0
  38. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/client.py +0 -0
  39. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/cloudflare_sandbox.py +0 -0
  40. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/commands.py +0 -0
  41. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/console.py +0 -0
  42. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/contents.py +0 -0
  43. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/coreweave_sandbox.py +0 -0
  44. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/datalayer_sandbox.py +0 -0
  45. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/daytona_sandbox.py +0 -0
  46. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/docker_sandbox.py +0 -0
  47. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/e2b_sandbox.py +0 -0
  48. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/__init__.py +0 -0
  49. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/__main__.py +0 -0
  50. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/accounts.py +0 -0
  51. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/adapters/__init__.py +0 -0
  52. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/adapters/datalayer.py +0 -0
  53. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/bases.py +0 -0
  54. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/breaker.py +0 -0
  55. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/canonical.py +0 -0
  56. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/conformance.py +0 -0
  57. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  58. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  59. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  60. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/contract.py +0 -0
  61. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/doctor/__init__.py +0 -0
  62. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/doctor/build.py +0 -0
  63. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  64. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  65. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/errors.py +0 -0
  66. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/files.py +0 -0
  67. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/image_import.py +0 -0
  68. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/lifecycle.py +0 -0
  69. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/policy.py +0 -0
  70. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/redact.py +0 -0
  71. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/resolve.py +0 -0
  72. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/resolve_conda.py +0 -0
  73. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/environments/schema.py +0 -0
  74. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/eval_sandbox.py +0 -0
  75. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/exceptions.py +0 -0
  76. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/filesystem.py +0 -0
  77. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/google_colab.py +0 -0
  78. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/google_colab_sandbox.py +0 -0
  79. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/interfaces.py +0 -0
  80. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/jupyter_ingress.py +0 -0
  81. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  82. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/kaggle.py +0 -0
  83. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/kaggle_execute.py +0 -0
  84. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/kaggle_live.py +0 -0
  85. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/kaggle_sandbox.py +0 -0
  86. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/lifecycle.py +0 -0
  87. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/manage.py +0 -0
  88. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/modal_sandbox.py +0 -0
  89. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/models.py +0 -0
  90. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/monty_sandbox.py +0 -0
  91. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/provider_ingress.py +0 -0
  92. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/code_sandboxes/providers.py +0 -0
  93. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docker/Dockerfile +0 -0
  94. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/.gitignore +0 -0
  95. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/LICENSE +0 -0
  96. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/Makefile +0 -0
  97. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/README.md +0 -0
  98. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/babel.config.js +0 -0
  99. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/api-reference/_category_.yml +0 -0
  100. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/api-reference/index.mdx +0 -0
  101. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/cli/_category_.yml +0 -0
  102. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/cli/index.mdx +0 -0
  103. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/contribute/_category_.yml +0 -0
  104. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/contribute/index.mdx +0 -0
  105. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/environments/_category_.yml +0 -0
  106. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/environments/contract.mdx +0 -0
  107. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/environments/index.mdx +0 -0
  108. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/environments/specification.mdx +0 -0
  109. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/environments/spike-2026-09.mdx +0 -0
  110. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/examples/_category_.yml +0 -0
  111. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/examples/index.mdx +0 -0
  112. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/guide/_category_.yml +0 -0
  113. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/guide/index.mdx +0 -0
  114. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/index.mdx +0 -0
  115. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/install/_category_.yml +0 -0
  116. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/install/index.mdx +0 -0
  117. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/provider-ingress.mdx +0 -0
  118. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/_category_.yml +0 -0
  119. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/cloudflare.mdx +0 -0
  120. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/coreweave.mdx +0 -0
  121. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/datalayer.mdx +0 -0
  122. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/daytona.mdx +0 -0
  123. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/docker.mdx +0 -0
  124. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/e2b.mdx +0 -0
  125. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/eval.mdx +0 -0
  126. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/google-colab.mdx +0 -0
  127. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/index.mdx +0 -0
  128. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/jupyter-server.mdx +0 -0
  129. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/kaggle.mdx +0 -0
  130. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/modal.mdx +0 -0
  131. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docs/providers/monty.mdx +0 -0
  132. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/docusaurus.config.js +0 -0
  133. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/package.json +0 -0
  134. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/sidebars.js +0 -0
  135. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/components/HomepageRow1.js +0 -0
  136. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/components/HomepageRow1.module.css +0 -0
  137. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/components/HomepageRow2.js +0 -0
  138. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/components/HomepageRow2.module.css +0 -0
  139. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/components/HomepageRow3.js +0 -0
  140. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/components/HomepageRow3.module.css +0 -0
  141. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/css/custom.css +0 -0
  142. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/pages/index.module.css +0 -0
  143. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/pages/markdown-page.md +0 -0
  144. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/theme/CustomDocItem.tsx +0 -0
  145. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/src/theme/IconsTitle.tsx +0 -0
  146. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/bricks.svg +0 -0
  147. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/building-construction.svg +0 -0
  148. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/cloud-sun.svg +0 -0
  149. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/cloud.svg +0 -0
  150. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/collaboration.svg +0 -0
  151. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/copy-left.svg +0 -0
  152. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/datalayer/logo.png +0 -0
  153. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/datalayer/logo.svg +0 -0
  154. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/datalayer/personas/business.svg +0 -0
  155. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  156. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/datalayer/personas/devops.svg +0 -0
  157. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/datalayer/project/project-line.svg +0 -0
  158. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/favicon.ico +0 -0
  159. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/jupyter.svg +0 -0
  160. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/open-source.svg +0 -0
  161. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/react-js.svg +0 -0
  162. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/react-js2.svg +0 -0
  163. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/ringed-planet.svg +0 -0
  164. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/robot.svg +0 -0
  165. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/rocket.svg +0 -0
  166. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/shield.svg +0 -0
  167. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/sparkles.svg +0 -0
  168. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/streamlit.gif +0 -0
  169. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/super-hero.svg +0 -0
  170. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/target.svg +0 -0
  171. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/docs/static/img/thumbs-up.svg +0 -0
  172. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/README.md +0 -0
  173. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/Makefile +0 -0
  174. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/cloudflare_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/coreweave_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/datalayer_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/daytona_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/docker_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/e2b_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/eval_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/google_colab_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/kaggle_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/modal_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/exec/monty_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/Makefile +0 -0
  187. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/cloudflare_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/coreweave_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/datalayer_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/daytona_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/docker_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/e2b_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/eval_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/google_colab_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/kaggle_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/modal_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/examples/repl/monty_sandbox_example.py +0 -0
  199. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/pyproject.toml +0 -0
  200. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/__init__.py +0 -0
  201. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/conftest.py +0 -0
  202. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  203. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_bridge_mount.py +0 -0
  204. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_builds.py +0 -0
  205. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_cli_exec.py +0 -0
  206. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_cli_repl.py +0 -0
  207. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_client.py +0 -0
  208. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_client_contents.py +0 -0
  209. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_client_files.py +0 -0
  210. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_client_outcome_error_output.py +0 -0
  211. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_cloudflare.py +0 -0
  212. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_commands_envelope.py +0 -0
  213. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_console.py +0 -0
  214. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_contents_contract.py +0 -0
  215. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_contents_live_matrix.py +0 -0
  216. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_coreweave.py +0 -0
  217. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_datalayer_sandbox.py +0 -0
  218. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_daytona.py +0 -0
  219. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_e2b.py +0 -0
  220. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_accounts.py +0 -0
  221. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_bases.py +0 -0
  222. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_breaker.py +0 -0
  223. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_builders.py +0 -0
  224. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_canonical.py +0 -0
  225. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_conformance.py +0 -0
  226. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_constraints.py +0 -0
  227. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_contract.py +0 -0
  228. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_cross_variant_report.py +0 -0
  229. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_datalayer_builder.py +0 -0
  230. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_doctor.py +0 -0
  231. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_e2b_builder.py +0 -0
  232. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_errors.py +0 -0
  233. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_image_import.py +0 -0
  234. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_lifecycle.py +0 -0
  235. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_live_matrix.py +0 -0
  236. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_redact.py +0 -0
  237. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_resolve.py +0 -0
  238. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_environment_resolve_conda.py +0 -0
  239. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_eval.py +0 -0
  240. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  241. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_factory.py +0 -0
  242. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_google_colab.py +0 -0
  243. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_integration.py +0 -0
  244. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_jupyter_ingress.py +0 -0
  245. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_jupyter_server.py +0 -0
  246. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_jupyter_server_streaming.py +0 -0
  247. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_kaggle.py +0 -0
  248. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_kaggle_execute.py +0 -0
  249. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_kaggle_live.py +0 -0
  250. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_kaggle_session.py +0 -0
  251. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_kernel_client_compatibility.py +0 -0
  252. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_lifecycle_vocabulary.py +0 -0
  253. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_manage.py +0 -0
  254. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_manage_datalayer.py +0 -0
  255. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_modal_google_colab_sandbox.py +0 -0
  256. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_modal_session.py +0 -0
  257. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_models.py +0 -0
  258. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_output_line_fidelity.py +0 -0
  259. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_provider_catalog_names.py +0 -0
  260. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_provider_ingress.py +0 -0
  261. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_providers.py +0 -0
  262. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/tests/test_sandboxes.py +0 -0
  263. {code_sandboxes-1.9.18 → code_sandboxes-1.9.20}/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.18
3
+ Version: 1.9.20
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.18"
6
+ __version__ = "1.9.20"
@@ -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)
@@ -230,8 +230,7 @@ def _install_packages(image: Any, lock_text: str) -> Any:
230
230
  # Packages install as root: every Modal build step already runs as
231
231
  # root regardless of any `USER` line (see the module docstring), so
232
232
  # this is stating what is already true rather than asking for it.
233
- "uv pip sync --system --require-hashes "
234
- f"--find-links {WHEELHOUSE_IMAGE_PATH} {_LOCK_PATH}",
233
+ f"uv pip sync --system --require-hashes --find-links {WHEELHOUSE_IMAGE_PATH} {_LOCK_PATH}",
235
234
  )
236
235
 
237
236
 
@@ -245,6 +244,40 @@ def _post_install(
245
244
  return image
246
245
 
247
246
 
247
+ def _intermediates_of(built: Any) -> tuple[str, ...]:
248
+ """Every layer under a built image, by id, deepest first (E2-05, E2-09).
249
+
250
+ Each chained builder call leaves an image of its own, and deleting the
251
+ artifact does not delete them. Modal offers no call that lists an
252
+ account's images, so an intermediate nobody wrote down at build time can
253
+ never be found again — which is why this is recorded rather than
254
+ discovered. Confirmed live on 2026-09-17: a three-call chain built through
255
+ `Image.build` hydrates an `object_id` on every image in `deps()`, not only
256
+ on the last.
257
+
258
+ The built image itself is not an intermediate: it is the artifact.
259
+ """
260
+ found: list[str] = []
261
+ seen: set[int] = set()
262
+
263
+ def walk(image: Any) -> None:
264
+ if id(image) in seen:
265
+ return
266
+ seen.add(id(image))
267
+ for dependency in getattr(image, "deps", lambda: ())():
268
+ if hasattr(dependency, "deps"):
269
+ walk(dependency)
270
+ object_id = getattr(image, "object_id", None)
271
+ if object_id and image is not built:
272
+ found.append(str(object_id))
273
+
274
+ try:
275
+ walk(built)
276
+ except Exception:
277
+ return ()
278
+ return tuple(dict.fromkeys(found))
279
+
280
+
248
281
  class Builder(ManagedBuilder):
249
282
  """Modal: the capability half (E2-06) and the build (E2-05)."""
250
283
 
@@ -253,7 +286,7 @@ class Builder(ManagedBuilder):
253
286
  title = "Modal"
254
287
  #: A `packages` list and, for conda (E3-02), an `environment.yml`
255
288
  #: dependency file installed with `micromamba_install`.
256
- build_sources = ("packages", "dependencyFile")
289
+ build_sources = ("packages", "dependencyFile", "dockerfile")
257
290
  dependency_formats = ("conda",)
258
291
  #: Modal runs GPUs, in the owner's workspace (E2-17). This builder does
259
292
  #: not build one yet: see `build`'s own guard.
@@ -479,6 +512,7 @@ class Builder(ManagedBuilder):
479
512
  mutable_alias=name,
480
513
  provider_account=provider_account(self.variant, self._provider_secrets()) or None,
481
514
  contract_version=spec.contract or SANDBOX_CONTRACT_V1.version,
515
+ intermediates=_intermediates_of(built),
482
516
  )
483
517
 
484
518
  def _resolved_secrets(self, request: BuildRequest) -> tuple[list[BuildSecret], dict[str, str]]:
@@ -631,6 +665,56 @@ class Builder(ManagedBuilder):
631
665
  raise self._provider_error("ask whether the image exists", error) from error
632
666
  return True
633
667
 
668
+ def delete(self, artifact: ArtifactReference) -> None:
669
+ """Delete the image, and every intermediate layer this build recorded (E2-05, E2-09).
670
+
671
+ Deleting the artifact does not delete the layers under it, and Modal
672
+ offers no call that lists an account's images, so what is removed is
673
+ what `build` wrote down — an intermediate nobody recorded can never be
674
+ found again.
675
+
676
+ Two answers are outcomes rather than failures, both found live on
677
+ 2026-09-17:
678
+
679
+ * **Already gone** is success. A replay of a collection must delete the
680
+ same set again with no harm, the same way the Datalayer collector
681
+ treats an artifact that is not there.
682
+ * **Not ours to delete.** The bottom of a chain can be an image the
683
+ workspace does not own — Modal's own `debian_slim` answers
684
+ `PermissionDenied` — and an image somebody else owns was never this
685
+ artifact's to collect. It is logged and stepped over, not raised.
686
+ """
687
+ sdk = self._modal_sdk()
688
+ client = self._client(sdk)
689
+ synchronizer, api_pb2 = self._modal_internals()
690
+
691
+ async def _delete(image_id: str) -> None:
692
+ await client.stub.ImageDelete(api_pb2.ImageDeleteRequest(image_id=image_id))
693
+
694
+ # The artifact last: an intermediate is only reachable while the
695
+ # record naming it survives, so a half-done collection that has
696
+ # dropped the image would strand them.
697
+ for image_id in (*artifact.intermediates, artifact.provider_artifact_id):
698
+ if not image_id:
699
+ continue
700
+ try:
701
+ # A bare coroutine on Modal's stub silently does nothing, so
702
+ # this runs on the SDK's own loop — see `_delete_secret`.
703
+ synchronizer.wrap(_delete)(image_id)
704
+ except sdk.exception.NotFoundError:
705
+ continue
706
+ except Exception as error:
707
+ if "permission" in str(error).lower():
708
+ self._log(
709
+ f"The Modal image {image_id} is not this account's to delete: {error}"
710
+ )
711
+ continue
712
+ if image_id == artifact.provider_artifact_id:
713
+ raise self._provider_error("delete the image", error) from error
714
+ # One layer's refusal does not strand the rest, nor the
715
+ # artifact this was called to collect.
716
+ self._log(f"The Modal intermediate {image_id} could not be deleted: {error}")
717
+
634
718
  def _provider_error(self, what: str, error: BaseException) -> EnvironmentsError:
635
719
  return EnvironmentsError(
636
720
  PROVIDER_ERROR,
@@ -106,6 +106,76 @@ def signature_tag(digest: str) -> str:
106
106
  return text.replace(":", "-", 1) + ".sig"
107
107
 
108
108
 
109
+ #: Where a CycloneDX component keeps its licence, in the order they are read.
110
+ _CYCLONEDX_LICENSE_KEYS = ("id", "name")
111
+ #: Where an SPDX package keeps its licence. `licenseConcluded` is what the
112
+ #: tool decided; `licenseDeclared` is what the package claimed. BuildKit
113
+ #: writes SPDX, so this is the one that matters in practice.
114
+ _SPDX_LICENSE_KEYS = ("licenseConcluded", "licenseDeclared")
115
+ #: What SPDX writes when it could not tell, which is not a licence.
116
+ _SPDX_UNKNOWN = frozenset({"NOASSERTION", "NONE", ""})
117
+
118
+
119
+ def _spdx_licenses(document: Mapping[str, Any]) -> set[str]:
120
+ """What an SPDX document names, which is what BuildKit's `attest:sbom=` writes.
121
+
122
+ `licenseConcluded` is what the tool decided and `licenseDeclared` what the
123
+ package claimed, so the concluded one is read first and the declared one
124
+ only when it said nothing.
125
+ """
126
+ found: set[str] = set()
127
+ for package in document.get("packages") or ():
128
+ if not isinstance(package, Mapping):
129
+ continue
130
+ for key in _SPDX_LICENSE_KEYS:
131
+ value = str(package.get(key) or "").strip()
132
+ if value and value.upper() not in _SPDX_UNKNOWN:
133
+ found.add(value)
134
+ break
135
+ return found
136
+
137
+
138
+ def _cyclonedx_licenses(document: Mapping[str, Any]) -> set[str]:
139
+ """What a CycloneDX document names, by id, by name, or as an expression."""
140
+ found: set[str] = set()
141
+ for component in document.get("components") or ():
142
+ if not isinstance(component, Mapping):
143
+ continue
144
+ for entry in component.get("licenses") or ():
145
+ if not isinstance(entry, Mapping):
146
+ continue
147
+ licence = entry.get("license")
148
+ if isinstance(licence, Mapping):
149
+ for key in _CYCLONEDX_LICENSE_KEYS:
150
+ value = str(licence.get(key) or "").strip()
151
+ if value:
152
+ found.add(value)
153
+ break
154
+ expression = str(entry.get("expression") or "").strip()
155
+ if expression:
156
+ found.add(expression)
157
+ return found
158
+
159
+
160
+ def licenses_of(document: Any) -> list[str]:
161
+ """Every licence an SBOM names, deduplicated and sorted.
162
+
163
+ Reads both shapes the ecosystem writes: SPDX, which is what BuildKit's
164
+ `attest:sbom=` produces, and CycloneDX. A document in neither shape, or one
165
+ that names nothing, answers an empty list rather than raising: a
166
+ publication's licence list is worth having and never worth failing a build
167
+ over.
168
+
169
+ This is what a published version's snapshot carries (D-12, E2-16). Until
170
+ it did, `licenses` came from the scan summary — and the registry's scanner
171
+ reports vulnerabilities, not licences, so every publication froze an empty
172
+ list beside an SBOM reference.
173
+ """
174
+ if not isinstance(document, Mapping):
175
+ return []
176
+ return sorted(_spdx_licenses(document) | _cyclonedx_licenses(document))
177
+
178
+
109
179
  @dataclass(frozen=True)
110
180
  class AttestationResult:
111
181
  """What the workflow stores about an artifact once both gates have passed."""
@@ -117,6 +187,8 @@ class AttestationResult:
117
187
  size_bytes: int | None = None
118
188
  signed_now: bool = True
119
189
  """False when a replay found the signature that was already there."""
190
+ licenses: tuple[str, ...] = ()
191
+ """What the SBOM named, frozen onto the artifact for a publication to carry."""
120
192
 
121
193
  def body(self) -> dict[str, Any]:
122
194
  """The mapping `activities_environments.attest` answers."""
@@ -127,6 +199,7 @@ class AttestationResult:
127
199
  "signature_ref": self.signature_ref,
128
200
  "size_bytes": self.size_bytes,
129
201
  "signed_now": self.signed_now,
202
+ "licenses": list(self.licenses),
130
203
  }
131
204
 
132
205
 
@@ -515,8 +588,14 @@ class Attestor:
515
588
  size_bytes: int | None = None,
516
589
  sbom_ref: str = "",
517
590
  provenance_ref: str = "",
591
+ sbom: Any = None,
518
592
  ) -> AttestationResult:
519
- """Scan, then sign: the order the Operator's check depends on (D-11)."""
593
+ """Scan, then sign: the order the Operator's check depends on (D-11).
594
+
595
+ `sbom`, when the caller has the document, is read for the licences a
596
+ publication carries; the registry's scanner reports vulnerabilities
597
+ and never licences, so there is nowhere else they come from.
598
+ """
520
599
  self.can_sign()
521
600
  decision = self.scan(repository=repository, digest=digest)
522
601
  if not decision.passed:
@@ -540,6 +619,7 @@ class Attestor:
540
619
  if size_bytes is not None
541
620
  else self.size_of(repository=repository, digest=digest),
542
621
  signed_now=signed_now,
622
+ licenses=tuple(licenses_of(sbom)),
543
623
  )
544
624
 
545
625
  def size_of(self, *, repository: str, digest: str) -> int | None:
@@ -28,7 +28,12 @@ from __future__ import annotations
28
28
  import os
29
29
  from typing import Any
30
30
 
31
- from .errors import BUILD_SECRET_UNAVAILABLE, EnvironmentsError
31
+ from .errors import (
32
+ BUILD_SECRET_UNAVAILABLE,
33
+ CAPABILITY_UNSUPPORTED,
34
+ PROVIDER_ERROR,
35
+ EnvironmentsError,
36
+ )
32
37
  from .spec import BuildSecret
33
38
 
34
39
  __all__ = [
@@ -150,3 +155,141 @@ def resolve_build_secret(
150
155
  retryable=False,
151
156
  )
152
157
  return value
158
+
159
+
160
+ #: The managed variants an owner keeps a credential for (D-8). `datalayer` is
161
+ #: not one: the platform's own builder uses the platform's own registry.
162
+ PROVIDER_VARIANTS = frozenset({"daytona", "e2b", "modal"})
163
+
164
+
165
+ def resolve_provider_credential(
166
+ variant: str,
167
+ *,
168
+ owner_uid: str,
169
+ iam_url: str | None = None,
170
+ api_key: str | None = None,
171
+ timeout: float = 10.0,
172
+ transport: Any = None,
173
+ ) -> dict[str, str]:
174
+ """The owner's own credential for a managed variant (PLAN_ENVS.md E2-01, D-8).
175
+
176
+ A managed artifact exists only in the account the credential opens, so the
177
+ build runs with the owner's keys and records a non-secret fingerprint of
178
+ that account. The value IAM holds is a JSON object of the environment
179
+ names the provider's own SDK reads — ``DAYTONA_API_KEY``,
180
+ ``E2B_API_KEY``, ``MODAL_TOKEN_ID`` and ``MODAL_TOKEN_SECRET``, and
181
+ optionally the account names ``environments/accounts.py`` reads to
182
+ fingerprint it — because that is the shape ``BuildCredential`` carries and
183
+ every adapter already reads.
184
+
185
+ Raises ``DL_ENV_CAPABILITY_UNSUPPORTED`` — not retryable — when the owner
186
+ has no credential for this variant. A build must be told it cannot run in
187
+ an account nobody configured, rather than falling back to whatever keys
188
+ the worker happens to hold: that would put one owner's artifact in
189
+ another's account, which is the one thing D-8 exists to prevent.
190
+ """
191
+ variant = (variant or "").strip().lower()
192
+ if variant not in PROVIDER_VARIANTS:
193
+ raise EnvironmentsError(
194
+ CAPABILITY_UNSUPPORTED,
195
+ f"`{variant}` is not a managed variant a credential is kept for",
196
+ detail={"variant": variant},
197
+ retryable=False,
198
+ )
199
+ key = api_key if api_key is not None else os.environ.get(IAM_API_KEY_VARIABLE, "")
200
+ if not key:
201
+ raise EnvironmentsError(
202
+ CAPABILITY_UNSUPPORTED,
203
+ f"No {IAM_API_KEY_VARIABLE} to ask IAM for the {variant} credential with",
204
+ detail={"variant": variant, "missing": IAM_API_KEY_VARIABLE},
205
+ retryable=False,
206
+ )
207
+ base = (iam_url if iam_url is not None else os.environ.get(IAM_URL_VARIABLE, "")).rstrip("/")
208
+ if not base:
209
+ raise EnvironmentsError(
210
+ CAPABILITY_UNSUPPORTED,
211
+ f"No {IAM_URL_VARIABLE} to ask for the {variant} credential",
212
+ detail={"variant": variant, "missing": IAM_URL_VARIABLE},
213
+ retryable=False,
214
+ )
215
+ try:
216
+ import httpx
217
+ except ImportError as error:
218
+ raise EnvironmentsError(
219
+ CAPABILITY_UNSUPPORTED,
220
+ "No HTTP client to resolve a provider credential with: install "
221
+ "`code-sandboxes[environments-builder]`",
222
+ detail={"variant": variant, "missing": "httpx"},
223
+ retryable=False,
224
+ ) from error
225
+ url = f"{base}/api/iam/v1/secrets/provider/{variant}/value"
226
+ try:
227
+ with httpx.Client(transport=transport, timeout=timeout) as client:
228
+ response = client.get(
229
+ url,
230
+ params={"owner_uid": owner_uid},
231
+ headers={"X-API-Key": key},
232
+ )
233
+ except httpx.HTTPError as error:
234
+ raise EnvironmentsError(
235
+ PROVIDER_ERROR,
236
+ f"IAM could not be reached for the {variant} credential: {error}",
237
+ detail={"variant": variant},
238
+ retryable=True,
239
+ ) from error
240
+ if response.status_code == 404:
241
+ raise EnvironmentsError(
242
+ CAPABILITY_UNSUPPORTED,
243
+ f"This owner has no {variant} credential: a managed build runs in "
244
+ f"their own account, so one must be kept before {variant} can build",
245
+ detail={"variant": variant, "status": 404},
246
+ retryable=False,
247
+ )
248
+ if response.status_code != 200:
249
+ raise EnvironmentsError(
250
+ PROVIDER_ERROR,
251
+ f"IAM refused the {variant} credential: {response.status_code}",
252
+ detail={"variant": variant, "status": response.status_code},
253
+ retryable=True,
254
+ )
255
+ try:
256
+ body = response.json()
257
+ except ValueError as error:
258
+ raise EnvironmentsError(
259
+ PROVIDER_ERROR,
260
+ f"IAM answered the {variant} credential with a body that is not valid JSON",
261
+ detail={"variant": variant},
262
+ retryable=True,
263
+ ) from error
264
+ return _provider_secrets_of(body.get("value") if isinstance(body, dict) else None, variant)
265
+
266
+
267
+ def _provider_secrets_of(value: Any, variant: str) -> dict[str, str]:
268
+ """The environment names a provider credential carries, as IAM answered.
269
+
270
+ A mapping of names to values: IAM gathers them from the secrets the owner
271
+ already keeps, one per environment variable, and decodes them, so the
272
+ worker is not asked to know how a secret is stored.
273
+ """
274
+ if not isinstance(value, dict) or not value:
275
+ raise EnvironmentsError(
276
+ CAPABILITY_UNSUPPORTED,
277
+ f"IAM answered the {variant} credential with no usable value; a "
278
+ "provider credential is the environment names that provider's own "
279
+ "SDK reads",
280
+ detail={"variant": variant},
281
+ retryable=False,
282
+ )
283
+ secrets = {
284
+ str(name): str(item)
285
+ for name, item in value.items()
286
+ if str(name).strip() and str(item).strip()
287
+ }
288
+ if not secrets:
289
+ raise EnvironmentsError(
290
+ CAPABILITY_UNSUPPORTED,
291
+ f"The {variant} credential names nothing",
292
+ detail={"variant": variant},
293
+ retryable=False,
294
+ )
295
+ return secrets
@@ -141,6 +141,14 @@ class ArtifactReference(_Model):
141
141
  provider_account: str | None = None
142
142
  contract_version: str
143
143
  architecture: str = "linux/amd64"
144
+ #: The layers this build left behind that deleting the artifact does not
145
+ #: delete (E2-05, E2-09). Modal is the variant that has them: each chained
146
+ #: builder call leaves an image of its own with an id, and Modal offers no
147
+ #: call that lists an account's images — `ImageGetOrCreate`, `ImageFromId`,
148
+ #: `ImageGetByTag`, `ImageListTags`, `ImageTagRevisions`, `ImagePublish`
149
+ #: and `ImageDelete`, and nothing that enumerates — so an intermediate
150
+ #: nobody wrote down is an intermediate nobody can ever find again.
151
+ intermediates: tuple[str, ...] = ()
144
152
 
145
153
  @model_validator(mode="after")
146
154
  def _immutable(self) -> ArtifactReference:
@@ -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(