code-sandboxes 1.9.3__tar.gz → 1.9.5__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.5}/CHANGELOG.md +56 -0
  2. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/PKG-INFO +1 -1
  3. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/datalayer.py +14 -4
  5. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/modal.py +102 -48
  6. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/attest.py +95 -7
  7. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/bases.py +28 -0
  8. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/image_import.py +89 -39
  9. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/policy.py +22 -1
  10. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/resolve.py +108 -22
  11. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_attest.py +121 -0
  12. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_bases.py +22 -0
  13. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_datalayer_builder.py +11 -0
  14. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_image_import.py +85 -0
  15. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_managed_builders.py +12 -10
  16. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_modal_builder.py +92 -25
  17. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_resolve.py +101 -6
  18. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/build.yml +0 -0
  19. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/environments-live.yml +0 -0
  20. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/py-code-style.yml +0 -0
  21. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/py-tests.yml +0 -0
  22. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/py-typing.yml +0 -0
  23. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/reusable-python.yml +0 -0
  24. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.gitignore +0 -0
  25. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.licenserc.yaml +0 -0
  26. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.pre-commit-config.yaml +0 -0
  27. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.vscode/settings.json +0 -0
  28. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/Dockerfile +0 -0
  29. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/LICENSE +0 -0
  30. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/Makefile +0 -0
  31. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/README.md +0 -0
  32. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/build.log +0 -0
  33. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/__init__.py +0 -0
  34. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/base.py +0 -0
  35. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/bridge_mount.py +0 -0
  36. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/builds.py +0 -0
  37. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/cli.py +0 -0
  38. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/client.py +0 -0
  39. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/cloudflare_sandbox.py +0 -0
  40. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/commands.py +0 -0
  41. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/console.py +0 -0
  42. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/contents.py +0 -0
  43. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/coreweave_sandbox.py +0 -0
  44. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/datalayer_sandbox.py +0 -0
  45. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/daytona_sandbox.py +0 -0
  46. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/docker_sandbox.py +0 -0
  47. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/e2b_sandbox.py +0 -0
  48. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/__init__.py +0 -0
  49. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/__main__.py +0 -0
  50. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/accounts.py +0 -0
  51. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/__init__.py +0 -0
  52. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/daytona.py +0 -0
  53. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/e2b.py +0 -0
  54. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/managed.py +0 -0
  55. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/breaker.py +0 -0
  56. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/build_secrets.py +0 -0
  57. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/builders.py +0 -0
  58. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/canonical.py +0 -0
  59. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/conformance.py +0 -0
  60. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  61. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  62. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  63. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/contract.py +0 -0
  64. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/__init__.py +0 -0
  65. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/build.py +0 -0
  66. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  67. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  68. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/errors.py +0 -0
  69. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/files.py +0 -0
  70. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/lifecycle.py +0 -0
  71. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/redact.py +0 -0
  72. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/schema.py +0 -0
  73. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/spec.py +0 -0
  74. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/eval_sandbox.py +0 -0
  75. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/exceptions.py +0 -0
  76. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/filesystem.py +0 -0
  77. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/google_colab.py +0 -0
  78. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/google_colab_sandbox.py +0 -0
  79. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/interfaces.py +0 -0
  80. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_ingress.py +0 -0
  81. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  82. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle.py +0 -0
  83. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_execute.py +0 -0
  84. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_live.py +0 -0
  85. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_sandbox.py +0 -0
  86. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/lifecycle.py +0 -0
  87. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/manage.py +0 -0
  88. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/modal_sandbox.py +0 -0
  89. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/models.py +0 -0
  90. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/monty_sandbox.py +0 -0
  91. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/provider_ingress.py +0 -0
  92. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/providers.py +0 -0
  93. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docker/Dockerfile +0 -0
  94. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/.gitignore +0 -0
  95. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/LICENSE +0 -0
  96. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/Makefile +0 -0
  97. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/README.md +0 -0
  98. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/babel.config.js +0 -0
  99. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/api-reference/_category_.yml +0 -0
  100. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/api-reference/index.mdx +0 -0
  101. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/cli/_category_.yml +0 -0
  102. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/cli/index.mdx +0 -0
  103. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/contribute/_category_.yml +0 -0
  104. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/contribute/index.mdx +0 -0
  105. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/_category_.yml +0 -0
  106. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/contract.mdx +0 -0
  107. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/index.mdx +0 -0
  108. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/specification.mdx +0 -0
  109. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/spike-2026-09.mdx +0 -0
  110. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/examples/_category_.yml +0 -0
  111. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/examples/index.mdx +0 -0
  112. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/guide/_category_.yml +0 -0
  113. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/guide/index.mdx +0 -0
  114. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/index.mdx +0 -0
  115. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/install/_category_.yml +0 -0
  116. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/install/index.mdx +0 -0
  117. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/provider-ingress.mdx +0 -0
  118. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/_category_.yml +0 -0
  119. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/cloudflare.mdx +0 -0
  120. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/coreweave.mdx +0 -0
  121. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/datalayer.mdx +0 -0
  122. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/daytona.mdx +0 -0
  123. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/docker.mdx +0 -0
  124. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/e2b.mdx +0 -0
  125. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/eval.mdx +0 -0
  126. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/google-colab.mdx +0 -0
  127. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/index.mdx +0 -0
  128. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/jupyter-server.mdx +0 -0
  129. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/kaggle.mdx +0 -0
  130. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/modal.mdx +0 -0
  131. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/monty.mdx +0 -0
  132. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docusaurus.config.js +0 -0
  133. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/package.json +0 -0
  134. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/sidebars.js +0 -0
  135. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.js +0 -0
  136. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.module.css +0 -0
  137. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.js +0 -0
  138. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.module.css +0 -0
  139. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.js +0 -0
  140. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.module.css +0 -0
  141. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/css/custom.css +0 -0
  142. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/pages/index.module.css +0 -0
  143. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/pages/markdown-page.md +0 -0
  144. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/theme/CustomDocItem.tsx +0 -0
  145. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/theme/IconsTitle.tsx +0 -0
  146. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/bricks.svg +0 -0
  147. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/building-construction.svg +0 -0
  148. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/cloud-sun.svg +0 -0
  149. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/cloud.svg +0 -0
  150. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/collaboration.svg +0 -0
  151. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/copy-left.svg +0 -0
  152. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.png +0 -0
  153. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.svg +0 -0
  154. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/business.svg +0 -0
  155. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  156. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/devops.svg +0 -0
  157. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/project/project-line.svg +0 -0
  158. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/favicon.ico +0 -0
  159. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/jupyter.svg +0 -0
  160. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/open-source.svg +0 -0
  161. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/react-js.svg +0 -0
  162. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/react-js2.svg +0 -0
  163. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/ringed-planet.svg +0 -0
  164. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/robot.svg +0 -0
  165. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/rocket.svg +0 -0
  166. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/shield.svg +0 -0
  167. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/sparkles.svg +0 -0
  168. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/streamlit.gif +0 -0
  169. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/super-hero.svg +0 -0
  170. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/target.svg +0 -0
  171. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/thumbs-up.svg +0 -0
  172. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/README.md +0 -0
  173. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/Makefile +0 -0
  174. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/cloudflare_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/coreweave_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/datalayer_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/daytona_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/docker_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/e2b_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/eval_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/google_colab_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/kaggle_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/modal_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/monty_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/Makefile +0 -0
  187. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/cloudflare_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/coreweave_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/datalayer_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/daytona_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/docker_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/e2b_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/eval_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/google_colab_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/kaggle_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/modal_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/monty_sandbox_example.py +0 -0
  199. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/pyproject.toml +0 -0
  200. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/schemas/environment-v1alpha1.json +0 -0
  201. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/__init__.py +0 -0
  202. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/conftest.py +0 -0
  203. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  204. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_bridge_mount.py +0 -0
  205. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_builds.py +0 -0
  206. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_cli_exec.py +0 -0
  207. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_cli_repl.py +0 -0
  208. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client.py +0 -0
  209. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client_contents.py +0 -0
  210. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client_files.py +0 -0
  211. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client_outcome_error_output.py +0 -0
  212. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_cloudflare.py +0 -0
  213. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_commands_envelope.py +0 -0
  214. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_console.py +0 -0
  215. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_contents_contract.py +0 -0
  216. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_contents_live_matrix.py +0 -0
  217. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_coreweave.py +0 -0
  218. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_datalayer_sandbox.py +0 -0
  219. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_daytona.py +0 -0
  220. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_e2b.py +0 -0
  221. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_accounts.py +0 -0
  222. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_breaker.py +0 -0
  223. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_build_secrets.py +0 -0
  224. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_builders.py +0 -0
  225. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_canonical.py +0 -0
  226. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_conformance.py +0 -0
  227. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_constraints.py +0 -0
  228. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_contract.py +0 -0
  229. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_cross_variant_report.py +0 -0
  230. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_daytona_builder.py +0 -0
  231. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_doctor.py +0 -0
  232. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_e2b_builder.py +0 -0
  233. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_errors.py +0 -0
  234. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_lifecycle.py +0 -0
  235. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_live_matrix.py +0 -0
  236. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_redact.py +0 -0
  237. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_spec.py +0 -0
  238. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_eval.py +0 -0
  239. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  240. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_factory.py +0 -0
  241. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_google_colab.py +0 -0
  242. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_integration.py +0 -0
  243. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_jupyter_ingress.py +0 -0
  244. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_jupyter_server.py +0 -0
  245. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_jupyter_server_streaming.py +0 -0
  246. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle.py +0 -0
  247. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle_execute.py +0 -0
  248. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle_live.py +0 -0
  249. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle_session.py +0 -0
  250. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kernel_client_compatibility.py +0 -0
  251. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_lifecycle_vocabulary.py +0 -0
  252. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_manage.py +0 -0
  253. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_manage_datalayer.py +0 -0
  254. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_modal_google_colab_sandbox.py +0 -0
  255. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_modal_session.py +0 -0
  256. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_models.py +0 -0
  257. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_output_line_fidelity.py +0 -0
  258. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_provider_catalog_names.py +0 -0
  259. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_provider_ingress.py +0 -0
  260. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_providers.py +0 -0
  261. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_sandboxes.py +0 -0
  262. {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -8,6 +8,62 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.9.5
12
+
13
+ - **Attest waits for a just-pushed image's scan to exist**
14
+ (`environments.attest`; PLAN_ENV.md E1-08). Enhanced scanning starts after
15
+ the push, so the first answers for a new image are
16
+ `ScanNotFoundException`. Attest read that as an image with no scan and
17
+ failed the build at once (found live on r1, 2026-09-14). It is now waited
18
+ for like a running scan, within the same bound.
19
+ - **apt is pinned at, and installed from, the base channel's Ubuntu snapshot**
20
+ (`environments.bases`, `environments.resolve`,
21
+ `environments.adapters.datalayer`; PLAN_ENV.md E1-04). A channel records
22
+ the `snapshot.ubuntu.com` id its image was upgraded at. The solve runs
23
+ `apt-get` with `--snapshot` at that id, the lock records it
24
+ (`# datalayer-apt-snapshot:`), and the Datalayer builder installs its pins
25
+ from the same snapshot. This replaces a `deb` line that named only `main`
26
+ (`gdal-bin` is in `universe`) and left the live mirror enabled beside it.
27
+ `DATALAYER_APT_SNAPSHOT` must now be a snapshot id.
28
+ - **A private registry's credential is resolved and used** (`environments
29
+ .image_import`, `environments.resolve`; PLAN_ENV.md E3-04's second half).
30
+ `image.credentialSecretId` used to only clear the allowlist check; nothing
31
+ fetched or used the credential it named. It is now fetched through the
32
+ same IAM route a build secret is (E3-05), and sent as HTTP Basic on the
33
+ registry's own token exchange — the way `docker login` authenticates a
34
+ private pull — never to the registry named in the reference, and never
35
+ logged. `resolve_environment` and `resolve_image_base` take `owner_uid`
36
+ and `resolve_secret` to reach it; unused by a public registry or any other
37
+ source.
38
+ - **The Datalayer base channel's own five ESM-locked advisories are allowed**
39
+ (`environments.policy`; PLAN_ENV.md E1-08). Amazon Inspector marks all five
40
+ fixable, but the fix is an Ubuntu ESM package version a plain
41
+ `apt-get upgrade` cannot reach. Reviewed and added to `DEFAULT_POLICY
42
+ .allowed`, 2026-09-14, so they are recorded as allowed rather than missed,
43
+ and any other critical still blocks.
44
+
45
+ ## 1.9.4
46
+
47
+ - **A pushed Environment image is decided by its linux/amd64 image's scan**
48
+ (`environments.attest`; PLAN_ENV.md E1-08). The Datalayer builder pushes
49
+ with SBOM and provenance attestations, so the digest it records is an OCI
50
+ image index. Amazon Inspector scans the image inside it and answers
51
+ `UNSUPPORTED_IMAGE` for the index itself, so every real build failed at
52
+ `attest` with `DL_ENV_PROVIDER_ERROR` (found live on r1, 2026-09-14, the
53
+ first build to get past the scan-read permission). The attestor now reads
54
+ the manifest and, for an index, reads the scan of its `linux/amd64` image,
55
+ skipping the attestation manifest. The signature stays on the index, which
56
+ is what a pod pulls.
57
+ - **Modal attaches a build secret to the `postInstall` steps that name it**
58
+ (`environments.adapters.modal`; PLAN_ENV.md E3-05). Modal used to refuse
59
+ every `buildSecrets` spec. Each declared secret is now resolved from IAM
60
+ before Modal is touched, made into a Modal Secret of the build's own,
61
+ passed as `secrets=` to exactly the `run_commands` steps whose command
62
+ names it, and deleted after the build like the base-reader secret. Its
63
+ value is redacted from the log and from a failed build's error. A
64
+ `mountAs: file` secret is refused, because Modal passes secrets as
65
+ environment variables and a file would land in a layer.
66
+
11
67
  ## 1.9.3
12
68
 
13
69
  - **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.5
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.5"
@@ -68,7 +68,13 @@ from ..errors import (
68
68
  EnvironmentsError,
69
69
  )
70
70
  from ..files import files_step
71
- from ..resolve import WHEELHOUSE_IMAGE_PATH, WHEELHOUSE_PATH, apt_pins_in, locked_versions
71
+ from ..resolve import (
72
+ WHEELHOUSE_IMAGE_PATH,
73
+ WHEELHOUSE_PATH,
74
+ apt_pins_in,
75
+ apt_snapshot_in,
76
+ locked_versions,
77
+ )
72
78
  from ..spec import BuildSecret, Environment, command_names_secret
73
79
 
74
80
  __all__ = [
@@ -310,14 +316,18 @@ class Builder:
310
316
  lines.append(f"ENV {name}={shlex.quote(spec.env[name])}")
311
317
  if apt:
312
318
  # Pinned, and from the lock: `apt-get install <name>` a month later
313
- # is a different artifact.
319
+ # is a different artifact. From the snapshot the pins were taken
320
+ # from, when the lock names one: a pinned version can leave the
321
+ # live mirror (D-9).
314
322
  pinned = " ".join(f"{name}={apt[name]}" for name in sorted(apt))
323
+ snapshot = apt_snapshot_in(request.lock_text)
324
+ option = f" --snapshot {snapshot}" if snapshot else ""
315
325
  lines.extend(
316
326
  [
317
327
  "USER root",
318
328
  "RUN --mount=type=cache,target=/var/cache/apt,sharing=locked "
319
- "apt-get update -qq \\\n"
320
- f" && apt-get install -y --no-install-recommends {pinned} \\\n"
329
+ f"apt-get update -qq{option} \\\n"
330
+ f" && apt-get install -y --no-install-recommends{option} {pinned} \\\n"
321
331
  " && rm -rf /var/lib/apt/lists/*",
322
332
  ]
323
333
  )
@@ -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:
@@ -452,10 +530,20 @@ def _is_missing(error: BaseException) -> bool:
452
530
  return _error_code(error) in {
453
531
  "ImageNotFoundException",
454
532
  "RepositoryNotFoundException",
455
- "ScanNotFoundException",
456
533
  }
457
534
 
458
535
 
459
536
  def _is_in_progress(error: BaseException) -> bool:
460
- """ECR answers a scan that has not finished as an error, not a status."""
461
- return _error_code(error) in {"ScanInProgressException", "LimitExceededException"}
537
+ """ECR answers a scan that has not finished as an error, not a status.
538
+
539
+ `ScanNotFoundException` is one of those answers: enhanced scanning starts
540
+ after the push, so an image pushed a moment ago has no scan yet (found
541
+ live on r1, 2026-09-14, the first build to read its image's scan rather
542
+ than its index's). It is waited for like a running scan, within the same
543
+ bound.
544
+ """
545
+ return _error_code(error) in {
546
+ "ScanInProgressException",
547
+ "ScanNotFoundException",
548
+ "LimitExceededException",
549
+ }
@@ -36,6 +36,7 @@ __all__ = [
36
36
  "BaseChannelUnpublishedError",
37
37
  "approved_base",
38
38
  "approved_repositories",
39
+ "channel_snapshot",
39
40
  "is_approved_repository",
40
41
  "resolve_base",
41
42
  ]
@@ -44,6 +45,8 @@ __all__ = [
44
45
  ECR_BASE_PREFIX = "environments/base/"
45
46
 
46
47
  _DIGEST = re.compile(r"sha256:[0-9a-f]{64}")
48
+ #: A snapshot.ubuntu.com id: the moment, to the second, in UTC.
49
+ _SNAPSHOT = re.compile(r"\d{8}T\d{6}Z")
47
50
 
48
51
 
49
52
  class ApprovedBase(BaseModel):
@@ -59,6 +62,11 @@ class ApprovedBase(BaseModel):
59
62
  #: Channel, then variant, to the digest resolution pins. A channel with no
60
63
  #: variant is approved and not yet published.
61
64
  channels: dict[str, dict[str, str]] = Field(default_factory=dict)
65
+ #: Channel to the Ubuntu snapshot its apt versions are pinned against
66
+ #: (D-9): a ``snapshot.ubuntu.com`` id, taken just after the channel's
67
+ #: image upgraded its packages, so no pin is older than what the image
68
+ #: already carries. A channel with none pins against the base's mirror.
69
+ snapshots: dict[str, str] = Field(default_factory=dict)
62
70
 
63
71
  @field_validator("channels")
64
72
  @classmethod
@@ -75,6 +83,17 @@ class ApprovedBase(BaseModel):
75
83
  for channel, digests in channels.items()
76
84
  }
77
85
 
86
+ @field_validator("snapshots")
87
+ @classmethod
88
+ def _snapshot_ids_only(cls, snapshots: dict[str, str]) -> dict[str, str]:
89
+ for channel, snapshot in snapshots.items():
90
+ if not _SNAPSHOT.fullmatch(snapshot):
91
+ raise ValueError(
92
+ f"channel {channel!r} pins apt to {snapshot!r}, "
93
+ "which is not a snapshot id like 20260914T150000Z"
94
+ )
95
+ return snapshots
96
+
78
97
  @property
79
98
  def name(self) -> str:
80
99
  """The base's name without its namespace: ``python-cpu``."""
@@ -103,6 +122,7 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
103
122
  "sha256:cd09308a0c5e5adeec7fb5d8d29cf7455e78ba86f5c6c095a79068a280e1254f",
104
123
  )
105
124
  },
125
+ snapshots={"2026.09": "20260914T150000Z"},
106
126
  ),
107
127
  # E2-17: jupyter-python-cuda plus the same layer.
108
128
  ApprovedBase(
@@ -180,6 +200,14 @@ def resolve_base(
180
200
  return digest
181
201
 
182
202
 
203
+ def channel_snapshot(
204
+ ref: str, channel: str, bases: dict[str, ApprovedBase] = APPROVED_BASES
205
+ ) -> str:
206
+ """The Ubuntu snapshot ``ref`` at ``channel`` pins apt against, or ``""`` for none (D-9)."""
207
+ base = bases.get(ref)
208
+ return "" if base is None else base.snapshots.get(channel, "")
209
+
210
+
183
211
  def approved_repositories(bases: dict[str, ApprovedBase] = APPROVED_BASES) -> tuple[str, ...]:
184
212
  """Every repository name an approved base is published under."""
185
213
  names: list[str] = []