code-sandboxes 1.9.3__tar.gz → 1.9.4__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 (262) hide show
  1. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/CHANGELOG.md +22 -0
  2. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/PKG-INFO +1 -1
  3. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/modal.py +102 -48
  5. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/attest.py +82 -4
  6. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_attest.py +73 -0
  7. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_managed_builders.py +12 -10
  8. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_modal_builder.py +92 -25
  9. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/build.yml +0 -0
  10. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/environments-live.yml +0 -0
  11. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/py-code-style.yml +0 -0
  12. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/py-tests.yml +0 -0
  13. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/py-typing.yml +0 -0
  14. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/reusable-python.yml +0 -0
  15. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.gitignore +0 -0
  16. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.licenserc.yaml +0 -0
  17. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.pre-commit-config.yaml +0 -0
  18. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.vscode/settings.json +0 -0
  19. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/Dockerfile +0 -0
  20. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/LICENSE +0 -0
  21. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/Makefile +0 -0
  22. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/README.md +0 -0
  23. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/build.log +0 -0
  24. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/__init__.py +0 -0
  25. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/base.py +0 -0
  26. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/bridge_mount.py +0 -0
  27. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/builds.py +0 -0
  28. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/cli.py +0 -0
  29. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/client.py +0 -0
  30. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/cloudflare_sandbox.py +0 -0
  31. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/commands.py +0 -0
  32. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/console.py +0 -0
  33. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/contents.py +0 -0
  34. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/coreweave_sandbox.py +0 -0
  35. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/datalayer_sandbox.py +0 -0
  36. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/daytona_sandbox.py +0 -0
  37. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/docker_sandbox.py +0 -0
  38. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/e2b_sandbox.py +0 -0
  39. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/__init__.py +0 -0
  40. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/__main__.py +0 -0
  41. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/accounts.py +0 -0
  42. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/__init__.py +0 -0
  43. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/datalayer.py +0 -0
  44. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/daytona.py +0 -0
  45. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/e2b.py +0 -0
  46. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/managed.py +0 -0
  47. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/bases.py +0 -0
  48. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/breaker.py +0 -0
  49. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/build_secrets.py +0 -0
  50. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/builders.py +0 -0
  51. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/canonical.py +0 -0
  52. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/conformance.py +0 -0
  53. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  54. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  55. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  56. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/contract.py +0 -0
  57. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/__init__.py +0 -0
  58. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/build.py +0 -0
  59. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  60. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  61. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/errors.py +0 -0
  62. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/files.py +0 -0
  63. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/image_import.py +0 -0
  64. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/lifecycle.py +0 -0
  65. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/policy.py +0 -0
  66. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/redact.py +0 -0
  67. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/resolve.py +0 -0
  68. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/schema.py +0 -0
  69. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/spec.py +0 -0
  70. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/eval_sandbox.py +0 -0
  71. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/exceptions.py +0 -0
  72. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/filesystem.py +0 -0
  73. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/google_colab.py +0 -0
  74. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/google_colab_sandbox.py +0 -0
  75. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/interfaces.py +0 -0
  76. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/jupyter_ingress.py +0 -0
  77. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  78. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle.py +0 -0
  79. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_execute.py +0 -0
  80. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_live.py +0 -0
  81. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_sandbox.py +0 -0
  82. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/lifecycle.py +0 -0
  83. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/manage.py +0 -0
  84. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/modal_sandbox.py +0 -0
  85. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/models.py +0 -0
  86. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/monty_sandbox.py +0 -0
  87. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/provider_ingress.py +0 -0
  88. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/providers.py +0 -0
  89. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docker/Dockerfile +0 -0
  90. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/.gitignore +0 -0
  91. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/LICENSE +0 -0
  92. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/Makefile +0 -0
  93. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/README.md +0 -0
  94. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/babel.config.js +0 -0
  95. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/api-reference/_category_.yml +0 -0
  96. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/api-reference/index.mdx +0 -0
  97. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/cli/_category_.yml +0 -0
  98. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/cli/index.mdx +0 -0
  99. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/contribute/_category_.yml +0 -0
  100. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/contribute/index.mdx +0 -0
  101. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/_category_.yml +0 -0
  102. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/contract.mdx +0 -0
  103. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/index.mdx +0 -0
  104. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/specification.mdx +0 -0
  105. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/spike-2026-09.mdx +0 -0
  106. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/examples/_category_.yml +0 -0
  107. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/examples/index.mdx +0 -0
  108. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/guide/_category_.yml +0 -0
  109. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/guide/index.mdx +0 -0
  110. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/index.mdx +0 -0
  111. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/install/_category_.yml +0 -0
  112. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/install/index.mdx +0 -0
  113. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/provider-ingress.mdx +0 -0
  114. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/_category_.yml +0 -0
  115. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/cloudflare.mdx +0 -0
  116. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/coreweave.mdx +0 -0
  117. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/datalayer.mdx +0 -0
  118. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/daytona.mdx +0 -0
  119. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/docker.mdx +0 -0
  120. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/e2b.mdx +0 -0
  121. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/eval.mdx +0 -0
  122. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/google-colab.mdx +0 -0
  123. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/index.mdx +0 -0
  124. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/jupyter-server.mdx +0 -0
  125. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/kaggle.mdx +0 -0
  126. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/modal.mdx +0 -0
  127. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/monty.mdx +0 -0
  128. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docusaurus.config.js +0 -0
  129. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/package.json +0 -0
  130. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/sidebars.js +0 -0
  131. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow1.js +0 -0
  132. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow1.module.css +0 -0
  133. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow2.js +0 -0
  134. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow2.module.css +0 -0
  135. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow3.js +0 -0
  136. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow3.module.css +0 -0
  137. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/css/custom.css +0 -0
  138. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/pages/index.module.css +0 -0
  139. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/pages/markdown-page.md +0 -0
  140. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/theme/CustomDocItem.tsx +0 -0
  141. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/theme/IconsTitle.tsx +0 -0
  142. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/bricks.svg +0 -0
  143. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/building-construction.svg +0 -0
  144. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/cloud-sun.svg +0 -0
  145. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/cloud.svg +0 -0
  146. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/collaboration.svg +0 -0
  147. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/copy-left.svg +0 -0
  148. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/logo.png +0 -0
  149. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/logo.svg +0 -0
  150. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/business.svg +0 -0
  151. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  152. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/devops.svg +0 -0
  153. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/project/project-line.svg +0 -0
  154. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/favicon.ico +0 -0
  155. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/jupyter.svg +0 -0
  156. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/open-source.svg +0 -0
  157. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/react-js.svg +0 -0
  158. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/react-js2.svg +0 -0
  159. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/ringed-planet.svg +0 -0
  160. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/robot.svg +0 -0
  161. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/rocket.svg +0 -0
  162. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/shield.svg +0 -0
  163. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/sparkles.svg +0 -0
  164. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/streamlit.gif +0 -0
  165. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/super-hero.svg +0 -0
  166. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/target.svg +0 -0
  167. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/thumbs-up.svg +0 -0
  168. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/README.md +0 -0
  169. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/Makefile +0 -0
  170. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/cloudflare_sandbox_example.py +0 -0
  171. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/coreweave_sandbox_example.py +0 -0
  172. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/datalayer_sandbox_example.py +0 -0
  173. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/daytona_sandbox_example.py +0 -0
  174. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/docker_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/e2b_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/eval_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/google_colab_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/kaggle_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/modal_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/monty_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/Makefile +0 -0
  183. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/cloudflare_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/coreweave_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/datalayer_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/daytona_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/docker_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/e2b_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/eval_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/google_colab_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/kaggle_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/modal_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/monty_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/pyproject.toml +0 -0
  196. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/schemas/environment-v1alpha1.json +0 -0
  197. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/__init__.py +0 -0
  198. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/conftest.py +0 -0
  199. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  200. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_bridge_mount.py +0 -0
  201. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_builds.py +0 -0
  202. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_cli_exec.py +0 -0
  203. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_cli_repl.py +0 -0
  204. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client.py +0 -0
  205. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client_contents.py +0 -0
  206. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client_files.py +0 -0
  207. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client_outcome_error_output.py +0 -0
  208. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_cloudflare.py +0 -0
  209. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_commands_envelope.py +0 -0
  210. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_console.py +0 -0
  211. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_contents_contract.py +0 -0
  212. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_contents_live_matrix.py +0 -0
  213. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_coreweave.py +0 -0
  214. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_datalayer_sandbox.py +0 -0
  215. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_daytona.py +0 -0
  216. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_e2b.py +0 -0
  217. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_accounts.py +0 -0
  218. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_bases.py +0 -0
  219. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_breaker.py +0 -0
  220. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_build_secrets.py +0 -0
  221. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_builders.py +0 -0
  222. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_canonical.py +0 -0
  223. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_conformance.py +0 -0
  224. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_constraints.py +0 -0
  225. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_contract.py +0 -0
  226. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_cross_variant_report.py +0 -0
  227. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_datalayer_builder.py +0 -0
  228. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_daytona_builder.py +0 -0
  229. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_doctor.py +0 -0
  230. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_e2b_builder.py +0 -0
  231. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_errors.py +0 -0
  232. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_image_import.py +0 -0
  233. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_lifecycle.py +0 -0
  234. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_live_matrix.py +0 -0
  235. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_redact.py +0 -0
  236. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_resolve.py +0 -0
  237. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_spec.py +0 -0
  238. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_eval.py +0 -0
  239. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  240. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_factory.py +0 -0
  241. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_google_colab.py +0 -0
  242. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_integration.py +0 -0
  243. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_jupyter_ingress.py +0 -0
  244. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_jupyter_server.py +0 -0
  245. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_jupyter_server_streaming.py +0 -0
  246. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle.py +0 -0
  247. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle_execute.py +0 -0
  248. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle_live.py +0 -0
  249. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle_session.py +0 -0
  250. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kernel_client_compatibility.py +0 -0
  251. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_lifecycle_vocabulary.py +0 -0
  252. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_manage.py +0 -0
  253. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_manage_datalayer.py +0 -0
  254. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_modal_google_colab_sandbox.py +0 -0
  255. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_modal_session.py +0 -0
  256. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_models.py +0 -0
  257. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_output_line_fidelity.py +0 -0
  258. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_provider_catalog_names.py +0 -0
  259. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_provider_ingress.py +0 -0
  260. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_providers.py +0 -0
  261. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_sandboxes.py +0 -0
  262. {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -8,6 +8,28 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.9.4
12
+
13
+ - **A pushed Environment image is decided by its linux/amd64 image's scan**
14
+ (`environments.attest`; PLAN_ENV.md E1-08). The Datalayer builder pushes
15
+ with SBOM and provenance attestations, so the digest it records is an OCI
16
+ image index. Amazon Inspector scans the image inside it and answers
17
+ `UNSUPPORTED_IMAGE` for the index itself, so every real build failed at
18
+ `attest` with `DL_ENV_PROVIDER_ERROR` (found live on r1, 2026-09-14, the
19
+ first build to get past the scan-read permission). The attestor now reads
20
+ the manifest and, for an index, reads the scan of its `linux/amd64` image,
21
+ skipping the attestation manifest. The signature stays on the index, which
22
+ is what a pod pulls.
23
+ - **Modal attaches a build secret to the `postInstall` steps that name it**
24
+ (`environments.adapters.modal`; PLAN_ENV.md E3-05). Modal used to refuse
25
+ every `buildSecrets` spec. Each declared secret is now resolved from IAM
26
+ before Modal is touched, made into a Modal Secret of the build's own,
27
+ passed as `secrets=` to exactly the `run_commands` steps whose command
28
+ names it, and deleted after the build like the base-reader secret. Its
29
+ value is redacted from the log and from a failed build's error. A
30
+ `mountAs: file` secret is refused, because Modal passes secrets as
31
+ environment variables and a file would land in a layer.
32
+
11
33
  ## 1.9.3
12
34
 
13
35
  - **A build secret is mounted only on the `postInstall` commands that name it**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.3
3
+ Version: 1.9.4
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.3"
6
+ __version__ = "1.9.4"
@@ -104,16 +104,16 @@ builder does not know the launch-time `gpu=` argument to hand a size class
104
104
  either (D-20 leaves that to a later item; a GPU is Modal's own launch
105
105
  option, not an image property, per section 11.4 item 10).
106
106
 
107
- **`spec.buildSecrets` is refused, unlike what Modal's own SDK could do.**
108
- `run_commands`/`apt_install`/`dockerfile_commands` each take a per-step
109
- `secrets=` collection — Modal has a real per-step secret mechanism, unlike
110
- E2B or Daytona. What is missing is not Modal's capability but this
111
- builder's own: `resolve_build_secret` (E3-05) has landed (`code_sandboxes
112
- .environments.build_secrets`), but nothing here calls it yet — wiring it
113
- into a Modal `secrets=` collection, and its own creation/cleanup lifecycle
114
- (mirroring `_ecr_secret`/`_delete_secret` above), is real, separate,
115
- follow-up work, not attempted in this pass. Refused for now, with that
116
- reason, rather than silently built without it.
107
+ **A build secret is attached to the `postInstall` steps that name it (E3-05).**
108
+ `run_commands` takes a per-step `secrets=` collection, a mechanism E2B and
109
+ Daytona lack. Each declared secret is resolved from IAM
110
+ (`resolve_build_secret`) before Modal is touched, made into a Modal Secret of
111
+ this build's own, attached to exactly the `run_commands` steps whose command
112
+ names it (`command_names_secret`), and deleted after the build the way the
113
+ base-reader secret is. Its value is redacted from the build's log and from a
114
+ failed build's error. Modal hands a secret to a step only as environment
115
+ variables, so a `mountAs: file` secret is refused: making it a file would
116
+ write it into a layer.
117
117
 
118
118
  @module code_sandboxes.environments.adapters.modal
119
119
  """
@@ -129,6 +129,7 @@ from pathlib import Path
129
129
  from typing import Any
130
130
 
131
131
  from ..accounts import provider_account
132
+ from ..build_secrets import resolve_build_secret
132
133
  from ..builders import (
133
134
  ArtifactMetadata,
134
135
  ArtifactReference,
@@ -144,8 +145,9 @@ from ..errors import (
144
145
  EnvironmentsError,
145
146
  )
146
147
  from ..files import files_step
148
+ from ..redact import redact
147
149
  from ..resolve import WHEELHOUSE_IMAGE_PATH, apt_pins_in
148
- from ..spec import GPU_SIZE_CLASSES, Environment
150
+ from ..spec import GPU_SIZE_CLASSES, BuildSecret, Environment, command_names_secret
149
151
  from .managed import ManagedBuilder
150
152
 
151
153
  __all__ = ["UNIMPLEMENTED_INSTRUCTIONS", "Builder"]
@@ -204,6 +206,21 @@ def _modal_internals() -> tuple[Any, Any]:
204
206
  return synchronizer, api_pb2
205
207
 
206
208
 
209
+ def _scrubbed(text: str, values: dict[str, str]) -> str:
210
+ """The text with this build's secret values redacted, or as it was with none (E3-05)."""
211
+ return redact(text, values.values()) if values else text
212
+
213
+
214
+ def _post_install(
215
+ image: Any, commands: list[str], declared: list[BuildSecret], step_secrets: dict[str, Any]
216
+ ) -> Any:
217
+ """Each `postInstall` command as its own step, with the secrets it names (E3-05)."""
218
+ for command in commands:
219
+ named = [step_secrets[s.id] for s in declared if command_names_secret(command, s)]
220
+ image = image.run_commands(command, secrets=named) if named else image.run_commands(command)
221
+ return image
222
+
223
+
207
224
  class Builder(ManagedBuilder):
208
225
  """Modal: the capability half (E2-06) and the build (E2-05)."""
209
226
 
@@ -228,8 +245,12 @@ class Builder(ManagedBuilder):
228
245
  modal_internals: Callable[[], tuple[Any, Any]] | None = None,
229
246
  image_builder_version: str | None = None,
230
247
  region: str | None = None,
248
+ resolve_secret: Callable[..., str] = resolve_build_secret,
231
249
  ) -> None:
232
250
  super().__init__(log=log, credential=credential)
251
+ #: How one `BuildSecret`'s value is fetched (E3-05): IAM by default,
252
+ #: the same seam the Datalayer builder takes.
253
+ self._resolve_secret = resolve_secret
233
254
  self._modal_sdk = modal_sdk or _modal_sdk
234
255
  self._modal_internals = modal_internals or _modal_internals
235
256
  self._image_builder_version = image_builder_version or _IMAGE_BUILDER_VERSION
@@ -297,24 +318,19 @@ class Builder(ManagedBuilder):
297
318
  field=f"spec.commands.postInstall[{index}]",
298
319
  )
299
320
  )
300
- if environment.spec.build_secrets:
301
- # Modal's own per-step `secrets=` mechanism could carry one —
302
- # unlike E2B or Daytona — and `resolve_build_secret` (E3-05) has
303
- # landed, but nothing here calls it yet: wiring it into a Modal
304
- # `secrets=` collection is real, separate work (found in this
305
- # item's own implementation, not a provider limit).
306
- ids = ", ".join(secret.id for secret in environment.spec.build_secrets)
307
- findings.append(
308
- CapabilityFinding(
309
- code="DL_ENV_CAPABILITY_UNSUPPORTED",
310
- message=(
311
- f"`buildSecrets` ({ids}) is not wired into this builder yet — Modal's own "
312
- "per-step secrets could carry one, and resolve_build_secret (E3-05) can "
313
- "resolve one, but nothing here connects them"
314
- ),
315
- field="spec.buildSecrets",
321
+ for index, secret in enumerate(environment.spec.build_secrets):
322
+ if secret.mount_as == "file":
323
+ findings.append(
324
+ CapabilityFinding(
325
+ code="DL_ENV_CAPABILITY_UNSUPPORTED",
326
+ message=(
327
+ f"Modal gives `{secret.name}` to a step as an environment variable "
328
+ "only; a file would be written into a layer. Use `mountAs: env`, or "
329
+ "drop `modal` from the variants"
330
+ ),
331
+ field=f"spec.buildSecrets[{index}].mountAs",
332
+ )
316
333
  )
317
- )
318
334
  return findings
319
335
 
320
336
  # -- Building -------------------------------------------------------------
@@ -340,20 +356,9 @@ class Builder(ManagedBuilder):
340
356
  "needs is E2-17's, not built yet",
341
357
  detail={"variant": self.variant, "missing": "E2-17"},
342
358
  )
343
- if request.build_secret_ids or spec.build_secrets:
344
- # `_own_findings` already refuses this at `validate()` — this
345
- # guard is `build()`'s own, the same way the GPU one above is,
346
- # because nothing enforces that `build` is only ever called
347
- # after a passing `validate` (found in review): a caller
348
- # handing `build()` an already-resolved request could otherwise
349
- # get a successful image with a required credential silently
350
- # missing from it.
351
- raise EnvironmentsError(
352
- CAPABILITY_UNSUPPORTED,
353
- "Modal's build secrets are not wired into this builder yet (E3-05 has landed, "
354
- "but nothing here calls it)",
355
- detail={"variant": self.variant, "missing": "E3-05"},
356
- )
359
+ # Resolved before Modal is touched: a secret IAM will not give stops
360
+ # the build with nothing to clean up in the owner's workspace.
361
+ declared, values = self._resolved_secrets(request)
357
362
  sdk = self._modal_sdk()
358
363
  # Read from the process environment, not a per-call argument (see
359
364
  # the module docstring) — set before anything else touches the SDK.
@@ -362,7 +367,12 @@ class Builder(ManagedBuilder):
362
367
  name = f"dl-{request.environment.metadata.name}-v{request.version}-{request.build_uid}"
363
368
 
364
369
  ecr_secret = self._ecr_secret(sdk, client)
370
+ step_secrets: dict[str, Any] = {}
365
371
  try:
372
+ for secret in declared:
373
+ step_secrets[secret.id] = self._hydrated_secret(
374
+ sdk, client, {secret.name: values[secret.id]}, keep=step_secrets
375
+ )
366
376
  app = sdk.App.lookup(
367
377
  f"dl-{request.environment.metadata.name}", client=client, create_if_missing=True
368
378
  )
@@ -404,8 +414,7 @@ class Builder(ManagedBuilder):
404
414
  )
405
415
  for command in files_step(request.environment, variant=self.variant):
406
416
  image = image.run_commands(command)
407
- for command in spec.commands.post_install:
408
- image = image.run_commands(command)
417
+ image = _post_install(image, spec.commands.post_install, declared, step_secrets)
409
418
  image = image.workdir(_CONTENT_DIR).entrypoint([_ENTRYPOINT_PATH])
410
419
 
411
420
  logged: list[str] = []
@@ -423,13 +432,13 @@ class Builder(ManagedBuilder):
423
432
  # that raised inside the `with` block above never
424
433
  # reached the line that read it, so a failed build's
425
434
  # own log was silently dropped from the error detail.
426
- logged.extend(buffer.getvalue().splitlines())
435
+ logged.extend(_scrubbed(buffer.getvalue(), values).splitlines())
427
436
  raise EnvironmentsError(
428
437
  BUILD_FAILED,
429
- f"The Modal build failed: {error}",
438
+ f"The Modal build failed: {_scrubbed(str(error), values)}",
430
439
  detail={"variant": self.variant, "name": name, "log": logged[-20:]},
431
- ) from error
432
- logged.extend(buffer.getvalue().splitlines())
440
+ ) from (None if values else error)
441
+ logged.extend(_scrubbed(buffer.getvalue(), values).splitlines())
433
442
  for line in logged:
434
443
  self._log(line)
435
444
  # A name for people and dashboards only (§11.4 item 3): moves
@@ -439,6 +448,8 @@ class Builder(ManagedBuilder):
439
448
  # No standing credential is left in an account Datalayer does
440
449
  # not control, whether the build above succeeded or not (D-18).
441
450
  self._delete_secret(sdk, client, ecr_secret)
451
+ for secret in step_secrets.values():
452
+ self._delete_secret(sdk, client, secret)
442
453
 
443
454
  return ArtifactReference(
444
455
  variant=self.variant,
@@ -450,6 +461,25 @@ class Builder(ManagedBuilder):
450
461
  contract_version=spec.contract or SANDBOX_CONTRACT_V1.version,
451
462
  )
452
463
 
464
+ def _resolved_secrets(self, request: BuildRequest) -> tuple[list[BuildSecret], dict[str, str]]:
465
+ """The build secrets this build attaches, and each one's value from IAM (E3-05).
466
+
467
+ `_own_findings` refuses a `mountAs: file` secret at `validate()`. This
468
+ guards it too, the same way `build` guards a GPU class, because
469
+ nothing makes a caller validate first.
470
+ """
471
+ wanted = set(request.build_secret_ids)
472
+ declared = [s for s in request.environment.spec.build_secrets if s.id in wanted]
473
+ if any(secret.mount_as == "file" for secret in declared):
474
+ raise EnvironmentsError(
475
+ CAPABILITY_UNSUPPORTED,
476
+ "Modal gives a build secret to a step as an environment variable only; a "
477
+ "`mountAs: file` secret would be written into a layer",
478
+ detail={"variant": self.variant, "field": "spec.buildSecrets"},
479
+ )
480
+ values = {s.id: self._resolve_secret(s, owner_uid=request.owner_uid) for s in declared}
481
+ return declared, values
482
+
453
483
  def _ecr_secret(self, sdk: Any, client: Any) -> Any | None:
454
484
  """A Modal Secret carrying this build's own base-reader credential (D-17, D-18).
455
485
 
@@ -489,6 +519,30 @@ class Builder(ManagedBuilder):
489
519
  ) from error
490
520
  return secret
491
521
 
522
+ def _hydrated_secret(
523
+ self, sdk: Any, client: Any, env: dict[str, str], *, keep: dict[str, Any]
524
+ ) -> Any:
525
+ """A Modal Secret of this build's own for one build secret (E3-05).
526
+
527
+ Created in the owner's workspace like `_ecr_secret`'s, and deleted by
528
+ `build`'s own `finally`. A `hydrate` that fails is cleaned up here,
529
+ since `build` never got it back. The error names no value.
530
+ """
531
+ secret = sdk.Secret.from_dict(env)
532
+ try:
533
+ secret.hydrate(client=client)
534
+ except Exception as error:
535
+ self._delete_secret(sdk, client, secret)
536
+ # `from None`: Modal's own error is not kept, in case it echoes
537
+ # what it was given. Its type is enough to tell failures apart.
538
+ raise EnvironmentsError(
539
+ PROVIDER_ERROR,
540
+ f"Modal could not be given the build secret `{next(iter(env))}` "
541
+ f"({type(error).__name__})",
542
+ detail={"variant": self.variant, "made": len(keep)},
543
+ ) from None
544
+ return secret
545
+
492
546
  def _delete_secret(self, sdk: Any, client: Any, secret: Any | None) -> None:
493
547
  """Delete the app-owned copy Modal stores of this build's own secret.
494
548
 
@@ -32,6 +32,7 @@ clock — so the whole chain is tested without AWS and without a daemon.
32
32
 
33
33
  from __future__ import annotations
34
34
 
35
+ import json
35
36
  import logging
36
37
  import os
37
38
  import re
@@ -68,6 +69,20 @@ SCAN_STATUSES = {
68
69
  "refused": ("FAILED", "UNSUPPORTED_IMAGE", "FINDINGS_UNAVAILABLE"),
69
70
  }
70
71
 
72
+ #: The platform an Environment image is built for: the image of an index
73
+ #: whose scan is read.
74
+ _SCANNED_PLATFORM = ("linux", "amd64")
75
+
76
+ #: What a registry answers for an image index, and for a single image.
77
+ _INDEX_MEDIA_TYPES = (
78
+ "application/vnd.oci.image.index.v1+json",
79
+ "application/vnd.docker.distribution.manifest.list.v2+json",
80
+ )
81
+ _MANIFEST_MEDIA_TYPES = (
82
+ "application/vnd.oci.image.manifest.v1+json",
83
+ "application/vnd.docker.distribution.manifest.v2+json",
84
+ )
85
+
71
86
  #: How long a scan is waited for, and how often it is asked about.
72
87
  DEFAULT_SCAN_TIMEOUT_SECONDS = 15 * 60
73
88
  DEFAULT_SCAN_INTERVAL_SECONDS = 10.0
@@ -173,29 +188,34 @@ class Attestor:
173
188
  — retryable, because nothing about the version is wrong — and so is a
174
189
  scanner that refuses the image. A scan that finished is decided, and a
175
190
  blocking decision raises `DL_ENV_SCAN_BLOCKED`.
191
+
192
+ An artifact that is an image index is decided by the scan of its
193
+ linux/amd64 image: see `_scanned_digest`.
176
194
  """
177
195
  deadline = self._now() + self._timeout
196
+ scanned = self._scanned_digest(repository=repository, digest=digest)
197
+ where = {"repository": repository, "digest": digest, "scannedDigest": scanned}
178
198
  waited = 0
179
199
  while True:
180
- status, described = self._describe(repository=repository, digest=digest)
200
+ status, described = self._describe(repository=repository, digest=scanned)
181
201
  if status in SCAN_STATUSES["done"]:
182
202
  break
183
203
  if status in SCAN_STATUSES["refused"]:
184
204
  raise EnvironmentsError(
185
205
  PROVIDER_ERROR,
186
206
  f"The registry did not scan this artifact: {status}",
187
- detail={"repository": repository, "digest": digest, "scanStatus": status},
207
+ detail={**where, "scanStatus": status},
188
208
  )
189
209
  if self._now() >= deadline:
190
210
  raise EnvironmentsError(
191
211
  PROVIDER_ERROR,
192
212
  f"The scan of this artifact did not finish within {self._timeout:.0f}s "
193
213
  f"(last status {status or 'unknown'})",
194
- detail={"repository": repository, "digest": digest, "scanStatus": status},
214
+ detail={**where, "scanStatus": status},
195
215
  )
196
216
  waited += 1
197
217
  if waited == 1:
198
- self._log(f"Waiting for the scan of {digest}")
218
+ self._log(f"Waiting for the scan of {scanned}")
199
219
  self._sleep(self._interval)
200
220
  findings = findings_of(self._findings(described))
201
221
  decision = decide(
@@ -211,6 +231,64 @@ class Attestor:
211
231
  )
212
232
  return decision
213
233
 
234
+ def _scanned_digest(self, *, repository: str, digest: str) -> str:
235
+ """The digest whose scan decides this artifact: its own, or its linux/amd64 image's.
236
+
237
+ The Datalayer builder pushes with SBOM and provenance attestations, so
238
+ what it records is an OCI image index: the image, and an attestation
239
+ manifest beside it. Enhanced scanning scans the image and answers
240
+ `UNSUPPORTED_IMAGE` for the index (found live on r1, 2026-09-14), so the
241
+ scan read is that of the image the index names for linux/amd64. The
242
+ signature stays on the index, which is what a pod pulls.
243
+ """
244
+ try:
245
+ answer = self._client().batch_get_image(
246
+ repositoryName=repository,
247
+ imageIds=[{"imageDigest": digest}],
248
+ acceptedMediaTypes=[*_INDEX_MEDIA_TYPES, *_MANIFEST_MEDIA_TYPES],
249
+ )
250
+ except Exception as error:
251
+ raise EnvironmentsError(
252
+ PROVIDER_ERROR,
253
+ f"The manifest of {digest} could not be read: {error}",
254
+ detail={"repository": repository, "digest": digest},
255
+ ) from error
256
+ images = answer.get("images") or []
257
+ if not images:
258
+ raise EnvironmentsError(
259
+ PROVIDER_ERROR,
260
+ f"The registry has no image {digest}",
261
+ detail={"repository": repository, "digest": digest},
262
+ )
263
+ try:
264
+ manifest = json.loads(images[0].get("imageManifest") or "{}")
265
+ except ValueError:
266
+ manifest = {}
267
+ media_type = str(images[0].get("imageManifestMediaType") or manifest.get("mediaType") or "")
268
+ if media_type not in _INDEX_MEDIA_TYPES and "manifests" not in manifest:
269
+ return digest
270
+ os_name, architecture = _SCANNED_PLATFORM
271
+ for entry in manifest.get("manifests") or []:
272
+ annotations = entry.get("annotations") or {}
273
+ if annotations.get("vnd.docker.reference.type") == "attestation-manifest":
274
+ continue
275
+ platform = entry.get("platform") or {}
276
+ child = str(entry.get("digest") or "")
277
+ if (
278
+ platform.get("os") == os_name
279
+ and platform.get("architecture") == architecture
280
+ and _DIGEST.match(child)
281
+ ):
282
+ self._log(
283
+ f"Reading the scan of {child}, the {os_name}/{architecture} image of {digest}"
284
+ )
285
+ return child
286
+ raise EnvironmentsError(
287
+ PROVIDER_ERROR,
288
+ f"The image index {digest} holds no {os_name}/{architecture} image to scan",
289
+ detail={"repository": repository, "digest": digest},
290
+ )
291
+
214
292
  def _describe(self, *, repository: str, digest: str) -> tuple[str, dict[str, Any]]:
215
293
  """One `DescribeImageScanFindings`, as a status and what it answered."""
216
294
  try:
@@ -17,6 +17,7 @@ rows amount to, and a paraphrase of them would test the paraphrase.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import json
20
21
  import subprocess
21
22
 
22
23
  import pytest
@@ -88,16 +89,35 @@ class FakeEcr:
88
89
  findings=(),
89
90
  enhanced_findings=True,
90
91
  signed: set[str] | None = None,
92
+ manifest: dict | None = None,
91
93
  ) -> None:
92
94
  self.statuses = list(statuses)
93
95
  self.findings = list(findings)
94
96
  self.enhanced_findings = enhanced_findings
95
97
  self.signed = set(signed or set())
98
+ self.manifest = manifest
96
99
  self.asked = 0
100
+ self.scanned: list[str] = []
97
101
  self.described_tags: list[str] = []
98
102
 
103
+ def batch_get_image(self, repositoryName, imageIds, acceptedMediaTypes): # noqa: N803 - boto3's spelling
104
+ manifest = self.manifest or {
105
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
106
+ "layers": [],
107
+ }
108
+ return {
109
+ "images": [
110
+ {
111
+ "imageId": imageIds[0],
112
+ "imageManifestMediaType": manifest["mediaType"],
113
+ "imageManifest": json.dumps(manifest),
114
+ }
115
+ ]
116
+ }
117
+
99
118
  def describe_image_scan_findings(self, repositoryName, imageId): # noqa: N803 - boto3's spelling
100
119
  self.asked += 1
120
+ self.scanned.append(imageId["imageDigest"])
101
121
  status = self.statuses[min(self.asked, len(self.statuses)) - 1]
102
122
  if status == "RAISES_IN_PROGRESS":
103
123
  error = Exception("ScanInProgressException")
@@ -278,6 +298,59 @@ class TestTheScan:
278
298
  assert raised.value.code.code == "DL_ENV_PROVIDER_ERROR"
279
299
  assert raised.value.detail["digest"] == DIGEST
280
300
 
301
+ IMAGE = "sha256:" + "bb" * 32
302
+ ATTESTATION = "sha256:" + "cc" * 32
303
+
304
+ def an_index(self, *entries: dict) -> dict:
305
+ return {"mediaType": "application/vnd.oci.image.index.v1+json", "manifests": list(entries)}
306
+
307
+ def attestation_entry(self) -> dict:
308
+ return {
309
+ "digest": self.ATTESTATION,
310
+ "platform": {"os": "unknown", "architecture": "unknown"},
311
+ "annotations": {"vnd.docker.reference.type": "attestation-manifest"},
312
+ }
313
+
314
+ def test_an_index_is_decided_by_the_scan_of_its_linux_amd64_image(self) -> None:
315
+ """The builder pushes with attestations, so what it records is an
316
+ index, and the scanner answers `UNSUPPORTED_IMAGE` for an index (found
317
+ live on r1, 2026-09-14)."""
318
+ ecr = FakeEcr(
319
+ manifest=self.an_index(
320
+ self.attestation_entry(),
321
+ {"digest": self.IMAGE, "platform": {"os": "linux", "architecture": "amd64"}},
322
+ ),
323
+ findings=[enhanced("CVE-2026-1234", "CRITICAL", fixed="2.9.15")],
324
+ )
325
+ decision = an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
326
+ assert ecr.scanned == [self.IMAGE]
327
+ assert decision.decision == "blocked"
328
+
329
+ def test_an_index_with_no_linux_amd64_image_is_named(self) -> None:
330
+ ecr = FakeEcr(manifest=self.an_index(self.attestation_entry()))
331
+ with pytest.raises(EnvironmentsError) as raised:
332
+ an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
333
+ assert raised.value.code.code == "DL_ENV_PROVIDER_ERROR"
334
+ assert "linux/amd64" in raised.value.message
335
+ assert ecr.scanned == []
336
+
337
+ def test_a_single_image_is_scanned_by_its_own_digest(self) -> None:
338
+ ecr = FakeEcr()
339
+ an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
340
+ assert ecr.scanned == [DIGEST]
341
+
342
+ def test_a_refused_scan_names_both_the_artifact_and_the_image_read(self) -> None:
343
+ ecr = FakeEcr(
344
+ statuses=("UNSUPPORTED_IMAGE",),
345
+ manifest=self.an_index(
346
+ {"digest": self.IMAGE, "platform": {"os": "linux", "architecture": "amd64"}}
347
+ ),
348
+ )
349
+ with pytest.raises(EnvironmentsError) as raised:
350
+ an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
351
+ assert raised.value.detail["digest"] == DIGEST
352
+ assert raised.value.detail["scannedDigest"] == self.IMAGE
353
+
281
354
 
282
355
  # -- the signature --------------------------------------------------------------
283
356
 
@@ -235,17 +235,19 @@ class TestWhatIsSaidBeforeAnythingIsQueued:
235
235
  report = get_builder("datalayer").validate(environment(**spec))
236
236
  assert report.supported is True, messages(report)
237
237
 
238
- def test_modal_refuses_a_build_secret_too_but_not_for_lack_of_a_mechanism(self) -> None:
239
- """Modal has a real per-step `secrets=` mechanism, unlike E2B and
240
- Daytona — `supports_build_secrets` stays the default `True` — but
241
- nothing in this builder wires `resolve_build_secret` (E3-05) into
242
- it yet, so a spec naming one is still refused, for its own reason
243
- rather than the provider-capability one E2B/Daytona give (E2-05)."""
244
- spec = {"buildSecrets": [{"id": "dlsec_01J9BUILDSECRET0000000000", "name": "TOKEN"}]}
245
- report = get_builder("modal").validate(environment(**spec))
238
+ def test_modal_accepts_an_env_build_secret_and_refuses_a_file_one(self) -> None:
239
+ """Modal has a per-step `secrets=` mechanism, unlike E2B and Daytona,
240
+ and attaches a secret to the steps that name it (E3-05). It passes
241
+ environment variables only, so a `mountAs: file` secret, which would
242
+ have to be written into a layer, is refused."""
243
+ secret = {"id": "dlsec_01J9BUILDSECRET0000000000", "name": "TOKEN"}
244
+ report = get_builder("modal").validate(environment(buildSecrets=[secret]))
245
+ assert "spec.buildSecrets" not in " ".join(fields(report)), messages(report)
246
+ report = get_builder("modal").validate(
247
+ environment(buildSecrets=[{**secret, "mountAs": "file"}])
248
+ )
246
249
  assert report.supported is False, messages(report)
247
- assert "not wired into this builder" in messages(report)
248
- assert "spec.buildSecrets" in fields(report)
250
+ assert "spec.buildSecrets[0].mountAs" in fields(report)
249
251
 
250
252
  def test_every_finding_carries_the_capability_code(self) -> None:
251
253
  report = get_builder("e2b").validate(