code-sandboxes 1.9.4__tar.gz → 1.9.6__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.6}/CHANGELOG.md +42 -0
  2. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/PKG-INFO +1 -1
  3. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/__version__.py +1 -1
  4. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/adapters/datalayer.py +14 -4
  5. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/attest.py +13 -3
  6. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/bases.py +38 -4
  7. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/image_import.py +89 -39
  8. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/policy.py +22 -1
  9. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/resolve.py +108 -22
  10. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_attest.py +48 -0
  11. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_bases.py +25 -2
  12. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_datalayer_builder.py +11 -0
  13. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_image_import.py +85 -0
  14. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_resolve.py +101 -6
  15. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.github/workflows/build.yml +0 -0
  16. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.github/workflows/environments-live.yml +0 -0
  17. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.github/workflows/py-code-style.yml +0 -0
  18. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.github/workflows/py-tests.yml +0 -0
  19. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.github/workflows/py-typing.yml +0 -0
  20. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.github/workflows/reusable-python.yml +0 -0
  21. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.gitignore +0 -0
  22. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.licenserc.yaml +0 -0
  23. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.pre-commit-config.yaml +0 -0
  24. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/.vscode/settings.json +0 -0
  25. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/Dockerfile +0 -0
  26. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/LICENSE +0 -0
  27. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/Makefile +0 -0
  28. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/README.md +0 -0
  29. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/build.log +0 -0
  30. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/__init__.py +0 -0
  31. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/base.py +0 -0
  32. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/bridge_mount.py +0 -0
  33. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/builds.py +0 -0
  34. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/cli.py +0 -0
  35. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/client.py +0 -0
  36. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/cloudflare_sandbox.py +0 -0
  37. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/commands.py +0 -0
  38. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/console.py +0 -0
  39. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/contents.py +0 -0
  40. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/coreweave_sandbox.py +0 -0
  41. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/datalayer_sandbox.py +0 -0
  42. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/daytona_sandbox.py +0 -0
  43. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/docker_sandbox.py +0 -0
  44. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/e2b_sandbox.py +0 -0
  45. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/__init__.py +0 -0
  46. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/__main__.py +0 -0
  47. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/accounts.py +0 -0
  48. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/adapters/__init__.py +0 -0
  49. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/adapters/daytona.py +0 -0
  50. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/adapters/e2b.py +0 -0
  51. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/adapters/managed.py +0 -0
  52. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/adapters/modal.py +0 -0
  53. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/breaker.py +0 -0
  54. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/build_secrets.py +0 -0
  55. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/builders.py +0 -0
  56. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/canonical.py +0 -0
  57. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/conformance.py +0 -0
  58. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  59. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  60. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/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.6}/code_sandboxes/environments/contract.py +0 -0
  62. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/doctor/__init__.py +0 -0
  63. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/doctor/build.py +0 -0
  64. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  65. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  66. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/errors.py +0 -0
  67. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/files.py +0 -0
  68. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/lifecycle.py +0 -0
  69. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/redact.py +0 -0
  70. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/schema.py +0 -0
  71. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/environments/spec.py +0 -0
  72. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/eval_sandbox.py +0 -0
  73. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/exceptions.py +0 -0
  74. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/filesystem.py +0 -0
  75. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/google_colab.py +0 -0
  76. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/google_colab_sandbox.py +0 -0
  77. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/interfaces.py +0 -0
  78. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/jupyter_ingress.py +0 -0
  79. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  80. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/kaggle.py +0 -0
  81. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/kaggle_execute.py +0 -0
  82. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/kaggle_live.py +0 -0
  83. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/kaggle_sandbox.py +0 -0
  84. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/lifecycle.py +0 -0
  85. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/manage.py +0 -0
  86. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/modal_sandbox.py +0 -0
  87. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/models.py +0 -0
  88. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/monty_sandbox.py +0 -0
  89. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/provider_ingress.py +0 -0
  90. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/code_sandboxes/providers.py +0 -0
  91. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docker/Dockerfile +0 -0
  92. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/.gitignore +0 -0
  93. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/LICENSE +0 -0
  94. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/Makefile +0 -0
  95. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/README.md +0 -0
  96. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/babel.config.js +0 -0
  97. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/api-reference/_category_.yml +0 -0
  98. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/api-reference/index.mdx +0 -0
  99. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/cli/_category_.yml +0 -0
  100. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/cli/index.mdx +0 -0
  101. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/contribute/_category_.yml +0 -0
  102. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/contribute/index.mdx +0 -0
  103. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/environments/_category_.yml +0 -0
  104. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/environments/contract.mdx +0 -0
  105. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/environments/index.mdx +0 -0
  106. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/environments/specification.mdx +0 -0
  107. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/environments/spike-2026-09.mdx +0 -0
  108. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/examples/_category_.yml +0 -0
  109. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/examples/index.mdx +0 -0
  110. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/guide/_category_.yml +0 -0
  111. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/guide/index.mdx +0 -0
  112. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/index.mdx +0 -0
  113. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/install/_category_.yml +0 -0
  114. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/install/index.mdx +0 -0
  115. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/provider-ingress.mdx +0 -0
  116. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/_category_.yml +0 -0
  117. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/cloudflare.mdx +0 -0
  118. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/coreweave.mdx +0 -0
  119. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/datalayer.mdx +0 -0
  120. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/daytona.mdx +0 -0
  121. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/docker.mdx +0 -0
  122. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/e2b.mdx +0 -0
  123. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/eval.mdx +0 -0
  124. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/google-colab.mdx +0 -0
  125. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/index.mdx +0 -0
  126. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/jupyter-server.mdx +0 -0
  127. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/kaggle.mdx +0 -0
  128. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/modal.mdx +0 -0
  129. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docs/providers/monty.mdx +0 -0
  130. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/docusaurus.config.js +0 -0
  131. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/package.json +0 -0
  132. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/sidebars.js +0 -0
  133. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/components/HomepageRow1.js +0 -0
  134. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/components/HomepageRow1.module.css +0 -0
  135. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/components/HomepageRow2.js +0 -0
  136. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/components/HomepageRow2.module.css +0 -0
  137. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/components/HomepageRow3.js +0 -0
  138. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/components/HomepageRow3.module.css +0 -0
  139. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/css/custom.css +0 -0
  140. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/pages/index.module.css +0 -0
  141. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/pages/markdown-page.md +0 -0
  142. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/theme/CustomDocItem.tsx +0 -0
  143. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/src/theme/IconsTitle.tsx +0 -0
  144. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/bricks.svg +0 -0
  145. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/building-construction.svg +0 -0
  146. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/cloud-sun.svg +0 -0
  147. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/cloud.svg +0 -0
  148. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/collaboration.svg +0 -0
  149. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/copy-left.svg +0 -0
  150. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/datalayer/logo.png +0 -0
  151. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/datalayer/logo.svg +0 -0
  152. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/datalayer/personas/business.svg +0 -0
  153. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  154. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/datalayer/personas/devops.svg +0 -0
  155. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/datalayer/project/project-line.svg +0 -0
  156. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/favicon.ico +0 -0
  157. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/jupyter.svg +0 -0
  158. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/open-source.svg +0 -0
  159. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/react-js.svg +0 -0
  160. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/react-js2.svg +0 -0
  161. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/ringed-planet.svg +0 -0
  162. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/robot.svg +0 -0
  163. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/rocket.svg +0 -0
  164. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/shield.svg +0 -0
  165. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/sparkles.svg +0 -0
  166. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/streamlit.gif +0 -0
  167. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/super-hero.svg +0 -0
  168. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/target.svg +0 -0
  169. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/docs/static/img/thumbs-up.svg +0 -0
  170. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/README.md +0 -0
  171. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/Makefile +0 -0
  172. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/cloudflare_sandbox_example.py +0 -0
  173. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/coreweave_sandbox_example.py +0 -0
  174. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/datalayer_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/daytona_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/docker_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/e2b_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/eval_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/google_colab_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/kaggle_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/modal_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/exec/monty_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/Makefile +0 -0
  185. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/cloudflare_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/coreweave_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/datalayer_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/daytona_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/docker_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/e2b_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/eval_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/google_colab_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/kaggle_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/modal_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/examples/repl/monty_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/pyproject.toml +0 -0
  198. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/schemas/environment-v1alpha1.json +0 -0
  199. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/__init__.py +0 -0
  200. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/conftest.py +0 -0
  201. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  202. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_bridge_mount.py +0 -0
  203. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_builds.py +0 -0
  204. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_cli_exec.py +0 -0
  205. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_cli_repl.py +0 -0
  206. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_client.py +0 -0
  207. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_client_contents.py +0 -0
  208. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_client_files.py +0 -0
  209. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_client_outcome_error_output.py +0 -0
  210. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_cloudflare.py +0 -0
  211. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_commands_envelope.py +0 -0
  212. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_console.py +0 -0
  213. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_contents_contract.py +0 -0
  214. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_contents_live_matrix.py +0 -0
  215. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_coreweave.py +0 -0
  216. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_datalayer_sandbox.py +0 -0
  217. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_daytona.py +0 -0
  218. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_e2b.py +0 -0
  219. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_accounts.py +0 -0
  220. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_breaker.py +0 -0
  221. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_build_secrets.py +0 -0
  222. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_builders.py +0 -0
  223. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_canonical.py +0 -0
  224. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_conformance.py +0 -0
  225. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_constraints.py +0 -0
  226. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_contract.py +0 -0
  227. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_cross_variant_report.py +0 -0
  228. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_daytona_builder.py +0 -0
  229. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_doctor.py +0 -0
  230. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_e2b_builder.py +0 -0
  231. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_errors.py +0 -0
  232. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_lifecycle.py +0 -0
  233. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_live_matrix.py +0 -0
  234. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_managed_builders.py +0 -0
  235. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_modal_builder.py +0 -0
  236. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_redact.py +0 -0
  237. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_environment_spec.py +0 -0
  238. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_eval.py +0 -0
  239. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  240. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_factory.py +0 -0
  241. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_google_colab.py +0 -0
  242. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_integration.py +0 -0
  243. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_jupyter_ingress.py +0 -0
  244. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_jupyter_server.py +0 -0
  245. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_jupyter_server_streaming.py +0 -0
  246. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_kaggle.py +0 -0
  247. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_kaggle_execute.py +0 -0
  248. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_kaggle_live.py +0 -0
  249. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_kaggle_session.py +0 -0
  250. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_kernel_client_compatibility.py +0 -0
  251. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_lifecycle_vocabulary.py +0 -0
  252. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_manage.py +0 -0
  253. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_manage_datalayer.py +0 -0
  254. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_modal_google_colab_sandbox.py +0 -0
  255. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_modal_session.py +0 -0
  256. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_models.py +0 -0
  257. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_output_line_fidelity.py +0 -0
  258. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_provider_catalog_names.py +0 -0
  259. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_provider_ingress.py +0 -0
  260. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_providers.py +0 -0
  261. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_sandboxes.py +0 -0
  262. {code_sandboxes-1.9.4 → code_sandboxes-1.9.6}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -8,6 +8,48 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.9.6
12
+
13
+ - **`datalayer/python-cpu`'s `2026.09` channel points at the patched base**
14
+ (`environments.bases`; PLAN_ENV.md E1-05, E1-08). `jupyter-python` 0.2.2
15
+ upgrades the Ubuntu security packages and conda's own OpenSSL, and drops
16
+ JupyterLab's staging `yarn.lock` — the 31 fixable-critical findings that
17
+ blocked every build from the prior digest under the default scan policy.
18
+
19
+ ## 1.9.5
20
+
21
+ - **Attest waits for a just-pushed image's scan to exist**
22
+ (`environments.attest`; PLAN_ENV.md E1-08). Enhanced scanning starts after
23
+ the push, so the first answers for a new image are
24
+ `ScanNotFoundException`. Attest read that as an image with no scan and
25
+ failed the build at once (found live on r1, 2026-09-14). It is now waited
26
+ for like a running scan, within the same bound.
27
+ - **apt is pinned at, and installed from, the base channel's Ubuntu snapshot**
28
+ (`environments.bases`, `environments.resolve`,
29
+ `environments.adapters.datalayer`; PLAN_ENV.md E1-04). A channel records
30
+ the `snapshot.ubuntu.com` id its image was upgraded at. The solve runs
31
+ `apt-get` with `--snapshot` at that id, the lock records it
32
+ (`# datalayer-apt-snapshot:`), and the Datalayer builder installs its pins
33
+ from the same snapshot. This replaces a `deb` line that named only `main`
34
+ (`gdal-bin` is in `universe`) and left the live mirror enabled beside it.
35
+ `DATALAYER_APT_SNAPSHOT` must now be a snapshot id.
36
+ - **A private registry's credential is resolved and used** (`environments
37
+ .image_import`, `environments.resolve`; PLAN_ENV.md E3-04's second half).
38
+ `image.credentialSecretId` used to only clear the allowlist check; nothing
39
+ fetched or used the credential it named. It is now fetched through the
40
+ same IAM route a build secret is (E3-05), and sent as HTTP Basic on the
41
+ registry's own token exchange — the way `docker login` authenticates a
42
+ private pull — never to the registry named in the reference, and never
43
+ logged. `resolve_environment` and `resolve_image_base` take `owner_uid`
44
+ and `resolve_secret` to reach it; unused by a public registry or any other
45
+ source.
46
+ - **The Datalayer base channel's own five ESM-locked advisories are allowed**
47
+ (`environments.policy`; PLAN_ENV.md E1-08). Amazon Inspector marks all five
48
+ fixable, but the fix is an Ubuntu ESM package version a plain
49
+ `apt-get upgrade` cannot reach. Reviewed and added to `DEFAULT_POLICY
50
+ .allowed`, 2026-09-14, so they are recorded as allowed rather than missed,
51
+ and any other critical still blocks.
52
+
11
53
  ## 1.9.4
12
54
 
13
55
  - **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.6
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.6"
@@ -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``."""
@@ -89,9 +108,15 @@ class ApprovedBase(BaseModel):
89
108
  APPROVED_BASES: dict[str, ApprovedBase] = {
90
109
  base.ref: base
91
110
  for base in (
92
- # E1-05: jupyter-python:0.2.1 (the restored fork) plus the contract layer,
93
- # released 2026-09-12 to environments/base/python-cpu. One image, so every
94
- # variant pins the same digest until a variant needs a base of its own.
111
+ # E1-05: jupyter-python:0.2.2 (Ubuntu security packages and conda's own
112
+ # OpenSSL upgraded, JupyterLab's staging yarn.lock dropped) plus the
113
+ # contract layer, released 2026-09-14 to environments/base/python-cpu.
114
+ # One image, so every variant pins the same digest until a variant
115
+ # needs a base of its own. The prior digest, jupyter-python:0.2.1,
116
+ # carried 31 fixable-critical findings the scan (E1-08) blocks every
117
+ # build on; this one carries none the default policy does not already
118
+ # name (`policy.py`'s own `_BASE_CHANNEL_ALLOWED`, five ESM-locked
119
+ # ffmpeg/libcjson1 advisories, reviewed the same day).
95
120
  ApprovedBase(
96
121
  ref="datalayer/python-cpu",
97
122
  python_versions=("3.13",),
@@ -100,9 +125,10 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
100
125
  # `.spec.VARIANTS`, spelled out: `spec` imports from this
101
126
  # module, so importing it back here would be circular.
102
127
  ("datalayer", "e2b", "daytona", "modal"),
103
- "sha256:cd09308a0c5e5adeec7fb5d8d29cf7455e78ba86f5c6c095a79068a280e1254f",
128
+ "sha256:334adf6c2714c8919ef60beeca1db12e3531a391c9dde41932c782f81c432b36",
104
129
  )
105
130
  },
131
+ snapshots={"2026.09": "20260914T150000Z"},
106
132
  ),
107
133
  # E2-17: jupyter-python-cuda plus the same layer.
108
134
  ApprovedBase(
@@ -180,6 +206,14 @@ def resolve_base(
180
206
  return digest
181
207
 
182
208
 
209
+ def channel_snapshot(
210
+ ref: str, channel: str, bases: dict[str, ApprovedBase] = APPROVED_BASES
211
+ ) -> str:
212
+ """The Ubuntu snapshot ``ref`` at ``channel`` pins apt against, or ``""`` for none (D-9)."""
213
+ base = bases.get(ref)
214
+ return "" if base is None else base.snapshots.get(channel, "")
215
+
216
+
183
217
  def approved_repositories(bases: dict[str, ApprovedBase] = APPROVED_BASES) -> tuple[str, ...]:
184
218
  """Every repository name an approved base is published under."""
185
219
  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)