code-sandboxes 1.9.4__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.4 → code_sandboxes-1.9.5}/CHANGELOG.md +34 -0
  2. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/PKG-INFO +1 -1
  3. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/datalayer.py +14 -4
  5. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/attest.py +13 -3
  6. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/bases.py +28 -0
  7. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/image_import.py +89 -39
  8. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/policy.py +22 -1
  9. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/resolve.py +108 -22
  10. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_attest.py +48 -0
  11. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_bases.py +22 -0
  12. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_datalayer_builder.py +11 -0
  13. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_image_import.py +85 -0
  14. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_resolve.py +101 -6
  15. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/build.yml +0 -0
  16. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/environments-live.yml +0 -0
  17. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/py-code-style.yml +0 -0
  18. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/py-tests.yml +0 -0
  19. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/py-typing.yml +0 -0
  20. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/reusable-python.yml +0 -0
  21. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.gitignore +0 -0
  22. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.licenserc.yaml +0 -0
  23. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.pre-commit-config.yaml +0 -0
  24. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.vscode/settings.json +0 -0
  25. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/Dockerfile +0 -0
  26. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/LICENSE +0 -0
  27. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/Makefile +0 -0
  28. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/README.md +0 -0
  29. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/build.log +0 -0
  30. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/__init__.py +0 -0
  31. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/base.py +0 -0
  32. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/bridge_mount.py +0 -0
  33. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/builds.py +0 -0
  34. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/cli.py +0 -0
  35. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/client.py +0 -0
  36. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/cloudflare_sandbox.py +0 -0
  37. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/commands.py +0 -0
  38. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/console.py +0 -0
  39. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/contents.py +0 -0
  40. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/coreweave_sandbox.py +0 -0
  41. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/datalayer_sandbox.py +0 -0
  42. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/daytona_sandbox.py +0 -0
  43. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/docker_sandbox.py +0 -0
  44. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/e2b_sandbox.py +0 -0
  45. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/__init__.py +0 -0
  46. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/__main__.py +0 -0
  47. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/accounts.py +0 -0
  48. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/__init__.py +0 -0
  49. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/daytona.py +0 -0
  50. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/e2b.py +0 -0
  51. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/managed.py +0 -0
  52. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/modal.py +0 -0
  53. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/breaker.py +0 -0
  54. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/build_secrets.py +0 -0
  55. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/builders.py +0 -0
  56. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/canonical.py +0 -0
  57. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/conformance.py +0 -0
  58. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  59. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  60. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  61. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/contract.py +0 -0
  62. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/__init__.py +0 -0
  63. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/build.py +0 -0
  64. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  65. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  66. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/errors.py +0 -0
  67. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/files.py +0 -0
  68. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/lifecycle.py +0 -0
  69. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/redact.py +0 -0
  70. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/schema.py +0 -0
  71. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/spec.py +0 -0
  72. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/eval_sandbox.py +0 -0
  73. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/exceptions.py +0 -0
  74. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/filesystem.py +0 -0
  75. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/google_colab.py +0 -0
  76. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/google_colab_sandbox.py +0 -0
  77. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/interfaces.py +0 -0
  78. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_ingress.py +0 -0
  79. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  80. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle.py +0 -0
  81. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_execute.py +0 -0
  82. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_live.py +0 -0
  83. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_sandbox.py +0 -0
  84. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/lifecycle.py +0 -0
  85. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/manage.py +0 -0
  86. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/modal_sandbox.py +0 -0
  87. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/models.py +0 -0
  88. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/monty_sandbox.py +0 -0
  89. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/provider_ingress.py +0 -0
  90. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/providers.py +0 -0
  91. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docker/Dockerfile +0 -0
  92. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/.gitignore +0 -0
  93. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/LICENSE +0 -0
  94. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/Makefile +0 -0
  95. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/README.md +0 -0
  96. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/babel.config.js +0 -0
  97. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/api-reference/_category_.yml +0 -0
  98. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/api-reference/index.mdx +0 -0
  99. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/cli/_category_.yml +0 -0
  100. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/cli/index.mdx +0 -0
  101. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/contribute/_category_.yml +0 -0
  102. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/contribute/index.mdx +0 -0
  103. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/_category_.yml +0 -0
  104. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/contract.mdx +0 -0
  105. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/index.mdx +0 -0
  106. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/specification.mdx +0 -0
  107. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/spike-2026-09.mdx +0 -0
  108. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/examples/_category_.yml +0 -0
  109. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/examples/index.mdx +0 -0
  110. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/guide/_category_.yml +0 -0
  111. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/guide/index.mdx +0 -0
  112. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/index.mdx +0 -0
  113. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/install/_category_.yml +0 -0
  114. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/install/index.mdx +0 -0
  115. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/provider-ingress.mdx +0 -0
  116. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/_category_.yml +0 -0
  117. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/cloudflare.mdx +0 -0
  118. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/coreweave.mdx +0 -0
  119. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/datalayer.mdx +0 -0
  120. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/daytona.mdx +0 -0
  121. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/docker.mdx +0 -0
  122. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/e2b.mdx +0 -0
  123. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/eval.mdx +0 -0
  124. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/google-colab.mdx +0 -0
  125. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/index.mdx +0 -0
  126. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/jupyter-server.mdx +0 -0
  127. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/kaggle.mdx +0 -0
  128. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/modal.mdx +0 -0
  129. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/monty.mdx +0 -0
  130. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docusaurus.config.js +0 -0
  131. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/package.json +0 -0
  132. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/sidebars.js +0 -0
  133. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.js +0 -0
  134. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.module.css +0 -0
  135. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.js +0 -0
  136. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.module.css +0 -0
  137. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.js +0 -0
  138. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.module.css +0 -0
  139. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/css/custom.css +0 -0
  140. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/pages/index.module.css +0 -0
  141. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/pages/markdown-page.md +0 -0
  142. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/theme/CustomDocItem.tsx +0 -0
  143. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/theme/IconsTitle.tsx +0 -0
  144. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/bricks.svg +0 -0
  145. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/building-construction.svg +0 -0
  146. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/cloud-sun.svg +0 -0
  147. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/cloud.svg +0 -0
  148. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/collaboration.svg +0 -0
  149. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/copy-left.svg +0 -0
  150. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.png +0 -0
  151. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.svg +0 -0
  152. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/business.svg +0 -0
  153. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  154. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/devops.svg +0 -0
  155. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/project/project-line.svg +0 -0
  156. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/favicon.ico +0 -0
  157. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/jupyter.svg +0 -0
  158. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/open-source.svg +0 -0
  159. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/react-js.svg +0 -0
  160. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/react-js2.svg +0 -0
  161. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/ringed-planet.svg +0 -0
  162. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/robot.svg +0 -0
  163. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/rocket.svg +0 -0
  164. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/shield.svg +0 -0
  165. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/sparkles.svg +0 -0
  166. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/streamlit.gif +0 -0
  167. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/super-hero.svg +0 -0
  168. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/target.svg +0 -0
  169. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/thumbs-up.svg +0 -0
  170. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/README.md +0 -0
  171. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/Makefile +0 -0
  172. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/cloudflare_sandbox_example.py +0 -0
  173. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/coreweave_sandbox_example.py +0 -0
  174. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/datalayer_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/daytona_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/docker_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/e2b_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/eval_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/google_colab_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/kaggle_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/modal_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/monty_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/Makefile +0 -0
  185. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/cloudflare_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/coreweave_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/datalayer_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/daytona_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/docker_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/e2b_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/eval_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/google_colab_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/kaggle_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/modal_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/monty_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/pyproject.toml +0 -0
  198. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/schemas/environment-v1alpha1.json +0 -0
  199. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/__init__.py +0 -0
  200. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/conftest.py +0 -0
  201. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  202. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_bridge_mount.py +0 -0
  203. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_builds.py +0 -0
  204. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_cli_exec.py +0 -0
  205. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_cli_repl.py +0 -0
  206. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client.py +0 -0
  207. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client_contents.py +0 -0
  208. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client_files.py +0 -0
  209. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client_outcome_error_output.py +0 -0
  210. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_cloudflare.py +0 -0
  211. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_commands_envelope.py +0 -0
  212. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_console.py +0 -0
  213. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_contents_contract.py +0 -0
  214. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_contents_live_matrix.py +0 -0
  215. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_coreweave.py +0 -0
  216. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_datalayer_sandbox.py +0 -0
  217. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_daytona.py +0 -0
  218. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_e2b.py +0 -0
  219. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_accounts.py +0 -0
  220. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_breaker.py +0 -0
  221. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_build_secrets.py +0 -0
  222. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_builders.py +0 -0
  223. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_canonical.py +0 -0
  224. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_conformance.py +0 -0
  225. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_constraints.py +0 -0
  226. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_contract.py +0 -0
  227. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_cross_variant_report.py +0 -0
  228. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_daytona_builder.py +0 -0
  229. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_doctor.py +0 -0
  230. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_e2b_builder.py +0 -0
  231. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_errors.py +0 -0
  232. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_lifecycle.py +0 -0
  233. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_live_matrix.py +0 -0
  234. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_managed_builders.py +0 -0
  235. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_modal_builder.py +0 -0
  236. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_redact.py +0 -0
  237. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_spec.py +0 -0
  238. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_eval.py +0 -0
  239. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  240. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_factory.py +0 -0
  241. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_google_colab.py +0 -0
  242. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_integration.py +0 -0
  243. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_jupyter_ingress.py +0 -0
  244. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_jupyter_server.py +0 -0
  245. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_jupyter_server_streaming.py +0 -0
  246. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle.py +0 -0
  247. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle_execute.py +0 -0
  248. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle_live.py +0 -0
  249. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle_session.py +0 -0
  250. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kernel_client_compatibility.py +0 -0
  251. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_lifecycle_vocabulary.py +0 -0
  252. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_manage.py +0 -0
  253. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_manage_datalayer.py +0 -0
  254. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_modal_google_colab_sandbox.py +0 -0
  255. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_modal_session.py +0 -0
  256. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_models.py +0 -0
  257. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_output_line_fidelity.py +0 -0
  258. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_provider_catalog_names.py +0 -0
  259. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_provider_ingress.py +0 -0
  260. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_providers.py +0 -0
  261. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_sandboxes.py +0 -0
  262. {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -8,6 +8,40 @@
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
+
11
45
  ## 1.9.4
12
46
 
13
47
  - **A pushed Environment image is decided by its linux/amd64 image's scan**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.4
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.4"
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
  )
@@ -530,10 +530,20 @@ def _is_missing(error: BaseException) -> bool:
530
530
  return _error_code(error) in {
531
531
  "ImageNotFoundException",
532
532
  "RepositoryNotFoundException",
533
- "ScanNotFoundException",
534
533
  }
535
534
 
536
535
 
537
536
  def _is_in_progress(error: BaseException) -> bool:
538
- """ECR answers a scan that has not finished as an error, not a status."""
539
- 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] = []
@@ -13,20 +13,26 @@ path. Nothing downstream needs to know a build started from an import rather
13
13
  than an approved base; the only difference is where the `FROM` digest comes
14
14
  from.
15
15
 
16
- **Public registries only, for now.** A per-organization allowlist and
17
- credentials for a private registry are the rest of this box (E3-04's second
18
- half), left for E3-06's organization policy document to hold — the
19
- `policy.py` docstring already says as much about the scan threshold, and a
20
- registry allowlist is the same shape of decision. `DEFAULT_ALLOWED_REGISTRIES`
21
- below is a bootstrap standing in for that document before it exists, not a
22
- policy of its own: when E3-06 lands, an organization's own list replaces this
23
- one rather than adding to it.
16
+ **A public registry needs no credential; a private one names its own**
17
+ (E3-04's second half). `credentialSecretId` on the spec (the same `dlsec_...`
18
+ shape a `BuildSecret` uses) is resolved through the same IAM route a build
19
+ secret is (E3-05) — the value is fetched once, at the moment the digest is
20
+ resolved, and handed to the registry's own token exchange as HTTP Basic,
21
+ never logged, never stored, never sent to the registry named in the
22
+ reference. A per-organization allowlist is still E3-06's own organization
23
+ policy document to hold; `DEFAULT_ALLOWED_REGISTRIES` below is a bootstrap
24
+ standing in for it before it exists, not a policy of its own — a registry
25
+ outside this list is refused unless a credential was referenced for it, on
26
+ the read that naming one is a deliberate private registry rather than a
27
+ mistake. When E3-06 lands, an organization's own list replaces this one
28
+ rather than adding to it.
24
29
 
25
30
  @module code_sandboxes.environments.image_import
26
31
  """
27
32
 
28
33
  from __future__ import annotations
29
34
 
35
+ import base64
30
36
  import hashlib
31
37
  import re
32
38
  from dataclasses import dataclass
@@ -175,10 +181,11 @@ def refuse_unless_allowed(
175
181
  A registry named alongside `credentialSecretId` (E3-04's private half) is
176
182
  presumed a deliberate private one, its governance E3-06's own allowlist
177
183
  to hold once it exists — this bootstrap list is only ever the *public*
178
- default, never the only door. What resolving that credential into a real
179
- one to pull with is E3-05's mechanism, not built for anything yet; a spec
180
- that clears this check but has no real credential fails honestly at the
181
- registry instead, the same as an approved base with no digest published.
184
+ default, never the only door. The credential itself is resolved and used
185
+ at `resolve_image_digest` (E3-05's mechanism); a spec that clears this
186
+ check but names a secret IAM will not give, or that the registry refuses,
187
+ fails honestly there instead, the same as an approved base with no digest
188
+ published.
182
189
  """
183
190
  if has_credential or image_registry_allowed(image, allowlist):
184
191
  return
@@ -199,21 +206,68 @@ def _challenge(header: str) -> dict[str, str]:
199
206
  return dict(_CHALLENGE_FIELD.findall(header or ""))
200
207
 
201
208
 
209
+ def _token_for_challenge(client: Any, header: str, *, registry: str, credential: str | None) -> str:
210
+ """The pull token a `WWW-Authenticate: Bearer ...` challenge answers with.
211
+
212
+ With a `credential` (`username:password`), sent as HTTP Basic on this
213
+ request alone — exactly what `docker login` sends here — so the realm
214
+ mints a token scoped to what that credential can pull; with none, the
215
+ same request answers with a public, anonymous-scope token. Either way,
216
+ the credential itself never reaches the registry named in the reference,
217
+ only this one realm, and only as this one request's header.
218
+ """
219
+ challenge = _challenge(header)
220
+ realm = challenge.get("realm")
221
+ if not realm:
222
+ raise EnvironmentsError(
223
+ PROVIDER_ERROR,
224
+ f"{registry} refused a manifest request with no way to ask for a token",
225
+ detail={"registry": registry, "status": 401},
226
+ )
227
+ headers = {}
228
+ if credential:
229
+ encoded = base64.b64encode(credential.encode("utf-8")).decode("ascii")
230
+ headers["Authorization"] = f"Basic {encoded}"
231
+ response = client.get(
232
+ realm,
233
+ params={
234
+ key: value
235
+ for key, value in (
236
+ ("service", challenge.get("service")),
237
+ ("scope", challenge.get("scope")),
238
+ )
239
+ if value
240
+ },
241
+ headers=headers,
242
+ )
243
+ response.raise_for_status()
244
+ return str(response.json().get("token") or "")
245
+
246
+
202
247
  def resolve_image_digest(
203
248
  image: ImageReference,
204
249
  *,
205
250
  transport: Any = None,
206
251
  timeout: float = 10.0,
252
+ credential: str | None = None,
207
253
  ) -> str:
208
- """The reference's digest, over the registry's own v2 API — no credential,
209
- since only a public registry reaches this (§ this module's own docstring).
254
+ """The reference's digest, over the registry's own v2 API.
210
255
 
211
256
  Already pinned by digest, this is free: an operator who wrote `@sha256:…`
212
257
  meant exactly that image, and nothing is asked of the registry. A tag is
213
- resolved with one anonymous manifest request, following the registry's own
214
- `WWW-Authenticate` challenge for the one-time pull token every public
215
- registry answers with no credential (D-9's base digests are pinned the
216
- same way, just from a table Datalayer keeps instead of asked live).
258
+ resolved with one manifest request, following the registry's own
259
+ `WWW-Authenticate` challenge for the pull token every registry implementing
260
+ the distribution spec answers with (D-9's base digests are pinned the same
261
+ way, just from a table Datalayer keeps instead of asked live).
262
+
263
+ With no `credential`, the challenge is answered anonymously — the only
264
+ path a public registry needs. With one, `username:password` (E3-04's
265
+ private half), it is sent as HTTP Basic on the token request the challenge
266
+ names, exactly what `docker login` sends there: the registry mints a
267
+ token scoped to what that credential can pull, private repositories
268
+ included, rather than the public-only anonymous one. The credential itself
269
+ is never sent to the registry named in the reference, and never appears in
270
+ a log line or an exception message — only the token that answers it.
217
271
  """
218
272
  if image.pinned:
219
273
  return image.digest
@@ -234,28 +288,12 @@ def resolve_image_digest(
234
288
  try:
235
289
  response = client.get(url, headers=headers)
236
290
  if response.status_code == 401:
237
- challenge = _challenge(response.headers.get("www-authenticate", ""))
238
- realm = challenge.get("realm")
239
- if not realm:
240
- raise EnvironmentsError(
241
- PROVIDER_ERROR,
242
- f"{image.registry} refused an anonymous manifest request with no "
243
- "way to ask for a token",
244
- detail={"registry": image.registry, "status": 401},
245
- )
246
- token_response = client.get(
247
- realm,
248
- params={
249
- key: value
250
- for key, value in (
251
- ("service", challenge.get("service")),
252
- ("scope", challenge.get("scope")),
253
- )
254
- if value
255
- },
291
+ token = _token_for_challenge(
292
+ client,
293
+ response.headers.get("www-authenticate", ""),
294
+ registry=image.registry,
295
+ credential=credential,
256
296
  )
257
- token_response.raise_for_status()
258
- token = str(token_response.json().get("token") or "")
259
297
  response = client.get(url, headers={**headers, "Authorization": f"Bearer {token}"})
260
298
  except httpx.HTTPError as error:
261
299
  raise EnvironmentsError(
@@ -263,6 +301,18 @@ def resolve_image_digest(
263
301
  f"{image.registry} could not be reached to resolve {image}: {error}",
264
302
  detail={"registry": image.registry, "reference": str(image)},
265
303
  ) from error
304
+ if response.status_code in (401, 403):
305
+ raise EnvironmentsError(
306
+ PROVIDER_ERROR,
307
+ f"{image.registry} refused the credential for {image}"
308
+ if credential
309
+ else f"{image} needs a credential to resolve at {image.registry}",
310
+ detail={
311
+ "registry": image.registry,
312
+ "reference": str(image),
313
+ "status": response.status_code,
314
+ },
315
+ )
266
316
  if response.status_code == 404:
267
317
  raise EnvironmentsError(
268
318
  PROVIDER_ERROR,
@@ -129,8 +129,29 @@ class ScanPolicy:
129
129
  }
130
130
 
131
131
 
132
+ #: Advisories allowed on the Datalayer base channel itself (D-9), reviewed and
133
+ #: accepted 2026-09-14 rather than silently missed: all five are in `ffmpeg`
134
+ #: (`CVE-2024-35366`, `CVE-2024-35367`, `CVE-2024-35368`, `CVE-2026-40962`) and
135
+ #: `libcjson1` (`CVE-2025-57052`, pulled in only by `librist4`, itself only
136
+ #: needed by `ffmpeg`), on `datalayer/python-cpu:2026.09`. Amazon Inspector
137
+ #: reports each as fixable, but the fix is an Ubuntu ESM (Extended Security
138
+ #: Maintenance) package version — `7:6.1.1-3ubuntu5+esm5` and the like — not
139
+ #: reachable by a plain `apt-get upgrade`, only by a Pro subscription token
140
+ #: this deployment does not hold. `ffmpeg` has no other installed package
141
+ #: depending on it (`apt-cache rdepends --installed`), so it is kept for what
142
+ #: it is: an image-wide convenience (matplotlib's `FFMpegWriter` and the like),
143
+ #: not something the sandbox contract's own doctor checks for. Revisit once
144
+ #: Ubuntu ships a non-ESM fix, or the channel drops `ffmpeg`.
145
+ _BASE_CHANNEL_ALLOWED: tuple[str, ...] = (
146
+ "CVE-2024-35366",
147
+ "CVE-2024-35367",
148
+ "CVE-2024-35368",
149
+ "CVE-2026-40962",
150
+ "CVE-2025-57052",
151
+ )
152
+
132
153
  #: What every owner is scanned against until an organization says otherwise.
133
- DEFAULT_POLICY = ScanPolicy()
154
+ DEFAULT_POLICY = ScanPolicy(allowed=_BASE_CHANNEL_ALLOWED)
134
155
 
135
156
 
136
157
  @dataclass(frozen=True)
@@ -51,7 +51,8 @@ from functools import lru_cache
51
51
  from pathlib import Path
52
52
  from typing import Any, Callable, Protocol
53
53
 
54
- from .bases import APPROVED_BASES, ApprovedBase, resolve_base
54
+ from .bases import APPROVED_BASES, ApprovedBase, channel_snapshot, resolve_base
55
+ from .build_secrets import resolve_build_secret
55
56
  from .errors import (
56
57
  CAPABILITY_UNSUPPORTED,
57
58
  PACKAGE_NOT_FOUND,
@@ -67,10 +68,17 @@ from .image_import import (
67
68
  refuse_unless_allowed,
68
69
  resolve_image_digest,
69
70
  )
70
- from .spec import DependencyFileSpec, Environment, parse_environment, parse_requirements_txt
71
+ from .spec import (
72
+ BuildSecret,
73
+ DependencyFileSpec,
74
+ Environment,
75
+ parse_environment,
76
+ parse_requirements_txt,
77
+ )
71
78
 
72
79
  __all__ = [
73
80
  "APT_PIN_PREFIX",
81
+ "APT_SNAPSHOT_PREFIX",
74
82
  "CONSTRAINTS_PATH",
75
83
  "LOCK_FORMAT",
76
84
  "PROTECTED_PIN_PREFIX",
@@ -83,6 +91,7 @@ __all__ = [
83
91
  "ResolveRunner",
84
92
  "apt_pins",
85
93
  "apt_pins_in",
94
+ "apt_snapshot_in",
86
95
  "lock_document",
87
96
  "locked_versions",
88
97
  "merge_requirements",
@@ -117,6 +126,11 @@ WHEELHOUSE_PATH = Path(__file__).parent / "constraints" / "wheelhouse"
117
126
  #: How an apt pin is written in the lock. A comment, so every reader of a
118
127
  #: ``pip`` requirements file — the CLI's diff included — ignores it.
119
128
  APT_PIN_PREFIX = "# datalayer-apt: "
129
+ #: The Ubuntu snapshot the apt pins were taken from, as the lock records it.
130
+ #: The builder installs the pins from the same snapshot, since a pinned
131
+ #: version can leave the live mirror (D-9).
132
+ APT_SNAPSHOT_PREFIX = "# datalayer-apt-snapshot: "
133
+ _SNAPSHOT_ID = re.compile(r"^\d{8}T\d{6}Z$")
120
134
 
121
135
  #: How a protected pin is recorded in the lock.
122
136
  PROTECTED_PIN_PREFIX = "# datalayer-protected: "
@@ -315,6 +329,11 @@ class ResolveRequest:
315
329
  already has it baked in (E1-05); an imported image is somebody else's,
316
330
  and cannot be assumed to."""
317
331
 
332
+ apt_snapshot: str = ""
333
+ """The Ubuntu snapshot apt versions are pinned against, as
334
+ ``20260914T150000Z``: the base channel's (D-9). Empty pins against the
335
+ mirror the base names."""
336
+
318
337
 
319
338
  @dataclass
320
339
  class ResolveOutcome:
@@ -325,6 +344,9 @@ class ResolveOutcome:
325
344
  apt_source: str = ""
326
345
  """The mirror the apt versions were pinned against, for the lock's header."""
327
346
 
347
+ apt_snapshot: str = ""
348
+ """The snapshot id they were pinned against, which the builder installs from."""
349
+
328
350
 
329
351
  class ResolveRunner(Protocol):
330
352
  """Where a solve runs."""
@@ -599,6 +621,12 @@ class BuildkitResolveRunner:
599
621
  self._tlskey = tlskey or ""
600
622
  self._tlscacert = tlscacert or ""
601
623
  self._address = address or ""
624
+ if apt_snapshot and not _SNAPSHOT_ID.match(apt_snapshot):
625
+ raise ValueError(
626
+ f"apt_snapshot must be a snapshot.ubuntu.com id like 20260914T150000Z, "
627
+ f"not {apt_snapshot!r}"
628
+ )
629
+ #: The deployment's override of the base channel's snapshot.
602
630
  self._apt_snapshot = apt_snapshot
603
631
  self._timeout = timeout
604
632
 
@@ -648,18 +676,15 @@ class BuildkitResolveRunner:
648
676
  if request.apt:
649
677
  # `--simulate` names every package apt would install, transitive
650
678
  # ones included, each with the exact version the mirror serves at
651
- # the channel's date (D-9).
652
- snapshot = self._apt_snapshot
653
- if snapshot:
654
- codename = "$(. /etc/os-release; echo $VERSION_CODENAME)"
655
- lines.append(
656
- f"RUN printf 'deb {snapshot} {codename} main\\n' "
657
- "> /etc/apt/sources.list.d/datalayer-snapshot.list"
658
- )
679
+ # the channel's date (D-9). `--snapshot` points every suite and
680
+ # component the base's sources name at snapshot.ubuntu.com at that
681
+ # moment. The `deb` line this used to add named one component,
682
+ # `main`, which `gdal-bin` is not in, and left the live mirror
683
+ # enabled beside it (found 2026-09-14).
684
+ option = self._snapshot_option(request)
659
685
  lines.append(
660
- "RUN apt-get update -qq && apt-get install --simulate --no-install-recommends "
661
- + " ".join(request.apt)
662
- + " > /solve/apt.txt"
686
+ f"RUN apt-get update -qq{option} && apt-get install --simulate "
687
+ f"--no-install-recommends{option} " + " ".join(request.apt) + " > /solve/apt.txt"
663
688
  )
664
689
  lines.extend(
665
690
  [
@@ -671,6 +696,14 @@ class BuildkitResolveRunner:
671
696
  lines.append("COPY --from=solve /solve/apt.txt /apt.txt")
672
697
  return "\n".join(lines) + "\n"
673
698
 
699
+ def _snapshot(self, request: ResolveRequest) -> str:
700
+ """The deployment's override, else the base channel's snapshot."""
701
+ return self._apt_snapshot or request.apt_snapshot
702
+
703
+ def _snapshot_option(self, request: ResolveRequest) -> str:
704
+ snapshot = self._snapshot(request)
705
+ return f" --snapshot {snapshot}" if snapshot else ""
706
+
674
707
  def solve(
675
708
  self, request: ResolveRequest, log: Callable[[str], None] | None = None
676
709
  ) -> ResolveOutcome:
@@ -737,12 +770,19 @@ class BuildkitResolveRunner:
737
770
  if finished.returncode != 0:
738
771
  raise parse_resolver_failure(finished.stderr or finished.stdout or "")
739
772
  lock = (out / "lock.txt").read_text(encoding="utf-8")
740
- pins, source = {}, ""
773
+ pins, source, snapshot = {}, "", ""
741
774
  apt_file = out / "apt.txt"
742
775
  if request.apt and apt_file.exists():
743
776
  pins = apt_pins(apt_file.read_text(encoding="utf-8"))
744
- source = self._apt_snapshot or "the base channel's mirror"
745
- return ResolveOutcome(lock_text=lock, apt_pins=pins, apt_source=source)
777
+ snapshot = self._snapshot(request)
778
+ source = (
779
+ f"https://snapshot.ubuntu.com/ubuntu/{snapshot}"
780
+ if snapshot
781
+ else "the base channel's mirror"
782
+ )
783
+ return ResolveOutcome(
784
+ lock_text=lock, apt_pins=pins, apt_source=source, apt_snapshot=snapshot
785
+ )
746
786
 
747
787
  def _environment(self, request: ResolveRequest) -> dict[str, str] | None:
748
788
  """The registry credential, as client-side auth: `buildkitd` holds none (D-17)."""
@@ -830,6 +870,16 @@ def apt_pins_in(lock_text: str) -> dict[str, str]:
830
870
  return pins
831
871
 
832
872
 
873
+ def apt_snapshot_in(lock_text: str) -> str:
874
+ """The Ubuntu snapshot a lock's apt pins were taken from, or ``""`` (D-9)."""
875
+ for raw in lock_text.splitlines():
876
+ line = raw.strip()
877
+ if line.startswith(APT_SNAPSHOT_PREFIX.strip()):
878
+ snapshot = line[len(APT_SNAPSHOT_PREFIX.strip()) :].strip()
879
+ return snapshot if _SNAPSHOT_ID.match(snapshot) else ""
880
+ return ""
881
+
882
+
833
883
  def lock_document(
834
884
  outcome: ResolveOutcome,
835
885
  *,
@@ -856,6 +906,8 @@ def lock_document(
856
906
  header.append(f"{APT_PIN_PREFIX}{pin[0]}={pin[1]}")
857
907
  if outcome.apt_pins and outcome.apt_source:
858
908
  header.append(f"# datalayer-apt-source: {outcome.apt_source}")
909
+ if outcome.apt_pins and outcome.apt_snapshot:
910
+ header.append(f"{APT_SNAPSHOT_PREFIX}{outcome.apt_snapshot}")
859
911
  for constraint in merged.constraints:
860
912
  header.append(f"{PROTECTED_PIN_PREFIX}{constraint}")
861
913
  text = "\n".join(header) + "\n" + outcome.lock_text.lstrip("\n")
@@ -904,6 +956,8 @@ def resolve_image_base(
904
956
  *,
905
957
  allowlist: tuple[str, ...] = DEFAULT_ALLOWED_REGISTRIES,
906
958
  transport: Any = None,
959
+ owner_uid: str = "",
960
+ resolve_secret: Callable[..., str] = resolve_build_secret,
907
961
  ) -> dict[str, str]:
908
962
  """Every wanted variant's base, from an imported image rather than an approved one (E3-04).
909
963
 
@@ -911,10 +965,18 @@ def resolve_image_base(
911
965
  never a per-variant table the way an approved base's channel is, and
912
966
  every variant this phase builds for is `linux/amd64` regardless (D-9's
913
967
  own bases are single-arch too). Only a registry in ``allowlist`` is ever
914
- resolved — `spec_findings` already refuses the rest before a build is
915
- asked for; this is the same rule kept here for whatever resolves directly
916
- without validating first, so nothing that reaches the network was never
917
- checked.
968
+ resolved, unless the spec names a private one's credential — the same
969
+ rule `spec_findings` already refuses the rest by, kept here too for
970
+ whatever resolves directly without validating first, so nothing that
971
+ reaches the network was never checked.
972
+
973
+ A private registry (``image.credential_secret_id`` set, E3-04's second
974
+ half) has its credential fetched from IAM through `resolve_secret` — the
975
+ same seam a build secret is resolved through (E3-05), since this is the
976
+ same mechanism: an id a spec names, a value fetched only at the moment
977
+ it is used, and never returned, logged, or stored anywhere past this call.
978
+ ``owner_uid`` is required to fetch one; resolving an uncredentialed image
979
+ needs neither.
918
980
  """
919
981
  image = environment.spec.build.image
920
982
  if image is None or not image.reference.strip():
@@ -925,7 +987,11 @@ def resolve_image_base(
925
987
  )
926
988
  parsed = parse_image_reference(image.reference)
927
989
  refuse_unless_allowed(parsed, allowlist, has_credential=bool(image.credential_secret_id))
928
- digest = resolve_image_digest(parsed, transport=transport)
990
+ credential = None
991
+ if image.credential_secret_id:
992
+ secret = BuildSecret(id=image.credential_secret_id, name="registry-credential")
993
+ credential = resolve_secret(secret, owner_uid=owner_uid)
994
+ digest = resolve_image_digest(parsed, transport=transport, credential=credential)
929
995
  reference = f"{parsed.registry}/{parsed.repository}@{digest}"
930
996
  return dict.fromkeys(variants, reference)
931
997
 
@@ -1097,6 +1163,8 @@ def resolve_environment(
1097
1163
  uv: str | None = None,
1098
1164
  pyproject_run: Callable[..., subprocess.CompletedProcess[str]] | None = None,
1099
1165
  image_transport: Any = None,
1166
+ owner_uid: str = "",
1167
+ resolve_secret: Callable[..., str] = resolve_build_secret,
1100
1168
  ) -> dict[str, Any]:
1101
1169
  """A version's lock, and the base each variant builds from.
1102
1170
 
@@ -1132,6 +1200,12 @@ def resolve_environment(
1132
1200
  the registry request runs over — injected by tests
1133
1201
  (`httpx.MockTransport`), the real network otherwise. Unused by every
1134
1202
  other source.
1203
+ owner_uid, resolve_secret
1204
+ An `image` source naming a private registry's `credentialSecretId`
1205
+ (E3-04's second half): the owner the credential belongs to, and how
1206
+ its value is fetched — IAM by default, the same seam a build secret
1207
+ (E3-05) is resolved through. Unused by a public registry or any
1208
+ other source.
1135
1209
 
1136
1210
  Returns
1137
1211
  -------
@@ -1166,7 +1240,13 @@ def resolve_environment(
1166
1240
  )
1167
1241
  wanted = sorted({str(variant) for variant in variants} or {"datalayer"})
1168
1242
  resolved_bases = (
1169
- resolve_image_base(environment, wanted, transport=image_transport)
1243
+ resolve_image_base(
1244
+ environment,
1245
+ wanted,
1246
+ transport=image_transport,
1247
+ owner_uid=owner_uid,
1248
+ resolve_secret=resolve_secret,
1249
+ )
1170
1250
  if source == "image"
1171
1251
  else resolve_bases(environment, wanted, bases, registry=_registry_of(credential))
1172
1252
  )
@@ -1204,6 +1284,12 @@ def resolve_environment(
1204
1284
  base_reference=solving_in,
1205
1285
  registry_auth=_registry_auth(credential),
1206
1286
  bootstrap_uv=(source == "image"),
1287
+ # An imported image is not an approved base, and names no channel.
1288
+ apt_snapshot=(
1289
+ ""
1290
+ if source == "image"
1291
+ else channel_snapshot(environment.spec.base.ref, environment.spec.base.channel, bases)
1292
+ ),
1207
1293
  )
1208
1294
  outcome = (runner or BuildkitResolveRunner()).solve(request, say)
1209
1295
  document = lock_document(