code-sandboxes 1.9.22__tar.gz → 1.9.27__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 (265) hide show
  1. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/PKG-INFO +1 -1
  2. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/__version__.py +1 -1
  3. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/adapters/datalayer.py +23 -3
  4. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/adapters/daytona.py +31 -6
  5. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/adapters/modal.py +101 -14
  6. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/attest.py +70 -14
  7. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/conformance.py +8 -0
  8. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/policy.py +133 -1
  9. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/resolve_conda.py +146 -1
  10. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/spec.py +116 -8
  11. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/manage.py +80 -6
  12. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/modal_sandbox.py +14 -2
  13. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_attest.py +85 -1
  14. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_datalayer_builder.py +35 -3
  15. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_daytona_builder.py +65 -0
  16. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_live_matrix.py +20 -2
  17. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_managed_builders.py +27 -7
  18. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_modal_builder.py +117 -7
  19. code_sandboxes-1.9.27/tests/test_environment_organization_policy.py +165 -0
  20. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_resolve_conda.py +93 -0
  21. code_sandboxes-1.9.27/tests/test_environment_spec_organization_policy.py +198 -0
  22. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_manage.py +68 -0
  23. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_manage_datalayer.py +2 -3
  24. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.github/workflows/build.yml +0 -0
  25. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.github/workflows/environments-live.yml +0 -0
  26. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.github/workflows/py-code-style.yml +0 -0
  27. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.github/workflows/py-tests.yml +0 -0
  28. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.github/workflows/py-typing.yml +0 -0
  29. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.github/workflows/reusable-python.yml +0 -0
  30. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.gitignore +0 -0
  31. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.licenserc.yaml +0 -0
  32. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.pre-commit-config.yaml +0 -0
  33. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/.vscode/settings.json +0 -0
  34. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/CHANGELOG.md +0 -0
  35. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/Dockerfile +0 -0
  36. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/LICENSE +0 -0
  37. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/Makefile +0 -0
  38. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/README.md +0 -0
  39. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/__init__.py +0 -0
  40. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/base.py +0 -0
  41. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/bridge_mount.py +0 -0
  42. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/builds.py +0 -0
  43. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/cli.py +0 -0
  44. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/client.py +0 -0
  45. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/cloudflare_sandbox.py +0 -0
  46. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/commands.py +0 -0
  47. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/console.py +0 -0
  48. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/contents.py +0 -0
  49. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/coreweave_sandbox.py +0 -0
  50. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/datalayer_sandbox.py +0 -0
  51. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/daytona_sandbox.py +0 -0
  52. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/docker_sandbox.py +0 -0
  53. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/e2b_sandbox.py +0 -0
  54. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/__init__.py +0 -0
  55. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/__main__.py +0 -0
  56. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/accounts.py +0 -0
  57. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/adapters/__init__.py +0 -0
  58. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/adapters/e2b.py +0 -0
  59. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/adapters/managed.py +0 -0
  60. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/bases.py +0 -0
  61. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/breaker.py +0 -0
  62. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/build_secrets.py +0 -0
  63. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/builders.py +0 -0
  64. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/canonical.py +0 -0
  65. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  66. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  67. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  68. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/contract.py +0 -0
  69. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/doctor/__init__.py +0 -0
  70. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/doctor/build.py +0 -0
  71. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  72. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  73. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/errors.py +0 -0
  74. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/files.py +0 -0
  75. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/image_import.py +0 -0
  76. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/lifecycle.py +0 -0
  77. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/redact.py +0 -0
  78. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/resolve.py +0 -0
  79. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/environments/schema.py +0 -0
  80. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/eval_sandbox.py +0 -0
  81. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/exceptions.py +0 -0
  82. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/filesystem.py +0 -0
  83. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/google_colab.py +0 -0
  84. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/google_colab_sandbox.py +0 -0
  85. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/interfaces.py +0 -0
  86. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/jupyter_ingress.py +0 -0
  87. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  88. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/kaggle.py +0 -0
  89. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/kaggle_execute.py +0 -0
  90. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/kaggle_live.py +0 -0
  91. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/kaggle_sandbox.py +0 -0
  92. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/lifecycle.py +0 -0
  93. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/models.py +0 -0
  94. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/monty_sandbox.py +0 -0
  95. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/provider_ingress.py +0 -0
  96. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/code_sandboxes/providers.py +0 -0
  97. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docker/Dockerfile +0 -0
  98. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/.gitignore +0 -0
  99. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/LICENSE +0 -0
  100. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/Makefile +0 -0
  101. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/README.md +0 -0
  102. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/babel.config.js +0 -0
  103. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/api-reference/_category_.yml +0 -0
  104. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/api-reference/index.mdx +0 -0
  105. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/cli/_category_.yml +0 -0
  106. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/cli/index.mdx +0 -0
  107. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/contribute/_category_.yml +0 -0
  108. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/contribute/index.mdx +0 -0
  109. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/environments/_category_.yml +0 -0
  110. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/environments/contract.mdx +0 -0
  111. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/environments/index.mdx +0 -0
  112. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/environments/specification.mdx +0 -0
  113. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/environments/spike-2026-09.mdx +0 -0
  114. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/examples/_category_.yml +0 -0
  115. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/examples/index.mdx +0 -0
  116. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/guide/_category_.yml +0 -0
  117. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/guide/index.mdx +0 -0
  118. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/index.mdx +0 -0
  119. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/install/_category_.yml +0 -0
  120. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/install/index.mdx +0 -0
  121. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/provider-ingress.mdx +0 -0
  122. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/_category_.yml +0 -0
  123. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/cloudflare.mdx +0 -0
  124. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/coreweave.mdx +0 -0
  125. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/datalayer.mdx +0 -0
  126. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/daytona.mdx +0 -0
  127. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/docker.mdx +0 -0
  128. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/e2b.mdx +0 -0
  129. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/eval.mdx +0 -0
  130. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/google-colab.mdx +0 -0
  131. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/index.mdx +0 -0
  132. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/jupyter-server.mdx +0 -0
  133. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/kaggle.mdx +0 -0
  134. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/modal.mdx +0 -0
  135. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docs/providers/monty.mdx +0 -0
  136. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/docusaurus.config.js +0 -0
  137. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/package.json +0 -0
  138. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/sidebars.js +0 -0
  139. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/components/HomepageRow1.js +0 -0
  140. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/components/HomepageRow1.module.css +0 -0
  141. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/components/HomepageRow2.js +0 -0
  142. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/components/HomepageRow2.module.css +0 -0
  143. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/components/HomepageRow3.js +0 -0
  144. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/components/HomepageRow3.module.css +0 -0
  145. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/css/custom.css +0 -0
  146. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/pages/index.module.css +0 -0
  147. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/pages/markdown-page.md +0 -0
  148. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/theme/CustomDocItem.tsx +0 -0
  149. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/src/theme/IconsTitle.tsx +0 -0
  150. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/bricks.svg +0 -0
  151. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/building-construction.svg +0 -0
  152. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/cloud-sun.svg +0 -0
  153. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/cloud.svg +0 -0
  154. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/collaboration.svg +0 -0
  155. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/copy-left.svg +0 -0
  156. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/datalayer/logo.png +0 -0
  157. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/datalayer/logo.svg +0 -0
  158. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/datalayer/personas/business.svg +0 -0
  159. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  160. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/datalayer/personas/devops.svg +0 -0
  161. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/datalayer/project/project-line.svg +0 -0
  162. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/favicon.ico +0 -0
  163. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/jupyter.svg +0 -0
  164. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/open-source.svg +0 -0
  165. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/react-js.svg +0 -0
  166. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/react-js2.svg +0 -0
  167. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/ringed-planet.svg +0 -0
  168. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/robot.svg +0 -0
  169. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/rocket.svg +0 -0
  170. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/shield.svg +0 -0
  171. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/sparkles.svg +0 -0
  172. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/streamlit.gif +0 -0
  173. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/super-hero.svg +0 -0
  174. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/target.svg +0 -0
  175. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/docs/static/img/thumbs-up.svg +0 -0
  176. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/README.md +0 -0
  177. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/Makefile +0 -0
  178. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/cloudflare_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/coreweave_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/datalayer_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/daytona_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/docker_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/e2b_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/eval_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/google_colab_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  187. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/kaggle_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/modal_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/exec/monty_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/Makefile +0 -0
  191. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/cloudflare_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/coreweave_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/datalayer_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/daytona_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/docker_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/e2b_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/eval_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/google_colab_sandbox_example.py +0 -0
  199. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  200. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/kaggle_sandbox_example.py +0 -0
  201. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/modal_sandbox_example.py +0 -0
  202. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/examples/repl/monty_sandbox_example.py +0 -0
  203. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/pyproject.toml +0 -0
  204. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/schemas/environment-v1alpha1.json +0 -0
  205. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/__init__.py +0 -0
  206. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/conftest.py +0 -0
  207. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  208. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_bridge_mount.py +0 -0
  209. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_builds.py +0 -0
  210. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_cli_exec.py +0 -0
  211. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_cli_repl.py +0 -0
  212. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_client.py +0 -0
  213. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_client_contents.py +0 -0
  214. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_client_files.py +0 -0
  215. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_client_outcome_error_output.py +0 -0
  216. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_cloudflare.py +0 -0
  217. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_commands_envelope.py +0 -0
  218. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_console.py +0 -0
  219. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_contents_contract.py +0 -0
  220. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_contents_live_matrix.py +0 -0
  221. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_coreweave.py +0 -0
  222. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_datalayer_sandbox.py +0 -0
  223. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_daytona.py +0 -0
  224. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_e2b.py +0 -0
  225. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_accounts.py +0 -0
  226. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_bases.py +0 -0
  227. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_breaker.py +0 -0
  228. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_build_secrets.py +0 -0
  229. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_builders.py +0 -0
  230. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_canonical.py +0 -0
  231. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_conformance.py +0 -0
  232. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_constraints.py +0 -0
  233. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_contract.py +0 -0
  234. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_cross_variant_report.py +0 -0
  235. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_doctor.py +0 -0
  236. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_e2b_builder.py +0 -0
  237. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_errors.py +0 -0
  238. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_image_import.py +0 -0
  239. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_lifecycle.py +0 -0
  240. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_redact.py +0 -0
  241. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_resolve.py +0 -0
  242. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_environment_spec.py +0 -0
  243. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_eval.py +0 -0
  244. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  245. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_factory.py +0 -0
  246. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_google_colab.py +0 -0
  247. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_integration.py +0 -0
  248. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_jupyter_ingress.py +0 -0
  249. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_jupyter_server.py +0 -0
  250. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_jupyter_server_streaming.py +0 -0
  251. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_kaggle.py +0 -0
  252. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_kaggle_execute.py +0 -0
  253. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_kaggle_live.py +0 -0
  254. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_kaggle_session.py +0 -0
  255. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_kernel_client_compatibility.py +0 -0
  256. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_lifecycle_vocabulary.py +0 -0
  257. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_modal_google_colab_sandbox.py +0 -0
  258. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_modal_session.py +0 -0
  259. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_models.py +0 -0
  260. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_output_line_fidelity.py +0 -0
  261. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_provider_catalog_names.py +0 -0
  262. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_provider_ingress.py +0 -0
  263. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_providers.py +0 -0
  264. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_sandboxes.py +0 -0
  265. {code_sandboxes-1.9.22 → code_sandboxes-1.9.27}/tests/test_the_kill_relaunch_drill.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.9.22
3
+ Version: 1.9.27
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.22"
6
+ __version__ = "1.9.27"
@@ -83,6 +83,7 @@ from ..resolve_conda import (
83
83
  conda_lock_pip_requirements,
84
84
  is_conda_lock,
85
85
  micromamba_bootstrap_dockerfile_line,
86
+ micromamba_install_command,
86
87
  )
87
88
  from ..spec import BuildSecret, Environment, command_names_secret
88
89
 
@@ -112,6 +113,24 @@ DEFAULT_MAX_ARTIFACT_BYTES = 20 * 1024**3
112
113
  # does; nothing about their behavior changed.
113
114
 
114
115
 
116
+ #: The Dockerfile frontend the generated build asks for.
117
+ #:
118
+ #: At least **1.10**, because `_secret_mount` renders `mountAs: env` as
119
+ #: `--mount=type=secret,env=NAME` and that key does not exist before it: 1.7,
120
+ #: which this pinned for its first year, answers `unexpected key 'env' in
121
+ #: 'env=NAME'` and fails the whole build. The mount and the pin were written
122
+ #: against each other's assumptions and never ran together, because no build
123
+ #: had ever carried a build secret — found on r1 on 2026-09-17 by the first
124
+ #: one that did, and checked against both frontends before changing it.
125
+ #:
126
+ #: Raising this is a change to every Datalayer build, so it is one constant
127
+ #: with one reason, not a literal inside the Dockerfile's own text.
128
+ DOCKERFILE_FRONTEND = "docker/dockerfile:1.10"
129
+
130
+ #: The first frontend with `--mount=type=secret,env=` (see above).
131
+ SECRET_ENV_FRONTEND = (1, 10)
132
+
133
+
115
134
  def _secret_mount(secret: BuildSecret) -> str:
116
135
  """The ``--mount=type=secret`` flag one `BuildSecret` renders as (E3-05).
117
136
 
@@ -283,7 +302,7 @@ class Builder:
283
302
  spec = request.environment.spec
284
303
  apt = apt_pins_in(request.lock_text)
285
304
  lines = [
286
- "# syntax=docker/dockerfile:1.7",
305
+ f"# syntax={DOCKERFILE_FRONTEND}",
287
306
  f"# Generated by Datalayer for {request.environment.metadata.name} "
288
307
  f"v{request.version}, build {request.build_uid}.",
289
308
  f"# Lock: {request.lock_digest}",
@@ -346,8 +365,9 @@ class Builder:
346
365
  micromamba_bootstrap_dockerfile_line(),
347
366
  "COPY lock.txt /opt/datalayer/lock.txt",
348
367
  "RUN --mount=type=cache,target=/opt/conda/pkgs "
349
- f"{MICROMAMBA_BINARY} install --yes --name base "
350
- "--file /opt/datalayer/lock.txt",
368
+ + micromamba_install_command(
369
+ "/opt/datalayer/lock.txt", micromamba=MICROMAMBA_BINARY
370
+ ),
351
371
  ]
352
372
  )
353
373
  if pip_requirements:
@@ -116,6 +116,7 @@ from ..resolve_conda import (
116
116
  conda_lock_pip_requirements,
117
117
  is_conda_lock,
118
118
  micromamba_bootstrap_command,
119
+ micromamba_install_command,
119
120
  )
120
121
  from ..spec import GPU_SIZE_CLASSES, Environment
121
122
  from .managed import ManagedBuilder
@@ -178,15 +179,18 @@ class Builder(ManagedBuilder):
178
179
  variant = "daytona"
179
180
  item = "E2-04"
180
181
  title = "Daytona"
181
- #: A `packages` list and, for conda (E3-02), an `environment.yml`
182
- #: dependency file installed with `micromamba`.
182
+ #: A `packages` list, or a dependency file: a conda `environment.yml`
183
+ #: installed with `micromamba` (E3-02), and a `requirements.txt` or a
184
+ #: `pyproject.toml` with its lock (E3-01), both of which resolve to the
185
+ #: very pip lock a `packages` list does — `build` tells a conda lock from
186
+ #: a pip one and nothing finer, so nothing here is format-specific.
183
187
  build_sources = ("packages", "dependencyFile", "dockerfile")
184
188
  #: None beyond the contract's own (E3-03): `Image.from_dockerfile` keeps
185
189
  #: the Dockerfile text as it is and Daytona builds it on a real Docker
186
190
  #: builder, so the grammar it accepts is Docker's. Checked in the SDK on
187
191
  #: 2026-09-17.
188
192
  forbidden_instructions = ()
189
- dependency_formats = ("conda",)
193
+ dependency_formats = ("requirements", "pyproject", "conda")
190
194
  #: Daytona runs GPUs, on its own hardware and the owner's account (E2-17).
191
195
  #: This builder does not build one yet: see `_own_findings`.
192
196
  gpu = True
@@ -370,9 +374,7 @@ class Builder(ManagedBuilder):
370
374
  # present the same as for a pip source. micromamba is
371
375
  # installed first: the approved base bakes uv but not it.
372
376
  image = image.run_commands(micromamba_bootstrap_command())
373
- image = image.run_commands(
374
- f"micromamba install --yes --name base --file {_LOCK_PATH}"
375
- )
377
+ image = image.run_commands(micromamba_install_command(_LOCK_PATH))
376
378
  pip_requirements = conda_lock_pip_requirements(request.lock_text)
377
379
  if pip_requirements:
378
380
  requirements = " ".join(shlex.quote(req) for req in pip_requirements)
@@ -568,6 +570,29 @@ class Builder(ManagedBuilder):
568
570
  raise self._provider_error("ask whether the snapshot exists", error) from error
569
571
  return True
570
572
 
573
+ def delete(self, artifact: ArtifactReference) -> None:
574
+ """Remove the snapshot, by id; one already gone is removed (E2-18).
575
+
576
+ Deleting what is gone is a success because the collector deletes
577
+ first and marks second (E1-17): a sweep that died between the two
578
+ deletes again tomorrow, and must not be refused for having worked.
579
+
580
+ **By id, never by name.** The SDK takes either, and a name is reused
581
+ once its snapshot is deleted (E0-04) — deleting by name could remove
582
+ a later build's snapshot that inherited it.
583
+ """
584
+ sdk = self._daytona_sdk()
585
+ client = self._client(sdk)
586
+ snapshot = artifact.provider_artifact_id or artifact.immutable_reference
587
+ try:
588
+ client.snapshot.delete(snapshot)
589
+ except sdk.DaytonaNotFoundError:
590
+ self._log(f"The Daytona snapshot {snapshot} was already gone")
591
+ return
592
+ except Exception as error:
593
+ raise self._provider_error("delete the snapshot", error) from error
594
+ self._log(f"Deleted the Daytona snapshot {snapshot}")
595
+
571
596
  def smoke_test(
572
597
  self,
573
598
  artifact: ArtifactReference,
@@ -124,7 +124,7 @@ import contextlib
124
124
  import io
125
125
  import os
126
126
  import tempfile
127
- from collections.abc import Callable
127
+ from collections.abc import Callable, Sequence
128
128
  from pathlib import Path
129
129
  from typing import Any
130
130
 
@@ -135,6 +135,7 @@ from ..builders import (
135
135
  ArtifactReference,
136
136
  BuildRequest,
137
137
  CapabilityFinding,
138
+ ValidationResult,
138
139
  )
139
140
  from ..contract import SANDBOX_CONTRACT_V1
140
141
  from ..errors import (
@@ -535,23 +536,31 @@ class Builder(ManagedBuilder):
535
536
  return declared, values
536
537
 
537
538
  def _ecr_secret(self, sdk: Any, client: Any) -> Any | None:
538
- """A Modal Secret carrying this build's own base-reader credential (D-17, D-18).
539
-
540
- `None` when the credential carries no registry login: the base is
541
- then whatever the ambient workspace can already reach, the same
542
- fallback `_client` takes with no owner token.
539
+ """A Modal Secret carrying this build's base-reader session (D-17, D-18).
540
+
541
+ `from_aws_ecr` wants the IAM session itself — keys, token and region —
542
+ not the docker-login `AWS`/token pair Daytona and E2B take, which is
543
+ what the credential's `username`/`password` hold. So it reads
544
+ `aws_session`. Reading the login pair as IAM keys is what this did
545
+ until 2026-09-18: the worker minted `AWS`/<ECR token>, this wrote it
546
+ as `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, and AWS answered "The
547
+ security token included in the request is invalid" on the base pull,
548
+ so no Modal build through the worker could ever succeed.
549
+
550
+ `None` when the credential carries no session: the base is then
551
+ whatever the ambient workspace can already reach, the same fallback
552
+ `_client` takes with no owner token.
543
553
  """
544
- username = str(getattr(self._credential, "username", "") or "")
545
- password = str(getattr(self._credential, "password", "") or "")
546
- if not (username and password):
554
+ session = dict(getattr(self._credential, "aws_session", None) or {})
555
+ if not (session.get("AWS_ACCESS_KEY_ID") and session.get("AWS_SECRET_ACCESS_KEY")):
547
556
  return None
548
- # `from_aws_ecr` wants IAM-shaped credentials, not the docker-login
549
- # `AWS`/token pair Daytona and E2B take (found live, 2026-09-13):
550
- # the same D-17 session, read differently.
551
557
  secret = sdk.Secret.from_dict(
552
558
  {
553
- "AWS_ACCESS_KEY_ID": username,
554
- "AWS_SECRET_ACCESS_KEY": password,
559
+ **{
560
+ name: str(session[name])
561
+ for name in ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN")
562
+ if session.get(name)
563
+ },
555
564
  "AWS_REGION": self._region,
556
565
  }
557
566
  )
@@ -665,6 +674,84 @@ class Builder(ManagedBuilder):
665
674
  raise self._provider_error("ask whether the image exists", error) from error
666
675
  return True
667
676
 
677
+ def smoke_test(
678
+ self,
679
+ artifact: ArtifactReference,
680
+ *,
681
+ environment: Any = None,
682
+ lock_text: str | None = None,
683
+ secret_values: Sequence[str] = (),
684
+ ) -> ValidationResult:
685
+ """Launch the image and run Appendix B's core tier in it (E2-05).
686
+
687
+ This box's own `Done when` asks for exactly this — "the live test
688
+ launches by image id and passes the core tier" — and it refused
689
+ through `ManagedBuilder`, so **no Modal build could reach
690
+ `succeeded`**: the image was built in the owner's workspace and the
691
+ build recorded failed at this step, the same state Daytona was in
692
+ until 2026-09-17.
693
+
694
+ **Launched by image id, never by name**: a published name is mutable
695
+ by design (§6), so only the id says which artifact ran. Launched
696
+ through `ModalSandbox` — the same class a person's launch uses —
697
+ rather than a hand-rolled `Sandbox.create`, because a hand-rolled
698
+ launch is exactly what hid, on 2026-09-13, that every image this
699
+ builder made died the instant it was launched for real.
700
+
701
+ **As the owner** (D-8): the sandbox is created with the same client
702
+ the build used, so it runs in the workspace the image is in.
703
+
704
+ **Restarted by replacing the sandbox.** Modal has no in-place
705
+ restart, so check 7's restart stops this sandbox and starts another
706
+ from the same image, which is the stronger form of the question.
707
+
708
+ The sandbox is terminated whether the tier passed or not.
709
+ """
710
+ if environment is None:
711
+ raise EnvironmentsError(
712
+ CAPABILITY_UNSUPPORTED,
713
+ "A Modal smoke test needs the version's spec: the core tier "
714
+ "asks for the Python version it declared and the packages its "
715
+ "lock pinned, and an artifact carries neither",
716
+ detail={"variant": self.variant},
717
+ )
718
+ from ...modal_sandbox import ModalSandbox
719
+ from ...models import SandboxConfig
720
+ from ..conformance import expected_packages, run_core_tier
721
+
722
+ sdk = self._modal_sdk()
723
+ sandbox = ModalSandbox(
724
+ config=SandboxConfig(name=f"smoke-{artifact.provider_artifact_id}"),
725
+ app_name=f"dl-{environment.metadata.name}",
726
+ image_id=artifact.provider_artifact_id,
727
+ client=self._client(sdk),
728
+ )
729
+ self._log(f"Launching {artifact.provider_artifact_id} to smoke-test it")
730
+ try:
731
+ sandbox.start()
732
+ return run_core_tier(
733
+ sandbox,
734
+ python_version=environment.spec.language.version,
735
+ expected_packages=expected_packages(environment, lock_text or ""),
736
+ secret_values=tuple(secret_values),
737
+ restart=lambda: self._restart(sandbox),
738
+ )
739
+ except EnvironmentsError:
740
+ raise
741
+ except Exception as error:
742
+ raise self._provider_error("smoke-test the image", error) from error
743
+ finally:
744
+ try:
745
+ sandbox.stop()
746
+ except Exception as error:
747
+ self._log(f"The smoke-test sandbox could not be stopped: {error}")
748
+
749
+ @staticmethod
750
+ def _restart(sandbox: Any) -> None:
751
+ """Check 7's restart, as Modal can do it: a new sandbox from the same image."""
752
+ sandbox.stop()
753
+ sandbox.start()
754
+
668
755
  def delete(self, artifact: ArtifactReference) -> None:
669
756
  """Delete the image, and every intermediate layer this build recorded (E2-05, E2-09).
670
757
 
@@ -49,7 +49,16 @@ from .errors import (
49
49
  SCAN_BLOCKED,
50
50
  EnvironmentsError,
51
51
  )
52
- from .policy import DEFAULT_POLICY, PolicyDecision, ScanPolicy, decide, findings_of
52
+ from .policy import (
53
+ DEFAULT_POLICY,
54
+ UNRESTRICTED_POLICY,
55
+ EnvironmentsPolicy,
56
+ PolicyDecision,
57
+ ScanPolicy,
58
+ decide,
59
+ findings_of,
60
+ refuse_unlicensed,
61
+ )
53
62
 
54
63
  logger = logging.getLogger(__name__)
55
64
 
@@ -116,47 +125,72 @@ _SPDX_LICENSE_KEYS = ("licenseConcluded", "licenseDeclared")
116
125
  _SPDX_UNKNOWN = frozenset({"NOASSERTION", "NONE", ""})
117
126
 
118
127
 
119
- def _spdx_licenses(document: Mapping[str, Any]) -> set[str]:
120
- """What an SPDX document names, which is what BuildKit's `attest:sbom=` writes.
128
+ def _spdx_license_pairs(document: Mapping[str, Any]) -> list[tuple[str, str]]:
129
+ """Every SPDX package with a licence, as `(package, licence)`.
121
130
 
122
131
  `licenseConcluded` is what the tool decided and `licenseDeclared` what the
123
132
  package claimed, so the concluded one is read first and the declared one
124
133
  only when it said nothing.
125
134
  """
126
- found: set[str] = set()
135
+ found: list[tuple[str, str]] = []
127
136
  for package in document.get("packages") or ():
128
137
  if not isinstance(package, Mapping):
129
138
  continue
139
+ name = str(package.get("name") or "").strip()
130
140
  for key in _SPDX_LICENSE_KEYS:
131
141
  value = str(package.get(key) or "").strip()
132
142
  if value and value.upper() not in _SPDX_UNKNOWN:
133
- found.add(value)
143
+ # Kept even with no name: `licenses_of` reads only the licence
144
+ # half, and dropping an unnamed package's licence there would
145
+ # be exactly the silent behavior change this refactor must
146
+ # not make. `licenses_by_package`'s own callers decide what an
147
+ # empty package name means for them.
148
+ found.append((name, value))
134
149
  break
135
150
  return found
136
151
 
137
152
 
138
- def _cyclonedx_licenses(document: Mapping[str, Any]) -> set[str]:
139
- """What a CycloneDX document names, by id, by name, or as an expression."""
140
- found: set[str] = set()
153
+ def _cyclonedx_license_pairs(document: Mapping[str, Any]) -> list[tuple[str, str]]:
154
+ """Every CycloneDX component with a licence, as `(component, licence)`."""
155
+ found: list[tuple[str, str]] = []
141
156
  for component in document.get("components") or ():
142
157
  if not isinstance(component, Mapping):
143
158
  continue
159
+ name = str(component.get("name") or "").strip()
144
160
  for entry in component.get("licenses") or ():
145
161
  if not isinstance(entry, Mapping):
146
162
  continue
147
163
  licence = entry.get("license")
164
+ value = ""
148
165
  if isinstance(licence, Mapping):
149
166
  for key in _CYCLONEDX_LICENSE_KEYS:
150
167
  value = str(licence.get(key) or "").strip()
151
168
  if value:
152
- found.add(value)
153
169
  break
154
- expression = str(entry.get("expression") or "").strip()
155
- if expression:
156
- found.add(expression)
170
+ if not value:
171
+ value = str(entry.get("expression") or "").strip()
172
+ if value:
173
+ # Kept even with no component name; see the SPDX reader's own
174
+ # note above.
175
+ found.append((name, value))
157
176
  return found
158
177
 
159
178
 
179
+ def licenses_by_package(document: Any) -> list[tuple[str, str]]:
180
+ """Every `(package, licence)` an SBOM names, package first (E3-06).
181
+
182
+ The same two shapes `licenses_of` reads, kept attributed rather than
183
+ flattened: a policy that denies a licence has to name the package that
184
+ carries it, which `licenses_of`'s own deduplicated set of licence strings
185
+ alone cannot answer. Insertion order, not sorted — the order the SBOM's
186
+ own packages/components came in, which is what a refusal naming "the
187
+ first one" should mean deterministically for the same document.
188
+ """
189
+ if not isinstance(document, Mapping):
190
+ return []
191
+ return _spdx_license_pairs(document) + _cyclonedx_license_pairs(document)
192
+
193
+
160
194
  def licenses_of(document: Any) -> list[str]:
161
195
  """Every licence an SBOM names, deduplicated and sorted.
162
196
 
@@ -173,7 +207,7 @@ def licenses_of(document: Any) -> list[str]:
173
207
  """
174
208
  if not isinstance(document, Mapping):
175
209
  return []
176
- return sorted(_spdx_licenses(document) | _cyclonedx_licenses(document))
210
+ return sorted({licence for _package, licence in licenses_by_package(document)})
177
211
 
178
212
 
179
213
  @dataclass(frozen=True)
@@ -589,12 +623,21 @@ class Attestor:
589
623
  sbom_ref: str = "",
590
624
  provenance_ref: str = "",
591
625
  sbom: Any = None,
626
+ environments_policy: EnvironmentsPolicy = UNRESTRICTED_POLICY,
592
627
  ) -> AttestationResult:
593
628
  """Scan, then sign: the order the Operator's check depends on (D-11).
594
629
 
595
630
  `sbom`, when the caller has the document, is read for the licences a
596
631
  publication carries; the registry's scanner reports vulnerabilities
597
632
  and never licences, so there is nowhere else they come from.
633
+
634
+ **Refused before signing, never after (E3-06).** A licence an
635
+ organization's own policy denies is checked against the same `sbom`,
636
+ naming the package that carries it, and raised before `sign` runs —
637
+ an artifact whose licence policy denies it is never signed, the same
638
+ as one whose scan blocks it: a signature is Datalayer's word that an
639
+ artifact may run, and this is the second of the two words that go
640
+ into it.
598
641
  """
599
642
  self.can_sign()
600
643
  decision = self.scan(repository=repository, digest=digest)
@@ -609,6 +652,8 @@ class Attestor:
609
652
  "blocking": [finding.id for finding in decision.blocking],
610
653
  },
611
654
  )
655
+ pairs = licenses_by_package(sbom)
656
+ refuse_unlicensed(pairs, environments_policy)
612
657
  signature, signed_now = self.sign(registry=registry, repository=repository, digest=digest)
613
658
  return AttestationResult(
614
659
  decision=decision,
@@ -619,7 +664,7 @@ class Attestor:
619
664
  if size_bytes is not None
620
665
  else self.size_of(repository=repository, digest=digest),
621
666
  signed_now=signed_now,
622
- licenses=tuple(licenses_of(sbom)),
667
+ licenses=tuple(sorted({licence for _package, licence in pairs})),
623
668
  )
624
669
 
625
670
  def size_of(self, *, repository: str, digest: str) -> int | None:
@@ -665,8 +710,10 @@ def attest_artifact(
665
710
  artifact: Any,
666
711
  credential: Any = None,
667
712
  policy: ScanPolicy = DEFAULT_POLICY,
713
+ environments_policy: EnvironmentsPolicy = UNRESTRICTED_POLICY,
668
714
  log: Callable[[str], None] | None = None,
669
715
  size_bytes: int | None = None,
716
+ sbom: Any = None,
670
717
  attestor: Attestor | None = None,
671
718
  ) -> dict[str, Any]:
672
719
  """The `attest` seam of `EnvironmentBuildWorkflow` (E1-08, E1-09).
@@ -674,6 +721,13 @@ def attest_artifact(
674
721
  Takes the artifact the builder recorded and the build's credential, and
675
722
  answers the mapping the workflow stores: the scan's decision, the
676
723
  signature, the SBOM and provenance references, and the size.
724
+
725
+ `environments_policy` is the caller's organization's own narrowing of
726
+ what licence a build may carry (E3-06); `sbom`, when the caller has the
727
+ document, is what it is checked against — nobody fetches one here. A
728
+ caller with neither passes nothing through, and nothing is refused: the
729
+ platform default is unrestricted on licences, the same as every other
730
+ dimension of this policy until an organization writes one.
677
731
  """
678
732
  variant = str(getattr(artifact, "variant", "") or "")
679
733
  reference = str(getattr(artifact, "immutable_reference", "") or "")
@@ -724,6 +778,8 @@ def attest_artifact(
724
778
  repository=repository,
725
779
  digest=digest,
726
780
  size_bytes=size_bytes,
781
+ sbom=sbom,
782
+ environments_policy=environments_policy,
727
783
  ).body()
728
784
 
729
785
 
@@ -477,6 +477,14 @@ def expected_packages(environment: Any, lock_text: str) -> dict[str, str]:
477
477
 
478
478
  from .resolve import locked_versions
479
479
 
480
+ dependency_file = environment.spec.build.dependency_file
481
+ if dependency_file is not None and dependency_file.source_format == "conda":
482
+ # A conda source names its packages in the file, not in
483
+ # `packages.python`, and its lock is not a pip one (E3-02). Read the
484
+ # other way, check 5 was handed nothing and passed for it.
485
+ from .resolve_conda import conda_expected_packages
486
+
487
+ return conda_expected_packages(dependency_file.content, lock_text or "")
480
488
  pinned = locked_versions(lock_text) if lock_text else {}
481
489
  names: list[str] = []
482
490
  for text in environment.spec.packages.python.dependencies:
@@ -32,17 +32,20 @@ from collections.abc import Iterable, Mapping, Sequence
32
32
  from dataclasses import dataclass, field
33
33
  from typing import Any
34
34
 
35
- from .errors import SCAN_BLOCKED, EnvironmentsError
35
+ from .errors import POLICY_DENIED, SCAN_BLOCKED, EnvironmentsError
36
36
 
37
37
  __all__ = [
38
38
  "DEFAULT_POLICY",
39
39
  "SEVERITIES",
40
+ "EnvironmentsPolicy",
40
41
  "Finding",
41
42
  "PolicyDecision",
42
43
  "ScanPolicy",
43
44
  "decide",
45
+ "environments_policy_from_rules",
44
46
  "findings_of",
45
47
  "refuse_if_blocked",
48
+ "refuse_unlicensed",
46
49
  ]
47
50
 
48
51
  #: The severities a scanner reports, weakest first. Anything it reports that is
@@ -300,3 +303,132 @@ def refuse_if_blocked(decision: PolicyDecision, *, reference: str = "") -> None:
300
303
  "blocking": [finding.id for finding in decision.blocking],
301
304
  },
302
305
  )
306
+
307
+
308
+ @dataclass(frozen=True)
309
+ class EnvironmentsPolicy:
310
+ """An organization's own narrowing of what a build may do (E3-06).
311
+
312
+ Every allowlist is ``None`` until an organization writes one, meaning
313
+ that dimension is unrestricted beyond the platform's own defaults — the
314
+ approved bases, the public registries a spec's own validation already
315
+ checks, and nothing at all for indexes, packages or licences. Setting a
316
+ list, even an empty one, is a real policy: an organization that writes
317
+ ``allowed_licenses=()`` has denied every licence, and this does not
318
+ second-guess a policy that strict — the refusal it produces names
319
+ exactly why.
320
+
321
+ This narrows; it never widens. A base, index, registry, package or
322
+ licence the platform already refuses stays refused whatever an
323
+ organization allows — the same rule `refuse_widening` already keeps for
324
+ the gateway's own policy layers (`iam/datalayer_iam/services/
325
+ mcp_policies.py`), applied here to a different set of rules.
326
+ """
327
+
328
+ allowed_bases: tuple[str, ...] | None = None
329
+ allowed_indexes: tuple[str, ...] | None = None
330
+ allowed_registries: tuple[str, ...] | None = None
331
+ allowed_packages: tuple[str, ...] | None = None
332
+ allowed_licenses: tuple[str, ...] | None = None
333
+ scan: ScanPolicy = DEFAULT_POLICY
334
+ #: The policy document's own version, carried into E1-08's decision
335
+ #: record so a build made under one policy is not misread once an
336
+ #: organization changes it — the version answers "which policy" without
337
+ #: needing IAM asked again.
338
+ version: int | None = None
339
+
340
+ def allows(self, dimension: str, value: str) -> bool:
341
+ """Whether `value` clears this policy's allowlist for `dimension`.
342
+
343
+ `True` when the dimension is unrestricted (`None`) or the value is on
344
+ the list; the comparison is exact, not a prefix or a host match — an
345
+ index or a registry is compared the way `image_registry_allowed`
346
+ already compares one, and a base or a package by its own name.
347
+ """
348
+ allowed = getattr(self, f"allowed_{dimension}")
349
+ return allowed is None or value in allowed
350
+
351
+
352
+ #: No organization has written a policy: every dimension unrestricted, the
353
+ #: platform's own default scan threshold. The reading a caller with no
354
+ #: policy document gets, and what every check below is a no-op against.
355
+ UNRESTRICTED_POLICY = EnvironmentsPolicy()
356
+
357
+ #: What `environments_policy_from_rules` reads a list-shaped field as. Kept
358
+ #: beside `ScanPolicy`'s own fields so the two are validated the same way.
359
+ _LIST_FIELDS: tuple[str, ...] = (
360
+ "allowedBases",
361
+ "allowedIndexes",
362
+ "allowedRegistries",
363
+ "allowedPackages",
364
+ "allowedLicenses",
365
+ )
366
+
367
+
368
+ def environments_policy_from_rules(
369
+ rules: Mapping[str, Any] | None, *, version: int | None = None
370
+ ) -> EnvironmentsPolicy:
371
+ """The `environments` section of an organization's MCP policy document,
372
+ as the shape this module checks against (E3-06).
373
+
374
+ `rules` is the raw object IAM stores under the policy's own
375
+ ``environments`` key — camelCase, the same convention every other rule in
376
+ `iam/datalayer_iam/services/mcp_policies.py` already uses. Absent, or not
377
+ an object, answers :data:`UNRESTRICTED_POLICY`: an organization that has
378
+ not written this section has narrowed nothing, the same reading every
379
+ other rule in that module gives an unset one.
380
+
381
+ Never raises. A caller holding an organization's policy already trusts
382
+ IAM to have validated it at the write (`mcp_policies.validate_rules`);
383
+ asking twice, differently, would let the two readings disagree about
384
+ what a stored policy means.
385
+ """
386
+ if not isinstance(rules, Mapping):
387
+ return UNRESTRICTED_POLICY
388
+ lists: dict[str, tuple[str, ...] | None] = {}
389
+ for camel in _LIST_FIELDS:
390
+ value = rules.get(camel)
391
+ snake = "".join(
392
+ f"_{c.lower()}" if c.isupper() else c for c in camel
393
+ ) # allowedBases -> allowed_bases
394
+ lists[snake] = (
395
+ tuple(str(item) for item in value) if isinstance(value, (list, tuple)) else None
396
+ )
397
+ scan_rules = rules.get("scan") if isinstance(rules.get("scan"), Mapping) else {}
398
+ blocks_at = str(scan_rules.get("blocksAt") or DEFAULT_POLICY.blocks_at).upper()
399
+ scan = ScanPolicy(
400
+ blocks_at=blocks_at if blocks_at in SEVERITIES else DEFAULT_POLICY.blocks_at,
401
+ only_fixable=bool(scan_rules.get("onlyFixable", DEFAULT_POLICY.only_fixable)),
402
+ allowed=tuple(str(item) for item in (scan_rules.get("allowed") or ()))
403
+ or DEFAULT_POLICY.allowed,
404
+ )
405
+ return EnvironmentsPolicy(scan=scan, version=version, **lists)
406
+
407
+
408
+ def refuse_unlicensed(
409
+ pairs: Sequence[tuple[str, str]], policy: EnvironmentsPolicy = UNRESTRICTED_POLICY
410
+ ) -> None:
411
+ """Raise `DL_ENV_POLICY_DENIED` on the first licence this policy does not
412
+ allow, naming the package that carries it (E3-06).
413
+
414
+ `pairs` is `attest.licenses_by_package`'s own output: `(package,
415
+ licence)`, in the SBOM's own order, so the same artifact always names the
416
+ same first offender. A no-op when `policy.allowed_licenses` is unset —
417
+ every artifact today, until an organization writes one.
418
+ """
419
+ if policy.allowed_licenses is None:
420
+ return
421
+ for package, licence in pairs:
422
+ if not policy.allows("licenses", licence):
423
+ raise EnvironmentsError(
424
+ POLICY_DENIED,
425
+ f"`{licence}` ({package or 'an unnamed package'}) is not an allowed "
426
+ f"licence (the allowed ones are {', '.join(policy.allowed_licenses) or '(none)'})",
427
+ detail={
428
+ "field": "licenses",
429
+ "package": package,
430
+ "license": licence,
431
+ "allowed": list(policy.allowed_licenses),
432
+ **({"policyVersion": policy.version} if policy.version is not None else {}),
433
+ },
434
+ )