code-sandboxes 1.7.0__tar.gz → 1.8.2__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 (258) hide show
  1. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.github/workflows/build.yml +1 -1
  2. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.github/workflows/py-tests.yml +1 -1
  3. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.pre-commit-config.yaml +5 -0
  4. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/CHANGELOG.md +49 -2
  5. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/PKG-INFO +5 -1
  6. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/__version__.py +1 -1
  7. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/base.py +24 -1
  8. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/client.py +8 -1
  9. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/commands.py +1 -1
  10. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/datalayer_sandbox.py +10 -10
  11. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/daytona_sandbox.py +30 -2
  12. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/docker_sandbox.py +2 -3
  13. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/e2b_sandbox.py +10 -2
  14. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/__init__.py +19 -2
  15. code_sandboxes-1.8.2/code_sandboxes/environments/accounts.py +143 -0
  16. code_sandboxes-1.8.2/code_sandboxes/environments/adapters/datalayer.py +751 -0
  17. code_sandboxes-1.8.2/code_sandboxes/environments/adapters/daytona.py +90 -0
  18. code_sandboxes-1.8.2/code_sandboxes/environments/adapters/e2b.py +74 -0
  19. code_sandboxes-1.8.2/code_sandboxes/environments/adapters/managed.py +242 -0
  20. code_sandboxes-1.8.2/code_sandboxes/environments/adapters/modal.py +75 -0
  21. code_sandboxes-1.8.2/code_sandboxes/environments/attest.py +461 -0
  22. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/bases.py +13 -2
  23. code_sandboxes-1.8.2/code_sandboxes/environments/breaker.py +294 -0
  24. code_sandboxes-1.8.2/code_sandboxes/environments/build_secrets.py +152 -0
  25. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/builders.py +85 -2
  26. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/conformance.py +94 -5
  27. code_sandboxes-1.8.2/code_sandboxes/environments/constraints/wheelhouse/README.md +40 -0
  28. code_sandboxes-1.8.2/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  29. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/datalayer_sandbox.py +3 -1
  30. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/errors.py +16 -0
  31. code_sandboxes-1.8.2/code_sandboxes/environments/image_import.py +289 -0
  32. code_sandboxes-1.8.2/code_sandboxes/environments/policy.py +281 -0
  33. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/resolve.py +351 -13
  34. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/spec.py +260 -25
  35. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/jupyter_ingress.py +1 -2
  36. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/jupyter_server_sandbox.py +29 -0
  37. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/modal_sandbox.py +83 -22
  38. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/environments/index.mdx +3 -1
  39. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/environments/specification.mdx +1 -1
  40. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/pyproject.toml +6 -0
  41. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/schemas/environment-v1alpha1.json +75 -0
  42. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +1 -0
  43. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_bridge_mount.py +30 -18
  44. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_client.py +25 -0
  45. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_commands_envelope.py +5 -1
  46. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_contents_contract.py +7 -2
  47. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_contents_live_matrix.py +8 -9
  48. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_datalayer_sandbox.py +3 -1
  49. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_daytona.py +38 -0
  50. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_e2b.py +52 -1
  51. code_sandboxes-1.8.2/tests/test_environment_accounts.py +131 -0
  52. code_sandboxes-1.8.2/tests/test_environment_attest.py +425 -0
  53. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_bases.py +15 -4
  54. code_sandboxes-1.8.2/tests/test_environment_breaker.py +251 -0
  55. code_sandboxes-1.8.2/tests/test_environment_build_secrets.py +217 -0
  56. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_builders.py +127 -2
  57. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_conformance.py +31 -0
  58. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_contract.py +3 -3
  59. code_sandboxes-1.8.2/tests/test_environment_cross_variant_report.py +153 -0
  60. code_sandboxes-1.8.2/tests/test_environment_datalayer_builder.py +718 -0
  61. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_errors.py +2 -0
  62. code_sandboxes-1.8.2/tests/test_environment_image_import.py +202 -0
  63. code_sandboxes-1.8.2/tests/test_environment_managed_builders.py +350 -0
  64. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_redact.py +1 -3
  65. code_sandboxes-1.8.2/tests/test_environment_resolve.py +1068 -0
  66. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_spec.py +216 -1
  67. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_eval.py +10 -0
  68. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_jupyter_server.py +149 -0
  69. code_sandboxes-1.8.2/tests/test_modal_session.py +181 -0
  70. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_the_kill_relaunch_drill.py +9 -9
  71. code_sandboxes-1.7.0/tests/test_environment_resolve.py +0 -495
  72. code_sandboxes-1.7.0/tests/test_modal_session.py +0 -60
  73. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.github/workflows/py-code-style.yml +0 -0
  74. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.github/workflows/py-typing.yml +0 -0
  75. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.github/workflows/reusable-python.yml +0 -0
  76. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.gitignore +0 -0
  77. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.licenserc.yaml +0 -0
  78. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/.vscode/settings.json +0 -0
  79. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/Dockerfile +0 -0
  80. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/LICENSE +0 -0
  81. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/Makefile +0 -0
  82. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/README.md +0 -0
  83. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/__init__.py +0 -0
  84. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/bridge_mount.py +0 -0
  85. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/builds.py +0 -0
  86. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/cli.py +0 -0
  87. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/cloudflare_sandbox.py +0 -0
  88. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/console.py +0 -0
  89. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/contents.py +0 -0
  90. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/coreweave_sandbox.py +0 -0
  91. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/__main__.py +0 -0
  92. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/adapters/__init__.py +0 -0
  93. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/canonical.py +0 -0
  94. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  95. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/contract.py +0 -0
  96. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/__init__.py +0 -0
  97. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/build.py +0 -0
  98. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  99. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/files.py +0 -0
  100. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/lifecycle.py +0 -0
  101. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/redact.py +0 -0
  102. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/schema.py +0 -0
  103. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/eval_sandbox.py +0 -0
  104. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/exceptions.py +0 -0
  105. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/filesystem.py +0 -0
  106. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/google_colab.py +0 -0
  107. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/google_colab_sandbox.py +1 -1
  108. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/interfaces.py +0 -0
  109. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle.py +0 -0
  110. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle_execute.py +0 -0
  111. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle_live.py +0 -0
  112. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle_sandbox.py +1 -1
  113. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/lifecycle.py +0 -0
  114. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/manage.py +0 -0
  115. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/models.py +0 -0
  116. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/monty_sandbox.py +0 -0
  117. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/provider_ingress.py +0 -0
  118. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/code_sandboxes/providers.py +0 -0
  119. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docker/Dockerfile +0 -0
  120. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/.gitignore +0 -0
  121. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/LICENSE +0 -0
  122. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/Makefile +0 -0
  123. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/README.md +0 -0
  124. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/babel.config.js +0 -0
  125. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/api-reference/_category_.yml +0 -0
  126. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/api-reference/index.mdx +0 -0
  127. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/cli/_category_.yml +0 -0
  128. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/cli/index.mdx +0 -0
  129. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/contribute/_category_.yml +0 -0
  130. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/contribute/index.mdx +0 -0
  131. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/environments/_category_.yml +0 -0
  132. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/environments/contract.mdx +0 -0
  133. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/environments/spike-2026-09.mdx +0 -0
  134. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/examples/_category_.yml +0 -0
  135. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/examples/index.mdx +0 -0
  136. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/guide/_category_.yml +0 -0
  137. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/guide/index.mdx +0 -0
  138. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/index.mdx +0 -0
  139. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/install/_category_.yml +0 -0
  140. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/install/index.mdx +0 -0
  141. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/provider-ingress.mdx +0 -0
  142. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/_category_.yml +0 -0
  143. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/cloudflare.mdx +0 -0
  144. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/coreweave.mdx +0 -0
  145. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/datalayer.mdx +0 -0
  146. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/daytona.mdx +0 -0
  147. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/docker.mdx +0 -0
  148. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/e2b.mdx +0 -0
  149. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/eval.mdx +0 -0
  150. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/google-colab.mdx +0 -0
  151. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/index.mdx +0 -0
  152. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/jupyter-server.mdx +0 -0
  153. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/kaggle.mdx +0 -0
  154. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/modal.mdx +0 -0
  155. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docs/providers/monty.mdx +0 -0
  156. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/docusaurus.config.js +0 -0
  157. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/package.json +0 -0
  158. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/sidebars.js +0 -0
  159. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow1.js +0 -0
  160. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow1.module.css +0 -0
  161. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow2.js +0 -0
  162. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow2.module.css +0 -0
  163. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow3.js +0 -0
  164. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow3.module.css +0 -0
  165. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/css/custom.css +0 -0
  166. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/pages/index.module.css +0 -0
  167. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/pages/markdown-page.md +0 -0
  168. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/theme/CustomDocItem.tsx +0 -0
  169. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/src/theme/IconsTitle.tsx +0 -0
  170. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/bricks.svg +0 -0
  171. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/building-construction.svg +0 -0
  172. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/cloud-sun.svg +0 -0
  173. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/cloud.svg +0 -0
  174. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/collaboration.svg +0 -0
  175. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/copy-left.svg +0 -0
  176. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/logo.png +0 -0
  177. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/logo.svg +0 -0
  178. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/personas/business.svg +0 -0
  179. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  180. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/personas/devops.svg +0 -0
  181. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/project/project-line.svg +0 -0
  182. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/favicon.ico +0 -0
  183. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/jupyter.svg +0 -0
  184. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/open-source.svg +0 -0
  185. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/react-js.svg +0 -0
  186. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/react-js2.svg +0 -0
  187. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/ringed-planet.svg +0 -0
  188. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/robot.svg +0 -0
  189. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/rocket.svg +0 -0
  190. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/shield.svg +0 -0
  191. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/sparkles.svg +0 -0
  192. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/streamlit.gif +0 -0
  193. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/super-hero.svg +0 -0
  194. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/target.svg +0 -0
  195. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/docs/static/img/thumbs-up.svg +0 -0
  196. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/README.md +0 -0
  197. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/Makefile +0 -0
  198. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/cloudflare_sandbox_example.py +0 -0
  199. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/coreweave_sandbox_example.py +0 -0
  200. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/datalayer_sandbox_example.py +0 -0
  201. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/daytona_sandbox_example.py +0 -0
  202. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/docker_sandbox_example.py +0 -0
  203. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/e2b_sandbox_example.py +0 -0
  204. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/eval_sandbox_example.py +0 -0
  205. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/google_colab_sandbox_example.py +0 -0
  206. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  207. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/kaggle_sandbox_example.py +0 -0
  208. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/modal_sandbox_example.py +0 -0
  209. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/exec/monty_sandbox_example.py +0 -0
  210. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/Makefile +0 -0
  211. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/cloudflare_sandbox_example.py +0 -0
  212. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/coreweave_sandbox_example.py +0 -0
  213. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/datalayer_sandbox_example.py +0 -0
  214. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/daytona_sandbox_example.py +0 -0
  215. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/docker_sandbox_example.py +0 -0
  216. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/e2b_sandbox_example.py +0 -0
  217. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/eval_sandbox_example.py +0 -0
  218. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/google_colab_sandbox_example.py +0 -0
  219. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  220. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/kaggle_sandbox_example.py +0 -0
  221. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/modal_sandbox_example.py +0 -0
  222. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/examples/repl/monty_sandbox_example.py +0 -0
  223. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/__init__.py +0 -0
  224. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/conftest.py +0 -0
  225. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_builds.py +0 -0
  226. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_cli_exec.py +0 -0
  227. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_cli_repl.py +0 -0
  228. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_client_contents.py +0 -0
  229. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_client_files.py +0 -0
  230. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_client_outcome_error_output.py +0 -0
  231. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_cloudflare.py +0 -0
  232. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_console.py +0 -0
  233. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_coreweave.py +0 -0
  234. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_canonical.py +0 -0
  235. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_constraints.py +0 -0
  236. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_doctor.py +0 -0
  237. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_environment_lifecycle.py +0 -0
  238. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  239. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_factory.py +0 -0
  240. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_google_colab.py +0 -0
  241. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_integration.py +0 -0
  242. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_jupyter_ingress.py +0 -0
  243. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_jupyter_server_streaming.py +0 -0
  244. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_kaggle.py +0 -0
  245. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_kaggle_execute.py +0 -0
  246. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_kaggle_live.py +0 -0
  247. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_kaggle_session.py +0 -0
  248. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_kernel_client_compatibility.py +0 -0
  249. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_lifecycle_vocabulary.py +0 -0
  250. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_manage.py +0 -0
  251. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_manage_datalayer.py +0 -0
  252. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_modal_google_colab_sandbox.py +0 -0
  253. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_models.py +0 -0
  254. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_output_line_fidelity.py +0 -0
  255. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_provider_catalog_names.py +0 -0
  256. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_provider_ingress.py +0 -0
  257. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_providers.py +0 -0
  258. {code_sandboxes-1.7.0 → code_sandboxes-1.8.2}/tests/test_sandboxes.py +0 -0
@@ -16,7 +16,7 @@ jobs:
16
16
  uses: ./.github/workflows/reusable-python.yml
17
17
  with:
18
18
  python-version: ${{ matrix.python-version }}
19
- install-extras: test,typing
19
+ install-extras: test,typing,datalayer
20
20
  extra-packages: dbus-python keyring secretstorage
21
21
  run-tests: true
22
22
  test-command: pytest tests/ --cov=code_sandboxes --cov-report term-missing
@@ -37,7 +37,7 @@ jobs:
37
37
  uses: ./.github/workflows/reusable-python.yml
38
38
  with:
39
39
  python-version: ${{ matrix.python-version }}
40
- install-extras: test
40
+ install-extras: test,datalayer
41
41
  extra-packages: dbus-python keyring secretstorage
42
42
  run-tests: true
43
43
  test-command: pytest tests/ --cov=code_sandboxes --cov-report term-missing
@@ -9,6 +9,11 @@ repos:
9
9
  - id: check-case-conflict
10
10
  - id: check-executables-have-shebangs
11
11
  - id: requirements-txt-fixer
12
+ # Datalayer's protected constraints (PLAN_ENV.md, E1-04): the header
13
+ # explains why it is ordered the way it is, and the fixer's sort
14
+ # would push that header below a package line rather than order the
15
+ # packages beneath it.
16
+ exclude: ^code_sandboxes/environments/constraints/sandbox-contract-v1\.txt$
12
17
  - id: check-added-large-files
13
18
  - id: check-case-conflict
14
19
  - id: check-toml
@@ -8,6 +8,54 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.8.2
12
+
13
+ - **A sandbox can be asked whether it is still alive, and the answer no longer
14
+ comes from a flag we set ourselves.** `CodeSandboxClient.is_alive()` returned
15
+ `is_started`, which records that `start()` ran in this process and nothing
16
+ else, so it kept answering `True` after the backend was gone. `Sandbox` now
17
+ carries an `is_alive()` that variants override; the Jupyter Server variant
18
+ looks its kernel id up on the server the way `_find_existing_kernel` lists
19
+ them, and the client asks the sandbox instead of reading its own flag.
20
+ Variants that have no way to ask their provider inherit the base answer,
21
+ `is_started`, which is all they can honestly say.
22
+ - **Build secrets** (`environments.build_secrets`, PLAN_ENV.md E3-05).
23
+ `spec.buildSecrets` now resolves for real on the Datalayer variant: each
24
+ declared secret's value is fetched from IAM's own internal route at the
25
+ moment the `postInstall` step runs, mounted with BuildKit's
26
+ `--mount=type=secret` (an environment variable or a file under
27
+ `/run/secrets/`, per `mountAs`) in its own directory outside the build
28
+ context, and passed to `buildctl` by file path — never in argv, never in a
29
+ step result, never an `ARG`/`ENV` that would bake it into the image's
30
+ history. E2B and Daytona refuse a spec naming one outright: E0-04's spike
31
+ found only a registry login for the private base on either, never a
32
+ per-step arbitrary secret. A version with any build secret can never be
33
+ published to the Library (D-12). New codes `DL_ENV_BUILD_SECRET_UNAVAILABLE`
34
+ and `DL_ENV_PUBLICATION_BLOCKED`.
35
+
36
+ ## 1.8.0
37
+
38
+ - **The Datalayer builder** (`environments.adapters.datalayer`, PLAN_ENV.md
39
+ E1-07). The Dockerfile is generated from the lock — `uv pip sync --require-hashes`, apt at the versions the lock recorded, `env` before
40
+ anything installs, `postInstall` as uid 1000 with no network — and the push
41
+ is by digest with an SBOM and provenance attestation, under the operability
42
+ tag `v<n>-<build_uid>` so a retried build cannot collide with the attempt
43
+ before it. `inspect`, `resolve`, `exists` and `delete` go through the ECR
44
+ API. Needs the `environments-builder` extra.
45
+ - **The scan and the signature** (`environments.policy`,
46
+ `environments.attest`, E1-08, E1-09). A critical finding with a fixed
47
+ version blocks and one nothing fixes is recorded; the decision record keeps
48
+ the threshold it was decided under, so what stopped a build reads a month
49
+ later. cosign signs only once the scan passed, and a replay finds the
50
+ signature rather than pushing a second.
51
+ - **A sandbox launches from an artifact** (E2-02): `Sandbox.create(artifact=…)`
52
+ hands each variant its own argument — an E2B template build, a Daytona
53
+ snapshot, a Modal image id through `Image.from_id`.
54
+ - **Fixed, and a live defect:** Daytona's adapter took the image branch
55
+ whenever resources were requested, so a snapshot asked for with `cpu=` came
56
+ up from a plain Debian image running none of the snapshot's content, with
57
+ nothing saying so. The combination is refused (correction 13).
58
+
11
59
  ## 1.7.0
12
60
 
13
61
  - **A version is resolved into one lock** (`code_sandboxes.environments.resolve`,
@@ -56,8 +104,7 @@
56
104
  pinned exceptions left.
57
105
 
58
106
  - A Datalayer sandbox says when it is running code, so an interrupt can reach
59
- it. `Sandbox.interrupt` refuses before it delegates — `if not
60
- self._executing_event.is_set(): return False` — and `run_code` never set that
107
+ it. `Sandbox.interrupt` refuses before it delegates — `if not self._executing_event.is_set(): return False` — and `run_code` never set that
61
108
  event, so `is_executing` was always False, every interrupt was refused at the
62
109
  door, and the refusal was reported as "no code was running" to a caller
63
110
  watching a cell run.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code_sandboxes
3
- Version: 1.7.0
3
+ Version: 1.8.2
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
@@ -73,6 +73,9 @@ Provides-Extra: docker
73
73
  Requires-Dist: docker>=6.0; extra == 'docker'
74
74
  Provides-Extra: e2b
75
75
  Requires-Dist: e2b-code-interpreter>=2.9.1; extra == 'e2b'
76
+ Provides-Extra: environments-builder
77
+ Requires-Dist: boto3>=1.34; extra == 'environments-builder'
78
+ Requires-Dist: httpx>=0.27; extra == 'environments-builder'
76
79
  Provides-Extra: google-colab
77
80
  Provides-Extra: kaggle
78
81
  Requires-Dist: kaggle>=1.6; extra == 'kaggle'
@@ -85,6 +88,7 @@ Requires-Dist: modal>=1.5.2; extra == 'modal'
85
88
  Provides-Extra: monty
86
89
  Requires-Dist: pydantic-monty; extra == 'monty'
87
90
  Provides-Extra: test
91
+ Requires-Dist: httpx>=0.27; extra == 'test'
88
92
  Requires-Dist: ipykernel; extra == 'test'
89
93
  Requires-Dist: jupyter-server<3,>=1.6; extra == 'test'
90
94
  Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
@@ -3,4 +3,4 @@
3
3
 
4
4
  """Code Sandboxes."""
5
5
 
6
- __version__ = "1.7.0"
6
+ __version__ = "1.8.2"
@@ -198,6 +198,17 @@ class Sandbox(ABC):
198
198
  """Check if sandbox has been started."""
199
199
  return self._started
200
200
 
201
+ def is_alive(self) -> bool:
202
+ """Check whether the sandbox is still usable.
203
+
204
+ Distinct from :attr:`is_started`, which only records that `start` ran
205
+ in this process: a sandbox can be culled, evicted or shut down by
206
+ whoever else holds it, and nothing tells us. The base answer is
207
+ :attr:`is_started`, all a variant with no way to ask its provider can
208
+ honestly say; variants that can ask override this.
209
+ """
210
+ return self.is_started
211
+
201
212
  @property
202
213
  def is_executing(self) -> bool:
203
214
  """Check if the sandbox is currently executing code."""
@@ -508,6 +519,7 @@ class Sandbox(ABC):
508
519
  allowed_hosts: list[str] | None = None,
509
520
  tags: dict[str, str] | None = None,
510
521
  examples: list[tuple[str, str]] | None = None,
522
+ artifact: Any = None,
511
523
  **kwargs,
512
524
  ) -> Sandbox:
513
525
  """Factory method to create a sandbox of the specified variant.
@@ -589,6 +601,17 @@ class Sandbox(ABC):
589
601
 
590
602
  variant_value = normalize_variant(variant)
591
603
 
604
+ if artifact is not None:
605
+ # One neutral reference, translated into the argument its variant
606
+ # takes: a template build for E2B, a snapshot for Daytona, an
607
+ # image id for Modal (PLAN_ENV.md E2-02). A caller holding an
608
+ # Environment's artifact should not have to know which. What the
609
+ # caller passed itself wins, so naming both is not a surprise.
610
+ from .environments.builders import launch_arguments
611
+
612
+ for name_, value in launch_arguments(artifact, expected_variant=variant_value).items():
613
+ kwargs.setdefault(name_, value)
614
+
592
615
  if variant_value == "eval":
593
616
  sandbox = EvalSandbox(config=config, **kwargs)
594
617
  elif variant_value == "docker":
@@ -1066,7 +1089,7 @@ class Sandbox(ABC):
1066
1089
  from .exceptions import VariableNotFoundError
1067
1090
 
1068
1091
  raise VariableNotFoundError(name)
1069
- return _json.loads(printed[start + len(self._STDOUT_START):end])
1092
+ return _json.loads(printed[start + len(self._STDOUT_START) : end])
1070
1093
 
1071
1094
  def set_variable(self, name: str, value: Any, context: Context | None = None) -> None:
1072
1095
  """Set a variable in the sandbox.
@@ -497,7 +497,14 @@ class CodeSandboxClient:
497
497
  return self._sandbox.interrupt()
498
498
 
499
499
  def is_alive(self) -> bool:
500
- """Whether the sandbox is started and available for execution."""
500
+ """Whether the wrapped sandbox is still available for execution.
501
+
502
+ Asks the sandbox, which may check its provider. Sandboxes that expose
503
+ no liveness hook fall back to whether they report themselves started.
504
+ """
505
+ is_alive_fn = getattr(self._sandbox, "is_alive", None)
506
+ if callable(is_alive_fn):
507
+ return bool(is_alive_fn())
501
508
  return self.is_started
502
509
 
503
510
  def restart(self) -> None:
@@ -284,7 +284,7 @@ print({_ENVELOPE_START!r} + __cmd_json__.dumps(__cmd_output__) + {_ENVELOPE_END!
284
284
  duration=time.time() - start_time,
285
285
  )
286
286
  try:
287
- result = json.loads(printed[start + len(_ENVELOPE_START):end])
287
+ result = json.loads(printed[start + len(_ENVELOPE_START) : end])
288
288
  return CommandResult(
289
289
  exit_code=int(result["exit_code"]),
290
290
  stdout=str(result["stdout"]),
@@ -55,7 +55,6 @@ from .models import (
55
55
  SnapshotInfo,
56
56
  )
57
57
 
58
-
59
58
  logger = logging.getLogger(__name__)
60
59
 
61
60
 
@@ -580,10 +579,11 @@ class DatalayerSandbox(Sandbox):
580
579
  **This variant neither implemented it nor read the flag**, so the
581
580
  base class's default ran instead: it sets a flag, returns `True`, and
582
581
  stops nothing. (`google_colab` and `kaggle` are entitled to that
583
- default — they poll `_interrupt_requested` and stop cooperatively.) Everything above believed it. `tasks/cancel` marks a
584
- task `cancelled` and calls the interrupt registered by
585
- `execute_cell`; the interrupt answered yes; the cell went on running
586
- on a runtime that goes on being billed.
582
+ default — they poll `_interrupt_requested` and stop cooperatively.)
583
+ Everything above believed it. `tasks/cancel` marks a task `cancelled`
584
+ and calls the interrupt registered by `execute_cell`; the interrupt
585
+ answered yes; the cell went on running on a runtime that goes on
586
+ being billed.
587
587
 
588
588
  Measured on prod1 on 2026-09-07: a cell looping for two minutes,
589
589
  cancelled ten seconds in, `tasks/cancel` and `tasks/get` both
@@ -603,13 +603,11 @@ class DatalayerSandbox(Sandbox):
603
603
  """
604
604
  client = getattr(self._runtime, "sandbox_client", None)
605
605
  if client is None:
606
- logger.warning(
607
- "This sandbox has no runtime client, so nothing could be interrupted"
608
- )
606
+ logger.warning("This sandbox has no runtime client, so nothing could be interrupted")
609
607
  return False
610
608
  try:
611
609
  return bool(client.interrupt())
612
- except Exception as error: # noqa: BLE001 - a failed interrupt is an answer
610
+ except Exception as error:
613
611
  logger.warning("The runtime's kernel could not be interrupted: %s", error)
614
612
  return False
615
613
 
@@ -817,7 +815,9 @@ class DatalayerSandbox(Sandbox):
817
815
  try:
818
816
  # Set environment variables if provided
819
817
  if envs:
820
- env_code = "\n".join(f"import os; os.environ[{k!r}] = {v!r}" for k, v in envs.items())
818
+ env_code = "\n".join(
819
+ f"import os; os.environ[{k!r}] = {v!r}" for k, v in envs.items()
820
+ )
821
821
  self._runtime.execute(env_code)
822
822
 
823
823
  response = self._runtime.execute(code, timeout=execution_timeout)
@@ -510,8 +510,13 @@ class DaytonaSandbox(Sandbox):
510
510
  given = {key: value for key, value in settings.items() if value}
511
511
  return daytona.DaytonaConfig(**given) if given else None
512
512
 
513
- def _create_params(self, daytona: Any) -> Any:
514
- """What to ask Daytona for, from the configuration of this sandbox."""
513
+ def _create_params(self, daytona: Any) -> Any: # noqa: C901
514
+ """What to ask Daytona for, from the configuration of this sandbox.
515
+
516
+ Read top to bottom: what it refuses — a snapshot with resources, a
517
+ snapshot with an image — is the rule, and splitting the refusals
518
+ across helpers would hide them from the one place a reader looks.
519
+ """
515
520
  common: dict[str, Any] = {"labels": self._labels()}
516
521
  if self.config.env_vars:
517
522
  common["env_vars"] = dict(self.config.env_vars)
@@ -538,6 +543,27 @@ class DaytonaSandbox(Sandbox):
538
543
  common["auto_delete_interval"] = 0
539
544
  if self._spot:
540
545
  common.update(self._spot_params(resources))
546
+ if self._snapshot is not None:
547
+ # A snapshot carries the machine it was created for — Daytona bakes
548
+ # CPU, memory, disk and GPU into it, and it is region-scoped — so
549
+ # asking for resources beside one asks for two different machines.
550
+ # This used to take the image branch instead: the sandbox came up
551
+ # from a Debian image with the resources asked for, running none of
552
+ # the content the snapshot held, and nothing said so (PLAN_ENV.md
553
+ # correction 13, E2-02). A resource change is a new snapshot.
554
+ if resources is not None:
555
+ raise SandboxConfigurationError(
556
+ "A Daytona snapshot carries its own CPU, memory, disk and GPU, so "
557
+ "cpu=, memory= and gpu= cannot be passed with snapshot=: the sandbox "
558
+ "would have come up from a plain image instead, with none of the "
559
+ "snapshot's content. Create a snapshot for the machine you want, or "
560
+ "pass image= with the resources."
561
+ )
562
+ if self._image is not None:
563
+ raise SandboxConfigurationError(
564
+ "snapshot= and image= are two different things to start from; pass one. "
565
+ "A snapshot is what an Environment build records for this variant."
566
+ )
541
567
  if self._image is not None or resources is not None:
542
568
  image = self._image
543
569
  if image is None:
@@ -547,6 +573,8 @@ class DaytonaSandbox(Sandbox):
547
573
  else daytona.Image.debian_slim()
548
574
  )
549
575
  return daytona.CreateSandboxFromImageParams(image=image, resources=resources, **common)
576
+ # No snapshot named and nothing asked for: Daytona's own default, which
577
+ # is what `snapshot=None` means to it.
550
578
  return daytona.CreateSandboxFromSnapshotParams(snapshot=self._snapshot, **common)
551
579
 
552
580
  def _spot_params(self, resources: Any | None) -> dict[str, Any]:
@@ -10,19 +10,18 @@ through `jupyter-kernel-client` to execute code.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import logging
13
14
  import os
14
15
  import tempfile
15
16
  import time
16
17
  import uuid
17
18
 
18
- import logging
19
-
20
19
  import requests
21
20
 
22
21
  from .base import Sandbox, marks_execution
23
- from .jupyter_ingress import interrupt_kernel_client
24
22
  from .exceptions import SandboxConfigurationError, SandboxNotStartedError
25
23
  from .interfaces import ISandboxClient
24
+ from .jupyter_ingress import interrupt_kernel_client
26
25
  from .models import (
27
26
  CodeError,
28
27
  Context,
@@ -527,11 +527,19 @@ class E2BSandbox(Sandbox):
527
527
  on_result(translated)
528
528
 
529
529
  seconds = timeout if timeout is not None else self.config.timeout
530
+ # A language and a context are two ways of saying the same thing to
531
+ # E2B, and its SDK refuses both at once: "You can provide context or
532
+ # language, but not both at the same time." A context already carries
533
+ # the language it was created with, so the context wins when there is
534
+ # one — which is what check 14 of E0-04 found here (PLAN_ENV E2-02).
535
+ e2b_context = self._e2b_context(context)
536
+ chosen: dict[str, Any] = (
537
+ {"context": e2b_context} if e2b_context is not None else {"language": "python"}
538
+ )
530
539
  try:
531
540
  execution = self._sandbox.run_code(
532
541
  code,
533
- language="python",
534
- context=self._e2b_context(context),
542
+ **chosen,
535
543
  on_stdout=take_stdout,
536
544
  on_stderr=take_stderr,
537
545
  on_result=take_result,
@@ -15,6 +15,9 @@ provider-specific:
15
15
 
16
16
  - :mod:`.spec` — the canonical specification and its rules;
17
17
  - :mod:`.resolve` — the lock a version is resolved into, once (D-9);
18
+ - :mod:`.policy` — whether an artifact's scan lets it be used (D-11);
19
+ - :mod:`.accounts` — whose provider account a managed artifact lives in (D-8);
20
+ - :mod:`.attest` — the scan and the signature it needs before anything runs it;
18
21
  - :mod:`.contract` — ``sandbox-contract/v1`` and the Dockerfile validator;
19
22
  - :mod:`.lifecycle` — the version states and their legal moves;
20
23
  - :mod:`.errors` — the error taxonomy every provider error maps into;
@@ -28,6 +31,8 @@ The builders themselves are in :mod:`.adapters`, loaded by variant.
28
31
 
29
32
  from __future__ import annotations
30
33
 
34
+ from .accounts import account_fingerprints, fingerprint_matches, provider_account
35
+ from .attest import Attestor, attest_artifact, signature_tag
31
36
  from .bases import APPROVED_BASES, ApprovedBase
32
37
  from .builders import (
33
38
  ArtifactReference,
@@ -50,6 +55,7 @@ from .contract import (
50
55
  )
51
56
  from .errors import ERROR_CODES, EnvironmentsError, ErrorCode, map_provider_error
52
57
  from .lifecycle import VersionState, build_outcome, can_transition, transition
58
+ from .policy import DEFAULT_POLICY, PolicyDecision, ScanPolicy, decide, refuse_if_blocked
53
59
  from .resolve import (
54
60
  LOCK_FORMAT,
55
61
  BuildkitResolveRunner,
@@ -76,6 +82,7 @@ __all__ = [
76
82
  "API_VERSION",
77
83
  "APPROVED_BASES",
78
84
  "CONTRACT_V1",
85
+ "DEFAULT_POLICY",
79
86
  "ERROR_CODES",
80
87
  "LOCK_FORMAT",
81
88
  "SANDBOX_CONTRACT_V1",
@@ -83,8 +90,9 @@ __all__ = [
83
90
  "VARIANTS",
84
91
  "ApprovedBase",
85
92
  "ArtifactReference",
86
- "BuildkitResolveRunner",
93
+ "Attestor",
87
94
  "BuildRequest",
95
+ "BuildkitResolveRunner",
88
96
  "CapabilityReport",
89
97
  "CapabilitySet",
90
98
  "CheckResult",
@@ -94,23 +102,32 @@ __all__ = [
94
102
  "EnvironmentsError",
95
103
  "ErrorCode",
96
104
  "LocalResolveRunner",
105
+ "PolicyDecision",
97
106
  "ResolveRunner",
98
107
  "SandboxContract",
108
+ "ScanPolicy",
99
109
  "ValidationResult",
100
110
  "VersionState",
111
+ "account_fingerprints",
112
+ "attest_artifact",
101
113
  "build_outcome",
102
114
  "can_transition",
103
115
  "canonical_digest",
104
116
  "canonical_json",
105
117
  "check_dockerfile",
118
+ "decide",
119
+ "fingerprint_matches",
106
120
  "get_builder",
107
121
  "locked_versions",
108
- "merge_requirements",
109
122
  "map_provider_error",
123
+ "merge_requirements",
110
124
  "parse_environment",
111
125
  "parse_resolver_failure",
112
126
  "protected_pins",
127
+ "provider_account",
128
+ "refuse_if_blocked",
113
129
  "resolve_environment",
130
+ "signature_tag",
114
131
  "spec_digest",
115
132
  "spec_findings",
116
133
  "transition",
@@ -0,0 +1,143 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ #
3
+ # BSD 3-Clause License
4
+
5
+ """Whose provider account an artifact lives in (PLAN_ENV.md, D-8, E2-01).
6
+
7
+ A managed-variant build runs with the **environment owner's** provider
8
+ secret — their E2B team, their Daytona organization, their Modal workspace —
9
+ and the artifact it produces exists only there. So a launch has to answer a
10
+ question the artifact's reference cannot: *are the credentials in this
11
+ caller's hands the ones that can see it?* A template id from another team is
12
+ not a missing template, it is somebody else's; launching it would either fail
13
+ obscurely or, worse, start something unrelated that happens to share a name.
14
+
15
+ This module answers it without holding a secret and without asking a provider
16
+ anything:
17
+
18
+ - **A fingerprint is derived from the credentials themselves.** The account's
19
+ own identifier when the caller's configuration names one — Daytona's
20
+ organization, Modal's workspace — and otherwise a digest of the credential
21
+ that opens the account. A digest identifies the account exactly (the same
22
+ key always answers the same thing) and reveals nothing.
23
+ - **An account answers to several names**, so both sides compute the whole set
24
+ and a match on any one is a match. That is what lets an artifact recorded
25
+ before the organization was configured still launch after it is.
26
+ - **No provider call.** Asking E2B who a key belongs to would make a launch
27
+ wait on a third party to decide whether it may proceed, and would need a
28
+ credential scope the launch path does not otherwise need.
29
+
30
+ The comparison is deliberately conservative: **no fingerprint on either side
31
+ is not a match.** An artifact recorded by an older build, or a caller with no
32
+ provider secret at all, is refused with `provider_account_mismatch` rather
33
+ than let through — the failure of a refusal is a launch somebody has to
34
+ retry, and the failure of a pass is a sandbox running in an account nobody
35
+ chose.
36
+
37
+ @module code_sandboxes.environments.accounts
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import hashlib
43
+ from collections.abc import Mapping, Sequence
44
+
45
+ __all__ = [
46
+ "ACCOUNT_VARIABLES",
47
+ "CREDENTIAL_VARIABLES",
48
+ "account_fingerprints",
49
+ "fingerprint_matches",
50
+ "provider_account",
51
+ ]
52
+
53
+ #: What names an account, per variant, when the caller's configuration says
54
+ #: so. Read from the same variables the SDKs read, so a deployment that can
55
+ #: reach a provider can also name its account there — nothing extra to set.
56
+ ACCOUNT_VARIABLES: dict[str, tuple[str, ...]] = {
57
+ # Daytona takes the organization explicitly, and a JWT credential is only
58
+ # usable with it.
59
+ "daytona": ("DAYTONA_ORGANIZATION_ID",),
60
+ # Modal names the workspace in its profile; `MODAL_WORKSPACE` is what a
61
+ # deployment sets when it holds several.
62
+ "modal": ("MODAL_WORKSPACE", "MODAL_PROFILE"),
63
+ # E2B's API key belongs to a team, and its API answers which only to an
64
+ # access token — which a launch does not hold. A deployment that wants the
65
+ # team in the record sets it; otherwise the credential's digest stands for
66
+ # it.
67
+ "e2b": ("E2B_TEAM_ID",),
68
+ }
69
+
70
+ #: The credential whose digest stands for the account when nothing names it.
71
+ #: The first variable present is used, so the pair of a token id and its
72
+ #: secret digests on the id — the half that identifies, not the half that
73
+ #: authenticates.
74
+ CREDENTIAL_VARIABLES: dict[str, tuple[str, ...]] = {
75
+ "daytona": ("DAYTONA_API_KEY", "DAYTONA_JWT_TOKEN"),
76
+ "modal": ("MODAL_TOKEN_ID",),
77
+ "e2b": ("E2B_API_KEY",),
78
+ }
79
+
80
+
81
+ def _digest(value: str) -> str:
82
+ """A short, stable, non-reversible name for a credential.
83
+
84
+ Sixteen hex characters of sha256: enough that two accounts do not collide,
85
+ short enough to read in a record, and nothing anybody can turn back into
86
+ the key.
87
+ """
88
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]
89
+
90
+
91
+ def account_fingerprints(variant: str, secrets: Mapping[str, str] | None) -> tuple[str, ...]:
92
+ """Every name the account these secrets open answers to, strongest first.
93
+
94
+ `("daytona:org-42", "daytona:cred-1f2e3d4c5b6a7980")` — the organization
95
+ the caller named, and the digest of the key that opens it. A build records
96
+ the first; a launch compares against all of them, so an artifact recorded
97
+ before the organization was configured still launches after it is.
98
+
99
+ Empty when the secrets open no account of this variant, which is a
100
+ refusal and not a pass: see the module docstring.
101
+ """
102
+ name = str(variant or "").strip().lower()
103
+ held = {
104
+ str(key): str(value) for key, value in (secrets or {}).items() if str(value or "").strip()
105
+ }
106
+ if not held:
107
+ return ()
108
+ found: list[str] = []
109
+ for variable in ACCOUNT_VARIABLES.get(name, ()):
110
+ value = held.get(variable, "").strip()
111
+ if value:
112
+ found.append(f"{name}:{value}")
113
+ break
114
+ for variable in CREDENTIAL_VARIABLES.get(name, ()):
115
+ value = held.get(variable, "").strip()
116
+ if value:
117
+ found.append(f"{name}:cred-{_digest(value)}")
118
+ break
119
+ return tuple(found)
120
+
121
+
122
+ def provider_account(variant: str, secrets: Mapping[str, str] | None) -> str:
123
+ """The one fingerprint an artifact records: the strongest name available."""
124
+ found = account_fingerprints(variant, secrets)
125
+ return found[0] if found else ""
126
+
127
+
128
+ def fingerprint_matches(recorded: str, held: Sequence[str] | Mapping[str, str] | None) -> bool:
129
+ """Whether an artifact recorded for `recorded` may be launched by `held`.
130
+
131
+ `held` is what :func:`account_fingerprints` answered for the caller, or
132
+ the caller's secrets to work it out from. Nothing recorded, or nothing
133
+ held, is **not** a match.
134
+ """
135
+ wanted = str(recorded or "").strip()
136
+ if not wanted:
137
+ return False
138
+ if isinstance(held, Mapping):
139
+ variant = wanted.split(":", 1)[0]
140
+ names: Sequence[str] = account_fingerprints(variant, held)
141
+ else:
142
+ names = list(held or ())
143
+ return wanted in {str(name).strip() for name in names if str(name).strip()}