code-sandboxes 1.9.36__tar.gz → 1.9.37__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 (272) hide show
  1. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/PKG-INFO +1 -1
  2. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/__version__.py +1 -1
  3. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/datalayer.py +21 -0
  4. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/resolve.py +82 -0
  5. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/resolve_conda.py +15 -0
  6. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/specification.mdx +1 -1
  7. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/modal.mdx +24 -8
  8. code_sandboxes-1.9.37/tests/egress_logs/buildctl-base.log +16 -0
  9. code_sandboxes-1.9.37/tests/egress_logs/buildctl-curl.log +41 -0
  10. code_sandboxes-1.9.37/tests/egress_logs/buildctl-pip.log +43 -0
  11. code_sandboxes-1.9.37/tests/egress_logs/buildctl-uv.log +54 -0
  12. code_sandboxes-1.9.37/tests/egress_logs/micromamba.log +30 -0
  13. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_datalayer_builder.py +5 -2
  14. code_sandboxes-1.9.37/tests/test_environment_egress_refusals.py +146 -0
  15. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/build.yml +0 -0
  16. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/environments-live.yml +0 -0
  17. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/py-code-style.yml +0 -0
  18. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/py-tests.yml +0 -0
  19. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/py-typing.yml +0 -0
  20. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/reusable-python.yml +0 -0
  21. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.gitignore +0 -0
  22. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.licenserc.yaml +0 -0
  23. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.pre-commit-config.yaml +0 -0
  24. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.vscode/settings.json +0 -0
  25. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/CHANGELOG.md +0 -0
  26. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/Dockerfile +0 -0
  27. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/LICENSE +0 -0
  28. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/Makefile +0 -0
  29. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/README.md +0 -0
  30. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/__init__.py +0 -0
  31. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/base.py +0 -0
  32. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/bridge_mount.py +0 -0
  33. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/builds.py +0 -0
  34. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/cli.py +0 -0
  35. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/client.py +0 -0
  36. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/cloudflare_sandbox.py +0 -0
  37. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/commands.py +0 -0
  38. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/console.py +0 -0
  39. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/contents.py +0 -0
  40. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/coreweave_sandbox.py +0 -0
  41. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/datalayer_sandbox.py +0 -0
  42. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/daytona_sandbox.py +0 -0
  43. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/docker_sandbox.py +0 -0
  44. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/e2b_sandbox.py +0 -0
  45. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/__init__.py +0 -0
  46. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/__main__.py +0 -0
  47. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/accounts.py +0 -0
  48. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/__init__.py +0 -0
  49. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/daytona.py +0 -0
  50. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/e2b.py +0 -0
  51. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/managed.py +0 -0
  52. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/modal.py +0 -0
  53. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/attest.py +0 -0
  54. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/bases.py +0 -0
  55. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/breaker.py +0 -0
  56. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/build_secrets.py +0 -0
  57. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/builders.py +0 -0
  58. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/canonical.py +0 -0
  59. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/conformance.py +0 -0
  60. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
  61. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
  62. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
  63. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/contract.py +0 -0
  64. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/__init__.py +0 -0
  65. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/build.py +0 -0
  66. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
  67. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/report.schema.json +0 -0
  68. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/errors.py +0 -0
  69. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/files.py +0 -0
  70. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/image_import.py +0 -0
  71. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/lifecycle.py +0 -0
  72. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/policy.py +0 -0
  73. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/redact.py +0 -0
  74. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/schema.py +0 -0
  75. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/spec.py +0 -0
  76. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/eval_sandbox.py +0 -0
  77. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/exceptions.py +0 -0
  78. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/filesystem.py +0 -0
  79. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/google_colab.py +0 -0
  80. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/google_colab_sandbox.py +0 -0
  81. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/interfaces.py +0 -0
  82. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/jupyter_ingress.py +0 -0
  83. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/jupyter_server_sandbox.py +0 -0
  84. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle.py +0 -0
  85. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle_execute.py +0 -0
  86. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle_live.py +0 -0
  87. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle_sandbox.py +0 -0
  88. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/lifecycle.py +0 -0
  89. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/manage.py +0 -0
  90. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/modal_sandbox.py +0 -0
  91. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/models.py +0 -0
  92. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/monty_sandbox.py +0 -0
  93. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/provider_ingress.py +0 -0
  94. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/providers.py +0 -0
  95. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docker/Dockerfile +0 -0
  96. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/.gitignore +0 -0
  97. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/LICENSE +0 -0
  98. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/Makefile +0 -0
  99. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/README.md +0 -0
  100. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/babel.config.js +0 -0
  101. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/api-reference/_category_.yml +0 -0
  102. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/api-reference/index.mdx +0 -0
  103. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/cli/_category_.yml +0 -0
  104. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/cli/index.mdx +0 -0
  105. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/contribute/_category_.yml +0 -0
  106. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/contribute/index.mdx +0 -0
  107. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/_category_.yml +0 -0
  108. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/contract.mdx +0 -0
  109. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/index.mdx +0 -0
  110. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/spike-2026-09.mdx +0 -0
  111. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/examples/_category_.yml +0 -0
  112. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/examples/index.mdx +0 -0
  113. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/guide/_category_.yml +0 -0
  114. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/guide/index.mdx +0 -0
  115. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/index.mdx +0 -0
  116. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/install/_category_.yml +0 -0
  117. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/install/index.mdx +0 -0
  118. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/provider-ingress.mdx +0 -0
  119. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/_category_.yml +0 -0
  120. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/cloudflare.mdx +0 -0
  121. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/coreweave.mdx +0 -0
  122. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/datalayer.mdx +0 -0
  123. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/daytona.mdx +0 -0
  124. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/docker.mdx +0 -0
  125. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/e2b.mdx +0 -0
  126. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/eval.mdx +0 -0
  127. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/google-colab.mdx +0 -0
  128. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/index.mdx +0 -0
  129. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/jupyter-server.mdx +0 -0
  130. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/kaggle.mdx +0 -0
  131. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/monty.mdx +0 -0
  132. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docusaurus.config.js +0 -0
  133. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/package.json +0 -0
  134. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/sidebars.js +0 -0
  135. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow1.js +0 -0
  136. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow1.module.css +0 -0
  137. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow2.js +0 -0
  138. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow2.module.css +0 -0
  139. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow3.js +0 -0
  140. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow3.module.css +0 -0
  141. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/css/custom.css +0 -0
  142. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/pages/index.module.css +0 -0
  143. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/pages/markdown-page.md +0 -0
  144. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/theme/CustomDocItem.tsx +0 -0
  145. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/theme/IconsTitle.tsx +0 -0
  146. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/bricks.svg +0 -0
  147. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/building-construction.svg +0 -0
  148. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/cloud-sun.svg +0 -0
  149. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/cloud.svg +0 -0
  150. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/collaboration.svg +0 -0
  151. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/copy-left.svg +0 -0
  152. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/logo.png +0 -0
  153. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/logo.svg +0 -0
  154. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/personas/business.svg +0 -0
  155. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  156. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/personas/devops.svg +0 -0
  157. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/project/project-line.svg +0 -0
  158. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/favicon.ico +0 -0
  159. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/jupyter.svg +0 -0
  160. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/open-source.svg +0 -0
  161. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/react-js.svg +0 -0
  162. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/react-js2.svg +0 -0
  163. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/ringed-planet.svg +0 -0
  164. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/robot.svg +0 -0
  165. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/rocket.svg +0 -0
  166. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/shield.svg +0 -0
  167. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/sparkles.svg +0 -0
  168. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/streamlit.gif +0 -0
  169. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/super-hero.svg +0 -0
  170. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/target.svg +0 -0
  171. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/thumbs-up.svg +0 -0
  172. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/README.md +0 -0
  173. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/Makefile +0 -0
  174. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/cloudflare_sandbox_example.py +0 -0
  175. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/coreweave_sandbox_example.py +0 -0
  176. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/datalayer_sandbox_example.py +0 -0
  177. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/daytona_sandbox_example.py +0 -0
  178. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/docker_sandbox_example.py +0 -0
  179. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/e2b_sandbox_example.py +0 -0
  180. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/eval_sandbox_example.py +0 -0
  181. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/google_colab_sandbox_example.py +0 -0
  182. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/jupyter_server_sandbox_example.py +0 -0
  183. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/kaggle_sandbox_example.py +0 -0
  184. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/modal_sandbox_example.py +0 -0
  185. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/monty_sandbox_example.py +0 -0
  186. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/Makefile +0 -0
  187. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/cloudflare_sandbox_example.py +0 -0
  188. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/coreweave_sandbox_example.py +0 -0
  189. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/datalayer_sandbox_example.py +0 -0
  190. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/daytona_sandbox_example.py +0 -0
  191. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/docker_sandbox_example.py +0 -0
  192. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/e2b_sandbox_example.py +0 -0
  193. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/eval_sandbox_example.py +0 -0
  194. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/google_colab_sandbox_example.py +0 -0
  195. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/jupyter_server_sandbox_example.py +0 -0
  196. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/kaggle_sandbox_example.py +0 -0
  197. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/modal_sandbox_example.py +0 -0
  198. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/monty_sandbox_example.py +0 -0
  199. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/pyproject.toml +0 -0
  200. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/schemas/environment-v1alpha1.json +0 -0
  201. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/__init__.py +0 -0
  202. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/conftest.py +0 -0
  203. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
  204. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_bridge_mount.py +0 -0
  205. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_builds.py +0 -0
  206. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_cli_exec.py +0 -0
  207. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_cli_repl.py +0 -0
  208. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client.py +0 -0
  209. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client_contents.py +0 -0
  210. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client_files.py +0 -0
  211. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client_outcome_error_output.py +0 -0
  212. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_cloudflare.py +0 -0
  213. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_commands_envelope.py +0 -0
  214. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_console.py +0 -0
  215. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_contents_contract.py +0 -0
  216. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_contents_live_matrix.py +0 -0
  217. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_coreweave.py +0 -0
  218. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_datalayer_sandbox.py +0 -0
  219. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_daytona.py +0 -0
  220. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_e2b.py +0 -0
  221. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_accounts.py +0 -0
  222. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_attest.py +0 -0
  223. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_bases.py +0 -0
  224. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_breaker.py +0 -0
  225. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_build_secrets.py +0 -0
  226. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_builders.py +0 -0
  227. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_canonical.py +0 -0
  228. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_conformance.py +0 -0
  229. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_constraints.py +0 -0
  230. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_contract.py +0 -0
  231. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_cross_variant_report.py +0 -0
  232. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_daytona_builder.py +0 -0
  233. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_dockerfile_source.py +0 -0
  234. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_doctor.py +0 -0
  235. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_e2b_builder.py +0 -0
  236. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_errors.py +0 -0
  237. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_image_import.py +0 -0
  238. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_lifecycle.py +0 -0
  239. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_live_matrix.py +0 -0
  240. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_managed_builders.py +0 -0
  241. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_modal_builder.py +0 -0
  242. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_organization_policy.py +0 -0
  243. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_redact.py +0 -0
  244. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_resolve.py +0 -0
  245. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_resolve_conda.py +0 -0
  246. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_spec.py +0 -0
  247. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_spec_organization_policy.py +0 -0
  248. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_eval.py +0 -0
  249. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
  250. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_factory.py +0 -0
  251. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_google_colab.py +0 -0
  252. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_integration.py +0 -0
  253. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_jupyter_ingress.py +0 -0
  254. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_jupyter_server.py +0 -0
  255. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_jupyter_server_streaming.py +0 -0
  256. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle.py +0 -0
  257. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle_execute.py +0 -0
  258. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle_live.py +0 -0
  259. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle_session.py +0 -0
  260. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kernel_client_compatibility.py +0 -0
  261. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_lifecycle_vocabulary.py +0 -0
  262. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_manage.py +0 -0
  263. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_manage_datalayer.py +0 -0
  264. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_modal_google_colab_sandbox.py +0 -0
  265. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_modal_session.py +0 -0
  266. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_models.py +0 -0
  267. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_output_line_fidelity.py +0 -0
  268. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_provider_catalog_names.py +0 -0
  269. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_provider_ingress.py +0 -0
  270. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_providers.py +0 -0
  271. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_sandboxes.py +0 -0
  272. {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/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.36
3
+ Version: 1.9.37
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.36"
6
+ __version__ = "1.9.37"
@@ -78,6 +78,9 @@ from ..resolve import (
78
78
  apt_snapshot_in,
79
79
  buildkit_proxy,
80
80
  buildkit_proxy_options,
81
+ egress_findings,
82
+ egress_hosts_text,
83
+ egress_refused_hosts,
81
84
  locked_versions,
82
85
  )
83
86
  from ..resolve_conda import (
@@ -546,6 +549,24 @@ class Builder:
546
549
  self._log(f"Building {reference} from {request.resolved_base}")
547
550
  finished = self._invoke(command, timeout=self._max_build_seconds)
548
551
  if finished.returncode != 0:
552
+ refused = egress_refused_hosts(finished.stderr or "", proxy=self._proxy)
553
+ if refused:
554
+ # The one failure a log line hid best: a host the build
555
+ # pool may not reach, named as a finding (E2-19).
556
+ raise EnvironmentsError(
557
+ BUILD_FAILED,
558
+ f"The build pool's egress proxy refused {egress_hosts_text(refused)}: "
559
+ "a build reaches only the package indexes, mirrors and registries "
560
+ "the build pool allows. Put what it fetches in the build, or ask "
561
+ "for the host to be allowed",
562
+ detail={
563
+ "variant": self.variant,
564
+ "reference": reference,
565
+ "exit": finished.returncode,
566
+ "refused_hosts": refused,
567
+ "findings": egress_findings(refused),
568
+ },
569
+ )
549
570
  raise EnvironmentsError(
550
571
  BUILD_FAILED,
551
572
  "The build failed; its log says where",
@@ -101,6 +101,7 @@ __all__ = [
101
101
  "apt_snapshot_in",
102
102
  "buildkit_proxy",
103
103
  "buildkit_proxy_options",
104
+ "egress_refused_hosts",
104
105
  "lock_document",
105
106
  "locked_versions",
106
107
  "merge_requirements",
@@ -188,6 +189,74 @@ def buildkit_proxy_options(proxy: str) -> list[str]:
188
189
  return options
189
190
 
190
191
 
192
+ #: What each tool writes when the build pool's egress proxy refuses it a
193
+ #: tunnel (E2-19). Read from the tools themselves, through the chart's own
194
+ #: Squid refusing every host, on 2026-09-19: pip, uv 0.12, micromamba 2.3,
195
+ #: git, curl 8 without `-f`, wget, and a Go client — `buildkitd`'s own pulls.
196
+ #: Each is a refused CONNECT and nothing else: a host that answered 403 over
197
+ #: its own TLS says so differently. `curl -f` ("The requested URL returned
198
+ #: error: 403"), `wget -q` (nothing) and `apt` over plain HTTP ("403
199
+ #: Forbidden [IP: proxy]") cannot be told from the host's own 403, and are
200
+ #: left to the log.
201
+ _EGRESS_REFUSED = re.compile(
202
+ r"Tunnel connection failed: 403" # pip, requests, urllib3
203
+ r"|tunnel error: unsuccessful" # uv
204
+ r"|CONNECT tunnel failed, response 403" # curl, git, micromamba
205
+ r"|Proxy tunneling failed: Forbidden" # wget
206
+ r'|"https?://[^"\s]+": Forbidden' # Go: buildkitd, containerd
207
+ )
208
+ #: A host a line names: urllib3's `host='…'`, or the host of a URL.
209
+ _NAMED_HOST = re.compile(r"host='(?P<pool>[A-Za-z0-9.\-]+)'|https?://(?P<url>[A-Za-z0-9.\-]+)")
210
+ #: `buildctl`'s plain progress: `#12 0.874 <what the step wrote>`. Steps run
211
+ #: side by side, so a line is read against the lines of its own step.
212
+ _BUILDKIT_LINE = re.compile(r"^#(?P<step>\d+) (?:\d+\.\d+ )?(?P<text>.*)$")
213
+
214
+
215
+ def egress_refused_hosts(log: str, *, proxy: str = "", limit: int = 10) -> list[str]:
216
+ """The hosts the build pool's egress proxy refused, as a build's log shows them (E2-19).
217
+
218
+ A refusal is found by what the tool writes (`_EGRESS_REFUSED`), and its
219
+ host is the one the same line names — pip's `host='files.pythonhosted.org'`,
220
+ git's URL — or else the last one its step named before it: uv and
221
+ micromamba write the URL a line or four above the refusal, wget and a
222
+ silent `curl` only in the `RUN` line. The proxy's own address is never a
223
+ host it refused. In the order they were first refused, at most ``limit``.
224
+ """
225
+ own = {"127.0.0.1", "localhost"}
226
+ if proxy:
227
+ own.add(proxy.split("://", 1)[-1].rsplit(":", 1)[0])
228
+ hosts: list[str] = []
229
+ last: dict[str, str] = {}
230
+ for raw in log.splitlines():
231
+ line = _BUILDKIT_LINE.match(raw)
232
+ step, text = (line.group("step"), line.group("text")) if line else ("", raw)
233
+ named = [
234
+ host.lower().rstrip(".")
235
+ for match in _NAMED_HOST.finditer(text)
236
+ for host in (match.group("pool") or match.group("url"),)
237
+ if host and host.lower().rstrip(".") not in own
238
+ ]
239
+ if _EGRESS_REFUSED.search(text):
240
+ host = named[0] if named else last.get(step, "")
241
+ if host and host not in hosts:
242
+ hosts.append(host)
243
+ if len(hosts) >= limit:
244
+ break
245
+ if named:
246
+ last[step] = named[-1]
247
+ return hosts
248
+
249
+
250
+ def egress_findings(hosts: Sequence[str]) -> list[dict[str, str]]:
251
+ """The refused hosts as a failure's findings: what a page lists, not the log."""
252
+ return [{"kind": "egress_refused", "subject": host} for host in hosts]
253
+
254
+
255
+ def egress_hosts_text(hosts: Sequence[str]) -> str:
256
+ quoted = [f"`{host}`" for host in hosts]
257
+ return quoted[0] if len(quoted) == 1 else ", ".join(quoted[:-1]) + " and " + quoted[-1]
258
+
259
+
191
260
  #: How a protected pin is recorded in the lock.
192
261
  PROTECTED_PIN_PREFIX = "# datalayer-protected: "
193
262
 
@@ -521,6 +590,19 @@ def parse_resolver_failure(
521
590
  },
522
591
  )
523
592
 
593
+ # An index the build pool may not reach is not an outage: resolving again
594
+ # is refused again, so it is said with the host rather than retried.
595
+ refused = egress_refused_hosts(output)
596
+ if refused and "No solution found" not in text:
597
+ return EnvironmentsError(
598
+ PACKAGE_NOT_FOUND,
599
+ f"The build pool's egress proxy refused {egress_hosts_text(refused)}: a version "
600
+ "resolves only against the indexes the build pool allows. Use one of them, or "
601
+ "ask for the host to be allowed",
602
+ detail={**detail, "refused_hosts": refused, "findings": egress_findings(refused)},
603
+ retryable=False,
604
+ )
605
+
524
606
  pair = _conflicting_pair(mentions) if "No solution found" in text else None
525
607
  if pair:
526
608
  return EnvironmentsError(
@@ -70,6 +70,9 @@ from .resolve import (
70
70
  ProtectedPin,
71
71
  buildkit_proxy,
72
72
  buildkit_proxy_options,
73
+ egress_findings,
74
+ egress_hosts_text,
75
+ egress_refused_hosts,
73
76
  merge_requirements,
74
77
  )
75
78
 
@@ -529,6 +532,18 @@ def parse_conda_failure(output: str) -> EnvironmentsError:
529
532
  f"`{name}` is not in any channel this environment may read",
530
533
  detail={**detail, "package": name},
531
534
  )
535
+ # A channel the build pool may not reach: solving again is refused again,
536
+ # so it is said with the host rather than retried (E2-19).
537
+ refused = egress_refused_hosts(output)
538
+ if refused:
539
+ return EnvironmentsError(
540
+ PACKAGE_NOT_FOUND,
541
+ f"The build pool's egress proxy refused {egress_hosts_text(refused)}: a version "
542
+ "solves only against the channels the build pool allows. Use one of them, or "
543
+ "ask for the host to be allowed",
544
+ detail={**detail, "refused_hosts": refused, "findings": egress_findings(refused)},
545
+ retryable=False,
546
+ )
532
547
  lowered = text.lower()
533
548
  if (
534
549
  "could not solve" in lowered
@@ -37,7 +37,7 @@ A document is refused with `DL_ENV_SPEC_INVALID` when a field is wrong, and with
37
37
  | `commands.postInstall` | none | At most 32 non-empty commands, run after the packages are installed. |
38
38
  | `buildSecrets` | none | `id` is a Datalayer secret id, `dlsec_...`; `mountAs` is `env` or `file`; `name` is the variable or file name. Ids and names are unique. Secrets are injected into the build step that needs them and never written into the artifact. |
39
39
  | `resources.sizeClass` | `small` | `small`, `medium`, `large`, `gpu-small` or `gpu-large`. A GPU class needs an accelerator and a CUDA base; an accelerator needs a GPU class. Datalayer runs no GPU nodes yet, so a GPU class is built for Modal and Daytona, which bring their own GPUs; E2B has none. |
40
- | `resources.accelerator` | `none` | `none`, or `type`, `count` and `cuda`. `type` is the provider's own name for the GPU (Daytona: `H100`, `H200`, `RTX-PRO-6000`, `RTX-4090`, `RTX-5090`), and a GPU a variant does not offer is refused for it at `validate`. `cuda` is the base's toolkit: `datalayer/python-cuda:2026.09` carries 12.8, and another version is refused. |
40
+ | `resources.accelerator` | `none` | `none`, or `type`, `count` and `cuda`. `type` is the provider's own name for the GPU (Modal: `T4`, `L4`, `A10G`, `L40S`, `A100`, `A100-40GB`, `A100-80GB`, `H100`, `H200`, `B200`; Daytona: `H100`, `H200`, `RTX-PRO-6000`, `RTX-4090`, `RTX-5090`), and a GPU a variant does not offer is refused for it at `validate`. `cuda` is the base's toolkit: `datalayer/python-cuda:2026.09` carries 12.8, and another version is refused. |
41
41
  | `resources.hints` | none | `cpu`, `memoryGi`, `diskGi`: positive numbers, within the size class. |
42
42
  | `compatibility.variants.required` | `[datalayer]` | At least one of `datalayer`, `e2b`, `daytona`, `modal`. |
43
43
  | `compatibility.variants.optional` | none | The same variants, none of them also required. |
@@ -77,9 +77,11 @@ with Sandbox.create(
77
77
  An [Environment](/environments) built for Modal is an **image id**,
78
78
  `im-…` — pulled `from_aws_ecr` off the approved Datalayer base, with your
79
79
  lock installed by `uv pip sync --require-hashes` on top. A published name
80
- is worth having for operability, but a launch always pins the id: each
81
- build gets a fresh credential, so no layer cache survives between builds,
82
- and the id is the only reference that names one build and not another.
80
+ is worth having for operability, but a launch always pins the id: a name
81
+ moves when the same build is published again, and the id is the only
82
+ reference that names one build's image. Modal answers an identical image
83
+ definition with the image it already has, so two builds of the same lock
84
+ can share one id.
83
85
 
84
86
  **Bring your own account:** a build runs in the environment owner's own
85
87
  Modal workspace, from the same `MODAL_TOKEN_ID`/`MODAL_TOKEN_SECRET` pair
@@ -96,8 +98,22 @@ with no image id — general Modal sandbox usage here is unaffected).
96
98
  Confirmed live: a launched sandbox's identity check now passes, where it
97
99
  used to report `uid: 0`.
98
100
 
99
- **A known, open gap:** two of the core tier's checks — imports and
100
- filesystem — still fail on a launched Environment sandbox, for a reason
101
- not yet root-caused: reproduced on a real, contract-built image, not on a
102
- plain `ModalSandbox`, so it is specific to something about that image
103
- under repeated `exec`, not the identity fix above.
101
+ **The sandbox is held by `sleep infinity`, not the base's Jupyter.**
102
+ Modal keeps the base image's CMD, `start-jupyter.sh`, under the builder's
103
+ `ENTRYPOINT`, where Docker would reset it, so a sandbox started with no
104
+ command ran a Jupyter server as its main process. That server exits
105
+ within a minute and the sandbox ends with it: "Modal Sandbox is shutting
106
+ down", a few commands in. Since 1.9.36 the artifact's CMD is
107
+ `sleep infinity`, and `ModalSandbox` names `sleep infinity` when it
108
+ launches an artifact, which covers images built by earlier releases too.
109
+ Jupyter is started by exec when it is asked for
110
+ (`prepare_jupyter_server`).
111
+
112
+ **GPU versions.** On Modal a GPU is a launch option, not part of the
113
+ image: a GPU version's image builds like any other on the CUDA base, and
114
+ its sandboxes start on the GPU the version names. The build's smoke test
115
+ does that (`gpu="L4"`, or `"H100:2"` for two) and adds the contract's
116
+ check 11: the GPUs asked for are visible, and the image's CUDA is the
117
+ version's. A GPU Modal does not offer — it offers `T4`, `L4`, `A10G`,
118
+ `L40S`, `A100`, `A100-40GB`, `A100-80GB`, `H100`, `H200`, `B200` — is
119
+ refused at `validate`, before anything is built.
@@ -0,0 +1,16 @@
1
+ #1 [internal] load build definition from Dockerfile
2
+ #1 transferring dockerfile: 80B done
3
+ #1 DONE 0.0s
4
+
5
+ #2 [internal] load metadata for ghcr.io/astral-sh/uv:0.12.11
6
+ #2 ERROR: failed to do request: Head "https://ghcr.io/v2/astral-sh/uv/manifests/0.12.11": Forbidden
7
+ ------
8
+ > [internal] load metadata for ghcr.io/astral-sh/uv:0.12.11:
9
+ ------
10
+ Dockerfile:1
11
+ --------------------
12
+ 1 | >>> FROM ghcr.io/astral-sh/uv:0.12.11
13
+ 2 | RUN true
14
+ 3 |
15
+ --------------------
16
+ error: failed to solve: ghcr.io/astral-sh/uv:0.12.11: failed to resolve source metadata for ghcr.io/astral-sh/uv:0.12.11: failed to do request: Head "https://ghcr.io/v2/astral-sh/uv/manifests/0.12.11": Forbidden
@@ -0,0 +1,41 @@
1
+ #1 [internal] load build definition from Dockerfile
2
+ #1 transferring dockerfile: 325B done
3
+ #1 DONE 0.0s
4
+
5
+ #2 [internal] load metadata for docker.io/library/buildpack-deps:bookworm-scm
6
+ #2 DONE 0.2s
7
+
8
+ #3 [internal] load .dockerignore
9
+ #3 transferring context: 2B done
10
+ #3 DONE 0.0s
11
+
12
+ #4 [1/4] FROM docker.io/library/buildpack-deps:bookworm-scm@sha256:bb8654d03bd8341e702f1ae30adca91a11f4164da2d37dbe7a0bf49e4901bd03
13
+ #4 CACHED
14
+
15
+ #5 [2/4] RUN git clone -q https://github.com/psf/requests.git /tmp/r || true
16
+ #5 0.063 fatal: unable to access 'https://github.com/psf/requests.git/': CONNECT tunnel failed, response 403
17
+ #5 DONE 0.1s
18
+
19
+ #6 [3/4] RUN curl -fsSL https://raw.githubusercontent.com/psf/requests/main/README.md -o /dev/null || true
20
+ #6 0.065 curl: (22) The requested URL returned error: 403
21
+ #6 DONE 0.1s
22
+
23
+ #7 [4/4] RUN wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null
24
+ #7 0.060 --2026-09-19 08:44:09-- https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz
25
+ #7 0.066 Connecting to 172.18.0.2:3128... connected.
26
+ #7 0.067 Proxy tunneling failed: ForbiddenUnable to establish SSL connection.
27
+ #7 ERROR: process "/bin/sh -c wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null" did not complete successfully: exit code: 4
28
+ ------
29
+ > [4/4] RUN wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null:
30
+ 0.060 --2026-09-19 08:44:09-- https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz
31
+ 0.066 Connecting to 172.18.0.2:3128... connected.
32
+ 0.067 Proxy tunneling failed: ForbiddenUnable to establish SSL connection.
33
+ ------
34
+ Dockerfile:4
35
+ --------------------
36
+ 2 | RUN git clone -q https://github.com/psf/requests.git /tmp/r || true
37
+ 3 | RUN curl -fsSL https://raw.githubusercontent.com/psf/requests/main/README.md -o /dev/null || true
38
+ 4 | >>> RUN wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null
39
+ 5 |
40
+ --------------------
41
+ error: failed to solve: process "/bin/sh -c wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null" did not complete successfully: exit code: 4
@@ -0,0 +1,43 @@
1
+ #1 [internal] load build definition from Dockerfile
2
+ #1 transferring dockerfile: 192B done
3
+ #1 DONE 0.0s
4
+
5
+ #2 [internal] load metadata for docker.io/library/python:3.12-slim
6
+ #2 DONE 0.2s
7
+
8
+ #3 [internal] load .dockerignore
9
+ #3 transferring context: 2B done
10
+ #3 DONE 0.0s
11
+
12
+ #4 [1/3] FROM docker.io/library/python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9
13
+ #4 CACHED
14
+
15
+ #5 [2/3] RUN pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests
16
+ #5 0.887 Looking in indexes: https://pypi.example-private.io/simple
17
+ #5 0.890 WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
18
+ #5 1.391 WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
19
+ #5 2.393 WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
20
+ #5 4.395 WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
21
+ #5 8.397 WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
22
+ #5 8.401 ERROR: Could not find a version that satisfies the requirement requests (from versions: none)
23
+ #5 8.413 ERROR: No matching distribution found for requests
24
+ #5 ERROR: process "/bin/sh -c pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests" did not complete successfully: exit code: 1
25
+ ------
26
+ > [2/3] RUN pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests:
27
+ 0.887 Looking in indexes: https://pypi.example-private.io/simple
28
+ 0.890 WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
29
+ 1.391 WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
30
+ 2.393 WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
31
+ 4.395 WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
32
+ 8.397 WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
33
+ 8.401 ERROR: Could not find a version that satisfies the requirement requests (from versions: none)
34
+ 8.413 ERROR: No matching distribution found for requests
35
+ ------
36
+ Dockerfile:2
37
+ --------------------
38
+ 1 | FROM python:3.12-slim
39
+ 2 | >>> RUN pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests
40
+ 3 | RUN pip install --no-cache-dir requests
41
+ 4 |
42
+ --------------------
43
+ error: failed to solve: process "/bin/sh -c pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests" did not complete successfully: exit code: 1
@@ -0,0 +1,54 @@
1
+ #1 [internal] load build definition from Dockerfile
2
+ #1 transferring dockerfile: 193B done
3
+ #1 DONE 0.0s
4
+
5
+ #2 [internal] load metadata for docker.io/library/python:3.12-slim
6
+ #2 DONE 0.2s
7
+
8
+ #3 [internal] load .dockerignore
9
+ #3 transferring context: 2B done
10
+ #3 DONE 0.0s
11
+
12
+ #4 [1/3] FROM docker.io/library/python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9
13
+ #4 DONE 0.0s
14
+
15
+ #5 [internal] load build context
16
+ #5 transferring context: 26B done
17
+ #5 DONE 0.0s
18
+
19
+ #6 [2/3] COPY uv /usr/local/bin/uv
20
+ #6 CACHED
21
+
22
+ #7 [3/3] RUN uv pip install --system "requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz"
23
+ #7 0.175 Using Python 3.12.14 environment at: /usr/local
24
+ #7 9.090 × Failed to download and build `requests @
25
+ #7 9.090 │ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
26
+ #7 9.090 ├─▶ Request failed after 3 retries in 8.9s
27
+ #7 9.090 ├─▶ Failed to fetch:
28
+ #7 9.090 │ `https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
29
+ #7 9.090 ├─▶ error sending request for url
30
+ #7 9.090 │ (https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz)
31
+ #7 9.090 ├─▶ client error (Connect)
32
+ #7 9.090 ╰─▶ tunnel error: unsuccessful
33
+ #7 ERROR: process "/bin/sh -c uv pip install --system \"requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz\"" did not complete successfully: exit code: 1
34
+ ------
35
+ > [3/3] RUN uv pip install --system "requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz":
36
+ 0.175 Using Python 3.12.14 environment at: /usr/local
37
+ 9.090 × Failed to download and build `requests @
38
+ 9.090 │ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
39
+ 9.090 ├─▶ Request failed after 3 retries in 8.9s
40
+ 9.090 ├─▶ Failed to fetch:
41
+ 9.090 │ `https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
42
+ 9.090 ├─▶ error sending request for url
43
+ 9.090 │ (https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz)
44
+ 9.090 ├─▶ client error (Connect)
45
+ 9.090 ╰─▶ tunnel error: unsuccessful
46
+ ------
47
+ Dockerfile:3
48
+ --------------------
49
+ 1 | FROM python:3.12-slim
50
+ 2 | COPY uv /usr/local/bin/uv
51
+ 3 | >>> RUN uv pip install --system "requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz"
52
+ 4 |
53
+ --------------------
54
+ error: failed to solve: process "/bin/sh -c uv pip install --system \"requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz\"" did not complete successfully: exit code: 1
@@ -0,0 +1,30 @@
1
+ warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
2
+ CONNECT tunnel failed, response 403
3
+ warning libmamba Retrying in 2 seconds
4
+ warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/linux-64/repodata.json.zst]
5
+ CONNECT tunnel failed, response 403
6
+ warning libmamba Retrying in 2 seconds
7
+ warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
8
+ CONNECT tunnel failed, response 403
9
+ warning libmamba Retrying in 2 seconds
10
+ warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/linux-64/repodata.json.zst]
11
+ CONNECT tunnel failed, response 403
12
+ warning libmamba Retrying in 2 seconds
13
+ warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
14
+ CONNECT tunnel failed, response 403
15
+ warning libmamba Retrying in 2 seconds
16
+ warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/linux-64/repodata.json.zst]
17
+ CONNECT tunnel failed, response 403
18
+ warning libmamba Retrying in 2 seconds
19
+ warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
20
+ CONNECT tunnel failed, response 403
21
+ warning libmamba Retrying in 2 seconds
22
+ critical libmamba Multiple errors occurred:
23
+ Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
24
+ CONNECT tunnel failed, response 403
25
+ Subdir conda-forge/noarch not loaded!
26
+ If you run into this error repeatedly, your package cache may be corrupted.
27
+ Please try running `mamba clean -a` to remove this cache before retrying the operation.
28
+
29
+ If you still are having issues, please report the error on `mamba-org/mamba`'s issue tracker:
30
+ https://github.com/mamba-org/mamba/issues/new?assignees=&labels=&projects=&template=bug.yml
@@ -184,9 +184,12 @@ def a_builder(**changes) -> Builder:
184
184
  class Buildctl:
185
185
  """A `buildctl` that writes the metadata a successful build writes."""
186
186
 
187
- def __init__(self, *, digest: str | None = DIGEST, returncode: int = 0) -> None:
187
+ def __init__(
188
+ self, *, digest: str | None = DIGEST, returncode: int = 0, stderr: str = "solved\n"
189
+ ) -> None:
188
190
  self.digest = digest
189
191
  self.returncode = returncode
192
+ self.stderr = stderr
190
193
  self.argv: list[str] = []
191
194
  self.context: Path | None = None
192
195
  self.env: dict[str, str] | None = None
@@ -201,7 +204,7 @@ class Buildctl:
201
204
  Path(self.argv[position + 1]).write_text(
202
205
  json.dumps({"containerimage.digest": self.digest}), encoding="utf-8"
203
206
  )
204
- return subprocess.CompletedProcess(self.argv, self.returncode, "", "solved\n")
207
+ return subprocess.CompletedProcess(self.argv, self.returncode, "", self.stderr)
205
208
 
206
209
 
207
210
  # -- The Dockerfile ------------------------------------------------------------
@@ -0,0 +1,146 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ # Distributed under the terms of the Modified BSD License.
3
+
4
+ """A host the build pool's egress proxy refused, named as a finding (E2-19).
5
+
6
+ The logs under `egress_logs/` are real: `buildctl --progress=plain` against
7
+ `moby/buildkit:v0.33.0`, its steps sent through the build pool chart's own
8
+ Squid (`ubuntu/squid:6.6`, the chart's `squid.conf`, allowing `pypi.org`
9
+ alone), on 2026-09-19. Only the image layers' progress lines are left out.
10
+ `micromamba.log` is `micromamba create` 2.3.3 through the same proxy.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ from code_sandboxes.environments.errors import EnvironmentsError
20
+ from code_sandboxes.environments.resolve import (
21
+ egress_hosts_text,
22
+ egress_refused_hosts,
23
+ parse_resolver_failure,
24
+ )
25
+ from code_sandboxes.environments.resolve_conda import parse_conda_failure
26
+
27
+ from .test_environment_datalayer_builder import Buildctl, a_builder, a_request
28
+
29
+ LOGS = Path(__file__).parent / "egress_logs"
30
+
31
+
32
+ def a_log(name: str) -> str:
33
+ return (LOGS / name).read_text(encoding="utf-8")
34
+
35
+
36
+ class TestTheRefusedHostIsReadFromWhatEachToolWrites:
37
+ def test_pip_names_its_index_a_line_before_it_is_refused(self) -> None:
38
+ # pip's retries say `Tunnel connection failed: 403` with a path only;
39
+ # the host is the index it said it was looking in.
40
+ assert egress_refused_hosts(a_log("buildctl-pip.log")) == ["pypi.example-private.io"]
41
+
42
+ def test_uv_names_the_url_four_lines_above_its_tunnel_error(self) -> None:
43
+ assert egress_refused_hosts(a_log("buildctl-uv.log")) == ["github.com"]
44
+
45
+ def test_git_and_wget_are_named_and_curl_f_is_left_to_the_log(self) -> None:
46
+ # `curl -f` says only "returned error: 403", which a host answering
47
+ # 403 itself says too: not a finding.
48
+ assert egress_refused_hosts(a_log("buildctl-curl.log"), proxy="http://172.18.0.2:3128") == [
49
+ "github.com"
50
+ ]
51
+
52
+ def test_buildkitd_s_own_pull_of_a_base_is_named(self) -> None:
53
+ assert egress_refused_hosts(a_log("buildctl-base.log")) == ["ghcr.io"]
54
+
55
+ def test_micromamba_names_the_channel_a_line_above(self) -> None:
56
+ # The issue tracker's URL after the refusal is not what was refused.
57
+ assert egress_refused_hosts(a_log("micromamba.log")) == ["conda.anaconda.org"]
58
+
59
+ def test_a_log_with_no_refusal_names_nothing(self) -> None:
60
+ assert (
61
+ egress_refused_hosts(
62
+ "#5 [2/2] RUN pip install requests\n#5 1.2 Successfully installed\n"
63
+ )
64
+ == []
65
+ )
66
+
67
+ def test_a_host_s_own_403_over_its_own_tls_is_not_a_refusal(self) -> None:
68
+ log = (
69
+ "#6 [3/4] RUN curl -fsSL https://example.org/private -o /dev/null\n"
70
+ "#6 0.065 curl: (22) The requested URL returned error: 403\n"
71
+ )
72
+ assert egress_refused_hosts(log) == []
73
+
74
+ def test_steps_side_by_side_are_read_apart(self) -> None:
75
+ # Step 8's URL is not step 7's refusal.
76
+ log = (
77
+ "#7 [3/4] RUN wget https://blocked.example/a.tar.gz\n"
78
+ "#8 [4/4] RUN curl https://allowed.example/b\n"
79
+ "#8 0.1 ok\n"
80
+ "#7 0.067 Proxy tunneling failed: ForbiddenUnable to establish SSL connection.\n"
81
+ )
82
+ assert egress_refused_hosts(log) == ["blocked.example"]
83
+
84
+ def test_the_proxy_itself_is_never_a_refused_host(self) -> None:
85
+ log = (
86
+ "#7 0.066 Connecting to proxy http://10.0.0.5:3128\n"
87
+ "#7 0.067 Proxy tunneling failed: Forbidden\n"
88
+ )
89
+ assert egress_refused_hosts(log, proxy="http://10.0.0.5:3128") == []
90
+
91
+ def test_each_host_once_in_the_order_refused(self) -> None:
92
+ log = a_log("buildctl-uv.log") + a_log("buildctl-base.log") + a_log("buildctl-uv.log")
93
+ assert egress_refused_hosts(log) == ["github.com", "ghcr.io"]
94
+
95
+ def test_the_hosts_read_as_a_sentence(self) -> None:
96
+ assert egress_hosts_text(["a.io"]) == "`a.io`"
97
+ assert egress_hosts_text(["a.io", "b.io", "c.io"]) == "`a.io`, `b.io` and `c.io`"
98
+
99
+
100
+ class TestARefusalIsAFindingOfTheFailure:
101
+ def test_a_build_refused_a_host_names_it_rather_than_the_log(self) -> None:
102
+ run = Buildctl(returncode=1, digest=None, stderr=a_log("buildctl-uv.log"))
103
+ with pytest.raises(EnvironmentsError) as raised:
104
+ a_builder(run=run).build(a_request())
105
+ error = raised.value
106
+ assert error.code.code == "DL_ENV_BUILD_FAILED"
107
+ assert error.retryable is False
108
+ assert "`github.com`" in error.message
109
+ assert error.detail["refused_hosts"] == ["github.com"]
110
+ assert error.detail["findings"] == [{"kind": "egress_refused", "subject": "github.com"}]
111
+
112
+ def test_a_build_that_failed_otherwise_still_says_to_read_the_log(self) -> None:
113
+ run = Buildctl(
114
+ returncode=1, digest=None, stderr=a_log("buildctl-curl.log").split("#7 [4/4]")[0]
115
+ )
116
+ with pytest.raises(EnvironmentsError) as raised:
117
+ a_builder(run=run).build(a_request())
118
+ # git's refusal in step 5 is in this log too: it is named.
119
+ assert raised.value.detail["refused_hosts"] == ["github.com"]
120
+ with pytest.raises(EnvironmentsError) as plain:
121
+ a_builder(run=Buildctl(returncode=1, digest=None, stderr="#5 0.1 exit 1\n")).build(
122
+ a_request()
123
+ )
124
+ assert "findings" not in plain.value.detail
125
+ assert plain.value.message == "The build failed; its log says where"
126
+
127
+ def test_a_resolve_refused_an_index_is_not_retried_as_an_outage(self) -> None:
128
+ error = parse_resolver_failure(a_log("buildctl-pip.log"))
129
+ assert error.code.code == "DL_ENV_PACKAGE_NOT_FOUND"
130
+ assert error.retryable is False
131
+ assert "`pypi.example-private.io`" in error.message
132
+ assert error.detail["findings"] == [
133
+ {"kind": "egress_refused", "subject": "pypi.example-private.io"}
134
+ ]
135
+
136
+ def test_a_conda_solve_refused_a_channel_names_it(self) -> None:
137
+ error = parse_conda_failure(a_log("micromamba.log"))
138
+ assert error.code.code == "DL_ENV_PACKAGE_NOT_FOUND"
139
+ assert error.retryable is False
140
+ assert error.detail["refused_hosts"] == ["conda.anaconda.org"]
141
+
142
+ def test_an_unreachable_index_that_the_proxy_did_not_refuse_is_still_an_outage(self) -> None:
143
+ error = parse_resolver_failure(
144
+ "error: Failed to fetch: `https://pypi.org/simple/x/`\n Caused by: dns error\n"
145
+ )
146
+ assert error.code.code == "DL_ENV_PROVIDER_ERROR"
File without changes